"""postprocess 模块单元测试 — 选项洗牌、指代黑名单、逐字重复率、时间锚点、素材禁区、run_postprocess 编排。"""

from __future__ import annotations

import random

from app.question_gen.postprocess import (
    PostprocessResult,
    check_forbidden_material,
    check_referent_blacklist,
    check_verbatim,
    has_time_anchor,
    run_postprocess,
    shuffle_options,
)

# ---------------------------------------------------------------------------
# TestShuffleOptions
# ---------------------------------------------------------------------------


class TestShuffleOptions:
    """选项洗牌确定性与答案重映射测试。"""

    def test_deterministic_with_seed(self) -> None:
        """相同种子产生相同置换结果。"""
        options = ("A. 苹果", "B. 香蕉", "C. 橙子", "D. 葡萄")
        answer = "B"
        rng1 = random.Random(42)
        rng2 = random.Random(42)

        result1 = shuffle_options(options, answer, rng1)
        result2 = shuffle_options(options, answer, rng2)

        assert result1 == result2

    def test_answer_remapped_correctly(self) -> None:
        """洗牌后 answer 字母始终指向原正确文本。"""
        options = ("A. 苹果", "B. 香蕉", "C. 橙子", "D. 葡萄")
        answer = "B"  # 正确文本是 "香蕉"

        # 用多个不同种子确保覆盖不同排列
        for seed in range(100):
            rng = random.Random(seed)
            new_options, new_answer = shuffle_options(options, answer, rng)

            # 答案字母对应的选项文本必须包含 "香蕉"
            answer_idx = ord(new_answer) - ord("A")
            assert "香蕉" in new_options[answer_idx], (
                f"seed={seed}: 新答案 {new_answer} 对应 '{new_options[answer_idx]}'，不含 '香蕉'"
            )

    def test_all_options_preserved(self) -> None:
        """洗牌后选项内容集合不变（仅字母前缀重映射）。"""
        options = ("A. 苹果", "B. 香蕉", "C. 橙子", "D. 葡萄")
        answer = "A"
        rng = random.Random(7)

        new_options, _ = shuffle_options(options, answer, rng)

        # 提取纯文本（去除 "X. " 前缀）
        original_texts = {opt[3:] for opt in options}
        shuffled_texts = {opt[3:] for opt in new_options}
        assert original_texts == shuffled_texts

    def test_output_letters_sequential(self) -> None:
        """洗牌后选项前缀始终为 A/B/C/D 顺序。"""
        options = ("A. one", "B. two", "C. three", "D. four")
        answer = "C"
        rng = random.Random(99)

        new_options, _ = shuffle_options(options, answer, rng)
        for i, opt in enumerate(new_options):
            expected_prefix = f"{chr(ord('A') + i)}. "
            assert opt.startswith(expected_prefix), (
                f"选项 {i} 应以 '{expected_prefix}' 开头，实际为 '{opt[:3]}'"
            )


# ---------------------------------------------------------------------------
# TestReferentBlacklist
# ---------------------------------------------------------------------------


class TestReferentBlacklist:
    """指代黑名单检测测试。"""

    def test_clean_passes(self) -> None:
        """无自指代语言的正常题目返回空列表。"""
        q = "What is the main character doing in the kitchen?"
        violations = check_referent_blacklist(q)
        assert violations == []

    def test_this_clip_caught(self) -> None:
        """'this clip' 被检测为违规。"""
        q = "What happens in this clip after the man enters?"
        violations = check_referent_blacklist(q)
        assert len(violations) >= 1
        assert any("this clip" in v.lower() for v in violations)

    def test_the_video_caught(self) -> None:
        """'the video' 被检测为违规。"""
        q = "In the video, what color is the car?"
        violations = check_referent_blacklist(q)
        assert len(violations) >= 1

    def test_chinese_referent_caught(self) -> None:
        """中文自指代 '这段视频' 被检测。"""
        q = "这段视频中男人在做什么？"
        violations = check_referent_blacklist(q)
        assert len(violations) >= 1

    def test_this_scene_caught(self) -> None:
        """'this scene' 被检测为违规。"""
        q = "What is shown in this scene before the explosion?"
        violations = check_referent_blacklist(q)
        assert len(violations) >= 1

    def test_case_insensitive(self) -> None:
        """大小写不影响检测。"""
        q = "What does THIS CLIP show at the end?"
        violations = check_referent_blacklist(q)
        assert len(violations) >= 1

    def test_this_segment_caught(self) -> None:
        """'this segment' 被检测为违规。"""
        q = "What action is performed in this segment?"
        violations = check_referent_blacklist(q)
        assert len(violations) >= 1
        assert any("this segment" in v.lower() for v in violations)

    def test_this_frame_caught(self) -> None:
        """'this frame' 被检测为违规。"""
        q = "What object is visible in this frame?"
        violations = check_referent_blacklist(q)
        assert len(violations) >= 1

    def test_current_frame_caught(self) -> None:
        """'the current frame' 被检测为违规。"""
        q = "Based on the current frame, what is happening?"
        violations = check_referent_blacklist(q)
        assert len(violations) >= 1

    def test_frame_summary_caught(self) -> None:
        """'frame summary' 被检测为违规。"""
        q = "According to the frame summary, what is the main event?"
        violations = check_referent_blacklist(q)
        assert len(violations) >= 1


# ---------------------------------------------------------------------------
# TestVerbatim
# ---------------------------------------------------------------------------


class TestVerbatim:
    """逐字重复率检测测试。"""

    def test_zero_overlap(self) -> None:
        """正确选项与来源文本完全无重叠时返回 0.0。"""
        ratio = check_verbatim(
            question_text="What color is the sky?",
            correct_option="A. The sky is painted in bright orange",
            source_texts=["A man walks into a store and buys groceries for dinner."],
            window=6,
        )
        assert ratio == 0.0

    def test_full_copy_returns_one(self) -> None:
        """正确选项完全来自来源文本时返回 1.0。"""
        source = "The cat sat on the mat near the window looking outside"
        option = "A. The cat sat on the mat near the window looking outside"
        ratio = check_verbatim(
            question_text="What did the animal do?",
            correct_option=option,
            source_texts=[source],
            window=6,
        )
        assert ratio == 1.0

    def test_partial(self) -> None:
        """部分重叠返回 0-1 之间的值。"""
        source = "The large brown dog jumped over the lazy red fox quickly"
        # 选项中 6 词窗口 "large brown dog jumped over the" 出现在 source 中
        option = "A. A large brown dog jumped over the small fence"
        ratio = check_verbatim(
            question_text="What happened?",
            correct_option=option,
            source_texts=[source],
            window=6,
        )
        assert 0.0 < ratio < 1.0

    def test_empty_source_returns_zero(self) -> None:
        """来源文本为空列表时返回 0.0。"""
        ratio = check_verbatim(
            question_text="What is this?",
            correct_option="A. Something interesting",
            source_texts=[],
            window=6,
        )
        assert ratio == 0.0

    def test_short_option_below_window(self) -> None:
        """选项词数不足窗口大小时仍正常工作（返回 0.0）。"""
        ratio = check_verbatim(
            question_text="What?",
            correct_option="A. Hello world",
            source_texts=["Some very long source text that goes on and on"],
            window=6,
        )
        assert ratio == 0.0

    def test_question_text_overlap_detected(self) -> None:
        """题目文本从来源复制但选项原创时，仍检测到高重叠率。"""
        source = "The man walked slowly through the crowded market buying fresh vegetables"
        # 题目直接复制来源的大部分内容
        question = "The man walked slowly through the crowded market buying what?"
        # 选项完全原创
        option = "A. Fresh organic produce from the local farm stand"
        ratio = check_verbatim(
            question_text=question,
            correct_option=option,
            source_texts=[source],
            window=6,
        )
        assert ratio > 0.0, "题目文本的逐字重叠应被检测到"


# ---------------------------------------------------------------------------
# TestTimeAnchor
# ---------------------------------------------------------------------------


class TestTimeAnchor:
    """时间锚点检测测试。"""

    def test_timestamp(self) -> None:
        """包含 '01:30' 格式时间戳 → True。"""
        assert has_time_anchor("What happens at 01:30 in the recording?") is True

    def test_no_anchor(self) -> None:
        """无任何时间锚点 → False。"""
        assert has_time_anchor("What is the man doing in the park?") is False

    def test_beginning_phrase(self) -> None:
        """'at the beginning' → True。"""
        assert has_time_anchor("At the beginning, what does the woman say?") is True

    def test_chinese_beginning(self) -> None:
        """'开头' → True。"""
        assert has_time_anchor("视频开头出现了什么？") is True

    def test_chinese_ending(self) -> None:
        """'结尾' → True。"""
        assert has_time_anchor("在视频结尾发生了什么？") is True

    def test_minute_second_format(self) -> None:
        """'12:45' 格式 → True。"""
        assert has_time_anchor("At 12:45, who appears on screen?") is True


# ---------------------------------------------------------------------------
# TestCheckForbiddenMaterial
# ---------------------------------------------------------------------------


class TestCheckForbiddenMaterial:
    """素材禁区检测测试。"""

    def test_clean_material_passes(self) -> None:
        """正常素材无违规。"""
        source = "一个男人走进厨房，开始准备晚餐，切菜并烧水。"
        violations = check_forbidden_material(source, "Action Recognition")
        assert violations == []

    def test_t1_instant_action_detected(self) -> None:
        """T1 瞬时动作模式被检测。"""
        source = "球员瞬间射门得分。一闪而过的画面。"
        violations = check_forbidden_material(source, "Action Recognition")
        assert len(violations) >= 1

    def test_t1_scoreboard_temporal_detected(self) -> None:
        """T1 记分牌时序模式被检测。"""
        source = "比分从2:1变为3:1，记分牌显示更新。"
        violations = check_forbidden_material(source, "Counting Problem")
        assert len(violations) >= 1

    def test_t7_option_repetition_detected(self) -> None:
        """T7 选项重复模式被检测。"""
        source = "选项A和选项B完全相同。重复选项无法区分。"
        violations = check_forbidden_material(source, "Object Recognition")
        assert len(violations) >= 1

    def test_t7_counting_ambiguity_detected(self) -> None:
        """T7 计数边界口径含糊被检测。"""
        source = "大约有三到五个人，数量不确定，难以精确计数。"
        violations = check_forbidden_material(source, "Counting Problem")
        assert len(violations) >= 1


# ---------------------------------------------------------------------------
# TestRunPostprocess
# ---------------------------------------------------------------------------


class TestRunPostprocess:
    """run_postprocess 编排测试。"""

    def test_returns_postprocess_result(self) -> None:
        """返回类型正确。"""
        result = run_postprocess(
            question_text="What is the main character doing?",
            options=("A. Running", "B. Swimming", "C. Reading", "D. Sleeping"),
            answer="A",
            source_texts=["The protagonist goes for a morning jog in the park."],
            rng=random.Random(42),
        )
        assert isinstance(result, PostprocessResult)

    def test_shuffle_applied(self) -> None:
        """选项顺序被打乱（至少在多次运行中出现过不同排列）。"""
        options = ("A. Alpha", "B. Beta", "C. Gamma", "D. Delta")
        answer = "A"

        seen_orders: set[tuple[str, ...]] = set()
        for seed in range(50):
            result = run_postprocess(
                question_text="Pick one",
                options=options,
                answer=answer,
                source_texts=["Unrelated source text about something else entirely."],
                rng=random.Random(seed),
            )
            seen_orders.add(result.options)

        # 50 个不同种子应产生多于 1 种排列
        assert len(seen_orders) > 1

    def test_referent_violations_populated(self) -> None:
        """有指代违规时正确填入结果。"""
        result = run_postprocess(
            question_text="What happens in this clip?",
            options=("A. One", "B. Two", "C. Three", "D. Four"),
            answer="B",
            source_texts=["Some text here."],
            rng=random.Random(0),
        )
        assert len(result.referent_violations) >= 1

    def test_verbatim_ratio_range(self) -> None:
        """verbatim_ratio 始终在 [0.0, 1.0] 范围内。"""
        result = run_postprocess(
            question_text="What is shown?",
            options=(
                "A. A cat sits on the mat",
                "B. A dog runs",
                "C. A bird flies",
                "D. A fish swims",
            ),
            answer="A",
            source_texts=["A cat sits on the mat by the window."],
            rng=random.Random(1),
        )
        assert 0.0 <= result.verbatim_ratio <= 1.0

    def test_has_time_anchor_field(self) -> None:
        """时间锚点字段正确填写。"""
        result = run_postprocess(
            question_text="What happens at 02:30?",
            options=("A. X", "B. Y", "C. Z", "D. W"),
            answer="C",
            source_texts=["Some source text."],
            rng=random.Random(5),
        )
        assert result.has_time_anchor is True
