"""Unit tests for scripts/apply_proposal.py.

Pure tests: exercise the SQL builders, payload validation, and dispatch without
a live database. Runnable under pytest or standalone (`python3 tests/test_apply_proposal.py`).
"""

import json
import re
import sys
from pathlib import Path

try:
    import pytest
except ImportError:  # standalone fallback so the file runs without pytest installed
    import contextlib
    import types

    pytest = types.SimpleNamespace()

    @contextlib.contextmanager
    def _raises(exc):
        try:
            yield
        except exc:
            return
        raise AssertionError(f"expected {exc.__name__} to be raised")

    pytest.raises = _raises

REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT / "scripts"))

import apply_proposal as ap  # noqa: E402

CLAIM_A = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"
CLAIM_B = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"
SOURCE_A = "cccccccc-cccc-4ccc-8ccc-cccccccccccc"


def _approval_meta(proposal_type, apply_payload):
    return {
        "proposal_type": proposal_type,
        "payload": {"apply_payload": apply_payload},
        "reviewed_by_handle": "m3ta",
        "reviewed_by_agent_id": None,
        "reviewed_at": "2026-07-10T00:00:00+00:00",
        "review_note": "Reviewed exact strict payload.",
    }


# --- literals ------------------------------------------------------------- #
def test_sql_literal_escapes_single_quotes():
    assert ap.sql_literal("O'Brien") == "E'O''Brien'"


def test_sql_literal_escapes_backslashes_independently_of_session_mode():
    assert ap.sql_literal("a\\b'c") == "E'a\\\\b''c'"


def test_sql_literal_none_is_null():
    assert ap.sql_literal(None) == "null"


def test_sql_literal_bool_and_number():
    assert ap.sql_literal(True) == "true"
    assert ap.sql_literal(0.5) == "0.5"


# --- revise_strategy ------------------------------------------------------ #
def _revise_payload():
    return {
        "apply_payload": {
            "agent_id": "11111111-1111-1111-1111-111111111111",
            "strategy": {
                "diagnosis": "Species-level inflection.",
                "guiding_policy": "Build Teleo as a multi-agent public intelligence.",
                "proximate_objectives": ["route before creating", "keep KB calibrated"],
            },
            "strategy_nodes": [
                {"node_type": "diagnosis", "title": "Inflection", "body": "b1", "rank": 1},
                {"node_type": "policy", "title": "Multi-agent PI", "body": "b2", "rank": 1},
            ],
        }
    }


def test_revise_strategy_sql_shape():
    payload = _revise_payload()["apply_payload"]
    sql = ap.build_revise_strategy_sql(payload, "pid-1", "kb-apply", _approval_meta("revise_strategy", payload))
    assert sql.startswith("begin;")
    assert sql.rstrip().endswith("commit;")
    # deactivate old, insert versioned active, retire nodes, insert new nodes
    assert "update public.strategies" in sql
    assert "active = false" in sql
    assert "coalesce(max(version), 0) + 1" in sql
    assert "update public.strategy_nodes" in sql
    assert "insert into public.strategy_nodes" in sql
    # ledger + invariant
    assert "kb_stage.finish_approved_proposal" in sql
    assert "exactly one active strategy" in sql
    # proximate_objectives rendered as jsonb
    assert "::jsonb" in sql


def test_guarded_sql_canonicalizes_reviewed_timestamp_to_stable_utc_text():
    payload = _revise_payload()["apply_payload"]
    approval = _approval_meta("revise_strategy", payload)
    approval["reviewed_at"] = "2026-07-10T02:00:00.1+02:00"

    sql = ap.build_revise_strategy_sql(payload, "pid-1", "kb-apply", approval)

    assert sql.count("2026-07-10T00:00:00.100000Z") == 2
    assert "2026-07-10T02:00:00.1+02:00" not in sql


def test_guarded_sql_rejects_reviewed_timestamp_without_timezone():
    payload = _revise_payload()["apply_payload"]
    approval = _approval_meta("revise_strategy", payload)
    approval["reviewed_at"] = "2026-07-10T00:00:00"

    with pytest.raises(ValueError, match="timezone-aware ISO-8601"):
        ap.build_revise_strategy_sql(payload, "pid-1", "kb-apply", approval)


def test_revise_strategy_requires_agent_id():
    payload = _revise_payload()["apply_payload"]
    del payload["agent_id"]
    with pytest.raises(ValueError):
        ap.build_revise_strategy_sql(payload, "pid-1", None, _approval_meta("revise_strategy", payload))


def test_revise_strategy_rejects_bad_node_type():
    payload = _revise_payload()["apply_payload"]
    payload["strategy_nodes"][0]["node_type"] = "manifesto"
    with pytest.raises(ValueError):
        ap.build_revise_strategy_sql(payload, "pid-1", None, _approval_meta("revise_strategy", payload))


# --- add_edge ------------------------------------------------------------- #
def test_add_edge_sql_dedup_guard():
    payload = {"from_claim": CLAIM_A, "to_claim": CLAIM_B, "edge_type": "supersedes", "weight": 0.9}
    sql = ap.build_add_edge_sql(payload, "pid-2", None, _approval_meta("add_edge", payload))
    assert "insert into public.claim_edges" in sql
    assert "not exists" in sql
    assert "::edge_type" in sql
    assert "kb_stage.finish_approved_proposal" in sql
    assert "pg_advisory_xact_lock" in sql


def test_add_edge_semantic_row_accepts_match_and_rejects_weight_mismatch():
    payload = {"from_claim": CLAIM_A, "to_claim": CLAIM_B, "edge_type": "supports", "weight": 0.9}
    sql = ap.build_add_edge_sql(payload, "pid-2", None, _approval_meta("add_edge", payload))
    verify = sql[sql.index("do $verify$") : sql.index("kb_stage.finish_approved_proposal")]

    assert "weight is not distinct from 0.9" in verify
    assert "weight is distinct from 0.9" in verify
    assert "claim_edge row does not match strict apply payload" in verify
    assert sql.startswith("begin;")
    assert sql.rstrip().endswith("commit;")
    assert sql.index("raise exception 'apply_proposal: claim_edge row") < sql.index("kb_stage.finish_approved_proposal")


def test_add_edge_rejects_self_loop():
    payload = {"from_claim": "same", "to_claim": "same", "edge_type": "supports"}
    with pytest.raises(ValueError):
        ap.build_add_edge_sql(payload, "pid-2", None, _approval_meta("add_edge", payload))


# --- attach_evidence ------------------------------------------------------ #
def test_attach_evidence_sql_with_source_id():
    payload = {"evidence": [{"claim_id": CLAIM_A, "source_id": SOURCE_A, "role": "grounds", "weight": 0.78}]}
    sql = ap.build_attach_evidence_sql(payload, "pid-3", None, _approval_meta("attach_evidence", payload))
    assert "insert into public.claim_evidence" in sql
    assert "public.claim_evidence (id," not in sql
    assert "public.claim_evidence (claim_id, source_id, role, weight)" in sql
    assert "::evidence_role" in sql
    assert "not exists" in sql


def test_attach_evidence_semantic_row_accepts_match_and_rejects_weight_mismatch():
    payload = {"evidence": [{"claim_id": CLAIM_A, "source_id": SOURCE_A, "role": "grounds", "weight": 0.78}]}
    sql = ap.build_attach_evidence_sql(payload, "pid-3", None, _approval_meta("attach_evidence", payload))
    verify = sql[sql.index("do $verify$") : sql.index("kb_stage.finish_approved_proposal")]

    assert "weight is not distinct from 0.78" in verify
    assert "weight is distinct from 0.78" in verify
    assert "evidence row % does not match strict apply payload" in verify
    assert sql.startswith("begin;")
    assert sql.rstrip().endswith("commit;")
    assert sql.index("raise exception 'apply_proposal: evidence row") < sql.index("kb_stage.finish_approved_proposal")


def test_attach_evidence_requires_source_id():
    payload = {"evidence": [{"claim_id": "cccc"}]}
    with pytest.raises(ValueError):
        ap.build_attach_evidence_sql(payload, "pid-3", None, _approval_meta("attach_evidence", payload))


# --- approve_claim canonical bundle -------------------------------------- #
def _approve_claim_bundle():
    claim_a = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"
    claim_b = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"
    source_a = "cccccccc-cccc-4ccc-8ccc-cccccccccccc"
    source_b = "dddddddd-dddd-4ddd-8ddd-dddddddddddd"
    return {
        "contract_version": 2,
        "agent_id": "11111111-1111-4111-8111-111111111111",
        "claims": [
            {
                "id": claim_a,
                "type": "structural",
                "text": "Canonical claim A",
                "status": "open",
                "confidence": 0.8,
                "tags": ["test", "working-leo"],
                "created_by": None,
            },
            {
                "id": claim_b,
                "type": "concept",
                "text": "Canonical claim B",
                "status": "open",
                "confidence": None,
                "tags": [],
                "created_by": None,
            },
        ],
        "sources": [
            {
                "id": source_a,
                "source_type": "paper",
                "url": "https://example.test/a",
                "storage_path": None,
                "excerpt": "Evidence A",
                "hash": "hash-a",
                "created_by": None,
            },
            {
                "id": source_b,
                "source_type": "observation",
                "url": None,
                "storage_path": None,
                "excerpt": "Evidence B",
                "hash": "hash-b",
                "created_by": None,
            },
        ],
        "evidence": [
            {"claim_id": claim_a, "source_id": source_a, "role": "grounds", "weight": 0.9},
            {"claim_id": claim_b, "source_id": source_b, "role": "illustrates", "weight": None},
        ],
        "edges": [{"from_claim": claim_a, "to_claim": claim_b, "edge_type": "supports", "weight": 0.7}],
        "reasoning_tools": [
            {
                "id": "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee",
                "agent_id": None,
                "name": "Canonical test tool",
                "description": "A strict reasoning-tool row.",
                "category": "test",
            }
        ],
    }


def _approve_claim_v3_bundle():
    agent_id = "11111111-1111-4111-8111-111111111111"
    source_b = "dddddddd-dddd-4ddd-8ddd-dddddddddddd"
    evidence_a = {
        "id": "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee",
        "claim_id": CLAIM_A,
        "source_id": SOURCE_A,
        "polarity": "supports",
        "excerpt": "The primary record supports claim A.",
        "locator_json": {"section": "A", "paragraph": 1},
        "source_content_hash": "a" * 64,
        "rationale": "The cited paragraph directly supports the bounded proposition.",
        "created_by": agent_id,
    }
    evidence_b = {
        "id": "ffffffff-ffff-4fff-8fff-ffffffffffff",
        "claim_id": CLAIM_B,
        "source_id": source_b,
        "polarity": "challenges",
        "excerpt": "The primary record challenges claim B.",
        "locator_json": {"section": "B", "paragraph": 2},
        "source_content_hash": "b" * 64,
        "rationale": "The cited paragraph directly challenges the bounded proposition.",
        "created_by": agent_id,
    }
    evidence = [evidence_a, evidence_b]
    return {
        "contract_version": 3,
        "agent_id": agent_id,
        "claims": [
            {
                "id": CLAIM_A,
                "type": "empirical",
                "proposition": "Claim A has a reproducible evidence chain.",
                "body_markdown": "Claim A is bounded to the cited primary record and exact locator.",
                "scope": "V3 apply-writer disposable verification.",
                "access_scope": "collective_internal",
                "status": "active",
                "revision_criteria": "Revise if the cited source or locator cannot be reproduced.",
                "revision_kind": "original",
                "supersedes_id": None,
                "tags": ["v3", "apply-writer"],
                "created_by": agent_id,
            },
            {
                "id": CLAIM_B,
                "type": "conceptual",
                "proposition": "Claim B remains explicitly challengeable.",
                "body_markdown": "Claim B records a conceptual object together with its challenge evidence.",
                "scope": "V3 apply-writer disposable verification.",
                "access_scope": "collective_internal",
                "status": "contested",
                "revision_criteria": "Revise when the challenge is resolved or materially changes.",
                "revision_kind": "original",
                "supersedes_id": None,
                "tags": ["v3", "challenge"],
                "created_by": agent_id,
            },
        ],
        "sources": [
            {
                "id": SOURCE_A,
                "source_type": "paper",
                "canonical_url": "https://example.test/v3/a",
                "storage_uri": None,
                "excerpt": "Primary source A.",
                "content_hash": "a" * 64,
                "access_scope": "collective_internal",
                "ingestion_origin": "operator_upload",
                "provenance_status": "verified",
                "source_class": "primary_record",
                "auto_promotion_policy": "human_required",
                "captured_at": "2026-07-18T10:00:00+00:00",
                "created_by": agent_id,
            },
            {
                "id": source_b,
                "source_type": "observation",
                "canonical_url": None,
                "storage_uri": "gs://livingip-test/v3/source-b",
                "excerpt": "Primary source B.",
                "content_hash": "b" * 64,
                "access_scope": "collective_internal",
                "ingestion_origin": "system_import",
                "provenance_status": "verified",
                "source_class": "primary_record",
                "auto_promotion_policy": "human_required",
                "captured_at": "2026-07-18T10:01:00+00:00",
                "created_by": agent_id,
            },
        ],
        "evidence": evidence,
        "edges": [
            {
                "id": "12121212-1212-4121-8121-121212121212",
                "from_claim": CLAIM_A,
                "to_claim": CLAIM_B,
                "relation_type": "challenges",
                "rationale": "Claim A supplies a bounded reason to challenge claim B.",
                "scope_or_conditions": "Only inside the disposable V3 verification scope.",
                "created_by": agent_id,
            }
        ],
        "assessments": [
            {
                "id": "13131313-1313-4131-8131-131313131313",
                "claim_id": CLAIM_A,
                "support_tier": "strong",
                "challenge_tier": "none",
                "overall_state": "support_dominant",
                "rationale": "One exact primary record supports the bounded empirical claim.",
                "evidence_set_hash": ap._v3_evidence_set_hash(CLAIM_A, evidence),
                "supersedes_assessment_id": None,
            },
            {
                "id": "14141414-1414-4141-8141-141414141414",
                "claim_id": CLAIM_B,
                "support_tier": None,
                "challenge_tier": None,
                "overall_state": "challenge_dominant",
                "rationale": "The conceptual claim has an explicit challenge without numeric evidence tiers.",
                "evidence_set_hash": ap._v3_evidence_set_hash(CLAIM_B, evidence),
                "supersedes_assessment_id": None,
            },
        ],
        "reasoning_tools": [],
    }


def test_approve_claim_bundle_sql_is_atomic_and_row_verified():
    payload = _approve_claim_bundle()
    sql = ap.build_approve_claim_sql(
        payload,
        "99999999-9999-4999-8999-999999999999",
        None,
        _approval_meta("approve_claim", payload),
    )
    assert sql.startswith("begin;")
    assert sql.rstrip().endswith("commit;")
    assert "insert into public.claims" in sql
    assert "insert into public.sources" in sql
    assert "insert into public.claim_evidence" in sql
    assert "public.claim_evidence (id," not in sql
    assert "public.claim_evidence (claim_id, source_id, role, weight, created_by)" in sql
    assert "insert into public.claim_edges" in sql
    assert "insert into public.reasoning_tools" in sql
    assert "source hash collision" in sql
    assert "claim row does not match strict apply payload" in sql
    assert "source row does not match strict apply payload" in sql
    assert "reasoning tool row does not match strict apply payload" in sql
    assert "kb_stage.finish_approved_proposal" in sql


def test_v3_approve_claim_sql_writes_complete_epistemic_bundle() -> None:
    payload = _approve_claim_v3_bundle()
    proposal_id = "99999999-9999-4999-8999-999999999999"
    sql = ap.build_approve_claim_sql(
        payload,
        proposal_id,
        None,
        _approval_meta("approve_claim", payload),
    )

    shared_lock = f"select pg_catalog.pg_advisory_lock_shared({ap.V3_CATALOG_DDL_ADVISORY_LOCK_KEY});"
    shared_unlock = f"select pg_catalog.pg_advisory_unlock_shared({ap.V3_CATALOG_DDL_ADVISORY_LOCK_KEY});"
    assert sql.startswith(f"{shared_lock}\nbegin;")
    assert "set transaction isolation level serializable" in sql
    assert sql.rstrip().endswith(shared_unlock)
    assert sql.count(shared_lock) == 1
    assert sql.count(shared_unlock) == 1
    assert "insert into public.claims" in sql
    assert "body_markdown" in sql
    assert "accepted_by_proposal_id" in sql
    assert "insert into public.sources" in sql
    assert "content_hash" in sql
    assert "insert into public.claim_evidence (" in sql
    assert "locator_json" in sql
    assert "insert into public.claim_edges (" in sql
    assert "relation_type" in sql
    assert "insert into public.claim_evidence_assessments" in sql
    assert "evidence_set_hash" in sql
    assert "insert into public.reasoning_tools" not in sql
    expected_inserts = sum(len(payload[key]) for key in ("sources", "claims", "evidence", "edges", "assessments"))
    assert sql.lower().count("on conflict do nothing") == expected_inserts
    assert sql.index("do $v3_contract_guard$") < sql.index("kb_stage.assert_approved_proposal")
    relation_locks = [
        f"select kb_stage.{ap.V3_CATALOG_LOCK_FUNCTION}();",
        "lock table public.claim_edges in row exclusive mode;",
        "lock table public.claim_evidence in row exclusive mode;",
        "lock table public.claim_evidence_assessments in row exclusive mode;",
        "lock table public.claims in row exclusive mode;",
        "lock table public.sources in row exclusive mode;",
    ]
    ordered_catalog_guard = [
        shared_lock,
        "begin;",
        "set transaction isolation level serializable;",
        *relation_locks,
        "do $v3_contract_guard$",
        "commit;",
        shared_unlock,
    ]
    assert [sql.index(statement) for statement in ordered_catalog_guard] == sorted(
        sql.index(statement) for statement in ordered_catalog_guard
    )
    assert sql.index("kb_stage.assert_approved_proposal") < sql.index("insert into public.sources")
    assert sql.index("insert into public.claim_evidence_assessments") < sql.index("kb_stage.finish_approved_proposal")
    assert sql.index("on conflict do nothing") < sql.index("do $verify$")
    assert "set local timezone = 'UTC'" in sql
    assert "apply_proposal: V3 catalog contract is %s; refusing mutating transaction" in sql
    assert "and superseded_by is null" in sql
    expected_creator_checks = sum(len(payload[key]) for key in ("sources", "claims", "evidence", "edges"))
    assert sql.count("created_by is not distinct from") == expected_creator_checks


def test_v3_catalog_guard_requires_the_full_hardened_contract() -> None:
    sql = ap.build_v3_contract_state_query()

    assert ap.V3_CATALOG_DDL_ADVISORY_LOCK_KEY == 6072343533146485505
    assert ap.V3_PROTECTED_RELATIONS == (
        ("kb_stage", "kb_proposals"),
        ("public", "claim_edges"),
        ("public", "claim_evidence"),
        ("public", "claim_evidence_assessments"),
        ("public", "claims"),
        ("public", "sources"),
    )
    assert (
        tuple(sorted({(row["schema_name"], row["table_name"]) for row in ap.V3_EXPECTED_TRIGGER_CONTRACT}))
        == ap.V3_PROTECTED_RELATIONS
    )
    assert len(ap.V3_EXPECTED_TRIGGER_CONTRACT) == 24
    assert len({row["trigger_name"] for row in ap.V3_EXPECTED_TRIGGER_CONTRACT}) == 24
    assert all(row["when_expression"] is None for row in ap.V3_EXPECTED_TRIGGER_CONTRACT)
    assert "when not artifacts_present then 'V2'" in sql
    assert "when functions_valid and catalog_lock_function_valid and triggers_valid" in sql
    assert "and owner_role_valid and memberships_valid then 'V3'" in sql
    assert "else 'INVALID'" in sql
    assert "actual.tgenabled = 'O'" in sql
    assert "actual.tgtype = expected.trigger_type" in sql
    assert "actual.update_columns = expected.update_columns" in sql
    assert "actual.when_expression is not distinct from expected.when_expression" in sql
    assert "actual.argument_hex = expected.argument_hex" in sql
    assert "actual.tgconstraint = 0::pg_catalog.oid" in sql
    assert "actual.tgoldtable is null" in sql
    assert "actual.tgnewtable is null" in sql
    assert "actual.tgfoid" in sql
    assert "owner_role.rolname = E'kb_gate_owner'" in sql
    assert "actual.function_security_definer = expected.function_security_definer" in sql
    assert "actual.function_source_sha256 = expected.function_source_sha256" in sql
    assert f"procedure.proname = E'{ap.V3_CATALOG_LOCK_FUNCTION}'" in sql
    assert ap.V3_CATALOG_LOCK_FUNCTION_SOURCE_SHA256 in sql
    assert "and prorettype = 'pg_catalog.void'::pg_catalog.regtype" in sql
    assert "and grantee.rolname = 'kb_apply'" in sql
    assert "and execute_acl_exact" in sql
    assert "acl.grantee <> procedure.proowner" in sql
    assert "from pg_catalog.pg_auth_members membership" in sql


def test_migration_trigger_contract_and_catalog_queries_match_worker_builder() -> None:
    migration = (REPO_ROOT / "ops" / "teleo_v3_epistemic_contract.sql").read_text(encoding="utf-8")
    contract_match = re.search(r"\$trigger_contract\$\s*(\[.*?\])\s*\$trigger_contract\$", migration, re.DOTALL)

    assert contract_match is not None
    assert json.loads(contract_match.group(1)) == list(ap.V3_EXPECTED_TRIGGER_CONTRACT)
    exclusive_lock = f"select pg_catalog.pg_advisory_xact_lock({ap.V3_CATALOG_DDL_ADVISORY_LOCK_KEY});"
    assert migration.count(exclusive_lock) == 1
    assert migration.index(exclusive_lock) < migration.index(
        f"create or replace function kb_stage.{ap.V3_CATALOG_LOCK_FUNCTION}()"
    )
    assert migration.index(exclusive_lock) < migration.index(
        "create or replace function kb_stage.teleo_v3_enforce_apply_contract_cutover()"
    )
    assert migration.index(exclusive_lock) < migration.index("create or replace trigger")

    expected_query = ap.build_v3_contract_state_query(
        into="v_contract_state",
        expected_contract_expression="pg_catalog.current_setting('teleo.v3_expected_trigger_contract')",
    )
    for marker in ("STATE", "POSTFLIGHT"):
        query_match = re.search(
            rf"TELEO_V3_CONTRACT_{marker}_QUERY_BEGIN\n(.*?)\n\s*-- TELEO_V3_CONTRACT_{marker}_QUERY_END",
            migration,
            re.DOTALL,
        )
        assert query_match is not None
        assert query_match.group(1).strip() == expected_query + ";"


@pytest.mark.parametrize(
    ("mutation", "error"),
    [
        (lambda payload: payload["claims"][0].update(body_markdown=""), "body_markdown"),
        (lambda payload: payload["claims"][0].update(revision_kind="semantic"), "original claims only"),
        (lambda payload: payload["sources"][0].update(content_hash="not-a-hash"), "SHA-256"),
        (lambda payload: payload["evidence"][0].update(source_content_hash="b" * 64), "does not match"),
        (lambda payload: payload["assessments"][0].update(evidence_set_hash="c" * 64), "evidence_set_hash"),
        (
            lambda payload: payload["assessments"][0].update(
                supersedes_assessment_id="15151515-1515-4151-8151-151515151515"
            ),
            "assessment supersession",
        ),
        (lambda payload: payload.update(reasoning_tools=[{"id": CLAIM_A}]), "reasoning_tools"),
    ],
)
def test_v3_approve_claim_rejects_incomplete_or_unbound_payloads(mutation, error) -> None:
    payload = _approve_claim_v3_bundle()
    mutation(payload)
    with pytest.raises(ValueError, match=error):
        ap.build_approve_claim_sql(
            payload,
            "99999999-9999-4999-8999-999999999999",
            None,
            _approval_meta("approve_claim", payload),
        )


def test_v3_approve_claim_preserves_exact_reviewed_text_for_replay_receipts() -> None:
    payload = _approve_claim_v3_bundle()
    payload["claims"][0]["body_markdown"] = "  Exact reviewed body with intentional spacing.  "

    normalized = ap._normalize_v3_approve_claim(payload)

    assert normalized["claims"][0]["body_markdown"] == payload["claims"][0]["body_markdown"]


@pytest.mark.parametrize(
    ("captured_at", "expected"),
    [
        ("2026-07-18T10:00:00Z", "2026-07-18T10:00:00.000000Z"),
        ("2026-07-18T12:00:00+02:00", "2026-07-18T10:00:00.000000Z"),
        ("2026-07-18T12:00:00.123400+02:00", "2026-07-18T10:00:00.123400Z"),
        ("2026-07-18T10:00:00.1Z", "2026-07-18T10:00:00.100000Z"),
    ],
)
def test_v3_approve_claim_normalizes_capture_timestamp_to_fixed_utc(
    captured_at: str,
    expected: str,
) -> None:
    payload = _approve_claim_v3_bundle()
    payload["sources"][0]["captured_at"] = captured_at

    normalized = ap._normalize_v3_approve_claim(payload)

    assert normalized["sources"][0]["captured_at"] == expected


@pytest.mark.parametrize(
    "captured_at",
    [
        "2026-07-18 10:00:00+00:00",
        "2026-07-18T10:00:00",
        "2026-07-18T10:00:00.1234567Z",
        "not-a-timestamp",
    ],
)
def test_v3_approve_claim_rejects_untyped_capture_timestamp(captured_at: str) -> None:
    payload = _approve_claim_v3_bundle()
    payload["sources"][0]["captured_at"] = captured_at
    with pytest.raises(ValueError, match="timezone-aware ISO-8601"):
        ap._normalize_v3_approve_claim(payload)


@pytest.mark.parametrize(
    "spelling",
    [
        CLAIM_A.upper(),
        "{" + CLAIM_A + "}",
        CLAIM_A.replace("-", ""),
    ],
)
def test_v3_approve_claim_rejects_noncanonical_uuid_spellings(spelling: str) -> None:
    payload = _approve_claim_v3_bundle()
    payload["claims"][0]["id"] = spelling

    with pytest.raises(ValueError, match="canonical lowercase hyphenated UUID"):
        ap._normalize_v3_approve_claim(payload)


def test_approve_claim_semantic_rows_accept_matches_and_reject_payload_mismatches():
    payload = _approve_claim_bundle()
    sql = ap.build_approve_claim_sql(
        payload,
        "99999999-9999-4999-8999-999999999999",
        None,
        _approval_meta("approve_claim", payload),
    )
    verify = sql[sql.index("do $verify$") : sql.index("kb_stage.finish_approved_proposal")]

    assert "weight is not distinct from 0.9" in verify
    assert "weight is distinct from 0.9" in verify
    assert "weight is not distinct from 0.7" in verify
    assert "weight is distinct from 0.7" in verify
    assert "weight is not distinct from null" in verify
    assert "weight is distinct from null" in verify
    expected_creator = "E'11111111-1111-4111-8111-111111111111'::uuid"
    assert f"created_by is not distinct from {expected_creator}" in verify
    assert f"created_by is distinct from {expected_creator}" in verify
    assert "approve_claim: evidence row does not match strict apply payload" in verify
    assert "approve_claim: edge row does not match strict apply payload" in verify
    assert sql.startswith("begin;")
    assert sql.rstrip().endswith("commit;")
    assert sql.index("raise exception 'approve_claim: evidence row") < sql.index("kb_stage.finish_approved_proposal")


def test_approve_claim_bundle_rejects_duplicate_source_hashes():
    payload = _approve_claim_bundle()
    payload["sources"][1]["hash"] = payload["sources"][0]["hash"]
    with pytest.raises(ValueError):
        ap.build_approve_claim_sql(
            payload,
            "99999999-9999-4999-8999-999999999999",
            None,
            _approval_meta("approve_claim", payload),
        )


def test_approve_claim_bundle_rejects_invalid_enum_and_self_edge():
    payload = _approve_claim_bundle()
    payload["claims"][0]["type"] = "vibes"
    with pytest.raises(ValueError):
        ap.build_approve_claim_sql(
            payload,
            "99999999-9999-4999-8999-999999999999",
            None,
            _approval_meta("approve_claim", payload),
        )

    payload = _approve_claim_bundle()
    payload["edges"][0]["to_claim"] = payload["edges"][0]["from_claim"]
    with pytest.raises(ValueError):
        ap.build_approve_claim_sql(
            payload,
            "99999999-9999-4999-8999-999999999999",
            None,
            _approval_meta("approve_claim", payload),
        )


def test_approve_claim_rejects_unknown_keys_and_contract_versions():
    payload = _approve_claim_bundle()
    payload["contract_version"] = 1
    with pytest.raises(ValueError):
        ap.build_approve_claim_sql(
            payload,
            "99999999-9999-4999-8999-999999999999",
            None,
            _approval_meta("approve_claim", payload),
        )

    payload = _approve_claim_bundle()
    payload["contract_version"] = 2
    payload["claimz"] = payload.pop("claims")
    with pytest.raises(ValueError):
        ap.build_approve_claim_sql(
            payload,
            "99999999-9999-4999-8999-999999999999",
            None,
            _approval_meta("approve_claim", payload),
        )


def test_approve_claim_verifies_superseded_by_and_pins_string_mode():
    payload = _approve_claim_bundle()
    payload["contract_version"] = 2
    payload["claims"][0]["superseded_by"] = None
    sql = ap.build_approve_claim_sql(
        payload,
        "99999999-9999-4999-8999-999999999999",
        None,
        _approval_meta("approve_claim", payload),
    )
    assert "superseded_by" in sql
    assert "set local standard_conforming_strings = on" in sql.lower()


def test_apply_sql_locks_and_binds_approval_before_canonical_write():
    payload = _approve_claim_bundle()
    payload["contract_version"] = 2
    proposal = {
        "id": "99999999-9999-4999-8999-999999999999",
        "proposal_type": "approve_claim",
        "status": "approved",
        "payload": {"apply_payload": payload},
        "reviewed_by_handle": "m3ta",
        "reviewed_by_agent_id": None,
        "reviewed_at": "2026-07-10T00:00:00+00:00",
        "review_note": "Reviewed exact strict payload.",
    }
    sql = ap.build_apply_sql(proposal, "kb-apply")
    assert "kb_stage.assert_approved_proposal" in sql
    assert "kb_stage.finish_approved_proposal" in sql
    assert sql.index("kb_stage.assert_approved_proposal") < sql.index("insert into public.claims")
    assert sql.index("insert into public.claims") < sql.index("kb_stage.finish_approved_proposal")
    assert "Reviewed exact strict payload." in sql


def test_fresh_owned_apply_uses_strict_inserts_and_binds_preflight_hash():
    payload = _approve_claim_bundle()
    proposal = {
        "id": "99999999-9999-4999-8999-999999999999",
        "proposal_type": "approve_claim",
        "status": "approved",
        "payload": {"apply_payload": payload},
        "reviewed_by_handle": "m3ta",
        "reviewed_by_agent_id": None,
        "reviewed_at": "2026-07-10T00:00:00+00:00",
        "review_note": "Reviewed exact strict payload.",
    }
    preflight_sha256 = "a" * 64

    sql = ap.build_apply_sql(
        proposal,
        "kb-apply",
        fresh_owned=True,
        fresh_preflight_sha256=preflight_sha256,
    )

    assert preflight_sha256 in sql
    assert "set transaction isolation level serializable" in sql.lower()
    assert "on conflict" not in sql.lower()
    assert "where not exists" not in sql.lower()
    assert sql.index("set transaction isolation level serializable") < sql.index("kb_stage.assert_approved_proposal")


def test_build_apply_sql_requires_review_provenance():
    proposal = {
        "id": "99999999-9999-4999-8999-999999999999",
        "proposal_type": "approve_claim",
        "status": "approved",
        "payload": {"apply_payload": _approve_claim_bundle()},
    }
    with pytest.raises(ValueError):
        ap.build_apply_sql(proposal, "kb-apply")


def test_approve_claim_dispatch_and_prerequisite_grants():
    proposal = {
        "id": "99999999-9999-4999-8999-999999999999",
        "proposal_type": "approve_claim",
        "payload": {"apply_payload": _approve_claim_bundle()},
    }
    proposal.update(_approval_meta("approve_claim", proposal["payload"]["apply_payload"]))
    sql = ap.build_apply_sql(proposal, None)
    assert "insert into public.claims" in sql
    assert "approve_claim" in ap.APPLYABLE_TYPES

    prereqs = (REPO_ROOT / "scripts" / "kb_apply_prereqs.sql").read_text(encoding="utf-8")
    assert "grant select, insert on public.claims to kb_apply" in prereqs.lower()
    assert "grant select, insert on public.sources to kb_apply" in prereqs.lower()
    assert "grant select, insert on public.reasoning_tools to kb_apply" in prereqs.lower()


# --- dispatch + guards ---------------------------------------------------- #
def test_build_apply_sql_requires_apply_payload():
    proposal = {"id": "pid", "proposal_type": "add_edge", "payload": {"rationale": "x"}}
    with pytest.raises(ValueError):
        ap.build_apply_sql(proposal, None)


@pytest.mark.parametrize(
    ("proposal", "error"),
    [
        ({"proposal_type": "approve_claim", "payload": []}, "payload must be a JSON object"),
        (
            {"proposal_type": "approve_claim", "payload": {"apply_payload": []}},
            "payload.apply_payload must be a JSON object",
        ),
        (
            {
                "proposal_type": "approve_claim",
                "payload": {"apply_payload": {"contract_version": "3"}},
            },
            "must be integer 2 or 3",
        ),
        (
            {
                "proposal_type": "approve_claim",
                "payload": {"apply_payload": {"contract_version": 3.0}},
            },
            "must be integer 2 or 3",
        ),
        (
            {
                "proposal_type": "approve_claim",
                "payload": {"apply_payload": {"contract_version": True}},
            },
            "must be integer 2 or 3",
        ),
        (
            {
                "proposal_type": "add_edge",
                "payload": {"apply_payload": {"contract_version": 3}},
            },
            "must omit contract_version",
        ),
        (
            {
                "proposal_type": "add_edge",
                "payload": {"apply_payload": {"contract_version": None}},
            },
            "must omit contract_version",
        ),
    ],
)
def test_exact_contract_gate_rejects_malformed_and_hybrid_proposals(
    proposal: dict,
    error: str,
) -> None:
    with pytest.raises(ValueError, match=error):
        ap.proposal_contract_version(proposal)


def test_mutating_subprocess_contract_expectation_is_reasserted() -> None:
    payload = _approve_claim_v3_bundle()
    proposal = {
        "id": "99999999-9999-4999-8999-999999999999",
        "proposal_type": "approve_claim",
        "payload": {"apply_payload": payload},
    }

    assert ap.assert_expected_contract_version(proposal, 3) == 3
    with pytest.raises(ValueError, match="does not match expected version 2"):
        ap.assert_expected_contract_version(proposal, 2)


def test_build_apply_sql_rejects_unsupported_type():
    proposal = {"id": "pid", "proposal_type": "reject_claim", "payload": {"apply_payload": {}}}
    with pytest.raises(ValueError):
        ap.build_apply_sql(proposal, None)


def test_assert_applyable_blocks_pending():
    with pytest.raises(SystemExit):
        ap.assert_applyable({"id": "pid", "status": "pending_review"})


def test_assert_applyable_blocks_already_applied():
    with pytest.raises(SystemExit):
        ap.assert_applyable({"id": "pid", "status": "applied"})


def test_assert_applyable_allows_approved():
    ap.assert_applyable({"id": "pid", "status": "approved"})  # no raise


# --- ledger flip: concurrency guard + FK stamp --------------------------- #
def test_ledger_transition_uses_payload_bound_security_functions():
    payload = {"from_claim": CLAIM_A, "to_claim": CLAIM_B, "edge_type": "supports"}
    sql = ap.build_add_edge_sql(payload, "pid", None, _approval_meta("add_edge", payload))
    assert "kb_stage.assert_approved_proposal" in sql
    assert "kb_stage.finish_approved_proposal" in sql
    assert sql.index("kb_stage.assert_approved_proposal") < sql.index("insert into public.claim_edges")
    assert sql.index("insert into public.claim_edges") < sql.index("kb_stage.finish_approved_proposal")

    prereqs = (REPO_ROOT / "scripts" / "kb_apply_prereqs.sql").read_text(encoding="utf-8").lower()
    assert "status = 'approved'" in prereqs
    assert "for update" in prereqs
    assert "payload = p_payload" in prereqs


def test_ledger_flip_stamps_agent_fk():
    payload = {"from_claim": CLAIM_A, "to_claim": CLAIM_B, "edge_type": "supports"}
    sql = ap.build_add_edge_sql(payload, "pid", "kb-apply", _approval_meta("add_edge", payload))
    # Hard resolve into a variable + NOT-NULL assert, never a silent inline
    # subselect that would stamp NULL on an unresolved handle.
    assert "kb_stage.finish_approved_proposal" in sql
    assert "'kb-apply'" in sql


def test_build_apply_sql_defaults_applied_by_to_service_agent():
    payload = {"from_claim": CLAIM_A, "to_claim": CLAIM_B, "edge_type": "supports"}
    proposal = {
        "id": "pid",
        "proposal_type": "add_edge",
        "payload": {"apply_payload": payload},
    }
    proposal.update(_approval_meta("add_edge", payload))
    sql = ap.build_apply_sql(proposal, None)
    assert f"E'{ap.SERVICE_AGENT_HANDLE}'" in sql


def test_load_password_accepts_live_db_password_key(tmp_path):
    secrets_file = tmp_path / "kb-apply.env"
    secrets_file.write_text("KB_APPLY_DB_PASSWORD=live-secret\n", encoding="utf-8")
    assert ap.load_password(str(secrets_file)) == "live-secret"


if __name__ == "__main__":
    import traceback

    failures = 0
    for name, fn in sorted(globals().items()):
        if name.startswith("test_") and callable(fn):
            try:
                fn()
                print(f"PASS {name}")
            except Exception:
                failures += 1
                print(f"FAIL {name}")
                traceback.print_exc()
    sys.exit(1 if failures else 0)
