"""Burn — delete a providence_cache leaf with no children.

Burn vs falsify:
  falsify    keeps the row, flips falsification_state away from 'live'.
             Audit event = 'falsify'. History is preserved.
  burn       deletes the row entirely. Audit event = 'providence_burn'.
             Refuses if any falsifications reference this cache_key.

Burn is the kindergarten-stage operator: scratch work that built up
during a tree's genesis, before downstream consumers ingested it. Once
falsifications point at a record, burning would orphan them — the
default is to refuse, --force overrides for cleanup edge cases.
"""

from __future__ import annotations

import json
import time

import pytest

from aborist.cli import _burn_cache_key
from aborist.store import append_audit, connect, transaction


def _seed_providence_record(db_path, *, cache_key: str, question: str = "q?", audit_mode: str = "HYBRID") -> None:
    """Insert one minimal providence_cache row + a 'providence_query' audit
    event so the chain has prior state. Mirrors what runner.ask() persists."""
    conn = connect(db_path)
    try:
        with transaction(conn):
            event_hash = append_audit(
                conn,
                event_type="providence_query",
                subject_root=cache_key,
                body={"cache_key": cache_key, "question": question},
            )
            conn.execute(
                "INSERT INTO providence_cache "
                "(cache_key, source_root, document_uri, question_hash, question_text, "
                " answer_text, merkle_proof, model_profile_hash, conversation_hash, "
                " governance_policy_hash, schema_version, canonicalization_version, "
                " chunking_version, falsification_state, chain, audit_event_hash, "
                " created_at, hit_count, audit_mode, n_quotes, n_verified, "
                " unverified_quotes, verifier_method) "
                "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'live', 'private', ?, ?, 0, ?, 1, 1, NULL, 'quote')",
                (
                    cache_key,
                    "00" * 32,
                    "https://example.com/doc",
                    "qh",
                    question,
                    "answer",
                    json.dumps({}),
                    "mp",
                    "ch",
                    "gh",
                    "v9.8.0",
                    "norm-v1",
                    "tok-512-v1",
                    event_hash,
                    int(time.time()),
                    audit_mode,
                ),
            )
    finally:
        conn.close()


def _seed_falsification(db_path, *, cache_key: str) -> None:
    """Mark cache_key as falsified, leaving a falsifications row that makes
    the leaf "have children" for burn's purposes."""
    conn = connect(db_path)
    try:
        with transaction(conn):
            event_hash = append_audit(
                conn,
                event_type="falsify",
                subject_root=cache_key,
                body={"cache_key": cache_key, "to_state": "failed", "reason": "test"},
            )
            conn.execute(
                "INSERT INTO falsifications "
                "(cache_key, state, reason, by_actor, at, audit_event_hash) "
                "VALUES (?, 'failed', 'test', 'tester', ?, ?)",
                (cache_key, int(time.time()), event_hash),
            )
    finally:
        conn.close()


# ---------------------------------------------------------------------------
# Happy path
# ---------------------------------------------------------------------------


def test_burn_removes_row_and_writes_audit_event(tmp_path):
    db = tmp_path / "qa.db"
    KEY = "ab" * 32
    _seed_providence_record(db, cache_key=KEY, audit_mode="UNGROUNDED")

    result = _burn_cache_key(
        KEY, reason="kindergarten cleanup", by_actor="alice",
        shards_dir=None, db_path=db,
    )
    assert result["status"] == "burned"
    assert result["cache_key"] == KEY
    assert result["burned_audit_mode"] == "UNGROUNDED"
    assert result["reason"] == "kindergarten cleanup"
    assert result["by_actor"] == "alice"
    assert isinstance(result["audit_event_hash"], str) and len(result["audit_event_hash"]) == 64

    conn = connect(db)
    try:
        gone = conn.execute(
            "SELECT 1 FROM providence_cache WHERE cache_key = ?", (KEY,)
        ).fetchone()
        last = conn.execute(
            "SELECT event_type, body, subject_root FROM audit_events "
            "ORDER BY seq DESC LIMIT 1"
        ).fetchone()
    finally:
        conn.close()
    assert gone is None
    assert last["event_type"] == "providence_burn"
    assert last["subject_root"] == KEY
    body = json.loads(last["body"])
    assert body["cache_key"] == KEY
    assert body["burned_audit_mode"] == "UNGROUNDED"
    assert body["reason"] == "kindergarten cleanup"
    assert body["by_actor"] == "alice"
    assert body["forced"] is False


# ---------------------------------------------------------------------------
# Children gate
# ---------------------------------------------------------------------------


def test_burn_refuses_when_falsifications_exist(tmp_path):
    """A leaf with children must be falsified, not burned. Refusal preserves
    the row AND skips the audit event so callers can retry cleanly."""
    db = tmp_path / "qa.db"
    KEY = "cd" * 32
    _seed_providence_record(db, cache_key=KEY)
    _seed_falsification(db, cache_key=KEY)

    conn = connect(db)
    try:
        events_before = conn.execute("SELECT COUNT(*) FROM audit_events").fetchone()[0]
    finally:
        conn.close()

    result = _burn_cache_key(
        KEY, reason="oops", by_actor="alice", shards_dir=None, db_path=db,
    )
    assert result["status"] == "refused_has_children"
    assert result["child_falsifications"] == 1
    assert "hint" in result

    conn = connect(db)
    try:
        still_there = conn.execute(
            "SELECT cache_key FROM providence_cache WHERE cache_key = ?", (KEY,)
        ).fetchone()
        events_after = conn.execute("SELECT COUNT(*) FROM audit_events").fetchone()[0]
    finally:
        conn.close()
    assert still_there is not None
    # Refusal must NOT write a providence_burn event — the chain only records
    # actions actually taken.
    assert events_after == events_before


def test_burn_force_overrides_children_gate(tmp_path):
    """--force burns even with falsifications; audit event marks forced=True."""
    db = tmp_path / "qa.db"
    KEY = "ef" * 32
    _seed_providence_record(db, cache_key=KEY)
    _seed_falsification(db, cache_key=KEY)

    result = _burn_cache_key(
        KEY, reason="forced cleanup", by_actor="alice",
        shards_dir=None, db_path=db, force=True,
    )
    assert result["status"] == "burned"

    conn = connect(db)
    try:
        gone = conn.execute(
            "SELECT 1 FROM providence_cache WHERE cache_key = ?", (KEY,)
        ).fetchone()
        last = conn.execute(
            "SELECT body FROM audit_events ORDER BY seq DESC LIMIT 1"
        ).fetchone()
    finally:
        conn.close()
    assert gone is None
    body = json.loads(last["body"])
    assert body["forced"] is True
    assert body["child_falsifications_at_burn"] == 1


# ---------------------------------------------------------------------------
# Error paths
# ---------------------------------------------------------------------------


def test_burn_unknown_key_returns_not_found(tmp_path):
    db = tmp_path / "qa.db"
    # Empty DB; no records.
    result = _burn_cache_key(
        "00" * 32, reason="", by_actor="alice", shards_dir=None, db_path=db,
    )
    assert result["status"] == "not_found"
    assert result["cache_key"] == "00" * 32


# ---------------------------------------------------------------------------
# Audit chain integrity
# ---------------------------------------------------------------------------


def test_burn_preserves_audit_chain(tmp_path):
    """After burn, the chain has no dangling prev_event_hash references —
    same property `make chain-check` enforces."""
    db = tmp_path / "qa.db"
    KEY = "12" * 32
    _seed_providence_record(db, cache_key=KEY)

    _burn_cache_key(KEY, reason="t", by_actor="a", shards_dir=None, db_path=db)

    conn = connect(db)
    try:
        breaks = conn.execute(
            """
            SELECT COUNT(*) FROM audit_events a1
            LEFT JOIN audit_events a2 ON a2.event_hash = a1.prev_event_hash
            WHERE a1.prev_event_hash IS NOT NULL AND a2.event_hash IS NULL
            """
        ).fetchone()[0]
    finally:
        conn.close()
    assert breaks == 0


# ---------------------------------------------------------------------------
# CLI integration
# ---------------------------------------------------------------------------


def test_burn_cli_invocation(tmp_path, capsys):
    """`aborist burn` end-to-end through build_parser, including JSON output."""
    from aborist.cli import build_parser

    db = tmp_path / "qa.db"
    KEY = "fa" * 32
    _seed_providence_record(db, cache_key=KEY)

    parser = build_parser()
    args = parser.parse_args([
        "--db", str(db),
        "burn",
        "--cache-key", KEY,
        "--reason", "via cli",
        "--by-actor", "alice",
    ])
    rc = args.func(args)
    assert rc == 0

    captured = capsys.readouterr()
    payload = json.loads(captured.out)
    assert payload["status"] == "burned"
    assert payload["reason"] == "via cli"


def test_burn_kindergarten_burns_only_recent_rows(tmp_path, capsys):
    """Mass-burn rows younger than the window. Older rows are kept; the
    test seeds two rows, backdates one to 2h ago, and asserts the
    fresh one burns and the old one stays."""
    import time as _time
    from aborist.cli import build_parser
    from aborist.store import transaction

    db = tmp_path / "qa.db"
    OLD = "ab" * 32
    FRESH = "cd" * 32
    _seed_providence_record(db, cache_key=OLD, question="old", audit_mode="STRICT")
    _seed_providence_record(db, cache_key=FRESH, question="fresh", audit_mode="STRICT")

    # Backdate OLD to 2h ago.
    long_ago = int(_time.time()) - 7200
    conn = connect(db)
    try:
        with transaction(conn):
            conn.execute(
                "UPDATE providence_cache SET created_at = ? WHERE cache_key = ?",
                (long_ago, OLD),
            )
    finally:
        conn.close()

    parser = build_parser()
    args = parser.parse_args([
        "--db", str(db),
        "burn-kindergarten",
        "--kindergarten-seconds", "3600",
    ])
    rc = args.func(args)
    assert rc == 0
    payload = json.loads(capsys.readouterr().out)
    assert payload["status"] == "burned"
    assert payload["examined"] == 1   # only FRESH met the cutoff
    assert payload["burned"] == 1

    conn = connect(db)
    try:
        rows = {r["cache_key"]: r for r in conn.execute(
            "SELECT cache_key FROM providence_cache"
        ).fetchall()}
    finally:
        conn.close()
    assert OLD in rows
    assert FRESH not in rows


def test_burn_kindergarten_dry_run_writes_nothing(tmp_path, capsys):
    """--dry-run reports what would burn but doesn't write or audit."""
    from aborist.cli import build_parser

    db = tmp_path / "qa.db"
    KEY = "ef" * 32
    _seed_providence_record(db, cache_key=KEY)
    conn = connect(db)
    try:
        events_before = conn.execute(
            "SELECT COUNT(*) FROM audit_events"
        ).fetchone()[0]
    finally:
        conn.close()

    parser = build_parser()
    args = parser.parse_args([
        "--db", str(db),
        "burn-kindergarten",
        "--kindergarten-seconds", "3600",
        "--dry-run",
    ])
    rc = args.func(args)
    assert rc == 0
    payload = json.loads(capsys.readouterr().out)
    assert payload["status"] == "dry_run"
    assert payload["examined"] == 1
    assert payload["burned"] == 0
    # Row still exists; audit chain unchanged.
    conn = connect(db)
    try:
        still_there = conn.execute(
            "SELECT 1 FROM providence_cache WHERE cache_key=?", (KEY,)
        ).fetchone()
        events_after = conn.execute(
            "SELECT COUNT(*) FROM audit_events"
        ).fetchone()[0]
    finally:
        conn.close()
    assert still_there is not None
    assert events_after == events_before


def test_burn_kindergarten_zero_seconds_burns_everything(tmp_path, capsys):
    """0-second window = burn every live providence_cache row."""
    from aborist.cli import build_parser

    db = tmp_path / "qa.db"
    K1 = "11" * 32
    K2 = "22" * 32
    _seed_providence_record(db, cache_key=K1, question="q1")
    _seed_providence_record(db, cache_key=K2, question="q2")

    parser = build_parser()
    args = parser.parse_args([
        "--db", str(db),
        "burn-kindergarten",
        "--kindergarten-seconds", "0",
    ])
    rc = args.func(args)
    assert rc == 0
    payload = json.loads(capsys.readouterr().out)
    assert payload["burned"] == 2

    conn = connect(db)
    try:
        remaining = conn.execute("SELECT COUNT(*) FROM providence_cache").fetchone()[0]
    finally:
        conn.close()
    assert remaining == 0


def test_burn_kindergarten_respects_children_gate_without_force(tmp_path, capsys):
    """A row with falsifications is REFUSED unless --force. Audit chain
    still grows on burned rows; refused rows leave no audit entry."""
    from aborist.cli import build_parser

    db = tmp_path / "qa.db"
    BURNABLE = "33" * 32
    HAS_KIDS = "44" * 32
    _seed_providence_record(db, cache_key=BURNABLE)
    _seed_providence_record(db, cache_key=HAS_KIDS)
    _seed_falsification(db, cache_key=HAS_KIDS)

    parser = build_parser()
    args = parser.parse_args([
        "--db", str(db),
        "burn-kindergarten",
        "--kindergarten-seconds", "3600",
    ])
    rc = args.func(args)
    assert rc == 0
    payload = json.loads(capsys.readouterr().out)
    assert payload["burned"] == 1
    assert payload["refused_has_children"] == 1


def test_burn_cli_returns_non_zero_on_refused(tmp_path, capsys):
    """Non-zero exit when refused so scripts can detect and react."""
    from aborist.cli import build_parser

    db = tmp_path / "qa.db"
    KEY = "fb" * 32
    _seed_providence_record(db, cache_key=KEY)
    _seed_falsification(db, cache_key=KEY)

    parser = build_parser()
    args = parser.parse_args([
        "--db", str(db),
        "burn",
        "--cache-key", KEY,
        "--reason", "wont land",
    ])
    rc = args.func(args)
    assert rc == 1
    payload = json.loads(capsys.readouterr().out)
    assert payload["status"] == "refused_has_children"
