"""Teaching fixture: review evidence completeness, not a production authorizer.
Run: python3 review-packet-evidence-gate.py
No network calls or external writes. Trusted inputs below are simulated.
"""
from copy import deepcopy
from dataclasses import dataclass, replace
import unittest


@dataclass(frozen=True)
class Context:
    operation: str
    target: str
    revision: str


@dataclass(frozen=True)
class Evidence:
    kind: str
    context: Context
    status: str
    outcome: str


# Owned by the workflow, never accepted from the agent's packet.
REQUIRED = {
    "validation": "passed",
    "authorization": "allowed",
    "effect": "committed",
    "settlement": "closed",
}


def gate(packet, expected, trusted_store):
    """Return blockers. Empty means evidence-complete, NOT permission to write.

    trusted_store represents a service-side verified evidence registry. An
    agent supplies opaque references, not records or 'verified' flags. A real
    registry must authenticate issuers and validate scope/freshness itself.
    expected comes from the workflow, not the packet.
    """
    blockers = []
    refs = packet.get("evidence", {})
    if not isinstance(refs, dict):
        return ["malformed_evidence"]
    for kind, acceptable in REQUIRED.items():
        ref = refs.get(kind)
        record = trusted_store.get(ref) if isinstance(ref, str) else None
        if record is None:
            blockers.append(f"{kind}:missing_or_untrusted")
        elif record.kind != kind or record.context != expected:
            blockers.append(f"{kind}:binding_mismatch")
        elif record.status != "verified":
            blockers.append(f"{kind}:unverified")
        elif record.outcome != acceptable:
            blockers.append(f"{kind}:unacceptable_outcome")
    # This example binds the summary to the one expected effect outcome.
    if packet.get("claimed_effect") != "committed":
        blockers.append("summary:inconsistent_or_missing")
    return blockers


def fixture():
    context = Context("op-17", "tenant-a/staging/routes/payments", "revision-41")
    store = {f"receipt:{k}": Evidence(k, context, "verified", outcome)
             for k, outcome in REQUIRED.items()}
    packet = {"evidence": {k: f"receipt:{k}" for k in REQUIRED},
              "claimed_effect": "committed"}
    return packet, context, store


class GateTests(unittest.TestCase):
    def setUp(self):
        self.packet, self.context, self.store = fixture()

    def check(self):
        return gate(self.packet, self.context, self.store)

    def test_complete_evidence(self):
        self.assertEqual(self.check(), [])

    def test_lost_receipt_blocks_even_when_summary_says_success(self):
        del self.store["receipt:effect"]
        self.assertIn("effect:missing_or_untrusted", self.check())

    def test_wrong_operation(self):
        record = self.store["receipt:effect"]
        self.store["receipt:effect"] = replace(record, context=replace(self.context, operation="op-other"))
        self.assertIn("effect:binding_mismatch", self.check())

    def test_wrong_tenant_or_revision(self):
        for context in (replace(self.context, target="tenant-b/staging/routes/payments"),
                        replace(self.context, revision="revision-40")):
            with self.subTest(context=context):
                self.store["receipt:effect"] = Evidence("effect", context, "verified", "committed")
                self.assertIn("effect:binding_mismatch", self.check())

    def test_present_failure_is_not_success(self):
        self.store["receipt:effect"] = replace(self.store["receipt:effect"], outcome="failed")
        self.assertIn("effect:unacceptable_outcome", self.check())

    def test_agent_cannot_delete_requirement(self):
        del self.packet["evidence"]["effect"]
        self.packet["required"] = ["validation"]
        self.assertIn("effect:missing_or_untrusted", self.check())

    def test_pending_settlement(self):
        self.store["receipt:settlement"] = replace(self.store["receipt:settlement"], status="pending")
        self.assertIn("settlement:unverified", self.check())

    def test_self_attestation_is_not_a_receipt(self):
        self.packet["evidence"]["effect"] = {"verified": True, "outcome": "committed"}
        self.assertIn("effect:missing_or_untrusted", self.check())

    def test_summary_contradiction(self):
        self.packet["claimed_effect"] = "failed"
        self.assertIn("summary:inconsistent_or_missing", self.check())

    def test_reconciliation_unblocks_completeness_only(self):
        receipt = self.store.pop("receipt:effect")
        self.assertTrue(self.check())
        self.store["receipt:effect"] = receipt  # Simulated authoritative lookup.
        self.assertEqual(self.check(), [])

    def test_malformed_evidence(self):
        self.packet["evidence"] = []
        self.assertEqual(self.check(), ["malformed_evidence"])

    def test_does_not_mutate_packet(self):
        before = deepcopy(self.packet)
        self.check()
        self.assertEqual(self.packet, before)


if __name__ == "__main__":
    unittest.main(verbosity=2)
