package incheon.product.geoview2d.layer.service.impl;

import com.fasterxml.jackson.core.JsonProcessingException;
import incheon.com.cmm.exception.BusinessException;
import incheon.com.cmm.exception.EntityNotFoundException;
import incheon.product.common.config.GeoViewProperties;
import incheon.product.common.geo.GisServerClient;
import incheon.product.geoview2d.layer.mapper.LayerMapper;
import incheon.product.geoview2d.layer.vo.TaskLayerSearchVO;
import incheon.product.geoview2d.layer.vo.TaskLayerVO;
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.web.client.RestClientException;

import java.util.*;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

/**
 * TaskLayerServiceImpl 단위 테스트.
 * 업무 레이어 CRUD 및 GIS 서버 연동 로직을 검증한다.
 */
@ExtendWith(MockitoExtension.class)
class TaskLayerServiceImplTest {

    @InjectMocks
    private TaskLayerServiceImpl taskLayerService;

    @Mock
    private LayerMapper layerMapper;

    @Mock
    private GisServerClient gisServerClient;

    @Mock
    private GeoViewProperties geoViewProperties;

    private GeoViewProperties.Layer layerConfig;
    private GeoViewProperties.GisServer gisServerConfig;
    private GeoViewProperties.Coordinate coordinateConfig;

    @BeforeEach
    void setUp() {
        layerConfig = new GeoViewProperties.Layer();
        layerConfig.setServicePrefix("ws");

        gisServerConfig = new GeoViewProperties.GisServer();
        gisServerConfig.setWorkspace("incheon");

        coordinateConfig = new GeoViewProperties.Coordinate();
        coordinateConfig.setServiceSrid(5186);

        lenient().when(geoViewProperties.getLayer()).thenReturn(layerConfig);
        lenient().when(geoViewProperties.getGisServer()).thenReturn(gisServerConfig);
        lenient().when(geoViewProperties.getCoordinate()).thenReturn(coordinateConfig);
    }

    @Nested
    @DisplayName("조회 메서드")
    class ReadMethods {

        @Test
        @DisplayName("getById는 mapper에 위임한다")
        void getById() {
            TaskLayerVO expected = new TaskLayerVO();
            expected.setTaskLyrId(1);
            when(layerMapper.selectTaskLayerById(1)).thenReturn(expected);

            assertThat(taskLayerService.getById(1)).isEqualTo(expected);
        }

        @Test
        @DisplayName("getAll은 mapper.findAll에 위임한다")
        void getAll() {
            List<TaskLayerVO> expected = List.of(new TaskLayerVO());
            when(layerMapper.findAll()).thenReturn(expected);

            assertThat(taskLayerService.getAll()).isEqualTo(expected);
        }

        @Test
        @DisplayName("getList는 페이징 offset을 계산한다")
        void getListPaging() {
            when(layerMapper.findAllWithPaging("keyword", "type", 10, 10)).thenReturn(List.of());

            taskLayerService.getList("keyword", "type", 2, 10);

            verify(layerMapper).findAllWithPaging("keyword", "type", 10, 10);
        }

        @Test
        @DisplayName("getTotalCount는 mapper.count에 위임한다")
        void getTotalCount() {
            when(layerMapper.count("kw", "tp")).thenReturn(5);
            assertThat(taskLayerService.getTotalCount("kw", "tp")).isEqualTo(5);
        }

        @Test
        @DisplayName("getTaskLayerList - removedGroupCd가 null이면 전체 목록 조회")
        void taskLayerListWithoutRemoved() {
            TaskLayerSearchVO searchVO = new TaskLayerSearchVO();
            when(layerMapper.selectTaskLayerList(searchVO)).thenReturn(List.of());

            taskLayerService.getTaskLayerList(searchVO, null);

            verify(layerMapper).selectTaskLayerList(searchVO);
            verify(layerMapper, never()).selectTaskLayerListNotInGroupCd(any(), any());
        }

        @Test
        @DisplayName("getTaskLayerList - removedGroupCd가 있으면 제외 목록 조회")
        void taskLayerListWithRemoved() {
            TaskLayerSearchVO searchVO = new TaskLayerSearchVO();
            Set<String> removed = Set.of("GRP01");
            when(layerMapper.selectTaskLayerListNotInGroupCd(searchVO, removed)).thenReturn(List.of());

            taskLayerService.getTaskLayerList(searchVO, removed);

            verify(layerMapper).selectTaskLayerListNotInGroupCd(searchVO, removed);
        }
    }

    @Nested
    @DisplayName("create — 생성 + GIS 서버 발행")
    class Create {

        @Test
        @DisplayName("servicePrefix를 설정하고 GIS 서버에 발행 후 insert한다")
        void publishAndInsert() throws JsonProcessingException {
            TaskLayerVO layer = new TaskLayerVO();
            layer.setLyrPhysNm("icsgp.test_table");
            layer.setLyrSrvcNm("test_layer");
            layer.setCntm((short) 5186);

            taskLayerService.create(layer);

            assertThat(layer.getLyrSrvcPrefix()).isEqualTo("ws");
            verify(gisServerClient).publishLayer("incheon", "test_layer", "icsgp.test_table", 5186);
            verify(layerMapper).insert(layer);
        }

        @Test
        @DisplayName("물리명이 schema.table 형식이 아니면 BusinessException 발생")
        void invalidPhysNm() {
            TaskLayerVO layer = new TaskLayerVO();
            layer.setLyrPhysNm("no_dot_table");
            layer.setLyrSrvcNm("test_layer");

            assertThatThrownBy(() -> taskLayerService.create(layer))
                    .isInstanceOf(BusinessException.class)
                    .hasMessageContaining("스키마.테이블");
        }
    }

    @Nested
    @DisplayName("update — 수정 + GIS 서버 연동")
    class Update {

        @Test
        @DisplayName("기존 레이어가 없으면 EntityNotFoundException 발생")
        void notFoundThrows() {
            TaskLayerVO layer = new TaskLayerVO();
            layer.setTaskLyrId(999);
            when(layerMapper.selectTaskLayerById(999)).thenReturn(null);

            assertThatThrownBy(() -> taskLayerService.update(layer))
                    .isInstanceOf(EntityNotFoundException.class);
        }

        @Test
        @DisplayName("서비스명이 변경되면 기존 삭제 후 재발행한다")
        void serviceNameChangedRePublish() throws JsonProcessingException {
            TaskLayerVO existing = new TaskLayerVO();
            existing.setTaskLyrId(1);
            existing.setLyrSrvcNm("old_layer");

            TaskLayerVO updated = new TaskLayerVO();
            updated.setTaskLyrId(1);
            updated.setLyrSrvcNm("new_layer");
            updated.setLyrPhysNm("icsgp.table1");
            updated.setCntm((short) 5186);

            when(layerMapper.selectTaskLayerById(1)).thenReturn(existing);

            taskLayerService.update(updated);

            verify(gisServerClient).deleteLayer("incheon", "old_layer");
            verify(gisServerClient).publishLayer("incheon", "new_layer", "icsgp.table1", 5186);
            verify(layerMapper).update(updated);
        }

        @Test
        @DisplayName("서비스명이 동일하면 updateLayer를 호출한다")
        void sameServiceNameCallsUpdate() {
            TaskLayerVO existing = new TaskLayerVO();
            existing.setTaskLyrId(1);
            existing.setLyrSrvcNm("same_layer");

            TaskLayerVO updated = new TaskLayerVO();
            updated.setTaskLyrId(1);
            updated.setLyrSrvcNm("same_layer");

            when(layerMapper.selectTaskLayerById(1)).thenReturn(existing);
            when(gisServerClient.layerExists("incheon", "same_layer")).thenReturn(true);

            taskLayerService.update(updated);

            verify(gisServerClient).updateLayer("incheon", "same_layer");
            verify(layerMapper).update(updated);
        }
    }

    @Nested
    @DisplayName("delete — 삭제 + GIS 서버 삭제")
    class Delete {

        @Test
        @DisplayName("기존 레이어가 있으면 GIS 서버에서도 삭제한다")
        void deleteExisting() {
            TaskLayerVO existing = new TaskLayerVO();
            existing.setLyrSrvcNm("del_layer");
            when(layerMapper.selectTaskLayerById(1)).thenReturn(existing);

            taskLayerService.delete(1);

            verify(gisServerClient).deleteLayer("incheon", "del_layer");
            verify(layerMapper).delete(1);
        }

        @Test
        @DisplayName("기존 레이어가 없어도 DB 삭제는 수행한다")
        void deleteNonExisting() {
            when(layerMapper.selectTaskLayerById(1)).thenReturn(null);

            taskLayerService.delete(1);

            verify(gisServerClient, never()).deleteLayer(anyString(), anyString());
            verify(layerMapper).delete(1);
        }

        @Test
        @DisplayName("GIS 서버 삭제 실패 시 예외를 삼키고 DB 삭제는 수행한다")
        void gisServerFailure() {
            TaskLayerVO existing = new TaskLayerVO();
            existing.setLyrSrvcNm("fail_layer");
            when(layerMapper.selectTaskLayerById(1)).thenReturn(existing);
            doThrow(new RestClientException("연결 실패")).when(gisServerClient).deleteLayer("incheon", "fail_layer");

            taskLayerService.delete(1);

            verify(layerMapper).delete(1);
        }
    }

    @Nested
    @DisplayName("verifyGeometryTable — 공간 테이블 검증")
    class VerifyGeometryTable {

        @Test
        @DisplayName("geometry_columns에 없으면 오류 반환")
        void noGeomColumn() {
            when(layerMapper.verifyGeometryTable("icsgp", "test_tbl")).thenReturn(List.of());

            Map<String, Object> result = taskLayerService.verifyGeometryTable("icsgp", "test_tbl", "svc", "phy");

            assertThat(result.get("success")).isEqualTo(false);
            assertThat((List<?>) result.get("errors")).isNotEmpty();
        }

        @Test
        @DisplayName("물리명/서비스명 중복 및 GIS 서버 존재 시 오류 목록에 추가")
        void duplicateErrors() {
            Map<String, Object> geomInfo = new HashMap<>();
            geomInfo.put("type", "POLYGON");
            geomInfo.put("srid", 5186);
            when(layerMapper.verifyGeometryTable("icsgp", "test_tbl")).thenReturn(List.of(geomInfo));
            when(layerMapper.verifyTaskPhyTable(anyMap())).thenReturn(1);
            when(layerMapper.verifyTaskSrvTable(anyMap())).thenReturn(1);
            when(gisServerClient.layerExists("incheon", "svc")).thenReturn(true);

            Map<String, Object> result = taskLayerService.verifyGeometryTable("icsgp", "test_tbl", "svc", "phy");

            assertThat(result.get("success")).isEqualTo(false);
            assertThat((List<?>) result.get("errors")).hasSize(3);
        }

        @Test
        @DisplayName("모든 검증 통과 시 success true")
        void allPass() {
            Map<String, Object> geomInfo = new HashMap<>();
            geomInfo.put("type", "POINT");
            geomInfo.put("srid", 5186);
            when(layerMapper.verifyGeometryTable("icsgp", "tbl")).thenReturn(List.of(geomInfo));
            when(layerMapper.verifyTaskPhyTable(anyMap())).thenReturn(0);
            when(layerMapper.verifyTaskSrvTable(anyMap())).thenReturn(0);
            when(gisServerClient.layerExists("incheon", "svc")).thenReturn(false);

            Map<String, Object> result = taskLayerService.verifyGeometryTable("icsgp", "tbl", "svc", "phy");

            assertThat(result.get("success")).isEqualTo(true);
            assertThat((List<?>) result.get("errors")).isEmpty();
        }
    }
}
