"""Tests for the inline action gate — block, don't just log."""
import pytest

from abom.gate import Action, ActionDenied, Gate, Verdict, evaluate
from abom.signer import verify_payload
from abom.audit import canonical_json
from abom.merkle import verify_inclusion_hex


def manifest(**controls):
    return {
        "abom": "0.1",
        "type": "CompositionManifest",
        "agent": {"name": "loan-agent", "version": "1.0.0"},
        "components": [
            {"type": "model", "name": "local/qwen2.5-coder"},
            {"type": "tool", "name": "read_docs"},
            {
                "type": "tool",
                "name": "http_fetch",
                "scope": "egress",
                "allowed_endpoints": ["internal-kyc.bank", "*.corp.internal"],
            },
        ],
        "controls": controls or {},
    }


# ---- pure policy --------------------------------------------------------
def test_declared_tool_is_allowed():
    v, rule, _ = evaluate(manifest(), Action(tool="read_docs"))
    assert v is Verdict.ALLOW and rule == "manifest_allows"


def test_undeclared_tool_is_denied_by_default():
    v, rule, _ = evaluate(manifest(), Action(tool="wire_transfer"))
    assert v is Verdict.DENY and rule == "tool_not_in_manifest"


def test_egress_endpoint_allowlist_enforced():
    ok, _, _ = evaluate(manifest(), Action(tool="http_fetch", endpoint="internal-kyc.bank"))
    assert ok is Verdict.ALLOW
    bad, rule, _ = evaluate(manifest(), Action(tool="http_fetch", endpoint="evil.example.com"))
    assert bad is Verdict.DENY and rule == "endpoint_not_allowed"


def test_endpoint_glob_match():
    v, _, _ = evaluate(manifest(), Action(tool="http_fetch", endpoint="api.corp.internal"))
    assert v is Verdict.ALLOW


def test_residency_blocks_confidential_egress():
    v, rule, _ = evaluate(
        manifest(),
        Action(tool="http_fetch", endpoint="internal-kyc.bank", data_classification="confidential"),
    )
    assert v is Verdict.DENY and rule == "residency"


# ---- the killer demo: block + notarize ----------------------------------
def test_wire_transfer_is_blocked_and_notarized():
    gate = Gate(manifest(), run_id="demo")
    # an injected agent tries to move money via an undeclared tool
    with pytest.raises(ActionDenied) as exc:
        gate.enforce(Action(tool="wire_transfer", args={"amount": 1_000_000}))
    decision = exc.value.decision
    assert decision.verdict is Verdict.DENY
    assert decision.rule == "tool_not_in_manifest"
    # the denial itself is notarized: inclusion proof + signed head
    assert decision.inclusion_proof is not None
    assert decision.head_signature is not None


def test_enforce_allows_declared_action():
    gate = Gate(manifest(), run_id="demo")
    d = gate.enforce(Action(tool="read_docs"))
    assert d.allowed


def test_every_decision_has_verifiable_inclusion_proof():
    gate = Gate(manifest(), run_id="demo")
    gate.check(Action(tool="read_docs"))
    gate.check(Action(tool="wire_transfer"))           # denied
    gate.check(Action(tool="http_fetch", endpoint="internal-kyc.bank"))
    assert gate.verify()["valid"]
    assert len(gate.decisions) == 3


def test_signed_tree_head_verifies():
    gate = Gate(manifest(), run_id="demo")
    d = gate.check(Action(tool="read_docs"))
    head_bytes = canonical_json(d.tree_head).encode("utf-8")
    assert verify_payload(head_bytes, d.head_signature)
    # tampering with the head breaks the signature
    forged = dict(d.tree_head, tree_size=999)
    assert not verify_payload(canonical_json(forged).encode("utf-8"), d.head_signature)


def test_tampered_decision_breaks_inclusion():
    gate = Gate(manifest(), run_id="demo")
    d = gate.check(Action(tool="read_docs"))
    # an auditor recomputes the entry; a changed verdict no longer matches the proof
    forged_entry = canonical_json(
        {"run_id": "demo", "seq": d.seq, "verdict": "allow", "rule": "FORGED",
         "action": d.action, "created_at": d.created_at}
    ).encode("utf-8")
    assert not verify_inclusion_hex(forged_entry, d.inclusion_proof)


# ---- adapters -----------------------------------------------------------
def test_gated_decorator_blocks_undeclared_tool():
    gate = Gate(manifest(), run_id="demo")

    @gate.gated()
    def wire_transfer(amount):
        return f"sent {amount}"

    with pytest.raises(ActionDenied):
        wire_transfer(500)


def test_gated_decorator_runs_declared_tool():
    gate = Gate(manifest(), run_id="demo")

    @gate.gated(tool_name="read_docs")
    def read_docs():
        return "ok"

    assert read_docs() == "ok"


def test_guard_tool_wraps_existing_callable():
    gate = Gate(manifest(), run_id="demo")
    calls = []
    raw = lambda **kw: calls.append(kw) or "done"
    guarded = gate.guard_tool(raw, tool_name="read_docs")
    assert guarded(q="x") == "done"
    assert calls == [{"q": "x"}]
