package incheon.product.geoview2d.search.web;

import incheon.com.cmm.exception.BusinessException;
import incheon.product.geoview2d.search.service.SearchService;
import incheon.product.geoview2d.search.vo.AddressCoordinateVO;
import incheon.product.geoview2d.search.vo.JibunSearchResultVO;
import incheon.product.geoview2d.search.vo.PoiVO;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import java.util.List;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

/**
 * SearchApiController MockMvc 테스트.
 * 엔드포인트 바인딩, 파라미터 매핑, 서비스 위임을 검증한다.
 */
@ExtendWith(MockitoExtension.class)
class SearchApiControllerTest {

    @InjectMocks
    private SearchApiController controller;

    @Mock
    private SearchService searchService;

    private MockMvc mockMvc;

    @BeforeEach
    void setup() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new JavaTimeModule());
        mockMvc = MockMvcBuilders.standaloneSetup(controller)
                .setMessageConverters(new StringHttpMessageConverter(), new MappingJackson2HttpMessageConverter(mapper))
                .build();
    }

    @Nested
    @DisplayName("GET /api/v1/product/g2d/search — 통합 검색")
    class UnifiedSearch {

        @Test
        @DisplayName("keyword 파라미터로 통합 검색 결과 반환")
        void searchAll() throws Exception {
            when(searchService.getJibunSearchInfo(anyString(), anyInt(), anyInt()))
                    .thenReturn(List.of());
            when(searchService.getPoiList(anyString(), any(), anyInt(), anyInt()))
                    .thenReturn(List.of());
            when(searchService.getAdmList(any())).thenReturn(List.of());

            mockMvc.perform(get("/api/v1/product/g2d/search")
                            .param("keyword", "인천 구월동 123"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.data.keyword").value("인천 구월동 123"))
                    .andExpect(jsonPath("$.data.type").value("all"));
        }

        @Test
        @DisplayName("type=address면 주소 검색만 수행")
        void searchAddressOnly() throws Exception {
            when(searchService.getJibunSearchInfo(anyString(), anyInt(), anyInt()))
                    .thenReturn(List.of());

            mockMvc.perform(get("/api/v1/product/g2d/search")
                            .param("keyword", "구월동 100")
                            .param("type", "address"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.data.address").isArray());
        }
    }

    @Nested
    @DisplayName("GET /api/v1/product/g2d/search/address/jibun — 지번 검색")
    class JibunSearch {

        @Test
        @DisplayName("keyword 파라미터로 지번 검색")
        void searchJibun() throws Exception {
            when(searchService.getJibunSearchInfo("구월동 123", 0, 10))
                    .thenReturn(List.of());

            mockMvc.perform(get("/api/v1/product/g2d/search/address/jibun")
                            .param("keyword", "구월동 123"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.data").isArray());
        }

        @Test
        @DisplayName("keyword 없으면 400")
        void missingKeyword() throws Exception {
            mockMvc.perform(get("/api/v1/product/g2d/search/address/jibun"))
                    .andExpect(status().isBadRequest());
        }
    }

    @Nested
    @DisplayName("GET /api/v1/product/g2d/search/address/coordinate — 좌표 조회")
    class CoordinateSearch {

        @Test
        @DisplayName("건물관리번호로 좌표 조회")
        void getCoordinate() throws Exception {
            AddressCoordinateVO vo = new AddressCoordinateVO();
            when(searchService.getCoordinateByBldgMngNo("12345")).thenReturn(vo);

            mockMvc.perform(get("/api/v1/product/g2d/search/address/coordinate")
                            .param("bldgMngNo", "12345"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.data").exists());
        }
    }

    @Nested
    @DisplayName("GET /api/v1/product/g2d/search/adm — 행정구역")
    class AdmSearch {

        @Test
        @DisplayName("기본 CTP 유형으로 행정구역 조회")
        void getAdmList() throws Exception {
            when(searchService.getAdmList(any())).thenReturn(List.of());
            when(searchService.getAdmListTotCnt(any())).thenReturn(0L);

            mockMvc.perform(get("/api/v1/product/g2d/search/adm"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.data.content").isArray());
        }
    }

    @Nested
    @DisplayName("POI CRUD 엔드포인트")
    class PoiEndpoints {

        @Test
        @DisplayName("GET /poi — 전체 POI 목록")
        void getAllPoi() throws Exception {
            when(searchService.getAllPoi()).thenReturn(List.of());

            mockMvc.perform(get("/api/v1/product/g2d/search/poi"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.data").isArray());
        }

        @Test
        @DisplayName("GET /poi/{nfId} — POI 상세")
        void getPoiById() throws Exception {
            PoiVO poi = new PoiVO();
            when(searchService.getPoiById("nf-001")).thenReturn(poi);

            mockMvc.perform(get("/api/v1/product/g2d/search/poi/nf-001"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.data").exists());
        }

        @Test
        @DisplayName("GET /poi/list — POI 페이징 조회")
        void getPoiList() throws Exception {
            when(searchService.getPoiList(anyString(), any(), anyInt(), anyInt()))
                    .thenReturn(List.of());
            when(searchService.getPoiTotalCount(anyString(), any())).thenReturn(0);

            mockMvc.perform(get("/api/v1/product/g2d/search/poi/list")
                            .param("searchKeyword", "test"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.data.content").isArray())
                    .andExpect(jsonPath("$.data.totalElements").value(0));
        }

        @Test
        @DisplayName("DELETE /poi/{nfId} — POI 삭제")
        void deletePoi() throws Exception {
            mockMvc.perform(delete("/api/v1/product/g2d/search/poi/nf-001"))
                    .andExpect(status().isOk());
            verify(searchService).deletePoi("nf-001");
        }
    }
}
