"""app/question_gen/loader.py 单元测试。"""

from __future__ import annotations

import json
from pathlib import Path

import pytest

from app.question_gen.loader import load_benchmark, stratified_sample
from core.types import GeneratedQuestion


@pytest.fixture()
def benchmark_dir(tmp_path: Path) -> Path:
    """创建包含 benchmark JSON 的临时目录。"""
    data = [
        {
            "question_id": "1-1",
            "task_type": "Action Reasoning",
            "question": "What happened?",
            "options": ["A. X", "B. Y", "C. Z", "D. W"],
            "answer": "A",
        },
        {
            "question_id": "1-2",
            "task_type": "OCR Problems",
            "question": "What text is shown?",
            "options": ["A. Hello", "B. World", "C. Foo", "D. Bar"],
            "answer": "B",
        },
    ]
    (tmp_path / "video_abc.json").write_text(json.dumps(data), encoding="utf-8")
    return tmp_path


class TestLoadBenchmark:
    def test_loads_questions_from_json(self, benchmark_dir: Path) -> None:
        questions = load_benchmark(benchmark_dir)
        assert len(questions) == 2

    def test_video_id_from_filename(self, benchmark_dir: Path) -> None:
        questions = load_benchmark(benchmark_dir)
        assert all(q.video_id == "video_abc" for q in questions)

    def test_fields_mapped_correctly(self, benchmark_dir: Path) -> None:
        questions = load_benchmark(benchmark_dir)
        q = questions[0]
        assert q.question_id == "1-1"
        assert q.task_type == "Action Reasoning"
        assert q.question == "What happened?"
        assert q.options == ("A. X", "B. Y", "C. Z", "D. W")
        assert q.answer == "A"

    def test_options_is_tuple(self, benchmark_dir: Path) -> None:
        questions = load_benchmark(benchmark_dir)
        assert isinstance(questions[0].options, tuple)

    def test_source_nodes_is_empty_tuple(self, benchmark_dir: Path) -> None:
        questions = load_benchmark(benchmark_dir)
        assert questions[0].source_nodes == ()

    def test_difficulty_defaults_to_medium_for_legacy(self, benchmark_dir: Path) -> None:
        questions = load_benchmark(benchmark_dir)
        assert questions[0].difficulty == "medium"

    def test_difficulty_from_json_when_present(self, tmp_path: Path) -> None:
        data = [
            {
                "question_id": "2-1",
                "task_type": "OCR Problems",
                "question": "Q?",
                "options": ["A. 1", "B. 2", "C. 3", "D. 4"],
                "answer": "C",
                "difficulty": "hard",
            }
        ]
        (tmp_path / "vid.json").write_text(json.dumps(data), encoding="utf-8")
        questions = load_benchmark(tmp_path)
        assert questions[0].difficulty == "hard"

    def test_empty_directory_returns_empty_list(self, tmp_path: Path) -> None:
        questions = load_benchmark(tmp_path)
        assert questions == []

    def test_sorted_by_filename(self, tmp_path: Path) -> None:
        for name in ["z_video.json", "a_video.json"]:
            data = [
                {
                    "question_id": f"{name}-1",
                    "task_type": "T",
                    "question": "Q?",
                    "options": ["A", "B", "C", "D"],
                    "answer": "A",
                }
            ]
            (tmp_path / name).write_text(json.dumps(data), encoding="utf-8")
        questions = load_benchmark(tmp_path)
        assert questions[0].video_id == "a_video"
        assert questions[1].video_id == "z_video"

    def test_returns_generated_question_instances(self, benchmark_dir: Path) -> None:
        questions = load_benchmark(benchmark_dir)
        assert all(isinstance(q, GeneratedQuestion) for q in questions)

    def test_loads_real_benchmark(self) -> None:
        """使用真实 benchmark 数据验证加载正确性。"""
        real_dir = Path("store/questions/benchmarks/Video-MME")
        if not real_dir.exists():
            pytest.skip("真实 benchmark 数据不存在")
        questions = load_benchmark(real_dir)
        assert len(questions) > 0
        for q in questions:
            assert isinstance(q, GeneratedQuestion)
            assert len(q.options) == 4
            assert q.answer in ("A", "B", "C", "D")

    def test_malformed_json_raises(self, tmp_path: Path) -> None:
        """非法 JSON 文件应抛出 json.JSONDecodeError。"""
        (tmp_path / "bad.json").write_text("not valid json{{{", encoding="utf-8")
        with pytest.raises(json.JSONDecodeError):
            load_benchmark(tmp_path)

    def test_missing_required_field_raises(self, tmp_path: Path) -> None:
        """缺少必需字段（如 question_id）应抛出 KeyError。"""
        data = [{"task_type": "T", "question": "Q?", "options": ["A"], "answer": "A"}]
        (tmp_path / "vid.json").write_text(json.dumps(data), encoding="utf-8")
        with pytest.raises(KeyError):
            load_benchmark(tmp_path)


def _make_questions(n: int, task_type: str = "T") -> list[GeneratedQuestion]:
    """辅助函数：批量构造题目。"""
    return [
        GeneratedQuestion(
            question_id=f"{task_type}-{i}",
            video_id="v1",
            task_type=task_type,
            question=f"Q{i}?",
            options=("A", "B", "C", "D"),
            answer="A",
            source_nodes=(),
            difficulty="medium",
        )
        for i in range(n)
    ]


class TestStratifiedSample:
    def test_natural_distribution(self) -> None:
        questions = _make_questions(20)
        result = stratified_sample(
            questions=questions,
            correctness={},
            size=10,
            correct_ratio=None,
            task_types=None,
            seed=42,
            min_per_class=None,
        )
        assert len(result) == 10

    def test_natural_distribution_pool_insufficient(self) -> None:
        questions = _make_questions(5)
        with pytest.raises(ValueError, match="自然分布采样不足"):
            stratified_sample(
                questions=questions,
                correctness={},
                size=10,
                correct_ratio=None,
                task_types=None,
                seed=42,
                min_per_class=None,
            )

    def test_ratio_stratified(self) -> None:
        questions = _make_questions(20)
        correctness = {f"T-{i}": i < 10 for i in range(20)}
        result = stratified_sample(
            questions=questions,
            correctness=correctness,
            size=10,
            correct_ratio=0.6,
            task_types=None,
            seed=42,
            min_per_class=None,
        )
        assert len(result) == 10
        correct_count = sum(1 for q in result if correctness.get(q.question_id, False))
        assert correct_count == 6

    def test_ratio_stratified_correct_first(self) -> None:
        questions = _make_questions(20)
        correctness = {f"T-{i}": i < 10 for i in range(20)}
        result = stratified_sample(
            questions=questions,
            correctness=correctness,
            size=10,
            correct_ratio=0.5,
            task_types=None,
            seed=42,
            min_per_class=None,
        )
        n_correct = round(10 * 0.5)
        for q in result[:n_correct]:
            assert correctness.get(q.question_id, False) is True
        for q in result[n_correct:]:
            assert correctness.get(q.question_id, False) is False

    def test_ratio_stratified_pool_insufficient(self) -> None:
        questions = _make_questions(10)
        correctness = {f"T-{i}": True for i in range(10)}
        with pytest.raises(ValueError, match="分层不足"):
            stratified_sample(
                questions=questions,
                correctness=correctness,
                size=10,
                correct_ratio=0.5,
                task_types=None,
                seed=42,
                min_per_class=None,
            )

    def test_task_types_filter(self) -> None:
        q_a = _make_questions(10, task_type="TypeA")
        q_b = _make_questions(10, task_type="TypeB")
        result = stratified_sample(
            questions=q_a + q_b,
            correctness={},
            size=5,
            correct_ratio=None,
            task_types=["TypeA"],
            seed=42,
            min_per_class=None,
        )
        assert all(q.task_type == "TypeA" for q in result)

    def test_unknown_correctness_treated_as_wrong(self) -> None:
        questions = _make_questions(20)
        correctness = {f"T-{i}": True for i in range(10)}
        result = stratified_sample(
            questions=questions,
            correctness=correctness,
            size=10,
            correct_ratio=0.5,
            task_types=None,
            seed=42,
            min_per_class=None,
        )
        n_correct = round(10 * 0.5)
        for q in result[:n_correct]:
            assert q.question_id in correctness

    def test_seed_reproducibility(self) -> None:
        questions = _make_questions(20)
        r1 = stratified_sample(
            questions=questions,
            correctness={},
            size=10,
            correct_ratio=None,
            task_types=None,
            seed=123,
            min_per_class=None,
        )
        r2 = stratified_sample(
            questions=questions,
            correctness={},
            size=10,
            correct_ratio=None,
            task_types=None,
            seed=123,
            min_per_class=None,
        )
        assert [q.question_id for q in r1] == [q.question_id for q in r2]

    def test_different_seeds_differ(self) -> None:
        questions = _make_questions(20)
        r1 = stratified_sample(
            questions=questions,
            correctness={},
            size=10,
            correct_ratio=None,
            task_types=None,
            seed=1,
            min_per_class=None,
        )
        r2 = stratified_sample(
            questions=questions,
            correctness={},
            size=10,
            correct_ratio=None,
            task_types=None,
            seed=2,
            min_per_class=None,
        )
        assert [q.question_id for q in r1] != [q.question_id for q in r2]

    def test_min_per_class_backfill(self) -> None:
        q_a = _make_questions(10, task_type="TypeA")
        q_b = _make_questions(10, task_type="TypeB")
        all_q = q_a + q_b
        correctness = {q.question_id: True for q in q_a[:5]}
        result = stratified_sample(
            questions=all_q,
            correctness=correctness,
            size=3,
            correct_ratio=None,
            task_types=None,
            seed=42,
            min_per_class=2,
        )
        type_counts: dict[str, int] = {}
        for q in result:
            type_counts[q.task_type] = type_counts.get(q.task_type, 0) + 1
        assert type_counts.get("TypeA", 0) >= 2
        assert type_counts.get("TypeB", 0) >= 2

    def test_min_per_class_partial_backfill(self) -> None:
        q_sparse = _make_questions(1, task_type="Sparse")
        q_main = _make_questions(10, task_type="Main")
        result = stratified_sample(
            questions=q_sparse + q_main,
            correctness={},
            size=5,
            correct_ratio=None,
            task_types=None,
            seed=42,
            min_per_class=3,
        )
        sparse_in_result = [q for q in result if q.task_type == "Sparse"]
        assert len(sparse_in_result) == 1

    def test_min_per_class_no_duplicates(self) -> None:
        q_a = _make_questions(5, task_type="TypeA")
        q_b = _make_questions(5, task_type="TypeB")
        result = stratified_sample(
            questions=q_a + q_b,
            correctness={},
            size=3,
            correct_ratio=None,
            task_types=None,
            seed=42,
            min_per_class=2,
        )
        ids = [q.question_id for q in result]
        assert len(ids) == len(set(ids))

    def test_backfill_enumerates_all_pool_types(self) -> None:
        q_main = _make_questions(10, task_type="Main")
        q_rare = _make_questions(3, task_type="Rare")
        result = stratified_sample(
            questions=q_main + q_rare,
            correctness={},
            size=2,
            correct_ratio=None,
            task_types=None,
            seed=0,
            min_per_class=1,
        )
        types_in_result = {q.task_type for q in result}
        assert "Rare" in types_in_result
