package incheon.product.geoview3d.theme.service.impl;

import incheon.com.cmm.exception.BusinessException;
import incheon.product.geoview3d.theme.mapper.ThemeMapper;
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.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
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;

/**
 * ThemeServiceImpl 단위 테스트.
 * SQL Injection 방어용 validateSqlIdentifier 검증에 집중한다. (P0)
 */
@ExtendWith(MockitoExtension.class)
class ThemeServiceImplTest {

    @InjectMocks
    private ThemeServiceImpl themeService;

    @Mock
    private ThemeMapper themeMapper;

    @Nested
    @DisplayName("validateSqlIdentifier — SQL Injection 방어")
    class ValidateSqlIdentifier {

        @ParameterizedTest
        @NullAndEmptySource
        @DisplayName("null 또는 빈 문자열이면 BusinessException 발생")
        void nullOrEmpty(String filterName) {
            assertThatThrownBy(() -> themeService.getFilterValues("theme1", filterName))
                    .isInstanceOf(BusinessException.class)
                    .hasMessageContaining("유효하지 않은 식별자");
        }

        @ParameterizedTest
        @ValueSource(strings = {
                "DROP TABLE users",
                "col; DELETE FROM t",
                "a' OR '1'='1",
                "col--comment",
                "1invalid",
                "col name",
                "col<script>"
        })
        @DisplayName("SQL Injection 패턴의 filterName이면 BusinessException 발생")
        void sqlInjectionPatterns(String filterName) {
            assertThatThrownBy(() -> themeService.getFilterValues("theme1", filterName))
                    .isInstanceOf(BusinessException.class)
                    .hasMessageContaining("유효하지 않은 식별자");
        }

        @Test
        @DisplayName("유효한 식별자(영문_숫자)는 통과")
        void validIdentifier() {
            when(themeMapper.selectThemeFilterSourceTable("theme1")).thenReturn("icsgp.valid_table");
            when(themeMapper.selectThemeFilterList("icsgp.valid_table", "valid_column"))
                    .thenReturn(List.of("val1", "val2"));

            List<String> result = themeService.getFilterValues("theme1", "valid_column");
            assertThat(result).containsExactly("val1", "val2");
        }

        @Test
        @DisplayName("스키마.테이블 형태 식별자도 통과")
        void validSchemaTableIdentifier() {
            when(themeMapper.selectThemeFilterSourceTable("t1")).thenReturn("my_schema.my_table");
            when(themeMapper.selectThemeFilterList("my_schema.my_table", "col_a"))
                    .thenReturn(List.of("x"));

            List<String> result = themeService.getFilterValues("t1", "col_a");
            assertThat(result).containsExactly("x");
        }
    }

    @Nested
    @DisplayName("getFilterValues — 분기 검증")
    class GetFilterValues {

        @Test
        @DisplayName("sourceTable이 null이면 빈 리스트 반환")
        void nullSourceTableReturnsEmpty() {
            when(themeMapper.selectThemeFilterSourceTable("unknown")).thenReturn(null);

            List<String> result = themeService.getFilterValues("unknown", "valid_col");
            assertThat(result).isEmpty();
            verify(themeMapper, never()).selectThemeFilterList(anyString(), anyString());
        }

        @Test
        @DisplayName("sourceTable이 SQL Injection 패턴이면 BusinessException")
        void maliciousSourceTable() {
            when(themeMapper.selectThemeFilterSourceTable("theme1")).thenReturn("DROP TABLE x;--");

            assertThatThrownBy(() -> themeService.getFilterValues("theme1", "valid_col"))
                    .isInstanceOf(BusinessException.class)
                    .hasMessageContaining("sourceTable");
        }
    }

    @Nested
    @DisplayName("getFilterChildValues — 분기 검증")
    class GetFilterChildValues {

        @Test
        @DisplayName("filterName1이 유효하지 않으면 BusinessException")
        void invalidFilterName1() {
            assertThatThrownBy(() ->
                    themeService.getFilterChildValues("t1", "DROP TABLE", "val", "valid_col"))
                    .isInstanceOf(BusinessException.class)
                    .hasMessageContaining("filterName1");
        }

        @Test
        @DisplayName("filterName2가 유효하지 않으면 BusinessException")
        void invalidFilterName2() {
            assertThatThrownBy(() ->
                    themeService.getFilterChildValues("t1", "valid_col", "val", "1_invalid"))
                    .isInstanceOf(BusinessException.class)
                    .hasMessageContaining("filterName2");
        }

        @Test
        @DisplayName("sourceTable이 null이면 빈 리스트 반환")
        void nullSourceTableReturnsEmpty() {
            when(themeMapper.selectThemeFilterSourceTable("unknown")).thenReturn(null);

            List<String> result = themeService.getFilterChildValues("unknown", "valid_col1", "val", "valid_col2");
            assertThat(result).isEmpty();
            verify(themeMapper, never()).selectThemeFilterListByParent(anyString(), anyString(), anyString(), anyString());
        }

        @Test
        @DisplayName("모든 검증 통과 시 정상 조회")
        void allValid() {
            when(themeMapper.selectThemeFilterSourceTable("t1")).thenReturn("icsgp.theme_data");
            when(themeMapper.selectThemeFilterListByParent("icsgp.theme_data", "col_a", "val1", "col_b"))
                    .thenReturn(List.of("child1", "child2"));

            List<String> result = themeService.getFilterChildValues("t1", "col_a", "val1", "col_b");
            assertThat(result).containsExactly("child1", "child2");
        }
    }

    @Nested
    @DisplayName("위임 메서드 — Mapper 호출 확인")
    class DelegationMethods {

        @Test
        @DisplayName("getThemeListJson은 mapper에 위임한다")
        void themeListJson() {
            when(themeMapper.selectThemeListJson()).thenReturn("[{\"id\":1}]");
            assertThat(themeService.getThemeListJson()).isEqualTo("[{\"id\":1}]");
        }

        @Test
        @DisplayName("getThemeJson은 mapper에 위임한다")
        void themeJson() {
            when(themeMapper.selectThemeJson("t1", "q", 10)).thenReturn("{\"id\":\"t1\"}");
            assertThat(themeService.getThemeJson("t1", "q", 10)).isEqualTo("{\"id\":\"t1\"}");
        }

        @Test
        @DisplayName("getElectionGeom은 mapper에 위임한다")
        void electionGeom() {
            when(themeMapper.selectElectionGeomByPoiId("poi1")).thenReturn("POLYGON(...)");
            assertThat(themeService.getElectionGeom("poi1")).isEqualTo("POLYGON(...)");
        }
    }
}
