#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
"""Regenerates every ADCR test vector from fixed inputs.

Each suite directory gets cases/<case>.json (one export per case) and
expected.json. Keys and pseudonyms are derived from fixed seeds, and the
timestamp tokens are read from vectors/keys/tokens/, so the vectors are
byte-stable. A statement with no token on disk is minted once with openssl
from the test authority (make_test_authority.py; its private keys sit in the
git-ignored .test-authority/) and the new token file is committed with the
vectors.

Before writing anything, every case is run through the reference verifier and
the run aborts unless the verdict and the set of finding names are exactly the
ones declared for the case: each negative case breaks one rule, and the
declaration says which.

A case may also carry a producer profile (PRF-01); it is written to
<suite>/profiles/<case>.json and named in expected.json, and the verifier runs
with --profile. Token files that no case uses any more are removed.

Usage: python3 -I generate_vectors.py [vectors_dir]
Licensed Apache-2.0.
"""
from __future__ import annotations

import base64
import copy
import hashlib
import hmac
import json
import os
import subprocess
import sys
import tempfile
from datetime import datetime, timedelta, timezone

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from adcr_verify import (RULEBOOK, CHAIN_ALG, anchor_statement_bytes, chain_hash,  # noqa: E402
                         find_openssl, parse_token, verify)

KEY = hashlib.sha256(b"adcr-test-account-key-v1").digest()
ACCOUNT = "acct_test_0001"
PRODUCER = "https://producer.example/profile"
RULE_SET = {
    "digest": hashlib.sha256(b"test rule set 2026-10").hexdigest(),
    "label": "test-rules-2026.10",
    "index_url": "https://producer.example/rules/",
}
PROVISION_5_1_C = {
    "eli": "http://data.europa.eu/eli/reg/2024/1689/art_5/par_1/pnt_c/oj",
    "consolidated_eli": "http://data.europa.eu/eli/reg/2024/1689/2026-07-27",
    "label": "Regulation (EU) 2024/1689, Article 5(1)(c)",
}
PROVISION_GDPR_6_1_B = {
    "eli": "http://data.europa.eu/eli/reg/2016/679/art_6/par_1/pnt_b/oj",
    "label": "Regulation (EU) 2016/679, Article 6(1)(b)",
}
PROVISION_NATIONAL = {
    "url": "https://www.gesetze-im-internet.de/betrvg/__87.html",
    "label": "Betriebsverfassungsgesetz, § 87 Abs. 1 Nr. 6",
}
SIGNERS = {  # signer name -> (certificate, key, chain) under keys/
    "tsa": ("tsa.pem", "tsa.key", "root.pem"),
    "tsa-expired": ("tsa-expired.pem", "tsa.key", "root.pem"),
    "other-tsa": ("other-tsa.pem", "other-tsa.key", "other-root.pem"),
}
KEYS_DIR = os.path.join(HERE, "vectors", "keys")
PRIVATE_DIR = os.path.join(HERE, ".test-authority")  # git-ignored; see make_test_authority.py
# CMT-02: both inputs carry a domain tag, and neither tag is a prefix of the other.
COMMITMENT_DOMAIN = b"commitment|"
PSEUDONYM_DOMAIN = b"pseudonym|"
USED_TOKENS: set = set()


# --- building blocks ----------------------------------------------------------

def commit(text: str, direction: str, key_version=1):
    return {"alg": "hmac-sha256-1", "value": hmac.new(KEY, COMMITMENT_DOMAIN + text.encode(), hashlib.sha256).hexdigest(),
            "key_version": key_version, "direction": direction}


def pseudonym(ident: str, key_version=1):
    return {"value": "p1:" + hmac.new(KEY, PSEUDONYM_DOMAIN + ident.encode(), hashlib.sha256).hexdigest()[:32],
            "key_version": key_version}


def ts(i: int, offset_ms=0):
    base = 1_760_000_000_000 + i * 60_000 + offset_ms  # fixed epoch, one minute apart
    dt = datetime.fromtimestamp(base / 1000, tz=timezone.utc)
    return dt.strftime("%Y-%m-%dT%H:%M:%S.") + f"{base % 1000:03d}Z"


def common(i: int, event_type: str, record_id=None):
    return {
        "rulebook": RULEBOOK, "record_version": "test-1", "record_id": record_id or f"rec_{i:04d}",
        "occurred_at": ts(i), "event_type": event_type,
        "account_id": ACCOUNT, "producer_id": PRODUCER, "deploying_party": ACCOUNT,
        "deciding_system": {"id": "assistant-hr", "version": "2026.10.1"},
        "retention": {"class": "standard", "erase_after": "2027-04-14T00:00:00Z"},
    }


def legal_hold(i: int, action="placed", from_seq=1, to_seq=3):
    """REC-19: a hold placed after the records it covers were chained is its own record."""
    r = common(i, "legal_hold")
    del r["deciding_system"]  # REC-07: a hold covers a range of the chain
    r["hold"] = {"id": "hold_2026_001", "action": action, "scope": {"from_seq": from_seq, "to_seq": to_seq},
                 "reason_code": "litigation.reasonably_anticipated"}
    return r


def decision(i: int, fired=True, record_id=None):
    r = common(i, "decision", record_id)
    r.update({
        "checker": {"id": "checker", "version": "1.0", "path": "rules"},
        "user": pseudonym(f"user{i}"), "user_role": "agent_operator",
        "commitments": [commit(f"input text {i}", "input"), commit(f"output text {i}", "output")],
        "correlation_id": f"corr_{i:04d}",
        "data_categories": [],
        "purpose_ref": "https://producer.example/systems/assistant-hr#intended-purpose",
        "verdict": "deny" if fired else "allow", "action": "blocked" if fired else "passed",
        "rules_fired": [{"rule_id": "rego-art5-1c-001", "rule_version": "3",
                         "provision": copy.deepcopy(PROVISION_5_1_C),
                         "citation": "the placing on the market ... of AI systems for the evaluation or "
                                     "classification of natural persons ... based on their social behaviour"}]
        if fired else [],
        "rule_set": copy.deepcopy(RULE_SET),
        "exemptions": {"claimed": [], "rejected": []},
    })
    return r


def about_person(r: dict, i: int, employee=False):
    """Make a decision record a decision about a natural person (REC-08, LAW-07, RGT-01/02/04)."""
    r["subject"] = pseudonym(f"subject{i}")
    r["data_categories"] = ["employment_history", "contact_details"]
    r["legal_basis"] = {"provision": copy.deepcopy(PROVISION_GDPR_6_1_B), "art22_exception": "22(2)(a)"}
    r["decision_mode"] = "solely_automated"
    r["notice"] = {"given": True, "given_at": ts(i, -30_000), "notice_ref": "https://producer.example/notices/ai-use-v3"}
    if employee:
        r["subject_is_employee"] = True
        r["works_agreement_ref"] = "https://producer.example/agreements/works-agreement-2026"
        r["notice"]["workers_informed_at"] = "2026-01-15T09:00:00.000Z"
    r["explanation_inputs"] = {
        "logic_ref": RULE_SET["digest"] + "#rego-art5-1c-001",
        "procedure_ref": "https://producer.example/procedures/screening-v4",
        "main_factors": [{"code": "experience_years", "importance": "high"},
                         {"code": "certification_match", "importance": "medium"}],
        "data_categories": ["employment_history", "contact_details"],
        "risks_ref": "https://producer.example/systems/assistant-hr/risk-statement-v2",
        "main_elements": {"outcome_code": "rejected", "effect_code": "application_closed",
                          "effective_date": "2025-10-09"},
        "sensitivity": [{"code": "experience_years", "direction": "up", "change_bucket": "small"}],
    }
    return r


def oversight(i: int, proposal: str, action: str, reviewer="rev1", **extra):
    r = common(i, "oversight")
    r.update({"proposal_ref": proposal, "oversight_action": action, "reviewer": pseudonym(reviewer),
              "reviewer_role": "case_officer", "authority_ref": "https://producer.example/roles/case-officer",
              "review_opened_at": ts(i, -20_000), "inputs_ref": f"https://producer.example/case-files/{proposal}",
              "reason_code": "policy.override.documented" if action in ("overridden", "reversed") else "review.ok"})
    r.update(extra)
    return r


def chain(records: list[dict]):
    prev, n = "", 0
    for r in records:
        r.pop("chain", None)
        if r.get("chain_status") == "pre_chain":
            continue
        n += 1
        h = chain_hash(prev, r)
        r["chain"] = {"seq": n, "prev": prev, "hash": h, "alg": CHAIN_ALG}
        prev = h
    return records


def rechain(records):
    return chain([{k: v for k, v in r.items() if k != "chain"} for r in records])


# --- tokens ---------------------------------------------------------------------

def mint(data: bytes, signer: str) -> bytes:
    """The TimeStampResp for data from the named test signer, minted once."""
    path = os.path.join(KEYS_DIR, "tokens", hashlib.sha256(data).hexdigest()[:32] + "-" + signer + ".tsr")
    USED_TOKENS.add(os.path.basename(path))
    if os.path.exists(path):
        with open(path, "rb") as fh:
            return fh.read()
    tool = find_openssl()
    if tool is None:
        raise SystemExit(f"no token on disk for {os.path.basename(path)} and no OpenSSL 1.1+ to mint it")
    cert, key, chain_pem = SIGNERS[signer]
    if not os.path.exists(os.path.join(PRIVATE_DIR, key)):
        raise SystemExit(f"no token on disk for {os.path.basename(path)} and no test key to mint it: run "
                         "make_test_authority.py (a new test authority; every token is minted again)")
    k = lambda n: os.path.join(KEYS_DIR, n)  # noqa: E731
    with tempfile.TemporaryDirectory() as tmp:
        with open(os.path.join(tmp, "serial"), "w") as fh:
            fh.write("01\n")
        with open(os.path.join(tmp, "data"), "wb") as fh:
            fh.write(data)
        subprocess.run([tool, "ts", "-query", "-data", "data", "-sha256", "-no_nonce", "-cert", "-out", "q.tsq"],
                       cwd=tmp, check=True, capture_output=True)
        subprocess.run([tool, "ts", "-reply", "-config", k("tsa.cnf"), "-queryfile", "q.tsq", "-signer", k(cert),
                        "-inkey", os.path.join(PRIVATE_DIR, key), "-chain", k(chain_pem), "-out", "r.tsr"],
                       cwd=tmp, check=True, capture_output=True)
        with open(os.path.join(tmp, "r.tsr"), "rb") as fh:
            der = fh.read()
    with open(path, "wb") as fh:
        fh.write(der)
    return der


def anchor_for(r: dict, signer="tsa", statement=None, token_over=None, tamper=None):
    st = statement or {"account_id": r["account_id"], "chain_alg": CHAIN_ALG,
                       "chain_hash": r["chain"]["hash"], "chain_seq": r["chain"]["seq"]}
    der = mint(anchor_statement_bytes(token_over or st), signer)
    if tamper == "signature":
        der = der[:-1] + bytes([der[-1] ^ 0x01])
    gen = parse_token(der)["gen_time"] + timedelta(seconds=1)
    return {"statement": st, "token_b64": base64.b64encode(der).decode(), "provider_url": "https://tsa.example/qtsp",
            "stored_at": gen.strftime("%Y-%m-%dT%H:%M:%S.000Z")}


def export(records: list[dict], anchors=None, **env_extra):
    chained = [r for r in records if "chain" in r]
    last = max((r["chain"]["seq"] for r in chained), default=0)
    if anchors is None:
        anchors = [anchor_for(max(chained, key=lambda r: r["chain"]["seq"]))] if chained else []
    newest = max((a["statement"]["chain_seq"] for a in anchors
                  if isinstance(a["statement"].get("chain_seq"), int) and a["statement"]["chain_seq"] <= last),
                 default=0)
    env = {
        "rulebook": RULEBOOK, "account_id": ACCOUNT, "producer_id": PRODUCER,
        "period": {"from": ts(0), "to": ts(len(records) + 1)},
        "clock_source": {"reference": "ntp.example, stratum 2", "tolerance_ms": 1000},
        "chain_summary": {"alg": CHAIN_ALG, "first_seq": 1 if chained else 0, "last_seq": last,
                          "chained_rows": len(chained), "pre_chain_rows": len(records) - len(chained)},
        "anchors": anchors,
        "anchor_coverage": {"newest_anchored_seq": newest, "unanchored_rows": max(0, last - newest)},
        "retention_profile_url": "https://producer.example/profile#retention",
        "interpretation_guide_url": "https://complyedge.io/rulebook/0.1.0/#annex-b",
    }
    env.update(env_extra)
    return {"envelope": env, "records": records}


def base(n=5):
    return chain([decision(i, fired=(i % 2 == 1)) for i in range(1, n + 1)])


# --- suites --------------------------------------------------------------------

def record_suite():
    c = {}

    def edit(fn, rechain_after=True):
        recs = base()
        fn(recs)
        return rechain(recs) if rechain_after else recs

    rich = base()
    rich[0]["reason_codes"] = ["art5.1c.social_scoring", "pattern:SC-04"]
    rich[0]["rules_fired"][0]["confidence"] = "0.92"
    rich[0]["native_result"] = "VIOLATION"
    rich[1]["commitments"][0]["source_ref"] = "urn:producer:conversation:7f3a/turn:2"
    rich[2]["data_categories"] = ["health"]
    rich[2]["special_category"] = True
    rich[2]["art4a_basis_ref"] = "https://producer.example/bias-detection/assessment-2026"
    rich[3]["fallback"] = {"kind": "degraded", "reason_code": "semantic_layer_timeout"}
    rich[4]["exemptions"] = {"claimed": [{"name": "research_exemption", "provision": copy.deepcopy(PROVISION_5_1_C)}],
                             "rejected": [{"name": "law_enforcement", "provision": copy.deepcopy(PROVISION_5_1_C),
                                           "reason_code": "not_a_competent_authority"}]}
    lf = common(6, "logging_failure")
    lf.update({"failure_kind": "anchor_failed", "detected_at": ts(6, 5_000)})
    del lf["deciding_system"]  # REC-07: an anchor failure is about the chain, not one AI system
    oo = common(7, "opt_out")
    cl = common(8, "classification")
    cl.update({"classification_result": "not_high_risk",
               "assessment_ref": "https://producer.example/assessments/assistant-hr-art6-3"})
    fb = common(9, "fallback")
    fb.update({"fallback": {"kind": "skipped", "reason_code": "engine_unavailable"}})
    unk = decision(10)
    unk["event_type"] = "x_producer_review_check"
    rich = rechain(rich + [lf, oo, cl, fb, unk, legal_hold(11)])
    c["record_valid"] = (export(rich), "verified", [])

    c["record_content_present"] = (export(edit(lambda r: r[0].update({"gen_ai.input.messages": [{"role": "user"}]}))),
                                   "failed", ["content_present"])
    c["record_person_in_clear"] = (export(edit(lambda r: r[1].update({"user": "alice@example.com"}))),
                                   "failed", ["person_in_clear"])
    c["record_person_key_version_missing"] = (export(edit(lambda r: r[1]["user"].pop("key_version"))),
                                              "failed", ["person_in_clear"])
    c["record_required_field_missing"] = (export(edit(lambda r: r[2].pop("retention"))),
                                          "failed", ["required_field_missing"])
    c["record_data_categories_missing"] = (export(edit(lambda r: r[1].pop("data_categories"))),
                                           "failed", ["required_field_missing"])
    c["record_rulebook_wrong"] = (export(edit(lambda r: r[1].update({"rulebook": "https://complyedge.io/rulebook/9.9.9"}))),
                                  "failed", ["invalid_value"])
    c["record_id_missing"] = (export(edit(lambda r: r[3].pop("record_id"))), "failed", ["required_field_missing"])
    c["record_id_duplicate"] = (export(edit(lambda r: r[3].update({"record_id": "rec_0001"}))), "failed", ["invalid_value"])
    c["record_occurred_at_malformed"] = (export(edit(lambda r: r[4].update({"occurred_at": "2025-10-09 08:57:20"}))),
                                         "failed", ["invalid_value"])

    def unknown_missing(r):
        r[2]["event_type"] = "x_producer_review_check"
        r[2].pop("verdict")
    c["record_event_type_unknown_missing_decision_fields"] = (export(edit(unknown_missing)), "failed",
                                                              ["required_field_missing"])
    c["record_deciding_system_missing"] = (export(edit(lambda r: r[0].pop("deciding_system"))),
                                           "failed", ["required_field_missing"])
    c["record_deciding_system_is_checker"] = (export(edit(lambda r: r[0].update(
        {"deciding_system": {"id": "checker", "version": "1.0"}}))), "failed", ["invalid_value"])
    c["record_account_id_mismatch"] = (export(edit(lambda r: r[2].update({"account_id": "acct_other"}))),
                                       "failed", ["invalid_value"])
    c["record_commitment_unkeyed"] = (export(edit(lambda r: r[0]["commitments"][0].update(
        {"alg": "sha256", "value": hashlib.sha256(b"input text 1").hexdigest()}))), "failed", ["invalid_value"])
    c["record_commitment_key_version_missing"] = (export(edit(lambda r: r[0]["commitments"][1].pop("key_version"))),
                                                  "failed", ["required_field_missing"])
    c["record_correlation_id_missing"] = (export(edit(lambda r: r[2].pop("correlation_id"))),
                                          "failed", ["required_field_missing"])
    c["record_verdict_invalid"] = (export(edit(lambda r: r[0].update({"verdict": "blocked"}))), "failed", ["invalid_value"])
    c["record_float_number"] = (export(edit(lambda r: r[0]["rules_fired"][0].update({"confidence": 0.92}), False)),
                                "failed", ["invalid_value"])
    c["record_integer_too_large"] = (export(edit(lambda r: r[0].update({"x_counter": 2 ** 60}), False)),
                                     "failed", ["invalid_value"])
    c["record_reason_code_free_text"] = (export(edit(lambda r: r[0].update(
        {"reason_codes": ["the user asked for a social score of a named neighbour"]}))), "failed", ["invalid_value"])
    c["record_exemption_rejected_without_reason"] = (export(edit(lambda r: r[0].update({"exemptions": {
        "claimed": [], "rejected": [{"name": "law_enforcement", "provision": copy.deepcopy(PROVISION_5_1_C)}]}}))),
        "failed", ["required_field_missing"])
    c["record_fallback_incomplete"] = (export(edit(lambda r: r[3].update({"fallback": {"kind": "degraded"}}))),
                                       "failed", ["required_field_missing"])

    def lf_missing(r):
        x = common(6, "logging_failure")
        x.update({"detected_at": ts(6, 5_000)})
        r.append(x)
    c["record_logging_failure_kind_missing"] = (export(edit(lf_missing)), "failed", ["required_field_missing"])

    def cl_missing(r):
        x = common(6, "classification")
        x.update({"classification_result": "not_high_risk"})
        r.append(x)
    c["record_classification_incomplete"] = (export(edit(cl_missing)), "failed", ["required_field_missing"])

    def write_failure_without_system(r):
        x = common(6, "logging_failure")
        x.update({"failure_kind": "write_failed", "detected_at": ts(6, 5_000)})
        del x["deciding_system"]
        r.append(x)
    c["record_write_failure_deciding_system_missing"] = (export(edit(write_failure_without_system)), "failed",
                                                         ["required_field_missing"])
    c["record_source_ref_not_a_locator"] = (export(edit(lambda r: r[1]["commitments"][0].update(
        {"source_ref": "patient asked whether the HIV test result can be shared with her employer"}))),
        "failed", ["invalid_value"])

    def hold_scope_missing(r):
        x = legal_hold(6)
        del x["hold"]["scope"]
        r.append(x)
    c["record_legal_hold_incomplete"] = (export(edit(hold_scope_missing)), "failed", ["required_field_missing"])
    c["record_legal_hold_on_retention"] = (export(edit(lambda r: r[0]["retention"].update({"legal_hold": True}))),
                                           "failed", ["invalid_value"])
    return c


def law_suite():
    c = {}

    def edit(fn):
        recs = base()
        fn(recs)
        return rechain(recs)

    valid = base()
    valid[2]["rules_fired"].append({"rule_id": "de-betrvg-87-001", "rule_version": "1",
                                    "provision": copy.deepcopy(PROVISION_NATIONAL),
                                    "citation": "Einführung und Anwendung von technischen Einrichtungen, die dazu "
                                                "bestimmt sind, das Verhalten oder die Leistung der Arbeitnehmer zu überwachen"})
    valid[3]["annex_iii_point"] = "4(a)"
    valid[4]["caller_asserted_provision"] = {"eli": "http://data.europa.eu/eli/reg/2016/679/art_22/oj",
                                             "label": "Regulation (EU) 2016/679, Article 22"}
    c["law_valid"] = (export(rechain(valid)), "verified", [])
    c["law_provision_unchecked"] = (export(edit(lambda r: r[0]["rules_fired"][0]["provision"].pop("eli"))),
                                    "failed", ["provision_unchecked"])
    c["law_caller_asserted_presented_as_checked"] = (export(edit(lambda r: r[0]["rules_fired"][0].update(
        {"caller_asserted": True}))), "failed", ["provision_unchecked"])
    c["law_rule_set_undigested"] = (export(edit(lambda r: r[0].update({"rule_set": {"label": "typed-by-hand"}}))),
                                    "failed", ["provision_unchecked"])
    c["law_purpose_ref_missing"] = (export(edit(lambda r: r[3].pop("purpose_ref"))), "failed", ["required_field_missing"])
    c["law_eli_malformed"] = (export(edit(lambda r: r[0]["rules_fired"][0]["provision"].update(
        {"eli": "https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32024R1689"}))),
        "failed", ["provision_unchecked"])
    c["law_consolidated_eli_malformed"] = (export(edit(lambda r: r[0]["rules_fired"][0]["provision"].update(
        {"consolidated_eli": "http://data.europa.eu/eli/reg/2024/1689/art_5/2026-07-27"}))),
        "failed", ["provision_unchecked"])
    c["law_citation_missing"] = (export(edit(lambda r: r[0]["rules_fired"][0].pop("citation"))),
                                 "failed", ["provision_unchecked"])

    def reassign(r):
        r[2]["rules_fired"][0]["provision"] = {"eli": "http://data.europa.eu/eli/reg/2024/1689/art_5/par_1/pnt_a/oj",
                                               "label": "Regulation (EU) 2024/1689, Article 5(1)(a)"}
    c["law_rule_id_reassigned"] = (export(edit(reassign)), "failed", ["invalid_value"])
    c["law_rule_set_missing_no_rule_fired"] = (export(edit(lambda r: r[1].pop("rule_set"))),
                                               "failed", ["required_field_missing"])
    return c


def chain_suite():
    c = {}
    recs = base(); c["chain_valid"] = (export(recs), "verified", [])
    recs = base(); recs[2]["verdict"] = "allow"; c["chain_hash_mismatch"] = (export(recs), "failed", ["hash_mismatch"])
    recs = base(); del recs[2]; c["chain_gap_mid"] = (export(recs), "failed", ["sequence_gap"])
    recs = base(); dup = copy.deepcopy(recs[1]); dup["record_id"] = "rec_dup"; recs.insert(2, dup)
    c["chain_duplicate_seq"] = (export(recs), "failed", ["sequence_duplicate"])
    recs = base(); recs[3]["chain"]["prev"] = "0" * 64
    recs[3]["chain"]["hash"] = chain_hash("0" * 64, {k: v for k, v in recs[3].items() if k != "chain"})
    c["chain_link_break"] = (export(recs), "failed", ["link_break"])
    recs = base(); recs.append(decision(9)); c["chain_unchained_after_start"] = (
        export(recs), "failed", ["unchained_after_chain_start"])
    recs = base(); old = decision(0); old["chain_status"] = "pre_chain"; recs.insert(0, old)
    c["chain_pre_chain_labelled"] = (export(chain(recs)), "unverified", ["pre_chain"])
    recs = base(); recs[2]["occurred_at"] = ts(0)
    c["chain_backdated"] = (export(rechain(recs)), "failed", ["backdated"])
    recs = base(); recs[1]["chain"]["alg"] = "sha256-v2"
    c["chain_alg_wrong"] = (export(recs), "failed", ["invalid_value"])
    return c


def anchor_suite():
    c = {}
    recs = base(); c["anchor_valid"] = (export(recs), "verified", [])
    recs = base(); c["anchor_missing"] = (export(recs, anchors=[]), "unverified", ["no_anchor"])
    recs = base(); c["anchor_tail_unanchored"] = (export(recs, anchors=[anchor_for(recs[2])]), "unverified",
                                                  ["unanchored_tail"])
    recs = base()
    st = {"account_id": ACCOUNT, "chain_alg": CHAIN_ALG, "chain_hash": "e" * 64, "chain_seq": 7}
    c["anchor_tail_truncated"] = (export(recs, anchors=[anchor_for(recs[-1], statement=st)]), "failed",
                                  ["truncated_tail"])
    recs = base(); st = {"account_id": ACCOUNT, "chain_alg": CHAIN_ALG, "chain_hash": "f" * 64, "chain_seq": 5}
    c["anchor_mismatch"] = (export(recs, anchors=[anchor_for(recs[-1], statement=st)]), "failed", ["anchor_mismatch"])
    recs = base()
    st = {"account_id": ACCOUNT, "chain_alg": CHAIN_ALG, "chain_hash": recs[-1]["chain"]["hash"], "chain_seq": 5,
          "commitment": recs[-1]["commitments"][0]["value"]}
    c["anchor_statement_carries_commitment"] = (export(recs, anchors=[anchor_for(recs[-1], statement=st)]), "failed",
                                                ["content_present"])
    recs = base(); a = anchor_for(recs[-1]); a["token_b64"] = base64.b64encode(b"this is not a token").decode()
    c["anchor_token_parse"] = (export(recs, anchors=[a]), "failed", ["anchor_token_parse"])
    recs = base(); a = anchor_for(recs[-1])
    a["token_b64"] = base64.b64encode(bytes.fromhex("30053003020102")).decode()  # PKIStatus rejection (2)
    c["anchor_token_status_rejected"] = (export(recs, anchors=[a]), "failed", ["anchor_token_status"])
    recs = base(); c["anchor_token_status_expired_signer"] = (
        export(recs, anchors=[anchor_for(recs[-1], signer="tsa-expired")]), "failed", ["anchor_token_status"])
    recs = base()
    other = {"account_id": ACCOUNT, "chain_alg": CHAIN_ALG, "chain_hash": recs[-2]["chain"]["hash"], "chain_seq": 4}
    c["anchor_token_imprint"] = (export(recs, anchors=[anchor_for(recs[-1], token_over=other)]), "failed",
                                 ["anchor_token_imprint"])
    recs = base(); c["anchor_token_signature"] = (export(recs, anchors=[anchor_for(recs[-1], tamper="signature")]),
                                                  "failed", ["anchor_token_signature"])
    recs = base(); c["anchor_token_untrusted_root"] = (export(recs, anchors=[anchor_for(recs[-1], signer="other-tsa")]),
                                                       "failed", ["anchor_token_trust"])
    recs = base(); c["anchor_no_trust_anchors"] = (export(recs), "refused", [])
    recs = base(); e = export(recs); e["envelope"]["anchor_coverage"]["newest_anchored_seq"] = 5
    e["envelope"]["anchor_coverage"]["unanchored_rows"] = 0
    e["envelope"]["anchors"] = [anchor_for(recs[2])]
    c["anchor_coverage_misstated"] = (e, "failed", ["invalid_value", "unanchored_tail"])
    return c


def oversight_records():
    """A decision log with every kind of human oversight the rulebook records."""
    recs = [decision(1), decision(2, fired=False), decision(3)]
    recs.append(oversight(4, "rec_0001", "accepted"))
    recs.append(oversight(5, "rec_0003", "overridden", reviewer="rev2", outcome_ref="rec_0006"))
    final = decision(6, fired=False)
    final.update({"decision_mode": "human", "human_decider": {"pseudonym": pseudonym("rev2"), "role": "case_officer",
                                                               "authority_ref": "https://producer.example/roles/case-officer"}})
    recs.append(final)
    recs.append(oversight(7, "rec_0002", "stopped", reviewer="rev3"))
    ident = decision(8)
    ident.update({"annex_iii_point": "1(a)", "action": "escalated", "verdict": "ask"})
    recs.append(ident)
    recs.append(oversight(9, "rec_0008", "verified", reviewer="rev4"))
    recs.append(oversight(10, "rec_0008", "verified", reviewer="rev5"))
    act = decision(11)
    act["identification_ref"] = "rec_0008"
    recs.append(act)
    ev = common(12, "oversight_evaluation")
    ev.update({"period": {"from": ts(0), "to": ts(12)},
               "action_counts": {"accepted": 1, "overridden": 1, "stopped": 1, "verified": 2},
               "evaluator_role": "oversight_lead", "representatives_informed": True})
    recs.append(ev)
    return recs


def oversight_suite():
    c = {}

    def edit(fn):
        recs = oversight_records()
        fn(recs)
        return export(rechain(recs))

    c["oversight_valid"] = (export(chain(oversight_records())), "verified", [])
    c["oversight_field_missing"] = (edit(lambda r: r[3].pop("authority_ref")), "failed", ["required_field_missing"])
    c["oversight_action_invalid"] = (edit(lambda r: r[3].update({"oversight_action": "approved"})), "failed",
                                     ["invalid_value"])
    c["oversight_outcome_ref_missing"] = (edit(lambda r: r[4].pop("outcome_ref")), "failed", ["required_field_missing"])
    c["oversight_merged_into_decision"] = (edit(lambda r: r[0].update({"oversight_action": "accepted",
                                                                       "reviewer": pseudonym("rev1")})),
                                           "failed", ["invalid_value"])
    c["oversight_proposal_not_decision"] = (edit(lambda r: r[3].update({"proposal_ref": "rec_0005"})), "failed",
                                            ["invalid_value"])
    c["oversight_proposal_unresolved"] = (edit(lambda r: r[3].update({"proposal_ref": "rec_9999"})), "unverified",
                                          ["reference_unresolved"])
    c["oversight_reviewer_in_clear"] = (edit(lambda r: r[3].update({"reviewer": "j.smith"})), "failed",
                                        ["person_in_clear"])
    c["oversight_dual_verification_missing"] = (edit(lambda r: r.pop(9)), "failed", ["dual_verification_missing"])
    c["oversight_dual_verification_same_reviewer"] = (edit(lambda r: r[9].update({"reviewer": pseudonym("rev4")})),
                                                      "failed", ["dual_verification_missing"])
    c["oversight_human_decider_missing"] = (edit(lambda r: r[5].pop("human_decider")), "failed",
                                            ["required_field_missing"])
    c["oversight_reviewer_rates_exposed"] = (edit(lambda r: r[-1].update({"reviewer_counts": [
        {"reviewer": pseudonym("rev2"), "action_counts": {"overridden": 1}}]})), "failed", ["reviewer_rates_exposed"])
    recs = oversight_records()
    recs[-1]["reviewer_counts"] = [{"reviewer": pseudonym("rev2"), "action_counts": {"overridden": 1}}]
    c["oversight_reviewer_rates_for_evaluation"] = (export(chain(recs), export_purpose="oversight_evaluation"),
                                                    "verified", [])
    return c


def rights_records():
    recs = [about_person(decision(1), 1), about_person(decision(2, fired=False), 2, employee=True), decision(3)]
    w = about_person(decision(4), 4)
    w.update({"withheld": True, "escrow_ref": "https://producer.example/escrow/case-0004"})
    recs.append(w)
    req = common(5, "contest")
    req.update({"decision_ref": "rec_0001", "request_kind": "human_intervention", "requested_at": ts(5, -1000)})
    recs.append(req)
    resp = common(6, "contest")
    resp.update({"decision_ref": "rec_0001", "request_kind": "human_intervention", "requested_at": ts(5, -1000),
                 "responded_at": ts(6, -1000), "reviewer": pseudonym("rev1"), "review_outcome": "rectified",
                 "rectified_at": ts(6, -500), "statement_ref": "https://producer.example/statements/case-0001"})
    recs.append(resp)
    nt = common(7, "notice")
    nt.update({"notice_kind": "explanation", "decision_ref": "rec_0001", "channel_ref": "portal:message-centre",
               "language": "de", "issued_at": ts(7, -1000), "statement_ref": "https://producer.example/explanations/case-0001"})
    recs.append(nt)
    inc = common(8, "incident")
    inc.update({"incident_kind": "serious_incident", "detected_at": ts(8, -50_000), "reported_at": ts(8, -1000),
                "report_ref": "https://producer.example/incidents/2025-0042"})
    recs.append(inc)
    return recs


def rights_suite():
    c = {}

    def edit(fn):
        recs = rights_records()
        fn(recs)
        return export(rechain(recs))

    c["rights_valid"] = (export(chain(rights_records())), "verified", [])
    c["rights_legal_basis_missing"] = (edit(lambda r: r[0].pop("legal_basis")), "failed", ["required_field_missing"])
    c["rights_legal_basis_unchecked"] = (edit(lambda r: r[0]["legal_basis"]["provision"].pop("eli")), "failed",
                                         ["provision_unchecked"])
    c["rights_art22_exception_invalid"] = (edit(lambda r: r[0]["legal_basis"].update({"art22_exception": "consent"})),
                                           "failed", ["invalid_value"])
    c["rights_decision_mode_missing"] = (edit(lambda r: r[0].pop("decision_mode")), "failed", ["required_field_missing"])
    c["rights_decision_mode_invalid"] = (edit(lambda r: r[0].update({"decision_mode": "automatic"})), "failed",
                                         ["invalid_value"])
    c["rights_notice_missing"] = (edit(lambda r: r[0].pop("notice")), "failed", ["required_field_missing"])
    c["rights_notice_given_incomplete"] = (edit(lambda r: r[0]["notice"].pop("given_at")), "failed",
                                           ["required_field_missing"])
    c["rights_employee_workers_not_informed"] = (edit(lambda r: r[1]["notice"].pop("workers_informed_at")), "failed",
                                                 ["required_field_missing"])
    c["rights_explanation_inputs_missing"] = (edit(lambda r: r[0].pop("explanation_inputs")), "failed",
                                              ["required_field_missing"])
    c["rights_explanation_incomplete"] = (edit(lambda r: r[0]["explanation_inputs"].pop("risks_ref")), "failed",
                                          ["required_field_missing"])
    c["rights_explanation_factor_value"] = (edit(lambda r: r[0]["explanation_inputs"]["main_factors"][0].update(
        {"value": "7"})), "failed", ["invalid_value"])
    c["rights_withheld_without_escrow"] = (edit(lambda r: r[3].pop("escrow_ref")), "failed", ["required_field_missing"])
    c["rights_contest_response_incomplete"] = (edit(lambda r: r[5].pop("statement_ref")), "failed",
                                               ["required_field_missing"])
    c["rights_contest_rectified_without_date"] = (edit(lambda r: r[5].pop("rectified_at")), "failed",
                                                  ["required_field_missing"])
    c["rights_contest_unresolved"] = (edit(lambda r: r[4].update({"decision_ref": "rec_9999"})), "unverified",
                                      ["reference_unresolved"])
    c["rights_notice_record_incomplete"] = (edit(lambda r: r[6].pop("language")), "failed", ["required_field_missing"])
    c["rights_notice_kind_invalid"] = (edit(lambda r: r[6].update({"notice_kind": "letter"})), "failed",
                                       ["invalid_value"])
    c["rights_incident_incomplete"] = (edit(lambda r: r[7].pop("report_ref")), "failed", ["required_field_missing"])
    c["rights_subject_in_clear"] = (edit(lambda r: r[0].update({"subject": "DE89370400440532013000"})), "failed",
                                    ["person_in_clear"])
    return c


def export_suite():
    c = {}
    sig = [{"alg": "Ed25519", "key_ref": "https://producer.example/keys/origin-2026#k1",
            "value": base64.b64encode(hashlib.sha256(b"test signature").digest() * 2).decode()}]
    recs = base(); c["export_valid"] = (export(recs, signatures=sig, export_purpose="audit"), "verified", [])
    recs = base(); e = export(recs); del e["envelope"]["clock_source"]
    c["export_envelope_field_missing"] = (e, "failed", ["required_field_missing"])
    recs = base(); e = export(recs); del e["envelope"]["clock_source"]["tolerance_ms"]
    c["export_clock_source_incomplete"] = (e, "failed", ["required_field_missing"])
    recs = base(); c["export_signature_undeclared"] = (export(recs, signatures=[{"value": sig[0]["value"]}]), "failed",
                                                       ["required_field_missing"])
    recs = base(); c["export_content_in_envelope"] = (export(recs, attachments=[
        {"commitment": recs[0]["commitments"][0]["value"], "text": "input text 1"}]), "failed", ["content_present"])
    recs = base(); e = export(recs); e["envelope"]["chain_summary"]["chained_rows"] = 6
    c["export_chain_summary_misstated"] = (e, "failed", ["invalid_value"])
    recs = base(); e = export(recs); e["envelope"]["rulebook"] = "https://complyedge.io/rulebook/0.0.1"
    c["export_rulebook_wrong"] = (e, "failed", ["invalid_value"])
    return c


def profile():
    """A producer profile (PRF-01) that matches export(base())."""
    return {
        "rulebook": RULEBOOK, "producer_id": PRODUCER,
        "classes": ["Record", "Law", "Chain", "Anchor", "Export"],
        "clock_source": {"reference": "ntp.example, stratum 2", "tolerance_ms": 1000},
        "anchoring": {"cadence": "every 1000 records or every hour, whichever comes first",
                      "providers": ["https://tsa.example/qtsp"]},
        "retention_classes": [{"class": "standard", "period_days": 365, "high_risk": False},
                              {"class": "high-risk", "period_days": 184, "high_risk": True}],
        "copies": [{"what": "primary record store", "gone_after_days": 0},
                   {"what": "encrypted backups", "gone_after_days": 35}],
        "event_types": ["decision", "oversight", "contest", "notice", "fallback", "logging_failure", "opt_out",
                        "incident", "classification", "oversight_evaluation", "legal_hold"],
        "reason_codes": ["litigation.reasonably_anticipated"],
        "rule_set_index_url": "https://producer.example/rules/",
        "interpretation_guide_url": "https://complyedge.io/rulebook/0.1.0/#annex-b",
        "commitment_domain": COMMITMENT_DOMAIN.decode(), "pseudonym_domain": PSEUDONYM_DOMAIN.decode(),
        "verification_interface_url": "https://producer.example/verify",
    }


def profile_suite():
    c = {}

    def case(name, edit_profile=None, edit_records=None, verdict="failed", findings=()):
        recs = base()
        if edit_records:
            edit_records(recs)
            recs = rechain(recs)
        prof = profile()
        if edit_profile:
            edit_profile(prof)
        c[name] = (export(recs), verdict, list(findings), prof)

    case("profile_valid", verdict="verified")
    case("profile_field_missing", lambda p: p.pop("verification_interface_url"), findings=["required_field_missing"])
    case("profile_high_risk_period_short", lambda p: p["retention_classes"][1].update({"period_days": 90}),
         findings=["invalid_value"])
    case("profile_domains_overlap", lambda p: p.update({"pseudonym_domain": "commitment|person|"}),
         findings=["invalid_value"])
    case("profile_copies_incomplete", lambda p: p["copies"][1].pop("gone_after_days"),
         findings=["required_field_missing"])
    case("profile_retention_class_undefined", edit_records=lambda r: r[0]["retention"].update({"class": "forever"}),
         findings=["profile_mismatch"])
    case("profile_event_type_unlisted", lambda p: p["event_types"].remove("decision"), findings=["profile_mismatch"])
    case("profile_reason_code_unlisted", edit_records=lambda r: r[0].update({"reason_codes": ["art5.1c.social_scoring"]}),
         findings=["profile_mismatch"])
    case("profile_clock_source_differs", lambda p: p["clock_source"].update({"tolerance_ms": 50}),
         findings=["profile_mismatch"])
    case("profile_producer_differs", lambda p: p.update({"producer_id": "https://other.example/profile"}),
         findings=["profile_mismatch"])
    return c


def interactions_suite():
    c = {}
    recs = base(); recs[0]["verdict"] = "allow"
    st = {"account_id": ACCOUNT, "chain_alg": CHAIN_ALG, "chain_hash": "e" * 64, "chain_seq": 8}
    c["interactions_edit_and_truncation"] = (export(recs, anchors=[anchor_for(recs[-1], statement=st)]), "failed",
                                             ["hash_mismatch", "truncated_tail"])
    recs = base(); recs[0]["user"] = "alice@example.com"; recs[0]["gen_ai.output.messages"] = [{"role": "assistant"}]
    c["interactions_person_and_content"] = (export(rechain(recs)), "failed", ["person_in_clear", "content_present"])
    recs = base(); old = decision(0); old["chain_status"] = "pre_chain"; recs.insert(0, old); recs = chain(recs)
    c["interactions_pre_chain_and_unanchored_tail"] = (export(recs, anchors=[anchor_for(recs[3])]), "unverified",
                                                       ["pre_chain", "unanchored_tail"])
    recs = base(); recs[1], recs[2] = recs[2], recs[1]
    recs[1]["chain"]["seq"], recs[2]["chain"]["seq"] = 2, 3
    c["interactions_reorder"] = (export(recs), "failed", ["link_break", "backdated"])
    recs = oversight_records(); recs[3]["proposal_ref"] = "rec_9999"; recs[4]["oversight_action"] = "approved"
    c["interactions_unresolved_and_invalid"] = (export(rechain(recs)), "failed", ["reference_unresolved", "invalid_value"])
    return c


SUITES = {
    "record_v1": record_suite, "law_v1": law_suite, "chain_v1": chain_suite, "anchor_v1": anchor_suite,
    "oversight_v1": oversight_suite, "rights_v1": rights_suite, "export_v1": export_suite,
    "profile_v1": profile_suite, "interactions_v1": interactions_suite,
}
TRUST = "../keys/trust-anchors.pem"
NO_TRUST = {"anchor_no_trust_anchors"}


def main(out_dir):
    with open(os.path.join(KEYS_DIR, "trust-anchors.pem"), "rb") as fh:
        trust = fh.read()
    built = {suite: fn() for suite, fn in SUITES.items()}
    problems = []
    for suite, cases in built.items():
        for name, (exp, verdict, findings, *prof) in cases.items():
            got = verify(json.loads(json.dumps(exp)), None if name in NO_TRUST else trust,
                         profile=json.loads(json.dumps(prof[0])) if prof else None)
            names = sorted({f["finding"] for f in got["findings"]})
            if got["verdict"] != verdict or names != sorted(set(findings)):
                problems.append(f"{suite}/{name}: declared {verdict} {sorted(set(findings))}, "
                                f"verifier says {got['verdict']} {names}: {got['findings'][:3]}")
    if problems:
        raise SystemExit("vectors not written; a case does not break exactly what it declares:\n  " + "\n  ".join(problems))
    for suite, cases in built.items():
        d = os.path.join(out_dir, suite, "cases")
        os.makedirs(d, exist_ok=True)
        for old in os.listdir(d):
            if old.endswith(".json") and old[:-5] not in cases:
                os.remove(os.path.join(d, old))
        expected = {"rulebook": RULEBOOK, "suite": suite, "trust_anchors": TRUST, "cases": {}}
        for name, (exp, verdict, findings, *prof) in cases.items():
            with open(os.path.join(d, name + ".json"), "w", encoding="utf-8") as fh:
                json.dump(exp, fh, indent=1, ensure_ascii=False, sort_keys=True)
                fh.write("\n")
            entry = {"verdict": verdict, "findings": sorted(set(findings))}
            if prof:
                pd = os.path.join(out_dir, suite, "profiles")
                os.makedirs(pd, exist_ok=True)
                with open(os.path.join(pd, name + ".json"), "w", encoding="utf-8") as fh:
                    json.dump(prof[0], fh, indent=1, ensure_ascii=False, sort_keys=True)
                    fh.write("\n")
                entry["profile"] = f"profiles/{name}.json"
            if name in NO_TRUST:
                entry["trust_anchors"] = None
            expected["cases"][name] = entry
        with open(os.path.join(out_dir, suite, "expected.json"), "w", encoding="utf-8") as fh:
            json.dump(expected, fh, indent=2, ensure_ascii=False)
            fh.write("\n")
        print(f"{suite}: {len(cases)} cases")
    tokens = os.path.join(KEYS_DIR, "tokens")
    for old in sorted(os.listdir(tokens)):
        if old.endswith(".tsr") and old not in USED_TOKENS:
            os.remove(os.path.join(tokens, old))


if __name__ == "__main__":
    main(sys.argv[1] if len(sys.argv) > 1 else os.path.join(HERE, "vectors"))
