package incheon.product.geoview3d.traffic.web;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import incheon.com.cmm.exception.RestApiExceptionHandler;
import incheon.product.geoview3d.traffic.service.CctvService;
import incheon.product.geoview3d.traffic.service.TrafficService;
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 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 java.util.Map;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
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;

/**
 * TrafficApiController MockMvc 테스트.
 * bbox 범위 검증(BL-010-3), page/size 클램핑(BL-018-1)을 중점 검증한다.
 */
@ExtendWith(MockitoExtension.class)
class TrafficApiControllerTest {

    @InjectMocks
    private TrafficApiController controller;

    @Mock
    private TrafficService trafficService;

    @Mock
    private CctvService cctvService;

    private MockMvc mockMvc;

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

    @Nested
    @DisplayName("GET /api/v1/product/g3d/traffic — 교통 정보 (bbox)")
    class TrafficInfo {

        @Test
        @DisplayName("정상 bbox로 교통 정보 조회")
        void validBbox() throws Exception {
            when(trafficService.getTrafficInfo(any(), anyDouble(), anyDouble(), anyDouble(), anyDouble()))
                    .thenReturn(List.of());

            mockMvc.perform(get("/api/v1/product/g3d/traffic")
                            .param("minX", "126.5")
                            .param("minY", "37.4")
                            .param("maxX", "126.6")
                            .param("maxY", "37.5"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.data").isArray());
        }

        @Test
        @DisplayName("min >= max 이면 400 (BL-010-3)")
        void invalidBboxMinGteMax() throws Exception {
            mockMvc.perform(get("/api/v1/product/g3d/traffic")
                            .param("minX", "127.0")
                            .param("minY", "37.0")
                            .param("maxX", "126.0")
                            .param("maxY", "38.0"))
                    .andExpect(status().isBadRequest());

            verify(trafficService, never()).getTrafficInfo(any(), anyDouble(), anyDouble(), anyDouble(), anyDouble());
        }

        @Test
        @DisplayName("bbox 범위 1.0도 초과면 400 (BL-010-3)")
        void bboxRangeTooWide() throws Exception {
            mockMvc.perform(get("/api/v1/product/g3d/traffic")
                            .param("minX", "126.0")
                            .param("minY", "37.0")
                            .param("maxX", "127.5")
                            .param("maxY", "38.0"))
                    .andExpect(status().isBadRequest());

            verify(trafficService, never()).getTrafficInfo(any(), anyDouble(), anyDouble(), anyDouble(), anyDouble());
        }
    }

    @Nested
    @DisplayName("GET /api/v1/product/g3d/traffic/roads — 도로명 검색")
    class RoadNames {

        @Test
        @DisplayName("정상 페이지네이션으로 도로명 검색")
        void normalPagination() throws Exception {
            when(trafficService.getRoadNames(any(), any(), anyInt(), anyInt()))
                    .thenReturn(Map.of("content", List.of(), "total", 0));

            mockMvc.perform(get("/api/v1/product/g3d/traffic/roads")
                            .param("page", "1")
                            .param("size", "10"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.data").exists());
        }

        @Test
        @DisplayName("page 0이면 1로 클램핑 (BL-018-1)")
        void pageZeroClamped() throws Exception {
            when(trafficService.getRoadNames(any(), any(), anyInt(), anyInt()))
                    .thenReturn(Map.of("content", List.of()));

            mockMvc.perform(get("/api/v1/product/g3d/traffic/roads")
                            .param("page", "0")
                            .param("size", "10"))
                    .andExpect(status().isOk());

            verify(trafficService).getRoadNames(null, null, 1, 10);
        }

        @Test
        @DisplayName("size 500 초과면 500으로 클램핑 (BL-018-1)")
        void sizeExceedsMaxClamped() throws Exception {
            when(trafficService.getRoadNames(any(), any(), anyInt(), anyInt()))
                    .thenReturn(Map.of("content", List.of()));

            mockMvc.perform(get("/api/v1/product/g3d/traffic/roads")
                            .param("page", "1")
                            .param("size", "1000"))
                    .andExpect(status().isOk());

            verify(trafficService).getRoadNames(null, null, 1, 500);
        }
    }

    @Nested
    @DisplayName("GET /api/v1/product/g3d/traffic/cctv — CCTV 목록 (bbox)")
    class CctvList {

        @Test
        @DisplayName("정상 bbox로 CCTV 목록 조회")
        void validBbox() throws Exception {
            when(cctvService.getCctvList(any(), anyDouble(), anyDouble(), anyDouble(), anyDouble()))
                    .thenReturn(List.of());

            mockMvc.perform(get("/api/v1/product/g3d/traffic/cctv")
                            .param("minX", "126.5")
                            .param("minY", "37.4")
                            .param("maxX", "126.6")
                            .param("maxY", "37.5"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.data").isArray());
        }

        @Test
        @DisplayName("CCTV bbox도 범위 검증 적용")
        void cctvBboxValidation() throws Exception {
            mockMvc.perform(get("/api/v1/product/g3d/traffic/cctv")
                            .param("minX", "127.0")
                            .param("minY", "37.0")
                            .param("maxX", "126.0")
                            .param("maxY", "38.0"))
                    .andExpect(status().isBadRequest());
        }
    }

    @Nested
    @DisplayName("GET /api/v1/product/g3d/traffic/cctv/redirect — CCTV URL 해석")
    class CctvRedirect {

        @Test
        @DisplayName("CCTV URL 리다이렉트 해석")
        void resolveCctvUrl() throws Exception {
            when(cctvService.getCctvRedirectUrl(anyString()))
                    .thenReturn("http://resolved.com/stream");

            mockMvc.perform(get("/api/v1/product/g3d/traffic/cctv/redirect")
                            .param("url", "http://example.com/cctv"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.message").value("http://resolved.com/stream"));
        }
    }
}
