"""QuestionGenStore 单元测试 — SQLite 出题管线日志记录器。

覆盖 schema 幂等性、run 生命周期、item 写入与门判定更新、难度更新。
"""

from __future__ import annotations

from dataclasses import dataclass
from enum import Enum
from typing import TYPE_CHECKING

import pytest

if TYPE_CHECKING:
    from pathlib import Path

from app.question_gen.run_store import QuestionGenStore, RunStats

# ---------------------------------------------------------------------------
# GateReport 测试替身（Task 6 尚未实现，此处构造兼容 duck-type）
# ---------------------------------------------------------------------------


class _Verdict(Enum):
    """门判定枚举替身。"""

    PASS = "pass"
    FAIL = "fail"
    SKIP = "skip"


@dataclass(frozen=True)
class _GateResult:
    """单门判定结果替身。"""

    verdict: _Verdict
    reason: str


@dataclass
class _MockGateReport:
    """GateReport 测试替身，duck-type 兼容。"""

    key_verify: _GateResult
    blind_answer: _GateResult
    multi_true: _GateResult
    leak_test: _GateResult

    @property
    def passed(self) -> bool:
        """所有门均 PASS 或 SKIP 时视为通过。"""
        results = [self.key_verify, self.blind_answer, self.multi_true, self.leak_test]
        return all(r.verdict != _Verdict.FAIL for r in results)

    @property
    def reject_reason(self) -> str | None:
        """返回首个 FAIL 门的 reason，若全部通过则 None。"""
        for r in [self.key_verify, self.blind_answer, self.multi_true, self.leak_test]:
            if r.verdict == _Verdict.FAIL:
                return r.reason
        return None


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture()
def store(tmp_path: Path) -> QuestionGenStore:
    """创建临时 SQLite 上的 QuestionGenStore 实例。"""
    db_path = tmp_path / "question_gen.db"
    s = QuestionGenStore(db_path=db_path)
    yield s
    s.close()


# ---------------------------------------------------------------------------
# TestQuestionGenStore
# ---------------------------------------------------------------------------


class TestQuestionGenStore:
    """QuestionGenStore 全功能验证。"""

    def test_schema_idempotent(self, tmp_path: Path) -> None:
        """连续两次初始化同一 db 不报错，表结构不变。"""
        db_path = tmp_path / "idem.db"
        s1 = QuestionGenStore(db_path=db_path)
        s2 = QuestionGenStore(db_path=db_path)
        # 第二次初始化不应抛异常
        s2.record_run_start(run_id="r-idem", git_sha="abc123", config_snapshot="{}")
        s1.close()
        s2.close()

    def test_record_run_lifecycle(self, store: QuestionGenStore) -> None:
        """start -> record items -> end -> get_stats 完整生命周期。"""
        run_id = "run-001"
        store.record_run_start(
            run_id=run_id,
            git_sha="deadbeef",
            config_snapshot='{"n_slots": 240}',
        )

        # 写入几个 item 来模拟统计
        for i in range(5):
            store.record_item(
                item_id=f"item-{i}",
                run_id=run_id,
                slot_id=f"slot-{i}",
                video_id="v001",
                family="retrieval",
                task_type="Object Recognition",
                skill_target="M1",
                attempt=1,
                question_text=f"问题{i}",
            )

        # 结束 run
        stats = RunStats(total_slots=10, accepted=5, rejected=3, heavy_sampled=2)
        store.record_run_end(run_id=run_id, status="completed", stats=stats)

        # 查询 stats
        retrieved = store.get_run_stats(run_id)
        assert retrieved.total_slots == 10
        assert retrieved.accepted == 5
        assert retrieved.rejected == 3
        assert retrieved.heavy_sampled == 2

    def test_record_item_and_gates(self, store: QuestionGenStore) -> None:
        """record_item 后 update_gates 正确更新门判定列与 final_status。"""
        run_id = "run-gates"
        store.record_run_start(run_id=run_id, git_sha="aaa111", config_snapshot="{}")
        item_id = "item-gate-1"
        store.record_item(
            item_id=item_id,
            run_id=run_id,
            slot_id="slot-0",
            video_id="v002",
            family="reasoning",
            task_type="Action Sequence",
            skill_target="M2",
            attempt=1,
            question_text="为什么这样做？",
        )

        # 所有门通过
        report_pass = _MockGateReport(
            key_verify=_GateResult(_Verdict.PASS, "ok"),
            blind_answer=_GateResult(_Verdict.PASS, "ok"),
            multi_true=_GateResult(_Verdict.SKIP, "n/a"),
            leak_test=_GateResult(_Verdict.PASS, "ok"),
        )
        store.update_gates(item_id=item_id, report=report_pass)

        # 验证数据库中的值
        import sqlite3

        conn = sqlite3.connect(str(store._db_path))
        conn.row_factory = sqlite3.Row
        row = conn.execute(
            "SELECT * FROM question_gen_items WHERE item_id=?", (item_id,)
        ).fetchone()
        conn.close()

        assert row["gate_key_verify"] == "pass"
        assert row["gate_blind_answer"] == "pass"
        assert row["gate_multi_true"] == "skip"
        assert row["gate_leak_test"] == "pass"
        assert row["gate_reject_reason"] is None
        assert row["final_status"] == "accepted"

    def test_update_gates_rejected(self, store: QuestionGenStore) -> None:
        """门判定失败时 final_status 为 rejected 且记录 reject_reason。"""
        run_id = "run-rej"
        store.record_run_start(run_id=run_id, git_sha="bbb222", config_snapshot="{}")
        item_id = "item-rej-1"
        store.record_item(
            item_id=item_id,
            run_id=run_id,
            slot_id="slot-1",
            video_id="v003",
            family="visual",
            task_type="Scene Transition",
            skill_target="M3",
            attempt=2,
            question_text="画面中有什么？",
        )

        report_fail = _MockGateReport(
            key_verify=_GateResult(_Verdict.PASS, "ok"),
            blind_answer=_GateResult(_Verdict.FAIL, "答案可由常识推断"),
            multi_true=_GateResult(_Verdict.SKIP, "n/a"),
            leak_test=_GateResult(_Verdict.SKIP, "n/a"),
        )
        store.update_gates(item_id=item_id, report=report_fail)

        import sqlite3

        conn = sqlite3.connect(str(store._db_path))
        conn.row_factory = sqlite3.Row
        row = conn.execute(
            "SELECT * FROM question_gen_items WHERE item_id=?", (item_id,)
        ).fetchone()
        conn.close()

        assert row["final_status"] == "rejected"
        assert row["gate_reject_reason"] == "答案可由常识推断"

    def test_update_difficulty(self, store: QuestionGenStore) -> None:
        """update_difficulty 正确写入 difficulty_steps。"""
        run_id = "run-diff"
        store.record_run_start(run_id=run_id, git_sha="ccc333", config_snapshot="{}")
        item_id = "item-diff-1"
        store.record_item(
            item_id=item_id,
            run_id=run_id,
            slot_id="slot-2",
            video_id="v004",
            family="enumeration",
            task_type="Counting",
            skill_target="M4",
            attempt=1,
            question_text="有多少个？",
        )

        store.update_difficulty(item_id=item_id, difficulty_steps=7)

        import sqlite3

        conn = sqlite3.connect(str(store._db_path))
        conn.row_factory = sqlite3.Row
        row = conn.execute(
            "SELECT difficulty_steps FROM question_gen_items WHERE item_id=?",
            (item_id,),
        ).fetchone()
        conn.close()

        assert row["difficulty_steps"] == 7

    def test_get_run_stats_not_found(self, store: QuestionGenStore) -> None:
        """查询不存在的 run_id 应报错。"""
        with pytest.raises(ValueError, match="run_id"):
            store.get_run_stats("nonexistent-run")

    def test_record_run_end_missing_run_raises(self, store: QuestionGenStore) -> None:
        """对不存在的 run_id 调用 record_run_end 应报错。"""
        stats = RunStats(total_slots=10, accepted=5, rejected=3, heavy_sampled=2)
        with pytest.raises(ValueError, match="run_id"):
            store.record_run_end(run_id="ghost-run", status="completed", stats=stats)

    def test_update_gates_missing_item_raises(self, store: QuestionGenStore) -> None:
        """对不存在的 item_id 调用 update_gates 应报错。"""
        report = _MockGateReport(
            key_verify=_GateResult(_Verdict.PASS, "ok"),
            blind_answer=_GateResult(_Verdict.PASS, "ok"),
            multi_true=_GateResult(_Verdict.PASS, "ok"),
            leak_test=_GateResult(_Verdict.PASS, "ok"),
        )
        with pytest.raises(ValueError, match="item_id"):
            store.update_gates(item_id="ghost-item", report=report)

    def test_mark_item_rejected(self, store: QuestionGenStore) -> None:
        """mark_item_rejected 将 final_status 设为 rejected 并记录原因。"""
        run_id = "run-mark-rej"
        store.record_run_start(run_id=run_id, git_sha="ddd444", config_snapshot="{}")
        item_id = "item-mark-rej-1"
        store.record_item(
            item_id=item_id,
            run_id=run_id,
            slot_id="slot-mark",
            video_id="v005",
            family="retrieval",
            task_type="Action Recognition",
            skill_target="M1",
            attempt=1,
            question_text="这是什么？",
        )

        # 先模拟门控通过（将 final_status 设为 accepted）
        report_pass = _MockGateReport(
            key_verify=_GateResult(_Verdict.PASS, "ok"),
            blind_answer=_GateResult(_Verdict.PASS, "ok"),
            multi_true=_GateResult(_Verdict.PASS, "ok"),
            leak_test=_GateResult(_Verdict.PASS, "ok"),
        )
        store.update_gates(item_id=item_id, report=report_pass)

        # 然后因去重被拒绝
        store.mark_item_rejected(item_id, "duplicate detected by embedding similarity")

        import sqlite3

        conn = sqlite3.connect(str(store._db_path))
        conn.row_factory = sqlite3.Row
        row = conn.execute(
            "SELECT final_status, gate_reject_reason FROM question_gen_items WHERE item_id=?",
            (item_id,),
        ).fetchone()
        conn.close()

        assert row["final_status"] == "rejected"
        assert row["gate_reject_reason"] == "duplicate detected by embedding similarity"

    def test_mark_item_rejected_missing_item_raises(self, store: QuestionGenStore) -> None:
        """对不存在的 item_id 调用 mark_item_rejected 应报错。"""
        with pytest.raises(ValueError, match="item_id"):
            store.mark_item_rejected(item_id="ghost-item", reason="duplicate")

    def test_update_difficulty_missing_item_raises(self, store: QuestionGenStore) -> None:
        """对不存在的 item_id 调用 update_difficulty 应报错。"""
        with pytest.raises(ValueError, match="item_id"):
            store.update_difficulty(item_id="ghost-item", difficulty_steps=5)
