"""Tests for the canonical CODE_QA pipeline (IntentRouterV2 -> retrieval -> evidence -> diagnostics)."""

from __future__ import annotations

from pathlib import Path

import pytest

from app.modules.rag.code_qa_pipeline import (
    CodeChunkItem,
    CodeQAPipelineRunner,
    EvidenceBundle,
    RetrievalRequest,
    RetrievalResult,
)
from app.modules.rag.code_qa_pipeline.evidence_gate import evaluate_evidence
from app.modules.rag.code_qa_pipeline.retrieval_request_builder import build_retrieval_request
from app.modules.rag.code_qa_pipeline.retrieval_result_builder import build_retrieval_result
from app.modules.rag.intent_router_v2 import ConversationState, IntentRouterV2
from tests.unit_tests.rag.intent_router_testkit import repo_context

_TEST_ROOT = Path(__file__).resolve().parent


def _make_router() -> IntentRouterV2:
    return IntentRouterV2()


def test_router_output_drives_retrieval_request() -> None:
    """Router output is directly convertible into RetrievalRequest."""
    router = _make_router()
    text = "Открой файл src/mail_order_bot/context.py"
    result = router.route(text, ConversationState(), repo_context())
    request = build_retrieval_request(result, "test-session-id")
    assert request.rag_session_id == "test-session-id"
    assert request.query
    assert request.sub_intent == "OPEN_FILE"
    assert "context.py" in str(request.path_scope) or request.path_scope
    assert "C0_SOURCE_CHUNKS" in request.requested_layers or request.requested_layers


def test_retrieval_result_normalized_structure() -> None:
    """Retrieval returns normalized RetrievalResult with expected fields."""
    raw = [
        {"layer": "C0_SOURCE_CHUNKS", "path": "src/foo.py", "title": "", "content": "def bar(): pass", "span_start": 1, "span_end": 2, "metadata": {}},
        {"layer": "C1_SYMBOL_CATALOG", "path": "src/foo.py", "title": "bar", "content": "", "metadata": {"symbol_id": "s1"}},
    ]
    report = {"executed_layers": ["C0_SOURCE_CHUNKS", "C1_SYMBOL_CATALOG"], "fallback": {"used": False}}
    sym = {"status": "resolved", "resolved_symbol": "bar"}
    result = build_retrieval_result(raw, report, sym)
    assert len(result.code_chunks) == 2
    assert result.resolved_symbol == "bar"
    assert result.symbol_resolution_status == "resolved"
    assert any(c.layer == "C1_SYMBOL_CATALOG" for c in result.code_chunks)
    assert len(result.layer_outcomes) == 2


def test_evidence_gate_open_file_sufficient() -> None:
    """OPEN_FILE with resolved path and chunks passes evidence gate."""
    chunk = CodeChunkItem(
        layer="C0_SOURCE_CHUNKS",
        path="src/context.py",
        title="",
        content="x",
        start_line=1,
        end_line=2,
        metadata={},
    )
    bundle = EvidenceBundle(
        resolved_sub_intent="OPEN_FILE",
        resolved_target="src/context.py",
        file_candidates=["src/context.py"],
        evidence_count=2,
        code_chunks=[chunk, chunk],
    )
    decision = evaluate_evidence(bundle)
    assert decision.passed is True
    assert not decision.failure_reasons


def test_evidence_gate_open_file_insufficient() -> None:
    """OPEN_FILE with no path and no chunks fails evidence gate."""
    bundle = EvidenceBundle(
        resolved_sub_intent="OPEN_FILE",
        resolved_target=None,
        file_candidates=[],
        evidence_count=0,
    )
    decision = evaluate_evidence(bundle)
    assert decision.passed is False
    assert "path_scope_empty" in decision.failure_reasons or "layer_c0_empty" in decision.failure_reasons
    assert decision.degraded_message


def test_evidence_gate_explain_insufficient() -> None:
    """EXPLAIN with no symbol resolution and few chunks fails."""
    bundle = EvidenceBundle(
        resolved_sub_intent="EXPLAIN",
        resolved_target=None,
        target_type="symbol",
        evidence_count=1,
    )
    decision = evaluate_evidence(bundle)
    assert decision.passed is False
    assert "insufficient_evidence" in decision.failure_reasons or "target_not_resolved" in decision.failure_reasons


def test_evidence_gate_find_entrypoints_insufficient() -> None:
    """FIND_ENTRYPOINTS with no entrypoints fails."""
    bundle = EvidenceBundle(
        resolved_sub_intent="FIND_ENTRYPOINTS",
        entrypoints=[],
        evidence_count=0,
    )
    decision = evaluate_evidence(bundle)
    assert decision.passed is False
    assert "entrypoints_not_found" in decision.failure_reasons


def test_evidence_gate_find_tests_insufficient() -> None:
    """FIND_TESTS with no test candidates fails."""
    bundle = EvidenceBundle(
        resolved_sub_intent="FIND_TESTS",
        resolved_target="Context",
        target_symbol_candidates=["Context"],
        test_evidence=[],
        evidence_count=1,
    )
    decision = evaluate_evidence(bundle)
    assert decision.passed is False
    assert "tests_not_found" in decision.failure_reasons


def test_diagnostics_report_has_failure_reasons() -> None:
    """Diagnostics report includes machine-readable failure reasons."""
    from app.modules.rag.code_qa_pipeline.diagnostics import build_diagnostics_report
    from app.modules.rag.code_qa_pipeline.evidence_bundle_builder import build_evidence_bundle

    router = _make_router()
    text = "Где тесты для НесуществующийКласс?"
    result = router.route(text, ConversationState(), repo_context())
    request = build_retrieval_request(result, "test-session")
    retrieval_result = build_retrieval_result([], {}, {"status": "not_found"})
    bundle = build_evidence_bundle(retrieval_result, result)
    evaluate_evidence(bundle)
    report = build_diagnostics_report(
        router_result=result,
        retrieval_request=request,
        retrieval_result=retrieval_result,
        evidence_bundle=bundle,
        answer_mode="degraded",
    )
    assert report.answer_mode in ("normal", "degraded", "insufficient")
    assert report.per_layer_outcome is not None
    assert report.router_result
    assert report.retrieval_request


def test_canonical_pipeline_with_fake_adapter() -> None:
    """Canonical pipeline runs end-to-end with fake adapter; diagnostics and evidence gate applied."""

    class FakeAdapter:
        def __init__(self, rows: list[dict]) -> None:
            self._rows = rows
            self._report: dict | None = None

        def retrieve_with_plan(self, rag_session_id: str, query: str, retrieval_spec, retrieval_constraints=None, *, query_plan=None) -> list[dict]:
            layers = [str(q.layer_id) for q in (retrieval_spec.layer_queries or [])]
            self._report = {"executed_layers": layers, "fallback": {"used": False}, "retrieval_by_layer_ms": {}}
            return list(self._rows)

        def retrieve_exact_files(self, rag_session_id: str, *, repo_id=None, paths: list, layers=None, limit=200, query="", ranking_profile="") -> list[dict]:
            self._report = {"executed_layers": list(layers or ["C0_SOURCE_CHUNKS"]), "fallback": {"used": False}}
            return [r for r in self._rows if r.get("path") in paths]

        def hydrate_resolved_symbol_sources(self, rag_session_id: str, base_query: str, rag_rows: list, symbol_resolution: dict, retrieval_spec, retrieval_constraints=None) -> list[dict]:
            return list(rag_rows)

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

        def consume_retrieval_report(self) -> dict | None:
            r, self._report = self._report, None
            return r

    two_chunks = [
        {"layer": "C0_SOURCE_CHUNKS", "path": "src/context.py", "content": "class Context:", "span_start": 1, "span_end": 2, "metadata": {}},
        {"layer": "C0_SOURCE_CHUNKS", "path": "src/context.py", "content": "def run(self): pass", "span_start": 3, "span_end": 4, "metadata": {}},
    ]
    adapter = FakeAdapter(two_chunks)
    router = _make_router()
    runner = CodeQAPipelineRunner(router=router, retrieval_adapter=adapter, repo_context=repo_context())
    result = runner.run("Открой файл src/context.py", "test-session", run_retrieval=True)
    assert result.router_result is not None
    assert result.retrieval_request is not None
    assert result.retrieval_result is not None
    assert result.evidence_bundle is not None
    assert result.diagnostics_report is not None
    assert result.answer_mode in ("normal", "degraded", "insufficient")
    assert result.retrieval_result.code_chunks
    assert result.diagnostics_report.router_result
    assert result.diagnostics_report.retrieval_request
