#!/usr/bin/env python3
"""Draft 2020-12, exact-state, and negative self-test validator."""
from __future__ import annotations
import copy, hashlib, json, sys
from pathlib import Path

ROOT = Path(__file__).resolve().parent


def fail(rule: str, cid: str, detail: str = "") -> None:
    raise ValueError(f"{cid}: [{rule}]" + (f" {detail}" if detail else ""))


def keyed(items, key):
    return {item[key]: item for item in items}


def assert_equal(rule, cid, actual, expected):
    if actual != expected:
        fail(rule, cid, f"expected {expected!r}, got {actual!r}")


ADJUSTMENT_SIGNED_FIELDS = ("adjustmentId", "ownerId", "delta", "authorizedBy", "reasonCode", "auditRecordId", "idempotencyKey", "authorizationId", "authorizationVersion", "authorizationSourceVersion", "requestedAt")

# These receipts model evidence fetched from the independently authenticated time
# authority/config channel. Fixture-owned clocks can cross-check a receipt, but can
# never redefine the timestamp used for jurisdiction decisions.
TRUSTED_TIME_RECEIPTS = {
    "time.receipt.200": {"timestamp": "200", "source": "jurisdiction-time-authority", "configVersion": "7", "signatureVerified": True},
    "time.receipt.201": {"timestamp": "201", "source": "jurisdiction-time-authority", "configVersion": "7", "signatureVerified": True},
}


def adjustment_hash(value):
    canonical = json.dumps({k: value[k] for k in ADJUSTMENT_SIGNED_FIELDS}, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
    return hashlib.sha256(canonical.encode("utf-8")).hexdigest()


def expected_snapshot(initial):
    return {k: copy.deepcopy(initial[k]) for k in ("purchases", "receipts", "claims", "cycles", "adjustments", "refundStates")}


def reject_unchanged(cid, initial, expected):
    assert_equal("zero-mutation-state", cid, expected["state"], expected_snapshot(initial))
    assert_equal("zero-mutation-ledger", cid, expected["ledgerAppends"], [])


def assert_webhook_transition(cid, event, initial, expected):
    receipt = {"eventId": event["eventId"], "payloadHash": event["payloadHash"], "objectId": event["objectId"], "eventType": event["type"]}
    effect = "payment_grant" if event["type"] == "payment_webhook" else "refund_reversal"
    claim = {"objectId": event["objectId"], "purchaseId": event["purchaseId"], "effect": effect}
    assert_equal("receipt-exact-transition", cid, expected["state"]["receipts"], initial["receipts"] + [receipt])
    assert_equal("claim-exact-transition", cid, expected["state"]["claims"], initial["claims"] + [claim])
    for name in ("purchases", "cycles", "adjustments"):
        assert_equal("webhook-unrelated-state", cid, expected["state"][name], initial[name])


def derive_adjustment(event, initial):
    if event["delta"] in {"0", "-0"}:
        return "rejected", "ADJUSTMENT_INVALID", "malformed_rejected"
    same = [a for a in initial["adjustments"] if a["idempotencyKey"] == event["idempotencyKey"]]
    if same:
        if same[0]["payloadHash"] == event["payloadHash"]:
            return "noop", None, "replay_same_hash_noop"
        return "rejected", "EVENT_PAYLOAD_CONFLICT", "replay_different_hash_rejected"
    auth = keyed(initial["operatorAuthorizations"], "authorizationId").get(event["authorizationId"])
    identity = auth and auth["operatorId"] == event["authorizedBy"] and auth["version"] == event["authorizationVersion"] and auth["source"] == "operator_authorization_registry" and auth["sourceVersion"] == event["authorizationSourceVersion"] and event["reasonCode"] in auth["scopes"]
    if not identity:
        return "rejected", "ADJUSTMENT_UNAUTHORIZED", "unauthorized_rejected"
    if not (int(auth["validFrom"]) <= int(event["requestedAt"]) < int(auth["expiresAt"])):
        return "rejected", "ADJUSTMENT_UNAUTHORIZED", "expired_rejected"
    return "accepted", None, "accepted"


def event_assertions(event):
    common = {"rollback", "provider_calls"}
    if event["type"] == "create_purchase":
        return common | {"canonical_purchase", "currency", "owner", "quantity", "amount", "tax", "jurisdiction", "registration"}
    if event["type"] == "payment_webhook":
        return common | {"canonical_purchase", "provider_event", "provider_object", "payload_hash", "currency", "owner", "quantity", "amount", "tax", "jurisdiction", "signature", "claim", "effect"}
    if event["type"] == "refund_webhook":
        return common | {"canonical_purchase", "provider_event", "provider_object", "payload_hash", "currency", "owner", "quantity", "amount", "tax", "jurisdiction", "signature", "claim", "effect", "cumulative_allocation"}
    if event["type"] == "operator_adjustment":
        return common | {"authorization", "payload_hash", "audit", "effect"}
    if event["type"] == "conversion_accept":
        return common | {"owner", "amount", "effect"}
    if event["type"] == "cycle_failed":
        return common | {"effect"}
    if event["type"] == "register_jurisdiction":
        return common | {"registration", "authorization"}
    raise ValueError(event["type"])


def validate_semantics(case):
    cid, initial, expected = case["caseId"], case["initial"], case["expected"]
    for collection, key in (("purchases","purchaseId"),("receipts","eventId"),("claims","objectId"),("cycles","cycleId"),("adjustments","adjustmentId"),("refundStates","purchaseId"),("operatorAuthorizations","authorizationId"),("ledgerBefore","key")):
        values=[x[key] for x in initial[collection]]
        if len(values)!=len(set(values)): fail("unique-initial",cid,collection)
    for collection, key in (("purchases","purchaseId"),("receipts","eventId"),("claims","objectId"),("cycles","cycleId"),("adjustments","adjustmentId"),("refundStates","purchaseId")):
        values=[x[key] for x in expected["state"][collection]]
        if len(values)!=len(set(values)): fail("unique-expected",cid,collection)
    if len({x["idempotencyKey"] for x in initial["adjustments"]}) != len(initial["adjustments"]): fail("adjustment-idempotency-unique",cid)
    trusted_receipt = TRUSTED_TIME_RECEIPTS.get(initial["trustedTimeReceiptId"])
    if not trusted_receipt or not trusted_receipt["signatureVerified"] or trusted_receipt["source"] != "jurisdiction-time-authority" or trusted_receipt["configVersion"] != "7":
        fail("trusted-time-receipt", cid)
    trusted_time = trusted_receipt["timestamp"]
    if not (initial["evaluationAt"] == initial["databaseNow"] == initial["authoritativeJurisdictionTime"] == trusted_time):
        fail("authoritative-jurisdiction-time",cid)

    # Validate canonical purchase arithmetic before transition-shape checks so a
    # subtotal mutation is diagnosed by its own invariant, not as unrelated state.
    for p in initial["purchases"]+expected["state"]["purchases"]:
        if int(p["itemSubtotalCents"]) != int(p["quantity"])*int(p["unitAmountCents"]): fail("purchase-subtotal",cid)

    # Declared outcomes never act as the oracle: derive them from authoritative inputs.
    for event in case["events"]:
        if event["type"] == "operator_adjustment":
            assert_equal("adjustment-payload-hash", cid, event["payloadHash"], adjustment_hash(event))
            for prior in initial["adjustments"]:
                assert_equal("adjustment-stored-payload-hash", cid, prior["payloadHash"], adjustment_hash(prior))
            derived = derive_adjustment(event, initial)
            assert_equal("adjustment-derived-outcome", cid, (expected["outcome"], expected["error"], expected["auditOutcome"]), derived)
        elif event["type"] == "create_purchase":
            reg = next((r for r in initial["registrations"] if r["jurisdiction"] == event["jurisdiction"] and r["version"] == event["registrationVersion"]), None)
            authority_time = trusted_time
            authority_ok = reg and reg["authorizationSourceVersion"] == event["registrationAuthorizationVersion"] and reg["evaluatedAt"] == authority_time and reg["databaseNow"] == authority_time
            in_window = authority_ok and int(reg["validFrom"]) <= int(authority_time) < int(reg["expiresAt"])
            derived = ("accepted", None) if in_window else (("rejected", "REGISTRATION_EXPIRED") if authority_ok and int(authority_time) >= int(reg["expiresAt"]) else ("rejected", "REGISTRATION_INVALID"))
            assert_equal("jurisdiction-derived-outcome", cid, (expected["outcome"], expected["error"]), derived)
        elif event["type"] == "conversion_accept" and expected["outcome"] != "correlated":
            eligible = event["fundingKind"] == "customer"
            cycle = {"cycleId": event["cycleId"], "ownerId": event["ownerId"], "fundingKind": event["fundingKind"], "state": "active", "debit": event["debit"], "compensationEligible": eligible}
            actual_cycles = expected["state"]["cycles"]
            if any(c["fundingKind"] == "goodwill" and c["compensationEligible"] for c in actual_cycles):
                fail("cycle-funding-eligibility", cid)
            assert_equal("conversion-cycle-derived-state", cid, actual_cycles, initial["cycles"] + [cycle])
            suffix = event["cycleId"].split(".")[-1]
            effect = {"key": f"ledger.debit.{suffix}", "delta": f"-{int(event['debit'])}", "reason": "conversion_debit", "ref": {"kind": "cycle", "id": event["cycleId"]}}
            assert_equal("conversion-ledger-effect", cid, expected["ledgerAppends"], [effect])
        elif event["type"] == "cycle_failed":
            cycle = keyed(initial["cycles"], "cycleId").get(event["cycleId"])
            if not cycle:
                fail("cycle-failure-source", cid)
            if cycle["fundingKind"] == "goodwill" and cycle["compensationEligible"]:
                fail("cycle-funding-eligibility", cid)
            eligible = cycle["fundingKind"] == "customer" and cycle["compensationEligible"] and cycle["state"] == "active" and event["failureClass"] == "terminal"
            derived = ("accepted", None) if eligible else ("rejected", "CYCLE_NOT_ELIGIBLE")
            assert_equal("cycle-failure-derived-outcome", cid, (expected["outcome"], expected["error"]), derived)
            if eligible:
                transitioned = copy.deepcopy(cycle); transitioned["state"] = "compensated"
                cycles = [transitioned if c["cycleId"] == cycle["cycleId"] else c for c in initial["cycles"]]
                assert_equal("cycle-failure-derived-state", cid, expected["state"]["cycles"], cycles)
                suffix = event["cycleId"].split(".")[-1]
                effect = {"key": f"ledger.comp.{suffix}", "delta": cycle["debit"], "reason": "cycle_failure_compensation", "ref": {"kind": "cycle", "id": cycle["cycleId"]}}
                assert_equal("cycle-failure-ledger-effect", cid, expected["ledgerAppends"], [effect])
            else:
                assert_equal("cycle-failure-derived-state", cid, expected["state"]["cycles"], initial["cycles"])
                assert_equal("cycle-failure-ledger-effect", cid, expected["ledgerAppends"], [])
        elif event["type"] in {"payment_webhook", "refund_webhook"}:
            truth = event["providerTruth"]
            envelope = {k: event[k] for k in truth if k != "paymentState"}
            if any(envelope[k] != truth[k] for k in envelope): fail("provider-truth-binding", cid)
            purchase = keyed(initial["purchases"], "purchaseId").get(truth["purchaseId"])
            receipts = keyed(initial["receipts"], "eventId")
            claims = keyed(initial["claims"], "objectId")
            if not event["signatureVerified"]: derived = ("rejected", "UNVERIFIED_EVENT")
            elif not purchase: derived = ("rejected", "PURCHASE_NOT_FOUND")
            elif truth["quantity"] != purchase["quantity"]: derived = ("rejected", "INVALID_QUANTITY")
            elif truth["amountCents"] != purchase["itemSubtotalCents"]: derived = ("rejected", "AMOUNT_MISMATCH")
            elif truth["currency"] != purchase["currency"]: derived = ("rejected", "CURRENCY_MISMATCH")
            elif truth["jurisdiction"] != purchase["jurisdiction"]: derived = ("rejected", "JURISDICTION_MISMATCH")
            elif truth["ownerId"] != purchase["ownerId"]: derived = ("rejected", "OWNER_MISMATCH")
            elif truth["taxAmountCents"] != purchase["taxAmountCents"]: derived = ("rejected", "AMOUNT_MISMATCH")
            elif event["type"] == "payment_webhook" and truth["paymentState"] != "paid": derived = ("rejected", "UNVERIFIED_EVENT")
            elif event["eventId"] in receipts:
                r = receipts[event["eventId"]]
                derived = ("noop", None) if r["payloadHash"] == event["payloadHash"] and r["objectId"] == event["objectId"] and event["objectId"] in claims else ("rejected", "EVENT_PAYLOAD_CONFLICT")
            elif event["objectId"] in claims: derived = ("rejected", "OBJECT_IDENTITY_CONFLICT")
            else: derived = ("accepted", None)
            assert_equal("webhook-derived-outcome", cid, (expected["outcome"], expected["error"]), derived)
            if derived[0] == "accepted":
                assert_webhook_transition(cid, event, initial, expected)

    if expected["outcome"] in {"rejected", "noop"}:
        reject_unchanged(cid, initial, expected)

    regs={(r["jurisdiction"],r["version"]):r for r in initial["registrations"]}
    for p in initial["purchases"]+expected["state"]["purchases"]:
        r=regs.get((p["jurisdiction"],p["registrationVersion"]))
        if not r: fail("registration-binding",cid)
        if r["authorizationSourceVersion"] != p["registrationAuthorizationVersion"]: fail("registration-auth-binding",cid)

    bindings={}
    for r in initial["receipts"]+expected["state"]["receipts"]:
        b=(r["payloadHash"],r["objectId"])
        if r["eventId"] in bindings and bindings[r["eventId"]]!=b: fail("receipt-binding",cid)
        bindings[r["eventId"]]=b
    receipt_objects={r["objectId"] for r in expected["state"]["receipts"]}
    purchase_ids={p["purchaseId"] for p in expected["state"]["purchases"]}
    for c in expected["state"]["claims"]:
        if c["purchaseId"] not in purchase_ids: fail("claim-purchase",cid)
        if c["objectId"] not in receipt_objects: fail("claim-receipt",cid)

    before=sum(int(x["delta"]) for x in initial["ledgerBefore"])
    appends=expected["ledgerAppends"]
    if len({x["key"] for x in appends})!=len(appends): fail("ledger-key-unique",cid)
    net=before+sum(int(x["delta"]) for x in appends)
    assert_equal("ledger-net",cid,int(expected["ledgerNet"]),net)
    assert_equal("spendable",cid,int(expected["spendableBalance"]),max(0,net))
    assert_equal("deficit",cid,int(expected["deficit"]),max(0,-net))
    if expected["outcome"] in {"rejected","noop","reconcile"} and appends: fail("rollback-ledger",cid)

    state_refunds=keyed(expected["state"]["refundStates"],"purchaseId")
    initial_refunds=keyed(initial["refundStates"],"purchaseId")
    for event in case["events"]:
        assert_equal("assertion-coverage",cid,set(expected["assertions"]),set().union(*(event_assertions(e) for e in case["events"])))
        if expected["providerCalls"] != "0": fail("provider-call-count",cid,"fixture events are inbound or persistence-only")
        if event["type"] in {"payment_webhook","refund_webhook"}:
            p=keyed(initial["purchases"],"purchaseId").get(event["purchaseId"])
            if expected["outcome"] in {"accepted","noop"} and not p: fail("webhook-purchase",cid)
            if p:
                for f in ("ownerId","quantity","currency","jurisdiction","taxAmountCents"):
                    if expected["outcome"] in {"accepted","noop"} and event[f]!=p[f]: fail("webhook-canonical-"+f,cid)
                if expected["outcome"] in {"accepted","noop"} and event["amountCents"]!=p["itemSubtotalCents"]: fail("webhook-canonical-amount",cid)
            if expected["outcome"]=="accepted" and not event["signatureVerified"]: fail("webhook-signature",cid)
        if event["type"]=="refund_webhook" and expected["outcome"]=="accepted":
            p=keyed(initial["purchases"],"purchaseId")[event["purchaseId"]]
            old=initial_refunds.get(event["purchaseId"],{"cumulativeRefundedItemSubtotalCents":"0","reversedCredits":"0"})
            old_c, old_r=int(old["cumulativeRefundedItemSubtotalCents"]),int(old["reversedCredits"])
            cumulative, subtotal, quantity=int(event["cumulativeRefundedItemSubtotalCents"]),int(p["itemSubtotalCents"]),int(p["quantity"])
            if cumulative<old_c: fail("refund-monotonicity",cid)
            if cumulative>subtotal: fail("refund-over-reversal",cid)
            if event["taxRefundedCents"]!="0" and cumulative==old_c: fail("refund-tax-only",cid)
            target=quantity if cumulative==subtotal else quantity*cumulative//subtotal
            if target<old_r or target>quantity: fail("refund-allocation-bounds",cid)
            delta=-(target-old_r)
            reversals=[x for x in appends if x["reason"] in {"payment_refund_reversal","payment_dispute_reversal"}]
            if len(reversals)!=1 or int(reversals[0]["delta"])!=delta: fail("refund-derived-delta",cid)
            s=state_refunds.get(event["purchaseId"])
            if not s or int(s["cumulativeRefundedItemSubtotalCents"])!=cumulative or int(s["reversedCredits"])!=target: fail("refund-exact-state",cid)
        if event["type"]=="create_purchase" and expected["outcome"]=="accepted":
            r=regs.get((event["jurisdiction"],event["registrationVersion"]))
            now=int(trusted_time)
            if not r or not (int(r["validFrom"])<=now<int(r["expiresAt"])): fail("registration-window",cid)
            if r and (r["evaluatedAt"]!=trusted_time or r["databaseNow"]!=trusted_time): fail("registration-evidence-time",cid)
        if event["type"]=="operator_adjustment":
            if event["delta"] in {"0", "-0"}:
                if expected["outcome"] != "rejected" or expected["auditOutcome"] != "malformed_rejected": fail("adjustment-malformed",cid)
            auth=keyed(initial["operatorAuthorizations"],"authorizationId").get(event["authorizationId"])
            valid=auth and auth["operatorId"]==event["authorizedBy"] and auth["version"]==event["authorizationVersion"] and auth["source"]=="operator_authorization_registry" and auth["sourceVersion"]==event["authorizationSourceVersion"] and int(auth["validFrom"])<=int(event["requestedAt"])<int(auth["expiresAt"]) and event["reasonCode"] in auth["scopes"]
            if expected["outcome"]=="accepted" and not valid: fail("adjustment-authorization",cid)
            same=[a for a in initial["adjustments"] if a["idempotencyKey"]==event["idempotencyKey"]]
            if same:
                same_hash=same[0]["payloadHash"]==event["payloadHash"]
                if same_hash and (expected["outcome"]!="noop" or expected["auditOutcome"]!="replay_same_hash_noop"): fail("adjustment-replay-same",cid)
                if not same_hash and (expected["outcome"]!="rejected" or expected["auditOutcome"]!="replay_different_hash_rejected"): fail("adjustment-replay-conflict",cid)

    winner=expected["winner"]
    if expected["outcome"]=="correlated":
        if not winner: fail("winner-required",cid)
        alternatives=winner["alternatives"]
        accepted=[a for a in alternatives if a["outcome"]=="accepted"]
        if len(accepted)!=1 or winner["matchingWinner"]!=accepted[0]["label"]: fail("winner-exactly-one",cid)
        if len(alternatives) != len(case["events"]): fail("race-one-to-one",cid,"alternative/event cardinality")
        if len({a["label"] for a in alternatives}) != len(alternatives) or len({a["submissionId"] for a in alternatives}) != len(alternatives): fail("race-unique",cid)
        events_by_submission={e.get("idempotencyKey"):e for e in case["events"]}
        if set(events_by_submission) != {a["submissionId"] for a in alternatives}: fail("race-one-to-one",cid,"submission set")
        for alternative in alternatives:
            event=events_by_submission[alternative["submissionId"]]
            if alternative["label"] != alternative["submissionId"]: fail("race-label-correlation",cid)
            if alternative["outcome"] == "accepted":
                if alternative["cycleId"] != event.get("cycleId") or alternative["jobId"] != winner["jobId"] or alternative["effectKey"] != winner["effectKey"]: fail("winner-correlation",cid)
            elif any(alternative[k] is not None for k in ("cycleId","jobId","effectKey")):
                fail("rejected-loser-identities",cid,alternative["label"])
        if any(a["outcome"]!="accepted" and a["label"]==winner["matchingWinner"] for a in alternatives): fail("winner-not-rejected",cid)
        event=events_by_submission.get(winner["matchingWinner"])
        if not event or accepted[0]["submissionId"]!=event["idempotencyKey"] or accepted[0]["cycleId"]!=event.get("cycleId") or accepted[0]["jobId"]!=winner["jobId"] or accepted[0]["effectKey"]!=winner["effectKey"]: fail("winner-correlation",cid)
        if winner["effectKey"] not in {x["key"] for x in appends}: fail("winner-effect",cid)
    elif winner is not None: fail("winner-unexpected",cid)


def negative_self_tests(cases, validator):
    by = {c["caseId"]: c for c in cases}
    probes = [
      ("BL-001", "purchase-subtotal", lambda c: c["expected"]["state"]["purchases"][0].update(itemSubtotalCents="1")),
      ("BL-007", "webhook-derived-outcome", lambda c: c["events"][0]["providerTruth"].update(paymentState="failed")),
      ("BL-007", "webhook-derived-outcome", lambda c: c["expected"].update(outcome="rejected", error="AMOUNT_MISMATCH")),
      ("BL-007", "provider-truth-binding", lambda c: c["events"][0]["providerTruth"].update(ownerId="owner.other")),
      ("BL-007", "webhook-derived-outcome", lambda c: c["events"][0].update(signatureVerified=False)),
      ("BL-010", "refund-monotonicity", lambda c: c["initial"].update(refundStates=[{"purchaseId":"purchase.p1","cumulativeRefundedItemSubtotalCents":"600","reversedCredits":"1"}])),
      ("BL-010", "refund-over-reversal", lambda c: c["events"][0].update(cumulativeRefundedItemSubtotalCents="1501")),
      ("BL-010", "refund-derived-delta", lambda c: (c["expected"]["ledgerAppends"][0].update(delta="-2"), c["expected"].update(ledgerNet="1", spendableBalance="1", deficit="0"))),
      ("BL-013", "conversion-cycle-derived-state", lambda c: c["expected"]["state"]["cycles"][0].update(state="compensated")),
      ("BL-014", "cycle-funding-eligibility", lambda c: c["expected"]["state"]["cycles"][0].update(compensationEligible=True)),
      ("BL-016", "cycle-funding-eligibility", lambda c: (c["initial"]["cycles"][0].update(compensationEligible=True), c["expected"]["state"]["cycles"][0].update(compensationEligible=True))),
      ("BL-018", "adjustment-payload-hash", lambda c: c["events"][0].update(delta="3")),
      ("BL-020", "adjustment-derived-outcome", lambda c: c["expected"].update(outcome="accepted", auditOutcome="accepted")),
      ("BL-021", "adjustment-derived-outcome", lambda c: c["initial"].update(operatorAuthorizations=[{"authorizationId":"auth.senior","operatorId":"operator.untrusted","version":"1","validFrom":"100","expiresAt":"300","scopes":["support_credit"],"source":"operator_authorization_registry","sourceVersion":"7"}])),
      ("BL-023", "adjustment-derived-outcome", lambda c: c["initial"]["operatorAuthorizations"][0].update(expiresAt="500")),
      ("BL-024", "adjustment-derived-outcome", lambda c: (c["events"][0].update(delta="1"), c["events"][0].update(payloadHash=adjustment_hash(c["events"][0])))),
      ("BL-025", "adjustment-derived-outcome", lambda c: c["expected"].update(auditOutcome="unauthorized_rejected")),
      ("BL-001", "jurisdiction-derived-outcome", lambda c: c["initial"]["registrations"][0].update(evaluatedAt="199")),
      ("BL-017", "authoritative-jurisdiction-time", lambda c: (c["initial"].update(evaluationAt="199", databaseNow="199", authoritativeJurisdictionTime="199"), c["initial"]["registrations"][0].update(evaluatedAt="199", databaseNow="199"))),
      ("BL-017", "jurisdiction-derived-outcome", lambda c: c["initial"]["registrations"][0].update(authorizationSourceVersion="2")),
      ("BL-022", "winner-exactly-one", lambda c: c["expected"]["winner"].update(matchingWinner="request.b")),
      ("BL-022", "rejected-loser-identities", lambda c: c["expected"]["winner"]["alternatives"][1].update(cycleId="cycle.race",jobId="job.race",effectKey="ledger.race")),
      ("BL-022", "race-unique", lambda c: c["expected"]["winner"]["alternatives"][1].update(submissionId="request.a")),
    ]
    for case_id, rule, mutate in probes:
        specimen=copy.deepcopy(by[case_id]); mutate(specimen)
        try: validate_semantics(specimen)
        except ValueError as e:
            if f"[{rule}]" not in str(e): fail("self-test-wrong-rule","SELFTEST",f"{rule}: {e}")
        else: fail("self-test-did-not-fire","SELFTEST",rule)

    for field, malformed in (("observedAt","not-a-date"),("sourceUri","not a uri"),("ownerEmail","not-an-email"),("traceId","not-a-uuid")):
        specimen=copy.deepcopy(cases[0]); specimen["evidenceMetadata"][field]=malformed
        if not list(validator.iter_errors(specimen)): fail("format-self-test-did-not-fire","SELFTEST",field)

    # Canonical hashing is invariant to input member order and deliberately differs
    # from noncanonical default JSON serialization.
    event=by["BL-018"]["events"][0]
    reversed_event={k:event[k] for k in reversed(tuple(event))}
    assert_equal("canonical-order-invariance","SELFTEST",adjustment_hash(reversed_event),event["payloadHash"])
    default=json.dumps({k:event[k] for k in ADJUSTMENT_SIGNED_FIELDS}).encode("utf-8")
    if hashlib.sha256(default).hexdigest()==event["payloadHash"]: fail("canonical-serialization-distinct","SELFTEST")


def main():
    try:
        from jsonschema import Draft202012Validator, FormatChecker
    except ImportError:
        print("BLOCKED: jsonschema dependency required",file=sys.stderr); return 2
    try:
        schema=json.loads((ROOT/"case.schema.json").read_text())
        cases=json.loads((ROOT/"cases.json").read_text())
        Draft202012Validator.check_schema(schema)
        v=Draft202012Validator(schema,format_checker=FormatChecker())
        for i,c in enumerate(cases):
            errors=sorted(v.iter_errors(c),key=lambda e:list(e.absolute_path))
            if errors: fail("schema",c.get("caseId",str(i)),"; ".join(e.message for e in errors))
            validate_semantics(c)
        negative_self_tests(cases, v)
    except (OSError,json.JSONDecodeError,ValueError) as e:
        print(f"FAILED: {e}",file=sys.stderr); return 1
    print(f"PASS: Draft 2020-12 + FormatChecker + semantic invariants + negative self-tests ({len(cases)} cases)")
    return 0

if __name__=="__main__": raise SystemExit(main())
