"""The witness — the append-only cosigner that turns a local log into one a
third party can start to rely on.

The witness holds its OWN key (separate from the log operator's) and remembers
the last tree head it cosigned. It refuses to cosign a new head unless a valid
RFC 6962 consistency proof shows the new tree is an append-only extension of the
old one — so an operator who silently rewrote or truncated the log cannot get the
witness's signature on the forged head.

Honest scope (asserted in the tests): a *self-hosted* witness on the operator's
own infrastructure is defense-in-depth + a migration scaffold, NOT split-view
protection — an operator holding both keys signs both. Real non-equivocation
needs an independent witness; this is the primitive that a design partner runs on
*their* side, and the seam an external witness network plugs into.
"""
import pytest

from abom import sign
from abom.merkle import MerkleLog
from abom.signer import LocalSigner
from abom.witness import Witness, WitnessRejected


def _log(*entries):
    log = MerkleLog()
    for e in entries:
        log.append(e)
    return log


@pytest.fixture
def witness(tmp_path):
    # the witness's key is DISTINCT from any operator/gate key
    return Witness(LocalSigner(sign.load_or_create_key(tmp_path / "witness.pem")))


# ---------------------------------------------------------------------------
# first cosign + append-only growth
# ---------------------------------------------------------------------------
def test_cosigns_first_head(witness):
    log = _log(b"a", b"b")
    cosig = witness.cosign(log.head().as_dict())
    assert cosig["tree_size"] == 2
    assert cosig["root_hash"] == log.head().root_hash
    assert cosig["witness_key_id"] == witness.key_id
    assert cosig["signature"]


def test_cosigns_append_only_extension(witness):
    log = _log(b"a", b"b")
    witness.cosign(log.head().as_dict())          # remembers size-2 root
    log.append(b"c")                               # honest append
    proof = log.prove_consistency(2)               # 2 -> 3 consistency proof
    cosig = witness.cosign(log.head().as_dict(), consistency=proof)
    assert cosig["tree_size"] == 3


# ---------------------------------------------------------------------------
# the whole point: reject a rewrite / truncation / equivocation
# ---------------------------------------------------------------------------
def test_rejects_head_that_is_not_an_extension(witness):
    log = _log(b"a", b"b", b"c")
    witness.cosign(log.head().as_dict())           # cosigned size-3

    # operator rewrites history: a DIFFERENT log of the same size
    forged = _log(b"a", b"b", b"EVIL")
    bad_proof = {"old_size": 3, "new_size": 3,
                 "proof": [], "new_root": forged.head().root_hash}
    with pytest.raises(WitnessRejected, match="consistency|not an append-only"):
        witness.cosign(forged.head().as_dict(), consistency=bad_proof)


def test_rejects_shrinking_log(witness):
    log = _log(b"a", b"b", b"c")
    witness.cosign(log.head().as_dict())           # cosigned size-3
    smaller = _log(b"a", b"b")                       # operator drops a leaf
    with pytest.raises(WitnessRejected, match="smaller|shrink|append-only"):
        witness.cosign(smaller.head().as_dict())


def test_rejects_missing_consistency_proof_when_growing(witness):
    log = _log(b"a", b"b")
    witness.cosign(log.head().as_dict())
    log.append(b"c")
    # grew from 2 -> 3 but no proof supplied
    with pytest.raises(WitnessRejected, match="consistency proof required"):
        witness.cosign(log.head().as_dict())


def test_re_cosigning_same_head_is_idempotent(witness):
    log = _log(b"a", b"b")
    head = log.head().as_dict()
    c1 = witness.cosign(head)
    c2 = witness.cosign(head)                        # same size, same root
    assert c1["root_hash"] == c2["root_hash"]


# ---------------------------------------------------------------------------
# a third party can verify the cosignature without trusting the operator
# ---------------------------------------------------------------------------
def test_cosignature_is_independently_verifiable(witness):
    log = _log(b"a", b"b")
    cosig = witness.cosign(log.head().as_dict())
    # a verifier with only the witness's public key + the head checks out
    assert Witness.verify_cosignature(
        cosig, witness_public_key_b64=witness.public_key_b64
    ) is True
    # tamper with the attested root -> verification fails
    tampered = {**cosig, "root_hash": "00" * 32}
    assert Witness.verify_cosignature(
        tampered, witness_public_key_b64=witness.public_key_b64
    ) is False


# ---------------------------------------------------------------------------
# state persistence — so a CLI witness enforces append-only across invocations
# ---------------------------------------------------------------------------
def test_witness_state_roundtrip(tmp_path):
    w1 = Witness(LocalSigner(sign.load_or_create_key(tmp_path / "w.pem")))
    log = _log(b"a", b"b", b"c")
    w1.cosign(log.head().as_dict())
    state = w1.state()
    assert state["last_size"] == 3
    assert state["last_root"] == log.head().root_hash

    # a fresh witness restored from that state remembers the checkpoint
    w2 = Witness(LocalSigner(sign.load_or_create_key(tmp_path / "w.pem")))
    w2.restore(state)
    # so it rejects a shrink, exactly as if it had cosigned in-process
    smaller = _log(b"a", b"b")
    with pytest.raises(WitnessRejected):
        w2.cosign(smaller.head().as_dict())


def test_restored_witness_requires_consistency_for_growth(tmp_path):
    w1 = Witness(LocalSigner(sign.load_or_create_key(tmp_path / "w.pem")))
    log = _log(b"a", b"b")
    w1.cosign(log.head().as_dict())

    w2 = Witness(LocalSigner(sign.load_or_create_key(tmp_path / "w.pem")))
    w2.restore(w1.state())
    log.append(b"c")
    # restored witness knows it's at size 2, so growth needs a proof
    with pytest.raises(WitnessRejected, match="consistency proof required"):
        w2.cosign(log.head().as_dict())
    # with the proof it cosigns
    cosig = w2.cosign(log.head().as_dict(), consistency=log.prove_consistency(2))
    assert cosig["tree_size"] == 3
