package incheon.product.geoview2d.download.repository;

import incheon.com.cmm.exception.BusinessException;
import incheon.product.common.config.GeoViewProperties;
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.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 org.springframework.jdbc.core.JdbcTemplate;

import java.util.Collections;
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.any;
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;

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

    @InjectMocks
    private SpatialDataRepository repository;

    @Mock
    private JdbcTemplate jdbcTemplate;

    @Mock
    private GeoViewProperties geoViewProperties;

    @Nested
    @DisplayName("validateTableName — SQL Injection 방어")
    class ValidateTableName {

        @ParameterizedTest
        @NullAndEmptySource
        @DisplayName("null 또는 빈 문자열이면 BusinessException 발생")
        void nullOrEmpty(String tableName) {
            assertThatThrownBy(() -> repository.selectGeometryColumnName(tableName))
                    .isInstanceOf(BusinessException.class)
                    .hasMessageContaining("유효하지 않은 테이블명");
        }

        @ParameterizedTest
        @ValueSource(strings = {
                "DROP TABLE users;--",
                "table; DROP TABLE x",
                "test' OR '1'='1",
                "schema.table; DELETE FROM t",
                "a b c",
                "../../../etc/passwd",
                "table<script>",
                "table${cmd}"
        })
        @DisplayName("SQL Injection 패턴이면 BusinessException 발생")
        void sqlInjectionPatterns(String tableName) {
            assertThatThrownBy(() -> repository.selectGeometryColumnName(tableName))
                    .isInstanceOf(BusinessException.class)
                    .hasMessageContaining("유효하지 않은 테이블명");
        }

        @Test
        @DisplayName("유효한 단일 테이블명은 통과")
        void validSimpleName() {
            when(jdbcTemplate.queryForList(anyString(), eq(String.class), eq("public"), eq("test_table")))
                    .thenReturn(List.of("geom"));

            String result = repository.selectGeometryColumnName("test_table");
            assertThat(result).isEqualTo("geom");
        }

        @Test
        @DisplayName("유효한 스키마.테이블명은 통과")
        void validSchemaTableName() {
            when(jdbcTemplate.queryForList(anyString(), eq(String.class), eq("iccom"), eq("my_table")))
                    .thenReturn(List.of("the_geom"));

            String result = repository.selectGeometryColumnName("iccom.my_table");
            assertThat(result).isEqualTo("the_geom");
        }

        @Test
        @DisplayName("한글 테이블명도 허용된다")
        void koreanTableName() {
            when(jdbcTemplate.queryForList(anyString(), eq(String.class), eq("public"), eq("건물정보")))
                    .thenReturn(Collections.emptyList());

            String result = repository.selectGeometryColumnName("건물정보");
            assertThat(result).isEqualTo("geom");
        }
    }

    @Nested
    @DisplayName("selectGeometryColumnName — 스키마 분리")
    class SchemaSplit {

        @Test
        @DisplayName("점(.) 없으면 schema=public, table=입력값")
        void noSchema() {
            when(jdbcTemplate.queryForList(anyString(), eq(String.class), eq("public"), eq("mytable")))
                    .thenReturn(Collections.emptyList());

            repository.selectGeometryColumnName("mytable");
            verify(jdbcTemplate).queryForList(anyString(), eq(String.class), eq("public"), eq("mytable"));
        }

        @Test
        @DisplayName("점(.) 있으면 schema=앞부분, table=뒷부분")
        void withSchema() {
            when(jdbcTemplate.queryForList(anyString(), eq(String.class), eq("myschema"), eq("mytable")))
                    .thenReturn(Collections.emptyList());

            repository.selectGeometryColumnName("myschema.mytable");
            verify(jdbcTemplate).queryForList(anyString(), eq(String.class), eq("myschema"), eq("mytable"));
        }

        @Test
        @DisplayName("geometry_columns에 결과가 없으면 'geom'을 기본 반환")
        void defaultGeom() {
            when(jdbcTemplate.queryForList(anyString(), eq(String.class), any(), any()))
                    .thenReturn(Collections.emptyList());

            String result = repository.selectGeometryColumnName("some_table");
            assertThat(result).isEqualTo("geom");
        }
    }

    @Nested
    @DisplayName("validateTableName — 모든 public 메서드에서 호출 확인")
    class AllPublicMethodsValidate {

        @Test
        @DisplayName("selectShapefileData도 유효성 검사 수행")
        void selectShapefileData() {
            assertThatThrownBy(() -> repository.selectShapefileData("DROP TABLE x;--", "geom", List.of("col1")))
                    .isInstanceOf(BusinessException.class);
        }

        @Test
        @DisplayName("streamData도 유효성 검사 수행")
        void streamData() {
            assertThatThrownBy(() -> repository.streamData("'; DROP TABLE--", "geom", List.of(), row -> {}))
                    .isInstanceOf(BusinessException.class);
        }

        @Test
        @DisplayName("streamDxfData도 유효성 검사 수행")
        void streamDxfData() {
            assertThatThrownBy(() -> repository.streamDxfData("1=1; --", "geom", List.of(), row -> {}))
                    .isInstanceOf(BusinessException.class);
        }

        @Test
        @DisplayName("streamGeoJsonFeatures도 유효성 검사 수행")
        void streamGeoJsonFeatures() {
            assertThatThrownBy(() -> repository.streamGeoJsonFeatures("malicious;input", "geom", List.of("col"), f -> {}))
                    .isInstanceOf(BusinessException.class);
        }

        @Test
        @DisplayName("selectData도 유효성 검사 수행")
        void selectData() {
            assertThatThrownBy(() -> repository.selectData("a; DELETE FROM b", List.of(), 0, 10))
                    .isInstanceOf(BusinessException.class);
        }
    }
}
