/**
 * 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.search.controller;

import static org.hamcrest.Matchers.*;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

import java.util.ArrayList;
import java.util.List;

import org.json.JSONArray;
import org.json.JSONObject;
import org.junit.jupiter.api.BeforeEach;
import org.mockito.ArgumentMatchers;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;

import com.softcorporation.sandi.base.model.Client;
import com.softcorporation.sandi.base.service.ClientService;
import com.softcorporation.sandi.search.model.SearchRequest;
import com.softcorporation.sandi.search.model.SearchResponse;
import com.softcorporation.sandi.search.service.SearchService;

/**
 * Unit tests for SearchController
 */
@ExtendWith(SpringExtension.class)
@WebMvcTest(SearchController.class)
class SearchControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private SearchService searchService;

    @MockBean
    private ClientService clientService;

    private Client testClient;
    private SearchRequest testSearchRequest;
    private SearchResponse testSearchResponse;

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

        testSearchRequest = new SearchRequest();
        testSearchRequest.setQuery("test query");
        testSearchRequest.setClientId("test-client");
        testSearchRequest.setPageNumber(0);
        testSearchRequest.setPageSize(10);

        testSearchResponse = new SearchResponse();
        testSearchResponse.setRequestId("test-request");
        testSearchResponse.setFoundResults(100L);
        testSearchResponse.setStart(0L);
        
        // Create test documents
        JSONArray documents = new JSONArray();
        JSONObject doc1 = new JSONObject();
        doc1.put("id", "doc1");
        doc1.put("title", "Test Document 1");
        doc1.put("content", "This is test content");
        documents.put(doc1);
        
        JSONObject doc2 = new JSONObject();
        doc2.put("id", "doc2");
        doc2.put("title", "Test Document 2");
        doc2.put("content", "This is more test content");
        documents.put(doc2);
        
        // Convert JSONArray to List for compatibility
        List<Object> resultsList = new ArrayList<>();
        for (int i = 0; i < documents.length(); i++) {
            resultsList.add(documents.get(i));
        }
        testSearchResponse.setResults(resultsList);
    }

    @Test
    void testSearch_Success() throws Exception {
        // Arrange
        when(clientService.getClient("test-client")).thenReturn(testClient);
        when(searchService.search(ArgumentMatchers.any(SearchRequest.class))).thenReturn(testSearchResponse);

        String requestBody = """
            {
                "query": "test query",
                "clientId": "test-client",
                "start": 0,
                "rows": 10
            }
            """;

        // Act & Assert
        mockMvc.perform(post("/search")
                .contentType(MediaType.APPLICATION_JSON)
                .content(requestBody))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.requestId", is("test-request")))
                .andExpect(jsonPath("$.totalResults", is(100)))
                .andExpect(jsonPath("$.start", is(0)))
                .andExpect(jsonPath("$.results", hasSize(2)));

        verify(clientService).getClient("test-client");
        verify(searchService).search(ArgumentMatchers.any(SearchRequest.class));
    }

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

        String requestBody = """
            {
                "query": "test query",
                "clientId": "non-existent-client",
                "start": 0,
                "rows": 10
            }
            """;

        // Act & Assert
        mockMvc.perform(post("/search")
                .contentType(MediaType.APPLICATION_JSON)
                .content(requestBody))
                .andExpect(status().isNotFound())
                .andExpect(jsonPath("$.error", containsString("Client not found")));

        verify(clientService).getClient("non-existent-client");
        verify(searchService, never()).search(ArgumentMatchers.any(SearchRequest.class));
    }

    @Test
    void testSearch_InactiveClient() throws Exception {
        // Arrange
        Client inactiveClient = new Client();
        inactiveClient.setClientId("inactive-client");
        inactiveClient.setActive(false);

        when(clientService.getClient("inactive-client")).thenReturn(inactiveClient);

        String requestBody = """
            {
                "query": "test query",
                "clientId": "inactive-client",
                "start": 0,
                "rows": 10
            }
            """;

        // Act & Assert
        mockMvc.perform(post("/search")
                .contentType(MediaType.APPLICATION_JSON)
                .content(requestBody))
                .andExpect(status().isForbidden())
                .andExpect(jsonPath("$.error", containsString("Client is not active")));

        verify(clientService).getClient("inactive-client");
        verify(searchService, never()).search(ArgumentMatchers.any(SearchRequest.class));
    }

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

        String requestBody = """
            {
                "query": "",
                "clientId": "test-client",
                "start": 0,
                "rows": 10
            }
            """;

        // Act & Assert
        mockMvc.perform(post("/search")
                .contentType(MediaType.APPLICATION_JSON)
                .content(requestBody))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.error", containsString("Query cannot be empty")));

        verify(clientService).getClient("test-client");
        verify(searchService, never()).search(ArgumentMatchers.any(SearchRequest.class));
    }

    @Test
    void testSearch_MissingClientId() throws Exception {
        String requestBody = """
            {
                "query": "test query",
                "start": 0,
                "rows": 10
            }
            """;

        // Act & Assert
        mockMvc.perform(post("/search")
                .contentType(MediaType.APPLICATION_JSON)
                .content(requestBody))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.error", containsString("Client ID is required")));

        verify(clientService, never()).getClient(ArgumentMatchers.any());
        verify(searchService, never()).search(ArgumentMatchers.any(SearchRequest.class));
    }

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

        String requestBody = """
            {
                "query": "test query",
                "clientId": "test-client",
                "start": -1,
                "rows": 10
            }
            """;

        // Act & Assert
        mockMvc.perform(post("/search")
                .contentType(MediaType.APPLICATION_JSON)
                .content(requestBody))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.error", containsString("Start parameter must be non-negative")));

        verify(clientService).getClient("test-client");
        verify(searchService, never()).search(ArgumentMatchers.any(SearchRequest.class));
    }

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

        String requestBody = """
            {
                "query": "test query",
                "clientId": "test-client",
                "start": 0,
                "rows": 0
            }
            """;

        // Act & Assert
        mockMvc.perform(post("/search")
                .contentType(MediaType.APPLICATION_JSON)
                .content(requestBody))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.error", containsString("Rows parameter must be positive")));

        verify(clientService).getClient("test-client");
        verify(searchService, never()).search(ArgumentMatchers.any(SearchRequest.class));
    }

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

        String requestBody = """
            {
                "query": "test query",
                "clientId": "test-client",
                "start": 0,
                "rows": 10001
            }
            """;

        // Act & Assert
        mockMvc.perform(post("/search")
                .contentType(MediaType.APPLICATION_JSON)
                .content(requestBody))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.error", containsString("Maximum rows allowed is 10000")));

        verify(clientService).getClient("test-client");
        verify(searchService, never()).search(ArgumentMatchers.any(SearchRequest.class));
    }

    @Test
    void testSearch_ServiceException() throws Exception {
        // Arrange
        when(clientService.getClient("test-client")).thenReturn(testClient);
        when(searchService.search(ArgumentMatchers.any(SearchRequest.class)))
            .thenThrow(new RuntimeException("Search service error"));

        String requestBody = """
            {
                "query": "test query",
                "clientId": "test-client",
                "start": 0,
                "rows": 10
            }
            """;

        // Act & Assert
        mockMvc.perform(post("/search")
                .contentType(MediaType.APPLICATION_JSON)
                .content(requestBody))
                .andExpect(status().isInternalServerError())
                .andExpect(jsonPath("$.error", containsString("Search service error")));

        verify(clientService).getClient("test-client");
        verify(searchService).search(ArgumentMatchers.any(SearchRequest.class));
    }

    @Test
    void testSearch_WithFacets() throws Exception {
        // Arrange
        when(clientService.getClient("test-client")).thenReturn(testClient);
        
        SearchResponse responseWithFacets = new SearchResponse();
        responseWithFacets.setRequestId("test-request");
        responseWithFacets.setFoundResults(100L);
        responseWithFacets.setStart(0L);
        responseWithFacets.setResults(new ArrayList<>());
        
        // Add facets - Note: This would require adding facets support to SearchResponse
        
        when(searchService.search(ArgumentMatchers.any(SearchRequest.class))).thenReturn(responseWithFacets);

        String requestBody = """
            {
                "query": "test query",
                "clientId": "test-client",
                "start": 0,
                "rows": 10,
                "facets": ["category"]
            }
            """;

        // Act & Assert
        mockMvc.perform(post("/search")
                .contentType(MediaType.APPLICATION_JSON)
                .content(requestBody))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.requestId", is("test-request")));

        verify(clientService).getClient("test-client");
        verify(searchService).search(ArgumentMatchers.any(SearchRequest.class));
    }

    @Test
    void testSearch_WithFilters() throws Exception {
        // Arrange
        when(clientService.getClient("test-client")).thenReturn(testClient);
        when(searchService.search(ArgumentMatchers.any(SearchRequest.class))).thenReturn(testSearchResponse);

        String requestBody = """
            {
                "query": "test query",
                "clientId": "test-client",
                "start": 0,
                "rows": 10,
                "filters": {
                    "category": "technology",
                    "author": "john doe"
                }
            }
            """;

        // Act & Assert
        mockMvc.perform(post("/search")
                .contentType(MediaType.APPLICATION_JSON)
                .content(requestBody))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.requestId", is("test-request")))
                .andExpect(jsonPath("$.totalResults", is(100)));

        verify(clientService).getClient("test-client");
        verify(searchService).search(ArgumentMatchers.any(SearchRequest.class));
    }

    @Test
    void testSearch_WithSorting() throws Exception {
        // Arrange
        when(clientService.getClient("test-client")).thenReturn(testClient);
        when(searchService.search(ArgumentMatchers.any(SearchRequest.class))).thenReturn(testSearchResponse);

        String requestBody = """
            {
                "query": "test query",
                "clientId": "test-client",
                "start": 0,
                "rows": 10,
                "sort": "title asc"
            }
            """;

        // Act & Assert
        mockMvc.perform(post("/search")
                .contentType(MediaType.APPLICATION_JSON)
                .content(requestBody))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.requestId", is("test-request")));

        verify(clientService).getClient("test-client");
        verify(searchService).search(ArgumentMatchers.any(SearchRequest.class));
    }

}