/**
 * Copyright (c) 2025 SoftCorporation LLC. All rights reserved.
 *
 * The Software License, Version 1.0
 *
 * SoftCorporation LLC. grants you ("Licensee") a non-exclusive, royalty free,
 * license to use, modify and redistribute this software in source and binary
 * code form, provided that the following conditions are met:
 *
 * 1. The end-user documentation included with the redistribution,
 *    if any, must include the following acknowledgment:
 *       "This product includes software developed by the
 *        SoftCorporation LLC. (http://www.softcorporation.com)."
 *    Alternately, this acknowledgment may appear in the software itself,
 *    if and wherever such third-party acknowledgments normally appear.
 *
 * 2. This software and SoftCorporation name must not be used to
 *    promote products derived from the software without prior
 *    written permission. For written permission, please contact
 *    info@softcorporation.com.
 *
 * This software is provided "AS IS," without a warranty of any kind.
 * ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES,
 * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A
 * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED.
 * IN NO EVENT SHALL THE SOFTCORPORATION BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION).
 *
 */
package com.softcorporation.sandi.base.service;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;

import com.softcorporation.sandi.base.model.TextChunk;
import com.softcorporation.sandi.base.service.LanguageModelService;
import com.softcorporation.sandi.base.util.Constants;
import com.softcorporation.sandi.base.util.EmbeddingException;

/**
 * Unit tests for TextEmbeddingService
 */
@ExtendWith(MockitoExtension.class)
class TextEmbeddingServiceTest {

    @Mock
    private LanguageModelService languageModelService;

    @InjectMocks
    private TextEmbeddingService textEmbeddingService;

    private static final String TEST_ID = "test-doc-1";
    private static final String TEST_HIGH_TEXT = "Important title";
    private static final String TEST_MEDIUM_TEXT = "Critical summary information";
    private static final String TEST_LOW_TEXT = "This is a longer text content that will be used for testing the chunking functionality. " +
            "It contains multiple sentences and paragraphs to test how the service breaks text into manageable chunks. " +
            "The text should be long enough to trigger the chunking logic and create multiple text chunks for processing. " +
            "This helps verify that the embedding service can handle large documents properly by splitting them appropriately.";

    @BeforeEach
    void setUp() {
        // Set default configuration values
        ReflectionTestUtils.setField(textEmbeddingService, "embeddingChunkNumMax", 1);
        ReflectionTestUtils.setField(textEmbeddingService, "embeddingChunkLengthMax", 200);
        ReflectionTestUtils.setField(textEmbeddingService, "embeddingChunkLengthMin", 50);
        ReflectionTestUtils.setField(textEmbeddingService, "embeddingChunkAddWeight", 0.25);
    }

    @Test
    void testGetDocumentEmbeddings_Success() throws Exception {
        // Arrange
    	List<double[]> mockEmbeddings = Arrays.asList(
    		      new double[]{0.1, 0.2, 0.3},
    		      new double[]{0.4, 0.5, 0.6}
    		  );
        
        when(languageModelService.getTextsEmbedding(any(), anyList())).thenReturn(mockEmbeddings);

        // Act
        List<Map<String, Object>> result = textEmbeddingService.getDocumentEmbeddings(
            null, null, null, null);

        // Assert
        assertNotNull(result);
        assertEquals(2, result.size());
        
        Map<String, Object> embeddingDoc = (Map<String, Object>) result.get(0);
        assertNotNull(embeddingDoc.get(Constants.CHUNK_FIELD));
        assertNotNull(embeddingDoc.get(Constants.CHUNK_TEXT));
        assertNotNull(embeddingDoc.get(Constants.CHUNK_BEGIN));
        assertNotNull(embeddingDoc.get(Constants.CHUNK_END));

        verify(languageModelService, atLeastOnce()).getTextsEmbedding(any(), anyList());
    }

    @Test
    void testGetDocumentEmbeddings_WithLargeText() throws Exception {
        // Arrange
        ReflectionTestUtils.setField(textEmbeddingService, "embeddingChunkLengthMax", 100);
        
        String largeText = "This is a very long text that should be broken into multiple chunks. " +
                "Each chunk should be processed separately to create embeddings. " +
                "The service should handle this properly by creating multiple text chunks and processing them in batches.";
    	List<double[]> mockEmbeddings = Arrays.asList(
  		      new double[]{0.1, 0.2, 0.3},
  		      new double[]{0.4, 0.5, 0.6}
  		  );
        
        when(languageModelService.getTextsEmbedding(any(), anyList())).thenReturn(mockEmbeddings);

        // Act
        List<Map<String, Object>> result = textEmbeddingService.getDocumentEmbeddings(
            null, null, null, null);

        // Assert
        assertNotNull(result);
        assertTrue(result.size() >= 1);
        
        // Verify that text chunks were created and processed
        @SuppressWarnings("unchecked")
		ArgumentCaptor<List<String>> textsCaptor = ArgumentCaptor.forClass(List.class);
        verify(languageModelService, atLeastOnce()).getTextsEmbedding(any(), textsCaptor.capture());
        
        List<String> capturedTexts = textsCaptor.getValue();
        assertNotNull(capturedTexts);
        assertTrue(capturedTexts.size() >= 1);
    }

    @Test
    void testGetDocumentEmbeddings_ThrowsIndexerException() throws Exception {
        // Arrange
        when(languageModelService.getTextsEmbedding(any(), anyList()))
            .thenThrow(new IOException("Embedding service error"));

        // Act & Assert
        EmbeddingException exception = assertThrows(EmbeddingException.class, () -> {
            textEmbeddingService.getDocumentEmbeddings(
                null, null, null, null);
        });

        assertEquals("Embedding service error", exception.getMessage());
        verify(languageModelService).getTextsEmbedding(any(), anyList());
    }

    @Test
    void testBreakTextInChunks_WithShortText() {
        // Arrange
        String shortText = "This is a short text that should create one chunk.";

        // Act
        List<TextChunk> result = textEmbeddingService.breakTextInChunks(shortText);

        // Assert
        assertNotNull(result);
        assertEquals(1, result.size());
        
        TextChunk chunk = result.get(0);
        assertEquals(shortText, chunk.getText());
        assertEquals(0, chunk.getBegin());
        assertEquals(shortText.length(), chunk.getEnd());
    }

    @Test
    void testBreakTextInChunks_WithLongText() {
        // Arrange
        ReflectionTestUtils.setField(textEmbeddingService, "embeddingChunkLengthMax", 100);
        ReflectionTestUtils.setField(textEmbeddingService, "embeddingChunkLengthMin", 20);
        
        String longText = 
        		"This is the               \t first paragraph with important                                                   information. " +
                "It contains multiple sentences and should be broken into chunks.\n\n" +
                "This is the second paragraph that continues the document content. " +
                "It also has multiple sentences and adds more context to the document.";

        // Act
        List<TextChunk> result = textEmbeddingService.breakTextInChunks(longText);

        // Assert
        assertNotNull(result);
        assertTrue(result.size() >= 2, "Long text should be broken into multiple chunks");
        
        // Verify chunk properties
        for (TextChunk chunk : result) {
            assertNotNull(chunk.getText());
            assertTrue(chunk.getText().length() >= 20, "Chunk should meet minimum length");
            assertTrue(chunk.getEnd() >= chunk.getBegin(), "Chunk end should be >= Chunk begin");
        }
    }

    @Test
    void testBreakTextInChunks_WithParagraphs() {
        // Arrange
        String textWithParagraphs = "First paragraph.\n\nSecond paragraph.\n\nThird paragraph.";

        // Act
        List<TextChunk> result = textEmbeddingService.breakTextInChunks(textWithParagraphs);

        // Assert
        assertNotNull(result);
        assertTrue(result.size() >= 1);
        
        // Verify that paragraph breaks are handled correctly
        for (TextChunk chunk : result) {
            assertNotNull(chunk.getText());
            assertFalse(chunk.getText().trim().isEmpty());
        }
    }

    @Test
    void testBreakTextInChunks_WithEmptyText() {
        // Act
        List<TextChunk> result = textEmbeddingService.breakTextInChunks("");

        // Assert
        assertNotNull(result);
        assertEquals(0, result.size());
    }

    @Test
    void testBreakTextInChunks_WithWhitespaceOnly() {
        // Act
        List<TextChunk> result = textEmbeddingService.breakTextInChunks("   \n\n   \t   ");

        // Assert
        assertNotNull(result);
        // Should not create chunks for whitespace-only text
        assertEquals(0, result.size());
    }

    @Test
    void testBreakTextInChunks_WithSpecialCharacters() {
        // Arrange
        String textWithSpecialChars = "Text with special characters: @#$%^&*(). " +
                "It also includes punctuation marks and numbers 123. " +
                "This should still be processed correctly by the chunking algorithm.";

        // Act
        List<TextChunk> result = textEmbeddingService.breakTextInChunks(textWithSpecialChars);

        // Assert
        assertNotNull(result);
        assertTrue(result.size() >= 1);
        
        TextChunk chunk = result.get(0);
        assertTrue(chunk.getText().contains("@#$%^&*()"));
        assertTrue(chunk.getText().contains("123"));
    }

    @Test
    void testConfigurationProperties() {
        // Arrange
        ReflectionTestUtils.setField(textEmbeddingService, "embeddingChunkNumMax", 5);
        ReflectionTestUtils.setField(textEmbeddingService, "embeddingChunkLengthMax", 1000);
        ReflectionTestUtils.setField(textEmbeddingService, "embeddingChunkLengthMin", 100);
        ReflectionTestUtils.setField(textEmbeddingService, "embeddingChunkAddWeight", 0.3);

        // Act
        int chunkNumMax = (Integer) ReflectionTestUtils.getField(textEmbeddingService, "embeddingChunkNumMax");
        int chunkLengthMax = (Integer) ReflectionTestUtils.getField(textEmbeddingService, "embeddingChunkLengthMax");
        int chunkLengthMin = (Integer) ReflectionTestUtils.getField(textEmbeddingService, "embeddingChunkLengthMin");
        double chunkAddWeight = (Double) ReflectionTestUtils.getField(textEmbeddingService, "embeddingChunkAddWeight");

        // Assert
        assertEquals(5, chunkNumMax);
        assertEquals(1000, chunkLengthMax);
        assertEquals(100, chunkLengthMin);
        assertEquals(0.3, chunkAddWeight);
    }

    @Test
    void testMultipleBatchProcessing() throws Exception {
        // Arrange
        ReflectionTestUtils.setField(textEmbeddingService, "embeddingChunkNumMax", 2);
        
        String largeText = 
        		"This is chunk one content. ".repeat(10) + "\n\n" +
                "This is chunk two content. ".repeat(10) + "\n\n" +
                "This is chunk three content. ".repeat(10);
        
    	List<double[]> mockEmbeddings1 = Arrays.asList(
    		      new double[]{0.1, 0.2, 0.3},
    		      new double[]{0.4, 0.5, 0.6}
    		  );
    	List<double[]> mockEmbeddings2 = Arrays.asList(
    		      new double[]{0.1, 0.2, 0.3},
    		      new double[]{0.4, 0.5, 0.6}
    		  );
        
        when(languageModelService.getTextsEmbedding(any(), anyList()))
            .thenReturn(mockEmbeddings1)
            .thenReturn(mockEmbeddings2);

        // Act
        List<Map<String, Object>> result = textEmbeddingService.getDocumentEmbeddings(
            null, null, null, null);

        // Assert
        assertNotNull(result);
        assertTrue(result.size() >= 1);
        
        // Verify multiple batch calls were made
        verify(languageModelService, atLeast(1)).getTextsEmbedding(any(), anyList());
    }

    @Test
    void testChunkCreationWithDifferentBreaks() {
        // Arrange
        String textWithBreaks = "Line 1.\nLine 2.\n\nParagraph break.\rCarriage return.\r\nWindows line break.";

        // Act
        List<TextChunk> result = textEmbeddingService.breakTextInChunks(textWithBreaks);

        // Assert
        assertNotNull(result);
        assertTrue(result.size() >= 1);
        
        // Verify that different line break types are handled
        TextChunk chunk = result.get(0);
        assertNotNull(chunk.getText());
        assertTrue(chunk.getText().length() > 0);
    }

    @Test
    void testEmbeddingDocumentStructure() throws Exception {
        // Arrange
    	List<double[]> mockEmbeddings = Arrays.asList(
    		      new double[]{0.1, 0.2, 0.3},
    		      new double[]{0.4, 0.5, 0.6}
    		  );
        
        when(languageModelService.getTextsEmbedding(any(), anyList())).thenReturn(mockEmbeddings);

        // Act
        List<Map<String, Object>> result = textEmbeddingService.getDocumentEmbeddings(
            null, null, null, null);

        // Assert
        assertNotNull(result);
        assertTrue(result.size() > 0);
        
        Object firstResult = result.get(0);
        assertTrue(firstResult instanceof Map);
        
        @SuppressWarnings("unchecked")
		Map<String, Object> embDoc = (Map<String, Object>) firstResult;
        
        // Verify required fields are present
        assertTrue(embDoc.containsKey(Constants.CHUNK_TEXT));
        assertTrue(embDoc.containsKey(Constants.CHUNK_FIELD));
        assertTrue(embDoc.containsKey(Constants.CHUNK_BEGIN));
        assertTrue(embDoc.containsKey(Constants.CHUNK_END));
        
        // Verify field types
        assertEquals(TEST_ID, embDoc.get(Constants.ID));
        assertTrue(embDoc.get(Constants.EMB) instanceof List);
        assertTrue(embDoc.get(Constants.CHUNK_BEGIN) instanceof Integer);
        assertTrue(embDoc.get(Constants.CHUNK_END) instanceof Integer);
    }
}