package incheon.pack.map.web;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import incheon.pack.map.service.PackMapService;
import incheon.pack.map.vo.PackMapLayerVO;
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.ArgumentCaptor;
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 static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
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;

/**
 * PackMapApiController MockMvc 테스트.
 * 페이지네이션 클램핑, 응답 메타데이터, 서비스 위임을 검증한다.
 */
@ExtendWith(MockitoExtension.class)
class PackMapApiControllerTest {

    @InjectMocks
    private PackMapApiController controller;

    @Mock
    private PackMapService packMapService;

    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/pack/map/layers — 레이어 목록")
    class Layers {

        @Test
        @DisplayName("정상 페이지네이션 파라미터로 레이어 목록 반환")
        void normalPagination() throws Exception {
            PackMapLayerVO layer = new PackMapLayerVO();
            layer.setTaskLyrNm("테스트 레이어");
            when(packMapService.selectLayerList(any())).thenReturn(List.of(layer));
            when(packMapService.selectLayerListTotCnt(any())).thenReturn(1);

            mockMvc.perform(get("/api/v1/pack/map/layers")
                            .param("pageIndex", "2")
                            .param("pageUnit", "10"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.data").isArray())
                    .andExpect(jsonPath("$.data[0].taskLyrNm").value("테스트 레이어"))
                    .andExpect(jsonPath("$.meta.page").value(2))
                    .andExpect(jsonPath("$.meta.size").value(10))
                    .andExpect(jsonPath("$.meta.total").value(1));
        }

        @Test
        @DisplayName("pageIndex 0이면 1로 클램핑")
        void pageIndexZeroClamped() throws Exception {
            when(packMapService.selectLayerList(any())).thenReturn(List.of());
            when(packMapService.selectLayerListTotCnt(any())).thenReturn(0);

            mockMvc.perform(get("/api/v1/pack/map/layers")
                            .param("pageIndex", "0"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.meta.page").value(1));

            ArgumentCaptor<PackMapLayerVO> captor = ArgumentCaptor.forClass(PackMapLayerVO.class);
            verify(packMapService).selectLayerList(captor.capture());
            assertThat(captor.getValue().getPageIndex()).isEqualTo(1);
        }

        @Test
        @DisplayName("음수 pageIndex면 1로 클램핑")
        void negativePageIndexClamped() throws Exception {
            when(packMapService.selectLayerList(any())).thenReturn(List.of());
            when(packMapService.selectLayerListTotCnt(any())).thenReturn(0);

            mockMvc.perform(get("/api/v1/pack/map/layers")
                            .param("pageIndex", "-5"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.meta.page").value(1));
        }

        @Test
        @DisplayName("pageUnit 0이면 1로 클램핑")
        void pageSizeZeroClamped() throws Exception {
            when(packMapService.selectLayerList(any())).thenReturn(List.of());
            when(packMapService.selectLayerListTotCnt(any())).thenReturn(0);

            mockMvc.perform(get("/api/v1/pack/map/layers")
                            .param("pageUnit", "0"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.meta.size").value(1));
        }

        @Test
        @DisplayName("pageUnit 500 초과면 500으로 클램핑 (DoS 방어)")
        void pageSizeExceedsMaxClamped() throws Exception {
            when(packMapService.selectLayerList(any())).thenReturn(List.of());
            when(packMapService.selectLayerListTotCnt(any())).thenReturn(0);

            mockMvc.perform(get("/api/v1/pack/map/layers")
                            .param("pageUnit", "1000"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.meta.size").value(500));

            ArgumentCaptor<PackMapLayerVO> captor = ArgumentCaptor.forClass(PackMapLayerVO.class);
            verify(packMapService).selectLayerList(captor.capture());
            assertThat(captor.getValue().getPageUnit()).isEqualTo(500);
        }

        @Test
        @DisplayName("pageUnit 정확히 500이면 클램핑 없음")
        void pageSizeExactlyMax() throws Exception {
            when(packMapService.selectLayerList(any())).thenReturn(List.of());
            when(packMapService.selectLayerListTotCnt(any())).thenReturn(0);

            mockMvc.perform(get("/api/v1/pack/map/layers")
                            .param("pageUnit", "500"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.meta.size").value(500));
        }

        @Test
        @DisplayName("서비스 위임 시 클램핑된 값이 전달되는지 확인")
        void clampedValuesPassedToService() throws Exception {
            when(packMapService.selectLayerList(any())).thenReturn(List.of());
            when(packMapService.selectLayerListTotCnt(any())).thenReturn(5);

            mockMvc.perform(get("/api/v1/pack/map/layers")
                            .param("pageIndex", "-1")
                            .param("pageUnit", "9999"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.meta.page").value(1))
                    .andExpect(jsonPath("$.meta.size").value(500))
                    .andExpect(jsonPath("$.meta.total").value(5));

            ArgumentCaptor<PackMapLayerVO> captor = ArgumentCaptor.forClass(PackMapLayerVO.class);
            verify(packMapService).selectLayerList(captor.capture());
            PackMapLayerVO captured = captor.getValue();
            assertThat(captured.getPageIndex()).isEqualTo(1);
            assertThat(captured.getPageUnit()).isEqualTo(500);
        }
    }

    @Nested
    @DisplayName("GET /api/v1/pack/map/layers/groups — 레이어 그룹 목록")
    class LayerGroups {

        @Test
        @DisplayName("레이어 그룹 목록 반환")
        void getLayerGroups() throws Exception {
            PackMapLayerVO group = new PackMapLayerVO();
            group.setLyrGroupCd("G001");
            group.setLyrGroupNm("기본 그룹");
            when(packMapService.selectLayerGroupList()).thenReturn(List.of(group));

            mockMvc.perform(get("/api/v1/pack/map/layers/groups"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.data").isArray())
                    .andExpect(jsonPath("$.data[0].lyrGroupCd").value("G001"))
                    .andExpect(jsonPath("$.data[0].lyrGroupNm").value("기본 그룹"));
        }
    }
}
