"""app/harness/factory.py 的单元测试。

验证 build_inference_deps 的返回类型、字段连接、以及错误路径。
"""

from __future__ import annotations

import json
from typing import TYPE_CHECKING
from unittest.mock import AsyncMock, MagicMock

if TYPE_CHECKING:
    from pathlib import Path

import numpy as np
import pytest

from app.harness.factory import InferenceDeps, build_inference_deps
from core.types import GeneratedQuestion


class TestBuildInferenceDeps:
    """build_inference_deps 工厂函数测试。"""

    def test_returns_inference_deps(self, tmp_path: Path) -> None:
        """用 fake adapters 验证返回类型和字段非 None。"""
        # 准备一棵最小树
        vid_dir = tmp_path / "videos" / "test_vid"
        vid_dir.mkdir(parents=True)
        (vid_dir / "frames").mkdir()
        minimal_tree = {
            "metadata": {"source_path": "test", "modality": "video"},
            "roots": [
                {
                    "id": "L1_000",
                    "card": {
                        "scene_summary": "s",
                        "main_setting": "s",
                        "key_entities": [],
                        "main_actions": [],
                        "topic_keywords": [],
                        "visible_text": [],
                        "temporal_flow": "s",
                    },
                    "time_range": [0, 10],
                    "children": [],
                }
            ],
        }
        (vid_dir / "tree.json").write_text(json.dumps(minimal_tree))

        # prompts
        prompts_dir = tmp_path / "prompts"
        prompts_dir.mkdir()
        (prompts_dir / "system.md").write_text("You are a search agent.")

        fake_llm = AsyncMock()
        fake_vlm = AsyncMock()
        fake_embed = MagicMock()
        fake_embed.dim = 4
        fake_embed.embed = lambda t: np.zeros((1, 4), dtype=np.float32)

        deps = build_inference_deps(
            store_dir=tmp_path,
            video_id="test_vid",
            prompts_dir=prompts_dir,
            skills_dir=None,
            skill_mode="none",
            embed_provider=fake_embed,
            llm=fake_llm,
            vlm=fake_vlm,
            ocr=None,
            verify_vision=False,
            anchor=False,
            assemble_mode="ids",
        )
        assert isinstance(deps, InferenceDeps)
        assert deps.llm is fake_llm
        assert callable(deps.tool_dispatch_fn)
        assert callable(deps.prompt_builder)

        # 验证 prompt_builder 实际可用（连接正确）
        fake_q = GeneratedQuestion(
            question_id="q1",
            video_id="test_vid",
            task_type="Object Recognition",
            question="What?",
            options=("A. X", "B. Y", "C. Z", "D. W"),
            answer="A",
            source_nodes=(),
            difficulty="medium",
        )
        system, user = deps.prompt_builder(fake_q)
        assert isinstance(system, str) and len(system) > 0
        assert isinstance(user, str) and "What?" in user

    def test_missing_tree_raises(self, tmp_path: Path) -> None:
        """tree.json 不存在时应抛出 FileNotFoundError。"""
        prompts_dir = tmp_path / "prompts"
        prompts_dir.mkdir()
        (prompts_dir / "system.md").write_text("x")
        vid_dir = tmp_path / "videos" / "nonexist"
        vid_dir.mkdir(parents=True)

        with pytest.raises(FileNotFoundError):
            build_inference_deps(
                store_dir=tmp_path,
                video_id="nonexist",
                prompts_dir=prompts_dir,
                skills_dir=None,
                skill_mode="none",
                embed_provider=MagicMock(),
                llm=AsyncMock(),
                vlm=AsyncMock(),
                ocr=None,
                verify_vision=False,
                anchor=False,
                assemble_mode="ids",
            )

    def test_with_skills_dir(self, tmp_path: Path) -> None:
        """提供 skills_dir 时 skill 信息应正确注入到 prompt_builder 输出。"""
        # 准备树
        vid_dir = tmp_path / "videos" / "vid1"
        vid_dir.mkdir(parents=True)
        (vid_dir / "frames").mkdir()
        minimal_tree = {
            "metadata": {"source_path": "test", "modality": "video"},
            "roots": [
                {
                    "id": "L1_000",
                    "card": {
                        "scene_summary": "test scene",
                        "main_setting": "indoor",
                        "key_entities": [],
                        "main_actions": [],
                        "topic_keywords": [],
                        "visible_text": [],
                        "temporal_flow": "linear",
                    },
                    "time_range": [0, 5],
                    "children": [],
                }
            ],
        }
        (vid_dir / "tree.json").write_text(json.dumps(minimal_tree))

        # prompts
        prompts_dir = tmp_path / "prompts"
        prompts_dir.mkdir()
        (prompts_dir / "system.md").write_text("Base system prompt.")

        # skills
        skills_dir = tmp_path / "skills"
        skills_dir.mkdir()
        (skills_dir / "always_nav.md").write_text(
            "---\nname: always_nav\nalways: true\n---\nAlways navigate broadly."
        )
        (skills_dir / "action_skill.md").write_text(
            "---\nname: action_skill\ntask_type: Action Reasoning\n---\nFocus on actions."
        )

        fake_llm = AsyncMock()
        fake_vlm = AsyncMock()
        fake_embed = MagicMock()
        fake_embed.dim = 4
        fake_embed.embed = lambda t: np.zeros((1, 4), dtype=np.float32)

        deps = build_inference_deps(
            store_dir=tmp_path,
            video_id="vid1",
            prompts_dir=prompts_dir,
            skills_dir=skills_dir,
            skill_mode="auto",
            embed_provider=fake_embed,
            llm=fake_llm,
            vlm=fake_vlm,
            ocr=None,
            verify_vision=False,
            anchor=False,
            assemble_mode="ids",
        )

        fake_q = GeneratedQuestion(
            question_id="q2",
            video_id="vid1",
            task_type="Action Reasoning",
            question="What happened?",
            options=("A. X", "B. Y", "C. Z", "D. W"),
            answer="B",
            source_nodes=(),
            difficulty="easy",
        )
        system, user = deps.prompt_builder(fake_q)
        # always skill 文本和 task_type skill 文本应出现在 system prompt 中
        assert "Always navigate broadly" in system
        assert "Focus on actions" in system
        assert "What happened?" in user

    def test_frozen_dataclass(self) -> None:
        """InferenceDeps 是 frozen dataclass，不可修改属性。"""
        deps = InferenceDeps(
            llm=AsyncMock(),
            tool_dispatch_fn=lambda: None,
            prompt_builder=lambda q: ("", ""),
        )
        with pytest.raises(AttributeError):
            deps.llm = AsyncMock()  # type: ignore[misc]
