"""The re-signing attacker — the trust hole the audit named.

`verify_obj(obj, trusted_keys=...)` already refuses an untrusted key id (see
test_sign.py). These tests assert the *callers* actually pin a key, so a forged
manifest re-signed with the attacker's own key is rejected — not waved through.

The polite attacker (tamper, don't re-sign) is covered elsewhere. The adversary
here re-signs, which is the realistic threat and the one that used to pass.
"""
import pytest

from abom import bom, sign
from abom.gate import Action, Gate
from abom.signer import LocalSigner


def _manifest(key):
    """A minimal but valid signed Composition Manifest signed by `key`."""
    return bom.build_composition(
        agent={"name": "victim-agent", "version": "1.0.0"},
        components=[{"type": "tool", "name": "read_docs"}],
        controls={},
        key=key,
    )


# ---------------------------------------------------------------------------
# verify_abom must authenticate the signer, not just the integrity.
# ---------------------------------------------------------------------------
def test_verify_abom_rejects_attacker_resigned_manifest(tmp_path):
    owner = sign.load_or_create_key(tmp_path / "owner.pem")
    attacker = sign.load_or_create_key(tmp_path / "attacker.pem")

    manifest = _manifest(owner)
    owner_kid = manifest["signature"]["key_id"]

    # Attacker rewrites the manifest and re-signs with THEIR key.
    manifest["components"].append({"type": "tool", "name": "wire_transfer"})
    forged = sign.sign_obj({k: v for k, v in manifest.items() if k != "signature"}, attacker)

    # Integrity alone is intact (attacker signed a consistent blob)...
    assert bom.verify_signature(forged) is True
    # ...but pinned to the owner's key, the forgery must be rejected.
    out = bom.verify_abom(forged, trusted_keys={owner_kid})
    assert out["ok"] is False
    assert any(f["rule"] == "signature" for f in out["findings"])


def test_verify_abom_accepts_owner_signed_manifest(tmp_path):
    owner = sign.load_or_create_key(tmp_path / "owner.pem")
    manifest = _manifest(owner)
    owner_kid = manifest["signature"]["key_id"]
    out = bom.verify_abom(manifest, trusted_keys={owner_kid})
    assert out["ok"] is True


# ---------------------------------------------------------------------------
# The Gate must refuse to enforce against a manifest it cannot authenticate.
# ---------------------------------------------------------------------------
def test_gate_refuses_untrusted_manifest(tmp_path):
    owner = sign.load_or_create_key(tmp_path / "owner.pem")
    attacker = sign.load_or_create_key(tmp_path / "attacker.pem")

    manifest = _manifest(owner)
    owner_kid = manifest["signature"]["key_id"]

    # Attacker adds a tool and re-signs with their own key.
    manifest["components"].append({"type": "tool", "name": "wire_transfer"})
    forged = sign.sign_obj({k: v for k, v in manifest.items() if k != "signature"}, attacker)

    # Building a gate pinned to the owner against a forged manifest must fail.
    with pytest.raises(ValueError, match="signature|trust|untrusted"):
        Gate(forged, trusted_keys={owner_kid})


def test_gate_accepts_trusted_manifest_and_still_gates(tmp_path):
    owner = sign.load_or_create_key(tmp_path / "owner.pem")
    manifest = _manifest(owner)
    owner_kid = manifest["signature"]["key_id"]

    g = Gate(manifest, trusted_keys={owner_kid}, signer=LocalSigner(sign.load_or_create_key(tmp_path / "owner.pem")))
    # declared tool -> allow; undeclared -> deny (gate still works)
    assert g.check(Action(tool="read_docs")).allowed is True
    assert g.check(Action(tool="wire_transfer")).allowed is False


def test_gate_unpinned_stays_backward_compatible(tmp_path):
    """Without trusted_keys, the gate keeps its current (unauthenticated) behaviour
    so existing callers don't break — pinning is opt-in but available."""
    owner = sign.load_or_create_key(tmp_path / "owner.pem")
    manifest = _manifest(owner)
    g = Gate(manifest, signer=LocalSigner(sign.load_or_create_key(tmp_path / "owner.pem")))
    assert g.check(Action(tool="read_docs")).allowed is True


# ---------------------------------------------------------------------------
# The gate's transparency log can be made durable (survives a restart).
# ---------------------------------------------------------------------------
def test_gate_persistent_log_survives_restart(tmp_path):
    from abom.gate import Action, Gate
    from abom.merkle import PersistentMerkleLog, verify_consistency_hex

    owner = sign.load_or_create_key(tmp_path / "owner.pem")
    manifest = _manifest(owner)
    signer = LocalSigner(sign.load_or_create_key(tmp_path / "owner.pem"))
    logp = tmp_path / "gate.mlog"

    g = Gate(manifest, signer=signer, log_path=logp)
    g.check(Action(tool="read_docs"))
    g.check(Action(tool="nope"))
    old_size = g.log.size
    old_root = g.log.head().root_hash

    # Fresh process: a new gate on the same log file sees prior decisions.
    reloaded = PersistentMerkleLog(logp)
    assert reloaded.size == old_size
    assert reloaded.head().root_hash == old_root

    # And a new decision extends the same append-only log.
    g2 = Gate(manifest, signer=signer, log_path=logp)
    g2.check(Action(tool="read_docs"))
    assert g2.log.size == old_size + 1
    cproof = g2.log.prove_consistency(old_size)
    assert verify_consistency_hex(old_root, cproof)
