package com.zioinfo.kintex.ai.nlquery;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.zioinfo.kintex.ai.nlquery.NlQuerySqlGuard.GuardedSql;
import com.zioinfo.kintex.common.error.ApiException;
import com.zioinfo.kintex.common.error.ErrorCode;
import org.junit.jupiter.api.Test;

/**
 * F092 Text-to-SQL 안전 가드 회귀 — 금지어·비SELECT·화이트리스트·PII·다중문·LIMIT 강제.
 * 이 가드가 LLM 산출 SQL 실행의 유일한 결정론 방어선이므로 위반 경로를 빠짐없이 검증한다.
 */
class NlQuerySqlGuardTest {

    private final NlQuerySqlGuard guard = new NlQuerySqlGuard();

    private ApiException reject(String sql) {
        return assertThrows(ApiException.class, () -> guard.validate(sql));
    }

    @Test
    void validSelect_passes_andLimitAppended() {
        GuardedSql g = guard.validate("SELECT id, name, status FROM event WHERE status = 'active'");
        assertTrue(g.sql().toLowerCase().contains("limit 200"), "LIMIT 200 강제 부여");
        assertEquals(200, g.limit());
        assertTrue(g.tables().contains("event"));
    }

    @Test
    void codeFence_stripped_andPasses() {
        GuardedSql g = guard.validate("```sql\nSELECT id FROM hall\n```");
        assertTrue(g.sql().toLowerCase().startsWith("select"));
        assertTrue(g.tables().contains("hall"));
    }

    @Test
    void existingLimitUnderCap_kept() {
        GuardedSql g = guard.validate("SELECT id FROM booth LIMIT 50");
        assertEquals(50, g.limit());
    }

    @Test
    void limitOverCap_rejected() {
        assertEquals(ErrorCode.VALIDATION, reject("SELECT id FROM booth LIMIT 5000").getCode());
    }

    // ── 금지어(쓰기/DDL/제어) ──
    @Test
    void insert_rejected() {
        assertEquals(ErrorCode.VALIDATION, reject("INSERT INTO event(id) VALUES ('x')").getCode());
    }

    @Test
    void update_rejected() {
        reject("UPDATE event SET status = 'x'");
    }

    @Test
    void delete_rejected() {
        reject("DELETE FROM event");
    }

    @Test
    void dropAndAlter_rejected() {
        reject("DROP TABLE event");
        reject("ALTER TABLE event ADD COLUMN x int");
    }

    @Test
    void selectWithForbiddenKeyword_rejected() {
        // SELECT 로 시작해도 본문에 쓰기/우회 키워드가 있으면 차단.
        reject("SELECT id FROM event WHERE id IN (SELECT id FROM event) ; DROP TABLE hall");
        reject("SELECT id INTO temp FROM event");
        reject("SELECT pg_sleep(10)");
    }

    // ── 비 SELECT / 다중문 / 주석 ──
    @Test
    void nonSelect_rejected() {
        reject("WITH x AS (SELECT 1) SELECT * FROM x");   // WITH 시작 금지(단일 SELECT 시작만)
        reject("TRUNCATE event");
    }

    @Test
    void multiStatement_rejected() {
        assertEquals(ErrorCode.VALIDATION,
                reject("SELECT id FROM event; SELECT id FROM hall").getCode());
    }

    @Test
    void comment_rejected() {
        reject("SELECT id FROM event -- comment");
        reject("SELECT id FROM event /* block */");
    }

    @Test
    void trailingSemicolon_allowedAndStripped() {
        GuardedSql g = guard.validate("SELECT id FROM event;");
        assertTrue(!g.sql().contains(";"));
    }

    // ── 화이트리스트 테이블 ──
    @Test
    void nonWhitelistTable_rejected() {
        assertEquals(ErrorCode.VALIDATION, reject("SELECT * FROM app_user").getCode());
        reject("SELECT * FROM company");
        reject("SELECT u.id FROM event e JOIN app_user u ON u.id = e.id");
    }

    @Test
    void systemCatalog_rejected() {
        reject("SELECT * FROM information_schema.tables");
        reject("SELECT * FROM pg_catalog.pg_tables");
    }

    // ── PII 컬럼 ──
    @Test
    void piiPhoneEmail_alwaysRejected() {
        assertEquals(ErrorCode.VALIDATION, reject("SELECT phone FROM visitor_registration").getCode());
        reject("SELECT email FROM lead");
    }

    @Test
    void nameOnPiiTable_rejected_butEventNameAllowed() {
        // visitor_registration/lead 의 name 은 PII → 차단.
        reject("SELECT name FROM visitor_registration");
        reject("SELECT l.name FROM lead l");
        // event.name 은 비-PII → 허용.
        GuardedSql g = guard.validate("SELECT name FROM event");
        assertTrue(g.tables().contains("event"));
    }

    @Test
    void emptyOrBlank_rejected() {
        reject("");
        reject("   ");
    }
}
