package incheon.product.geoview3d.data3d.web;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import incheon.com.cmm.exception.RestApiExceptionHandler;
import incheon.product.common.geo3d.ExternalApiClient;
import incheon.product.common.geo3d.GeoView3DProperties;
import incheon.product.geoview3d.data3d.service.Data3DService;
import incheon.product.geoview3d.data3d.vo.Data3DModelVO;
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.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
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.ArgumentMatchers.eq;
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.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

/**
 * Data3DApiController MockMvc 테스트.
 * SSRF 방어(check-path), API Key 검증(updateStatus)을 중점 검증한다.
 */
@ExtendWith(MockitoExtension.class)
class Data3DApiControllerTest {

    @InjectMocks
    private Data3DApiController controller;

    @Mock
    private Data3DService data3DService;

    @Mock
    private GeoView3DProperties properties;

    @Mock
    private ExternalApiClient externalApiClient;

    private MockMvc mockMvc;
    private ObjectMapper objectMapper;

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

    @Nested
    @DisplayName("GET /api/v1/product/g3d/data3d/check-path — 경로 확인 (SSRF 방어)")
    class CheckPath {

        @Test
        @DisplayName("정상 경로로 확인")
        void validPath() throws Exception {
            GeoView3DProperties.GisManager gisManager = new GeoView3DProperties.GisManager();
            gisManager.setManagerUrl("http://gis-manager.local");
            when(properties.getGisManager()).thenReturn(gisManager);
            when(externalApiClient.exchangeForGis(anyString(), eq(HttpMethod.GET), any(), eq(String.class)))
                    .thenReturn(ResponseEntity.ok("exists"));

            mockMvc.perform(get("/api/v1/product/g3d/data3d/check-path")
                            .param("path", "models/building/test.3ds"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.data.valid").value(true));
        }

        @Test
        @DisplayName("경로 트래버설(..) 차단")
        void pathTraversalBlocked() throws Exception {
            mockMvc.perform(get("/api/v1/product/g3d/data3d/check-path")
                            .param("path", "../etc/passwd"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.data.valid").value(false))
                    .andExpect(jsonPath("$.data.message").value("유효하지 않은 경로입니다."));

            verify(externalApiClient, never()).exchangeForGis(anyString(), any(), any(), any());
        }

        @Test
        @DisplayName("특수 문자 차단 (세미콜론, 공백)")
        void specialCharsBlocked() throws Exception {
            mockMvc.perform(get("/api/v1/product/g3d/data3d/check-path")
                            .param("path", "models;rm -rf /"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.data.valid").value(false));

            verify(externalApiClient, never()).exchangeForGis(anyString(), any(), any(), any());
        }

        @Test
        @DisplayName("빈 경로 차단")
        void emptyPathBlocked() throws Exception {
            mockMvc.perform(get("/api/v1/product/g3d/data3d/check-path")
                            .param("path", ""))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.data.valid").value(false));

            verify(externalApiClient, never()).exchangeForGis(anyString(), any(), any(), any());
        }
    }

    @Nested
    @DisplayName("PUT /api/v1/product/g3d/data3d/status — API Key 검증")
    class UpdateStatus {

        @Test
        @DisplayName("유효한 API Key로 상태 변경 성공")
        void validApiKey() throws Exception {
            GeoView3DProperties.ExternalApi externalApi = new GeoView3DProperties.ExternalApi();
            externalApi.setApiKey("valid-key-123");
            when(properties.getExternalApi()).thenReturn(externalApi);

            mockMvc.perform(put("/api/v1/product/g3d/data3d/status")
                            .param("dgtlPairMdlId", "1")
                            .param("status", "COMPLETED")
                            .param("serviceName", "building")
                            .header("X-API-Key", "valid-key-123"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.message").value("상태가 변경되었습니다."));

            verify(data3DService).updateStatus(1, "COMPLETED", "building");
        }

        @Test
        @DisplayName("잘못된 API Key이면 응답 코드 403")
        void invalidApiKey() throws Exception {
            GeoView3DProperties.ExternalApi externalApi = new GeoView3DProperties.ExternalApi();
            externalApi.setApiKey("valid-key-123");
            when(properties.getExternalApi()).thenReturn(externalApi);

            mockMvc.perform(put("/api/v1/product/g3d/data3d/status")
                            .param("dgtlPairMdlId", "1")
                            .param("status", "COMPLETED")
                            .param("serviceName", "building")
                            .header("X-API-Key", "wrong-key"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.code").value(403));

            verify(data3DService, never()).updateStatus(anyInt(), anyString(), anyString());
        }

        @Test
        @DisplayName("빈 API Key이면 응답 코드 403")
        void emptyApiKey() throws Exception {
            GeoView3DProperties.ExternalApi externalApi = new GeoView3DProperties.ExternalApi();
            externalApi.setApiKey("valid-key-123");
            when(properties.getExternalApi()).thenReturn(externalApi);

            mockMvc.perform(put("/api/v1/product/g3d/data3d/status")
                            .param("dgtlPairMdlId", "1")
                            .param("status", "COMPLETED")
                            .param("serviceName", "building")
                            .header("X-API-Key", ""))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.code").value(403));

            verify(data3DService, never()).updateStatus(anyInt(), anyString(), anyString());
        }
    }

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

        @Test
        @DisplayName("목록 조회 시 meta 포함")
        void getListWithMeta() throws Exception {
            when(data3DService.getList(any())).thenReturn(List.of());

            mockMvc.perform(get("/api/v1/product/g3d/data3d"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.data").isArray())
                    .andExpect(jsonPath("$.meta.totalCount").value(0));
        }

        @Test
        @DisplayName("상세 조회")
        void getById() throws Exception {
            Data3DModelVO model = new Data3DModelVO();
            model.setDgtlPairMdlId(42);
            model.setDgtlPairMdlNm("테스트 모델");
            when(data3DService.getById(42, "2")).thenReturn(model);

            mockMvc.perform(get("/api/v1/product/g3d/data3d/42"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.data.dgtlPairMdlId").value(42));
        }

        @Test
        @DisplayName("생성 시 @Valid 검증 — 필수 필드 누락이면 400")
        void createWithoutRequiredField() throws Exception {
            mockMvc.perform(post("/api/v1/product/g3d/data3d")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content("{}"))
                    .andExpect(status().isBadRequest());

            verify(data3DService, never()).create(any());
        }
    }
}
