"""Q&A: 8-dim cache_key, falsification-aware lookup, audit chain.

Mock client only — no network. Verifies the v9.8 admissibility invariant
end-to-end.
"""

from __future__ import annotations

import hashlib
from typing import Iterator

from aborist.document import Document
from aborist.ingest import ingest_source
from aborist.merkle import proof_from_dict, verify_proof
from aborist.qa import ask
from aborist.qa.client import StubClient
from aborist.qa.keys import (
    cache_key,
    conversation_hash,
    governance_policy_hash,
    model_profile_hash,
    question_hash,
)
from aborist.source import Source
from aborist.store import connect


class FakeSource(Source):
    source_type = "test"

    def __init__(self, docs: list[Document]):
        self.docs = docs

    def iter_documents(self) -> Iterator[Document]:
        yield from self.docs


def _doc(uri: str, content: str) -> Document:
    return Document(uri=uri, content=content, source_type="test", title=uri)


LONG = (
    "The eight forms of capital include living, social, and intellectual. " * 10
    + "Merkle providence proves answer derives from a specific source. " * 10
)


def _ingest_one(conn) -> str:
    ingest_source(conn, FakeSource([_doc("test://qa", LONG)]))
    return conn.execute(
        "SELECT document_root FROM documents WHERE document_uri='test://qa'"
    ).fetchone()["document_root"]


def test_ask_writes_record_with_strict_proof(tmp_path):
    db = tmp_path / "qa.db"
    conn = connect(db)
    try:
        root = _ingest_one(conn)
        # Verbatim quote from LONG → audit_mode=STRICT.
        client = StubClient(
            answer=(
                'The document states: '
                '"eight forms of capital include living, social, and intellectual"'
            )
        )
        result = ask(
            conn,
            document_root=root,
            question="What are the forms of capital?",
            client=client,
            model_id="test-model",
            revision="r1",
            quantization="fp8",
        )
        assert result["status"] == "cache_miss_then_written"
        assert result["audit_mode"] == "STRICT"
        assert result["n_quotes"] == 1
        assert result["n_verified"] == 1
        assert result["unverified_quotes"] == []

        # Stored proof reconstructs the source root.
        proof = proof_from_dict(result["merkle_proof"]["chunk_0_proof"])
        assert verify_proof(proof)
        assert proof.root.hex() == root

        # Providence cache row landed with correct fields.
        row = conn.execute("SELECT * FROM providence_cache").fetchone()
        assert row["source_root"] == root
        assert row["falsification_state"] == "live"
        assert row["chain"] == "private"
        assert row["audit_event_hash"] is not None

        # Audit chain has providence_write event.
        last = conn.execute(
            "SELECT event_type FROM audit_events ORDER BY seq DESC LIMIT 1"
        ).fetchone()["event_type"]
        assert last == "providence_write"
    finally:
        conn.close()


def test_ask_cache_hit_does_not_call_client(tmp_path):
    db = tmp_path / "hit.db"
    conn = connect(db)
    try:
        root = _ingest_one(conn)
        client = StubClient(answer="first answer")
        ask(conn, document_root=root, question="Q1?", client=client, model_id="m")
        assert len(client.calls) == 1

        # Second ask with identical inputs -> hit, no client call.
        result = ask(
            conn, document_root=root, question="Q1?", client=client, model_id="m"
        )
        assert result["status"] == "cache_hit"
        assert result["answer_text"] == "first answer"
        assert len(client.calls) == 1  # unchanged

        row = conn.execute(
            "SELECT hit_count FROM providence_cache"
        ).fetchone()
        assert row["hit_count"] == 1
    finally:
        conn.close()


def test_different_model_yields_different_cache_key(tmp_path):
    db = tmp_path / "model.db"
    conn = connect(db)
    try:
        root = _ingest_one(conn)
        client = StubClient(answer="ans")
        ask(conn, document_root=root, question="Q?", client=client, model_id="A")
        ask(conn, document_root=root, question="Q?", client=client, model_id="B")
        n = conn.execute("SELECT COUNT(*) FROM providence_cache").fetchone()[0]
        assert n == 2
        assert len(client.calls) == 2
    finally:
        conn.close()


def test_falsification_skips_cache_hit(tmp_path):
    """Stale records are ignored even when the 8-dim key matches."""
    db = tmp_path / "stale.db"
    conn = connect(db)
    try:
        root = _ingest_one(conn)
        client = StubClient(answer="first")
        first = ask(
            conn, document_root=root, question="Q?", client=client, model_id="m"
        )
        ckey = first["cache_key"]
        # Mark stale.
        conn.execute(
            "UPDATE providence_cache SET falsification_state='stale' "
            "WHERE cache_key=?",
            (ckey,),
        )
        # Asking again must MISS and call client; new record inserted (or
        # rejected on PRIMARY KEY collision since cache_key is the PK).
        try:
            ask(conn, document_root=root, question="Q?", client=client, model_id="m")
            # If accepted, we'd have 2 records — but PK collision will throw.
            assert False, "expected PK collision on stale-then-write"
        except Exception:  # IntegrityError from cache_key PK
            pass

        # Falsified record stays stale; no fresh record landed.
        rows = conn.execute(
            "SELECT cache_key, falsification_state FROM providence_cache"
        ).fetchall()
        assert len(rows) == 1
        assert rows[0]["falsification_state"] == "stale"
        assert len(client.calls) == 2  # second call did happen
    finally:
        conn.close()


def test_unknown_document(tmp_path):
    db = tmp_path / "u.db"
    conn = connect(db)
    try:
        result = ask(
            conn,
            document_root="00" * 32,
            question="Q?",
            client=StubClient(),
            model_id="m",
        )
        assert result["status"] == "unknown_document"
    finally:
        conn.close()


def test_cold_source_refuses(tmp_path):
    """Source must be hot — answer derived from evicted content can't be proved."""
    from aborist.evict import evict_to_cold

    db = tmp_path / "cold.db"
    conn = connect(db)
    try:
        root = _ingest_one(conn)
        evict_to_cold(conn)
        result = ask(
            conn,
            document_root=root,
            question="Q?",
            client=StubClient(),
            model_id="m",
        )
        assert result["status"] == "source_cold"
    finally:
        conn.close()


def test_cache_key_is_pure_function():
    """Hashes are deterministic; manual computation matches the runner."""
    src_root = "a" * 64
    qh = question_hash("What is X?")
    mh = model_profile_hash("m", "r", "q")
    msg = [{"role": "user", "content": "x"}]
    ch = conversation_hash(msg)
    gh = governance_policy_hash({"temperature": 0.1})
    k1 = cache_key(src_root, qh, mh, ch, gh, "v1", "v1", "v1")
    k2 = cache_key(src_root, qh, mh, ch, gh, "v1", "v1", "v1")
    assert k1 == k2
    # Bumping any dim changes the key.
    k3 = cache_key(src_root, qh, mh, ch, gh, "v2", "v1", "v1")
    assert k3 != k1
    # Pure SHA-256 of the joined string.
    expected = hashlib.sha256(
        "|".join([src_root, qh, mh, ch, gh, "v1", "v1", "v1"]).encode()
    ).hexdigest()
    assert k1 == expected
