/**
 * 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.index.service;

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

import java.nio.file.Files;
import java.nio.file.Path;

import org.json.JSONObject;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import com.softcorporation.sandi.base.model.Client;
import com.softcorporation.sandi.base.service.ClientService;
import com.softcorporation.sandi.base.util.Constants;
import com.softcorporation.sandi.index.model.IndexingJob;
import com.softcorporation.sandi.index.service.processors.SiteProcessingService;
import com.softcorporation.sandi.index.util.IndexerException;

/**
 * Unit tests for FileSiteProcessingService
 */
@ExtendWith(MockitoExtension.class)
class FileSiteProcessingServiceTest {

    @Mock
    private ClientService clientService;

    @Mock
    private IndexerService indexerService;

    @InjectMocks
    private SiteProcessingService fileSiteProcessingService;

    @TempDir
    Path tempDir;

    private Client testClient;
    private IndexingJob testJob;

    @BeforeEach
    void setUp() {
        testClient = new Client();
        testClient.setClientId("test-client");
        testClient.setName("Test Client");
        testClient.setCollection("test-collection");
        testClient.setActive(true);

        testJob = new IndexingJob();
        testJob.setJobId("test-job-1");
        testJob.setClientId("test-client");
        testJob.setLocation(tempDir.toString());
        testJob.setFileExtensions(".txt,.html,.pdf");
    }

    @Test
    void testExecuteIndexingJob_Success() throws Exception {
        // Arrange
        when(clientService.getClient("test-client")).thenReturn(testClient);
        doNothing().when(indexerService).indexDocument(anyString(), any(JSONObject.class));

        // Create test files
        Path textFile = tempDir.resolve("test.txt");
        Files.write(textFile, "This is test content".getBytes());
        
        Path htmlFile = tempDir.resolve("test.html");
        Files.write(htmlFile, "<html><body>HTML content</body></html>".getBytes());

        // Act
        assertDoesNotThrow(() -> {
            fileSiteProcessingService.executeIndexingJob(testJob);
        });

        // Assert
        verify(clientService).getClient("test-client");
        verify(indexerService, times(2)).indexDocument(eq("test-collection"), any(JSONObject.class));
        
        assertEquals(2, testJob.getTotalFiles());
        assertEquals(2, testJob.getSuccessfulDocuments());
        assertEquals(0, testJob.getFailedDocuments());
        assertEquals("All documents processed successfully", testJob.getMessage());
    }

    @Test
    void testExecuteIndexingJob_ClientNotFound() throws Exception {
        // Arrange
        when(clientService.getClient("test-client")).thenReturn(null);

        // Act & Assert
        IndexerException exception = assertThrows(IndexerException.class, () -> {
            fileSiteProcessingService.executeIndexingJob(testJob);
        });

        assertTrue(exception.getMessage().contains("Client not found"));
        verify(clientService).getClient("test-client");
        verify(indexerService, never()).indexDocument(anyString(), any(JSONObject.class));
    }

    @Test
    void testExecuteIndexingJob_DirectoryNotExists() throws Exception {
        // Arrange
        testJob.setLocation("/non/existent/directory");
        when(clientService.getClient("test-client")).thenReturn(testClient);

        // Act & Assert
        IndexerException exception = assertThrows(IndexerException.class, () -> {
            fileSiteProcessingService.executeIndexingJob(testJob);
        });

        assertTrue(exception.getMessage().contains("Directory does not exist"));
        verify(clientService).getClient("test-client");
        verify(indexerService, never()).indexDocument(anyString(), any(JSONObject.class));
    }

    @Test
    void testExecuteIndexingJob_WithFileExtensionFiltering() throws Exception {
        // Arrange
        when(clientService.getClient("test-client")).thenReturn(testClient);
        doNothing().when(indexerService).indexDocument(anyString(), any(JSONObject.class));

        testJob.setFileExtensions(".txt,.html"); // Only .txt and .html

        // Create test files with different extensions
        Path textFile = tempDir.resolve("test.txt");
        Files.write(textFile, "Text content".getBytes());
        
        Path htmlFile = tempDir.resolve("test.html");
        Files.write(htmlFile, "<html><body>HTML content</body></html>".getBytes());
        
        Path pdfFile = tempDir.resolve("test.pdf");
        Files.write(pdfFile, "PDF content".getBytes());
        
        Path docFile = tempDir.resolve("test.doc");
        Files.write(docFile, "DOC content".getBytes());

        // Act
        assertDoesNotThrow(() -> {
            fileSiteProcessingService.executeIndexingJob(testJob);
        });

        // Assert - Only .txt and .html should be processed
        verify(indexerService, times(2)).indexDocument(eq("test-collection"), any(JSONObject.class));
        assertEquals(2, testJob.getTotalFiles());
        assertEquals(2, testJob.getSuccessfulDocuments());
        assertEquals(0, testJob.getFailedDocuments());
    }

    @Test
    void testExecuteIndexingJob_DefaultFileExtensions() throws Exception {
        // Arrange
        when(clientService.getClient("test-client")).thenReturn(testClient);
        doNothing().when(indexerService).indexDocument(anyString(), any(JSONObject.class));

        testJob.setFileExtensions(null); // Should default to .html

        // Create test files
        Path htmlFile = tempDir.resolve("test.html");
        Files.write(htmlFile, "<html><body>HTML content</body></html>".getBytes());
        
        Path txtFile = tempDir.resolve("test.txt");
        Files.write(txtFile, "Text content".getBytes());

        // Act
        assertDoesNotThrow(() -> {
            fileSiteProcessingService.executeIndexingJob(testJob);
        });

        // Assert - Only .html should be processed with default extensions
        verify(indexerService, times(1)).indexDocument(eq("test-collection"), any(JSONObject.class));
        assertEquals(1, testJob.getTotalFiles());
        assertEquals(1, testJob.getSuccessfulDocuments());
    }

    @Test
    void testExecuteIndexingJob_WithNestedDirectories() throws Exception {
        // Arrange
        when(clientService.getClient("test-client")).thenReturn(testClient);
        doNothing().when(indexerService).indexDocument(anyString(), any(JSONObject.class));

        // Create nested directory structure
        Path subDir1 = tempDir.resolve("subdir1");
        Files.createDirectories(subDir1);
        Path subDir2 = tempDir.resolve("subdir2");
        Files.createDirectories(subDir2);
        Path nestedDir = subDir1.resolve("nested");
        Files.createDirectories(nestedDir);

        // Create files in different directories
        Files.write(tempDir.resolve("root.txt"), "Root content".getBytes());
        Files.write(subDir1.resolve("sub1.txt"), "Sub1 content".getBytes());
        Files.write(subDir2.resolve("sub2.txt"), "Sub2 content".getBytes());
        Files.write(nestedDir.resolve("nested.txt"), "Nested content".getBytes());

        // Act
        assertDoesNotThrow(() -> {
            fileSiteProcessingService.executeIndexingJob(testJob);
        });

        // Assert - All .txt files should be processed recursively
        verify(indexerService, times(4)).indexDocument(eq("test-collection"), any(JSONObject.class));
        assertEquals(4, testJob.getTotalFiles());
        assertEquals(4, testJob.getSuccessfulDocuments());
        assertEquals(0, testJob.getFailedDocuments());
    }

    @Test
    void testExecuteIndexingJob_IndexingFailure() throws Exception {
        // Arrange
        when(clientService.getClient("test-client")).thenReturn(testClient);
        doThrow(new IndexerException("Indexing failed"))
            .when(indexerService).indexDocument(anyString(), any(JSONObject.class));

        // Create test file
        Path textFile = tempDir.resolve("test.txt");
        Files.write(textFile, "Test content".getBytes());

        // Act
        assertDoesNotThrow(() -> {
            fileSiteProcessingService.executeIndexingJob(testJob);
        });

        // Assert
        verify(indexerService).indexDocument(eq("test-collection"), any(JSONObject.class));
        assertEquals(1, testJob.getTotalFiles());
        assertEquals(0, testJob.getSuccessfulDocuments());
        assertEquals(1, testJob.getFailedDocuments());
        assertTrue(testJob.getMessage().contains("failed documents"));
    }

    @Test
    void testExecuteIndexingJob_PartialFailure() throws Exception {
        // Arrange
        when(clientService.getClient("test-client")).thenReturn(testClient);
        
        // First call succeeds, second call fails
        doNothing()
            .doThrow(new IndexerException("Second file failed"))
            .when(indexerService).indexDocument(anyString(), any(JSONObject.class));

        // Create test files
        Files.write(tempDir.resolve("success.txt"), "Success content".getBytes());
        Files.write(tempDir.resolve("failure.txt"), "Failure content".getBytes());

        // Act
        assertDoesNotThrow(() -> {
            fileSiteProcessingService.executeIndexingJob(testJob);
        });

        // Assert
        verify(indexerService, times(2)).indexDocument(eq("test-collection"), any(JSONObject.class));
        assertEquals(2, testJob.getTotalFiles());
        assertEquals(1, testJob.getSuccessfulDocuments());
        assertEquals(1, testJob.getFailedDocuments());
        assertTrue(testJob.getMessage().contains("failed documents"));
    }

    @Test
    void testExecuteIndexingJob_EmptyDirectory() throws Exception {
        // Arrange
        when(clientService.getClient("test-client")).thenReturn(testClient);

        // Act
        assertDoesNotThrow(() -> {
            fileSiteProcessingService.executeIndexingJob(testJob);
        });

        // Assert
        verify(indexerService, never()).indexDocument(anyString(), any(JSONObject.class));
        assertEquals(0, testJob.getTotalFiles());
        assertEquals(0, testJob.getSuccessfulDocuments());
        assertEquals(0, testJob.getFailedDocuments());
        assertEquals("All documents processed successfully", testJob.getMessage());
    }

    @Test
    void testExecuteIndexingJob_NoMatchingFiles() throws Exception {
        // Arrange
        when(clientService.getClient("test-client")).thenReturn(testClient);
        testJob.setFileExtensions(".txt"); // Only .txt files

        // Create files with different extensions
        Files.write(tempDir.resolve("test.html"), "HTML content".getBytes());
        Files.write(tempDir.resolve("test.pdf"), "PDF content".getBytes());

        // Act
        assertDoesNotThrow(() -> {
            fileSiteProcessingService.executeIndexingJob(testJob);
        });

        // Assert
        verify(indexerService, never()).indexDocument(anyString(), any(JSONObject.class));
        assertEquals(0, testJob.getTotalFiles());
        assertEquals(0, testJob.getSuccessfulDocuments());
        assertEquals(0, testJob.getFailedDocuments());
    }

    @Test
    void testStopIndexingJob() throws Exception {
        // Arrange
        String jobId = "test-job-1";

        // Act
        assertDoesNotThrow(() -> {
            fileSiteProcessingService.stopIndexingJob(jobId);
        });

        // Assert - This is a simple setter operation, just verify no exception
        // The actual stopping logic would be tested in integration tests
    }

    @Test
    void testExecuteIndexingJob_WithJobStopping() throws Exception {
        // Arrange
        when(clientService.getClient("test-client")).thenReturn(testClient);
        doNothing().when(indexerService).indexDocument(anyString(), any(JSONObject.class));

        // Create multiple files
        for (int i = 1; i <= 5; i++) {
            Files.write(tempDir.resolve("file" + i + ".txt"), ("Content " + i).getBytes());
        }

        // Stop the job before processing
        fileSiteProcessingService.stopIndexingJob("test-job-1");

        // Act
        assertDoesNotThrow(() -> {
            fileSiteProcessingService.executeIndexingJob(testJob);
        });

        // Assert - Job should be cancelled early
        assertTrue(testJob.getMessage().contains("Cancelled") || 
                   testJob.getSuccessfulDocuments() < 5); // Some files may not be processed
    }

    @Test
    void testProcessFile_SuccessfulProcessing() throws Exception {
        // Arrange
        Path testFile = tempDir.resolve("test.txt");
        Files.write(testFile, "This is test content for Tika processing".getBytes());

        ArgumentCaptor<JSONObject> documentCaptor = ArgumentCaptor.forClass(JSONObject.class);
        doNothing().when(indexerService).indexDocument(eq("test-collection"), documentCaptor.capture());

        // Use reflection to call private method or test through public method
        when(clientService.getClient("test-client")).thenReturn(testClient);

        // Act - Test through executeIndexingJob which calls processFile internally
        assertDoesNotThrow(() -> {
            fileSiteProcessingService.executeIndexingJob(testJob);
        });

        // Assert
        verify(indexerService, atLeastOnce()).indexDocument(eq("test-collection"), any(JSONObject.class));
        
        JSONObject capturedDocument = documentCaptor.getValue();
        assertNotNull(capturedDocument);
        assertTrue(capturedDocument.has(Constants.URI_FIELD));
        assertTrue(capturedDocument.has(Constants.FILENAME_FIELD));
        assertTrue(capturedDocument.has(Constants.CONTENT_FIELD));
        assertTrue(capturedDocument.has(Constants.TIMESTAMP));
        
        assertEquals("test.txt", capturedDocument.getString(Constants.FILENAME_FIELD));
        assertTrue(capturedDocument.getString(Constants.CONTENT_FIELD).contains("test content"));
    }

    @Test
    void testProcessFile_WithMetadata() throws Exception {
        // Arrange
        Path htmlFile = tempDir.resolve("test.html");
        String htmlContent = """
            <html>
            <head>
                <title>Test Document Title</title>
                <meta name="author" content="Test Author">
                <meta name="description" content="Test Description">
            </head>
            <body>
                <h1>Test Heading</h1>
                <p>This is test paragraph content.</p>
            </body>
            </html>
            """;
        Files.write(htmlFile, htmlContent.getBytes());

        ArgumentCaptor<JSONObject> documentCaptor = ArgumentCaptor.forClass(JSONObject.class);
        doNothing().when(indexerService).indexDocument(eq("test-collection"), documentCaptor.capture());

        when(clientService.getClient("test-client")).thenReturn(testClient);
        testJob.setFileExtensions(".html");

        // Act
        assertDoesNotThrow(() -> {
            fileSiteProcessingService.executeIndexingJob(testJob);
        });

        // Assert
        verify(indexerService).indexDocument(eq("test-collection"), any(JSONObject.class));
        
        JSONObject capturedDocument = documentCaptor.getValue();
        assertNotNull(capturedDocument);
        assertTrue(capturedDocument.has(Constants.CONTENT_FIELD));
        
        // Tika should extract text content from HTML
        String content = capturedDocument.getString(Constants.CONTENT_FIELD);
        assertTrue(content.contains("Test Heading"));
        assertTrue(content.contains("test paragraph content"));
        
        // Should have metadata fields
        assertTrue(capturedDocument.has("title") || content.contains("Test Document Title"));
    }

    @Test
    void testProcessFile_UnsupportedFileType() throws Exception {
        // Arrange
        Path binaryFile = tempDir.resolve("test.bin");
        byte[] binaryData = new byte[]{0x00, 0x01, 0x02, 0x03, (byte)0xFF};
        Files.write(binaryFile, binaryData);

        when(clientService.getClient("test-client")).thenReturn(testClient);
        testJob.setFileExtensions(".bin");

        // Act & Assert
        // Binary files might not be processed successfully, but shouldn't crash
        assertDoesNotThrow(() -> {
            fileSiteProcessingService.executeIndexingJob(testJob);
        });

        // The job should complete even if file processing fails
        assertTrue(testJob.getTotalFiles() >= 0);
        assertTrue(testJob.getFailedDocuments() >= 0);
    }
}