from __future__ import annotations

from dataclasses import dataclass
from types import SimpleNamespace

from tests.pipeline_setup.suite_02_pipeline.pipeline_intent_rag.helpers.rag_db_adapter import RagDbAdapter


@dataclass(slots=True)
class _LayerQuery:
    layer_id: str
    top_k: int


class _FakeDimResolver:
    def resolve(self, rag_session_id: str) -> int:  # noqa: ARG002
        return 4


class _RecordingRepository:
    def __init__(self) -> None:
        self.retrieve_calls: list[dict] = []
        self.retrieve_lexical_calls: list[dict] = []
        self.retrieve_exact_calls: list[dict] = []

    def retrieve(self, rag_session_id: str, query_embedding: list[float], **kwargs) -> list[dict]:  # noqa: ARG002
        self.retrieve_calls.append(kwargs)
        layer = list(kwargs.get("layers") or [""])[0]
        if layer == "C1_SYMBOL_CATALOG":
            return [
                {
                    "path": "src/context.py",
                    "content": "class Context",
                    "layer": layer,
                    "title": "Context",
                    "span_start": 10,
                    "span_end": 20,
                    "metadata": {"symbol_id": "sym-1", "qname": "Context", "kind": "class"},
                },
                {
                    "path": "src/context.py",
                    "content": "class Context duplicate",
                    "layer": layer,
                    "title": "Context",
                    "span_start": 10,
                    "span_end": 20,
                    "metadata": {"symbol_id": "sym-1", "qname": "Context", "kind": "class"},
                },
            ]
        if layer == "C0_SOURCE_CHUNKS":
            return [
                {
                    "path": "src/context.py",
                    "content": "class Context: ...",
                    "layer": layer,
                    "title": "src/context.py:Context",
                    "span_start": 10,
                    "span_end": 30,
                    "metadata": {"chunk_index": 0, "blob_sha": "blob-1"},
                },
                {
                    "path": "src/context.py",
                    "content": "class Context: ... duplicate",
                    "layer": layer,
                    "title": "src/context.py:Context",
                    "span_start": 10,
                    "span_end": 30,
                    "metadata": {"chunk_index": 1, "blob_sha": "blob-1"},
                },
            ]
        if layer == "C2_DEPENDENCY_GRAPH":
            return [
                {
                    "path": "src/context.py",
                    "content": "Context.set writes_attr Context.data",
                    "layer": layer,
                    "title": "Context.set:writes_attr",
                    "span_start": 40,
                    "span_end": 40,
                    "metadata": {"edge_id": "edge-1"},
                }
            ]
        if layer == "C4_SEMANTIC_ROLES":
            return [
                {
                    "path": "src/context.py",
                    "content": "Context\nrole: pipeline_stage\n\nResponsibilities:\n- reads and writes state attributes",
                    "layer": layer,
                    "title": "Context",
                    "span_start": 10,
                    "span_end": 30,
                    "lexical_rank": 0,
                    "structural_rank": 10,
                    "metadata": {"symbol_name": "Context", "qname": "Context", "role": "pipeline_stage"},
                },
                {
                    "path": "src/control.py",
                    "content": "ControlChannel\nrole: model\n\nResponsibilities:\n- default model role",
                    "layer": layer,
                    "title": "ControlChannel",
                    "span_start": 1,
                    "span_end": 20,
                    "lexical_rank": 3,
                    "structural_rank": 40,
                    "metadata": {"symbol_name": "ControlChannel", "qname": "ControlChannel", "role": "model"},
                },
            ]
        return []

    def retrieve_lexical_code(self, rag_session_id: str, **kwargs) -> list[dict]:  # noqa: ARG002
        self.retrieve_lexical_calls.append(kwargs)
        return []

    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,
    ) -> list[dict]:  # noqa: ARG002
        self.retrieve_exact_calls.append({"paths": list(paths), "layers": list(layers or []), "limit": limit})
        return [
            {
                "path": "src/context.py",
                "content": "class Context: ...",
                "layer": "C0_SOURCE_CHUNKS",
                "title": "src/context.py:Context",
                "span_start": 10,
                "span_end": 30,
                "metadata": {"chunk_index": 0, "blob_sha": "blob-1"},
            },
            {
                "path": "src/context.py",
                "content": "class Context: duplicate",
                "layer": "C0_SOURCE_CHUNKS",
                "title": "src/context.py:Context",
                "span_start": 10,
                "span_end": 30,
                "metadata": {"chunk_index": 1, "blob_sha": "blob-1"},
            },
        ]


def _spec() -> SimpleNamespace:
    return SimpleNamespace(
        layer_queries=[
            _LayerQuery(layer_id="C1_SYMBOL_CATALOG", top_k=2),
            _LayerQuery(layer_id="C0_SOURCE_CHUNKS", top_k=3),
            _LayerQuery(layer_id="C2_DEPENDENCY_GRAPH", top_k=1),
        ],
        filters=SimpleNamespace(path_scope=[], test_policy="EXCLUDE"),
    )


def _constraints() -> SimpleNamespace:
    return SimpleNamespace(
        include_globs=["src/**"],
        exclude_globs=["tests/**"],
        prefer_globs=["tests/**"],
        test_file_globs=[],
        test_symbol_patterns=[],
    )


def test_retrieve_applies_per_layer_budget_and_dedupe() -> None:
    repository = _RecordingRepository()
    adapter = RagDbAdapter(repository=repository, dim_resolver=_FakeDimResolver())

    rows = adapter.retrieve("rag-1", "Explain Context", _spec(), _constraints())

    assert [call["limit"] for call in repository.retrieve_calls[:3]] == [2, 3, 1]
    assert repository.retrieve_calls[0]["layers"] == ["C1_SYMBOL_CATALOG"]
    assert repository.retrieve_calls[1]["layers"] == ["C0_SOURCE_CHUNKS"]
    assert repository.retrieve_calls[2]["layers"] == ["C2_DEPENDENCY_GRAPH"]
    assert repository.retrieve_calls[0]["prefer_path_prefixes"] == ["tests"]
    assert len([row for row in rows if row["layer"] == "C1_SYMBOL_CATALOG"]) == 1
    assert len([row for row in rows if row["layer"] == "C0_SOURCE_CHUNKS"]) == 1
    assert len([row for row in rows if row["layer"] == "C2_DEPENDENCY_GRAPH"]) == 1


def test_hydrate_resolved_symbol_adds_overlapping_c0_chunk() -> None:
    repository = _RecordingRepository()
    adapter = RagDbAdapter(repository=repository, dim_resolver=_FakeDimResolver())
    rag_rows = [
        {
            "path": "src/context.py",
            "content": "class Context",
            "layer": "C1_SYMBOL_CATALOG",
            "title": "Context",
            "span_start": 10,
            "span_end": 20,
            "metadata": {"symbol_id": "sym-1", "qname": "Context", "blob_sha": "blob-1"},
        }
    ]
    spec = _spec()
    constraints = _constraints()

    rows = adapter.hydrate_resolved_symbol_sources(
        rag_session_id="rag-1",
        base_query="Explain Context",
        rag_rows=rag_rows,
        symbol_resolution={
            "status": "resolved",
            "resolved_symbol": "Context",
            "alternatives": ["Context", "Context.set"],
        },
        retrieval_spec=spec,
        retrieval_constraints=constraints,
    )

    c0_rows = [row for row in rows if row["layer"] == "C0_SOURCE_CHUNKS"]
    assert len(c0_rows) == 1
    assert c0_rows[0]["path"] == "src/context.py"


def test_retrieve_exact_files_keeps_exact_scope_and_disables_fallback() -> None:
    repository = _RecordingRepository()
    adapter = RagDbAdapter(repository=repository, dim_resolver=_FakeDimResolver())

    rows = adapter.retrieve_exact_files(
        "rag-1",
        paths=["./src//context.py"],
        layers=["C0_SOURCE_CHUNKS"],
        limit=200,
        query="open file",
        ranking_profile="code",
    )
    report = adapter.consume_retrieval_report()

    assert repository.retrieve_exact_calls[0]["paths"] == ["src/context.py"]
    assert repository.retrieve_exact_calls[0]["layers"] == ["C0_SOURCE_CHUNKS"]
    assert len(rows) == 1
    assert rows[0]["path"] == "src/context.py"
    assert report is not None
    assert report["fallback"]["used"] is False
    assert report["applied"][0]["effective_path_scope"] == ["src/context.py"]


def test_explain_symbol_candidates_use_symbol_query_and_symbol_search_mode() -> None:
    repository = _RecordingRepository()
    adapter = RagDbAdapter(repository=repository, dim_resolver=_FakeDimResolver())
    spec = _spec()
    query_plan = SimpleNamespace(sub_intent="EXPLAIN", symbol_candidates=["Context"], symbol_kind_hint="class")

    rows = adapter.retrieve_with_plan(
        "rag-1",
        "Объясни как работает Context",
        spec,
        _constraints(),
        query_plan=query_plan,
    )
    report = adapter.consume_retrieval_report()

    assert rows
    assert repository.retrieve_calls[0]["query_text"] == "Context"
    assert report is not None
    assert report["retrieval_mode_by_layer"]["C1_SYMBOL_CATALOG"] == "symbol_search"


def test_explain_symbol_mode_disables_scope_relaxation_fallback() -> None:
    class _NoHitRepository(_RecordingRepository):
        def retrieve(self, rag_session_id: str, query_embedding: list[float], **kwargs) -> list[dict]:  # noqa: ARG002
            self.retrieve_calls.append(kwargs)
            if kwargs.get("path_prefixes") is not None:
                return []
            return [{"path": "src/other.py", "layer": "C1_SYMBOL_CATALOG", "title": "Other", "metadata": {"kind": "class"}}]

    repository = _NoHitRepository()
    adapter = RagDbAdapter(repository=repository, dim_resolver=_FakeDimResolver())
    spec = SimpleNamespace(
        layer_queries=[_LayerQuery(layer_id="C1_SYMBOL_CATALOG", top_k=2)],
        filters=SimpleNamespace(path_scope=["src/context.py"], test_policy="EXCLUDE"),
        rerank_profile="code",
    )
    query_plan = SimpleNamespace(sub_intent="EXPLAIN", symbol_candidates=["Context"], symbol_kind_hint="class")

    rows = adapter.retrieve_with_plan("rag-1", "Explain Context", spec, _constraints(), query_plan=query_plan)
    report = adapter.consume_retrieval_report()

    assert rows == []
    assert len(repository.retrieve_calls) == 1
    assert repository.retrieve_calls[0]["path_prefixes"] == ["src/context.py"]
    assert report is not None


def test_explain_symbol_mode_uses_symbol_query_for_non_c1_layers_and_filters_c4_noise() -> None:
    repository = _RecordingRepository()
    adapter = RagDbAdapter(repository=repository, dim_resolver=_FakeDimResolver())
    spec = SimpleNamespace(
        layer_queries=[
            _LayerQuery(layer_id="C1_SYMBOL_CATALOG", top_k=2),
            _LayerQuery(layer_id="C4_SEMANTIC_ROLES", top_k=4),
        ],
        filters=SimpleNamespace(path_scope=[], test_policy="EXCLUDE"),
        rerank_profile="code",
    )
    query_plan = SimpleNamespace(sub_intent="EXPLAIN", symbol_candidates=["Context"], symbol_kind_hint="class")

    rows = adapter.retrieve_with_plan("rag-1", "Объясни как работает Context", spec, _constraints(), query_plan=query_plan)
    report = adapter.consume_retrieval_report()

    c4_calls = [call for call in repository.retrieve_calls if call["layers"] == ["C4_SEMANTIC_ROLES"]]
    c4_rows = [row for row in rows if row["layer"] == "C4_SEMANTIC_ROLES"]

    assert c4_calls
    assert c4_calls[0]["query_text"] == "Context"
    assert len(c4_rows) == 1
    assert c4_rows[0]["title"] == "Context"
    assert report is not None
    assert report["fallback"]["used"] is False
