from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime

from app.modules.rag.intent_router_v2.models import (
    CodeRetrievalFilters,
    EvidencePolicy,
    IntentRouterResult,
    LayerQuery,
    QueryPlan,
    RetrievalConstraints,
    RetrievalSpec,
    SymbolResolution,
)
from tests.pipeline_setup.suite_02_pipeline.pipeline_intent_rag.helpers.models import PhraseCase
from tests.pipeline_setup.suite_02_pipeline.pipeline_intent_rag.helpers.pipeline_runner import IntentRouterRagPipelineRunner, RouterOnlyRunner


@dataclass(slots=True)
class _StaticRouter:
    result: IntentRouterResult

    def route(self, *_args, **_kwargs) -> IntentRouterResult:
        return self.result


class _StaticSessionResolver:
    def resolve(self, _case: PhraseCase) -> str:
        return "rag-1"


class _FakeRagAdapter:
    def __init__(self, rows: list[dict]) -> None:
        self._rows = rows
        self._last_report: dict | None = None
        self.exact_calls = 0

    def retrieve(self, rag_session_id: str, query: str, retrieval_spec, retrieval_constraints=None) -> list[dict]:
        path_scope = list(getattr(retrieval_spec.filters, "path_scope", []) or [])
        self._last_report = {
            "executed_layers": [str(item.layer_id) for item in list(retrieval_spec.layer_queries or [])],
            "retrieval_mode_by_layer": {str(item.layer_id): "exact_path_fetch" for item in retrieval_spec.layer_queries},
            "top_k_by_layer": {str(item.layer_id): int(item.top_k) for item in retrieval_spec.layer_queries},
            "filters_by_layer": {
                str(item.layer_id): {
                    "path_scope": list(path_scope),
                    "include_globs": list(getattr(retrieval_constraints, "include_globs", []) or []),
                    "exclude_globs": list(getattr(retrieval_constraints, "exclude_globs", []) or []),
                    "prefer_globs": list(getattr(retrieval_constraints, "prefer_globs", []) or []),
                }
                for item in retrieval_spec.layer_queries
            },
            "requests": [
                {
                    "layer": str(item.layer_id),
                    "query": query,
                    "path_scope": list(path_scope),
                    "include_globs": list(getattr(retrieval_constraints, "include_globs", []) or []),
                    "exclude_globs": list(getattr(retrieval_constraints, "exclude_globs", []) or []),
                    "prefer_globs": list(getattr(retrieval_constraints, "prefer_globs", []) or []),
                    "top_k": int(item.top_k),
                    "ranking_profile": str(getattr(retrieval_spec, "rerank_profile", "") or ""),
                }
                for item in retrieval_spec.layer_queries
            ],
            "applied": [
                {
                    "layer": str(item.layer_id),
                    "effective_path_scope": list(path_scope),
                    "normalized_include_globs": list(getattr(retrieval_constraints, "include_globs", []) or []),
                    "normalized_exclude_globs": list(getattr(retrieval_constraints, "exclude_globs", []) or []),
                    "normalized_prefer_globs": list(getattr(retrieval_constraints, "prefer_globs", []) or []),
                    "filter_stage": "pre_rank",
                    "candidates_before_filter": len(self._rows),
                    "candidates_after_filter": len(self._rows),
                }
                for item in retrieval_spec.layer_queries
            ],
            "fallback": {"used": False, "reason": None},
            "retrieval_by_layer_ms": {str(item.layer_id): 1 for item in retrieval_spec.layer_queries},
        }
        return list(self._rows)

    def retrieve_with_plan(
        self,
        rag_session_id: str,
        query: str,
        retrieval_spec,
        retrieval_constraints=None,
        *,
        query_plan=None,
    ) -> list[dict]:
        return self.retrieve(rag_session_id, query, retrieval_spec, retrieval_constraints)

    def consume_retrieval_report(self) -> dict | None:
        report = self._last_report
        self._last_report = None
        return report

    def retrieve_exact_files(
        self,
        *,
        rag_session_id: str,
        repo_id: str | None = None,
        paths: list[str],
        layers: list[str] | None = None,
        limit: int = 200,
        query: str = "",
        ranking_profile: str = "",
    ) -> list[dict]:
        self.exact_calls += 1
        layer = list(layers or ["C0_SOURCE_CHUNKS"])[0]
        normalized = list(paths)
        matched = [row for row in self._rows if str(row.get("path") or "") in set(normalized)]
        self._last_report = {
            "executed_layers": [layer],
            "retrieval_mode_by_layer": {layer: "exact_path_fetch"},
            "top_k_by_layer": {layer: int(limit)},
            "filters_by_layer": {
                layer: {
                    "path_scope": normalized,
                    "include_globs": [],
                    "exclude_globs": [],
                    "prefer_globs": [],
                }
            },
            "requests": [
                {
                    "layer": layer,
                    "query": query,
                    "path_scope": normalized,
                    "include_globs": [],
                    "exclude_globs": [],
                    "prefer_globs": [],
                    "top_k": int(limit),
                    "ranking_profile": ranking_profile,
                }
            ],
            "applied": [
                {
                    "layer": layer,
                    "effective_path_scope": normalized,
                    "normalized_include_globs": [],
                    "normalized_exclude_globs": [],
                    "normalized_prefer_globs": [],
                    "filter_stage": "pre_rank",
                    "candidates_before_filter": len(matched),
                    "candidates_after_filter": len(matched),
                }
            ],
            "fallback": {"used": False, "reason": None},
            "retrieval_by_layer_ms": {layer: 1},
        }
        return list(matched)

    def hydrate_resolved_symbol_sources(self, **kwargs) -> list[dict]:
        return list(kwargs["rag_rows"])

    def force_symbol_context_c0(self, rag_session_id: str, *, rag_rows: list[dict], symbol_resolution: dict, limit: int = 20) -> list[dict]:  # noqa: ARG002
        return list(rag_rows)


def _open_file_result() -> IntentRouterResult:
    path_scope = ["src/mail_order_bot/context.py"]
    return IntentRouterResult(
        intent="CODE_QA",
        retrieval_profile="code",
        graph_id="CodeQAGraph",
        conversation_mode="START",
        query_plan=QueryPlan(
            raw="Открой файл src/mail_order_bot/context.py",
            normalized="Открой файл src/mail_order_bot/context.py",
            sub_intent="OPEN_FILE",
            keyword_hints=[],
            path_hints=list(path_scope),
            doc_scope_hints=[],
            symbol_candidates=[],
            symbol_kind_hint="unknown",
        ),
        retrieval_spec=RetrievalSpec(
            domains=["CODE"],
            layer_queries=[LayerQuery(layer_id="C0_SOURCE_CHUNKS", top_k=8)],
            filters=CodeRetrievalFilters(test_policy="EXCLUDE", path_scope=list(path_scope), language=[]),
            rerank_profile="code",
        ),
        retrieval_constraints=RetrievalConstraints(
            include_globs=list(path_scope),
            exclude_globs=["tests/**"],
            prefer_globs=[],
            test_file_globs=[],
            test_symbol_patterns=[],
            max_candidates=20,
        ),
        symbol_resolution=SymbolResolution(status="not_requested"),
        evidence_policy=EvidencePolicy(require_spec=False),
    )


def _explain_result() -> IntentRouterResult:
    return IntentRouterResult(
        intent="CODE_QA",
        retrieval_profile="code",
        graph_id="CodeQAGraph",
        conversation_mode="START",
        query_plan=QueryPlan(
            raw="Объясни как работает Context",
            normalized="Объясни как работает Context",
            sub_intent="EXPLAIN",
            keyword_hints=["Context"],
            path_hints=[],
            doc_scope_hints=[],
            symbol_candidates=["Context"],
            symbol_kind_hint="class",
        ),
        retrieval_spec=RetrievalSpec(
            domains=["CODE"],
            layer_queries=[LayerQuery(layer_id="C1_SYMBOL_CATALOG", top_k=8), LayerQuery(layer_id="C0_SOURCE_CHUNKS", top_k=8)],
            filters=CodeRetrievalFilters(test_policy="EXCLUDE", path_scope=["src/mail_order_bot/context.py"], language=[]),
            rerank_profile="code",
        ),
        retrieval_constraints=RetrievalConstraints(
            include_globs=["src/**"],
            exclude_globs=["tests/**"],
            prefer_globs=[],
            test_file_globs=[],
            test_symbol_patterns=[],
            max_candidates=20,
        ),
        symbol_resolution=SymbolResolution(status="pending", alternatives=["Context"], confidence=0.0),
        evidence_policy=EvidencePolicy(require_spec=False),
    )


def test_router_only_diagnostics_contains_router_plan() -> None:
    case = PhraseCase(case_id="c1", text="Открой файл src/mail_order_bot/context.py")
    runner = RouterOnlyRunner(started_at=datetime(2026, 3, 5, 12, 0, 0), router=_StaticRouter(_open_file_result()))

    record = runner.run_case(case).to_record()

    assert record["summary"]["router"]["intent"] == "CODE_QA"
    assert record["summary"]["llm"]["answer_status"] == "partial"
    assert record["diagnostics"]["router_plan"]["sub_intent"] == "OPEN_FILE"
    assert record["diagnostics"]["router"]["conversation_mode"] == "START"
    assert record["diagnostics"]["llm"]["used_evidence_count"] == 0
    assert record["diagnostics"]["execution"]["executed_layers"] == []
    assert record["diagnostics"]["retrieval"] is None
    assert record["diagnostics"]["timings_ms"]["router"] >= 0


def test_router_rag_diagnostics_keeps_plan_and_request_scope() -> None:
    case = PhraseCase(case_id="c2", text="Открой файл src/mail_order_bot/context.py")
    route_result = _open_file_result()
    rag_adapter = _FakeRagAdapter(
        rows=[
            {
                "path": "src/mail_order_bot/context.py",
                "layer": "C0_SOURCE_CHUNKS",
                "title": "src/mail_order_bot/context.py:1-10",
                "content": "class Context: ...",
                "metadata": {"repo_id": "MailOrderBot"},
            }
        ]
    )
    runner = IntentRouterRagPipelineRunner(
        started_at=datetime(2026, 3, 5, 12, 0, 0),
        rag_adapter=rag_adapter,
        session_resolver=_StaticSessionResolver(),
        router=_StaticRouter(route_result),
    )

    record = runner.run_case(case).to_record()
    diagnostics = record["diagnostics"]

    assert record["summary"]["retrieval"]["layers_hit"] == ["C0_SOURCE_CHUNKS"]
    assert diagnostics["router_plan"]["path_scope"] == ["src/mail_order_bot/context.py"]
    assert diagnostics["router_plan"]["retrieval_constraints"]["include_globs"] == ["src/mail_order_bot/context.py"]
    assert diagnostics["retrieval"]["requests"][0]["path_scope"] == diagnostics["router_plan"]["path_scope"]
    assert diagnostics["retrieval"]["symbol_resolution"]["status"] == "not_requested"
    assert diagnostics["retrieval"]["layers"][0]["layer"] == "C0_SOURCE_CHUNKS"
    assert diagnostics["retrieval"]["fallback"]["used"] is False
    assert rag_adapter.exact_calls == 1


def test_open_file_strict_exact_path_returns_only_target_and_no_violation() -> None:
    case = PhraseCase(case_id="c4", text="Открой файл src/mail_order_bot/context.py")
    rag_adapter = _FakeRagAdapter(
        rows=[
            {
                "path": "src/mail_order_bot/context.py",
                "layer": "C0_SOURCE_CHUNKS",
                "title": "src/mail_order_bot/context.py:1-10",
                "content": "class Context: ...",
                "metadata": {"repo_id": "MailOrderBot"},
            },
            {
                "path": "src/mail_order_bot/other.py",
                "layer": "C0_SOURCE_CHUNKS",
                "title": "src/mail_order_bot/other.py:1-10",
                "content": "def helper(): ...",
                "metadata": {"repo_id": "MailOrderBot"},
            },
        ]
    )
    runner = IntentRouterRagPipelineRunner(
        started_at=datetime(2026, 3, 5, 12, 0, 0),
        rag_adapter=rag_adapter,
        session_resolver=_StaticSessionResolver(),
        router=_StaticRouter(_open_file_result()),
    )

    result = runner.run_case(case)
    record = result.to_record()
    diagnostics = record["diagnostics"]

    assert result.rag_rows
    assert all(str(row.get("path") or "") == "src/mail_order_bot/context.py" for row in result.rag_rows)
    assert diagnostics["execution"]["filters_by_layer"]["C0_SOURCE_CHUNKS"]["path_scope"] == [
        "src/mail_order_bot/context.py"
    ]
    assert diagnostics["retrieval"]["applied"][0]["effective_path_scope"] == ["src/mail_order_bot/context.py"]
    assert diagnostics["retrieval"]["fallback"]["used"] is False
    assert diagnostics["constraint_violations"] == []


def test_router_rag_open_file_violation_is_reported_when_target_file_not_returned() -> None:
    case = PhraseCase(case_id="c3", text="Открой файл src/mail_order_bot/context.py")
    rag_adapter = _FakeRagAdapter(
        rows=[
            {
                "path": "src/mail_order_bot/other.py",
                "layer": "C0_SOURCE_CHUNKS",
                "title": "src/mail_order_bot/other.py:1-10",
                "content": "def helper(): ...",
                "metadata": {"repo_id": "MailOrderBot"},
            }
        ]
    )
    runner = IntentRouterRagPipelineRunner(
        started_at=datetime(2026, 3, 5, 12, 0, 0),
        rag_adapter=rag_adapter,
        session_resolver=_StaticSessionResolver(),
        router=_StaticRouter(_open_file_result()),
    )

    record = runner.run_case(case).to_record()
    violation_types = [item["type"] for item in record["diagnostics"]["constraint_violations"]]

    assert "PATH_SCOPE_NOT_SATISFIED" in violation_types


def test_explain_with_symbol_candidates_returns_empty_rag_when_symbol_not_resolved() -> None:
    case = PhraseCase(case_id="c5", text="Объясни как работает Context")
    rag_adapter = _FakeRagAdapter(rows=[])
    runner = IntentRouterRagPipelineRunner(
        started_at=datetime(2026, 3, 5, 12, 0, 0),
        rag_adapter=rag_adapter,
        session_resolver=_StaticSessionResolver(),
        router=_StaticRouter(_explain_result()),
    )

    result = runner.run_case(case)
    record = result.to_record()
    violation_types = [item["type"] for item in record["diagnostics"]["constraint_violations"]]

    assert result.rag_rows == []
    assert record["diagnostics"]["retrieval"]["fallback"]["used"] is False
    assert "SYMBOL_RESOLUTION_FAILED" in violation_types
