//! The conformance corpus of extensions/holdings.md §8: `live-conformance/holdings/`.
//!
//! [`generate`] writes `vectors.json` from fixed seeds: every key is an Ed25519 key from a fixed
//! 32-byte seed, every instant a constant, every envelope is written here, and Ed25519 signing is
//! deterministic (RFC 8032), so the file is the same bytes on every machine. Each case states the
//! outcome it was written to have, declared here and never copied from [`super::verdict`],
//! [`super::holds`] or [`super::plan`]; [`verify`] requires the module to reach every outcome and
//! the corpus to give every reason of §7. `live-conformance/holdings_verify.py` checks the same
//! file from the extension's text, sharing no code with this module.

use super::*;
use crate::claim_receipt::{hex, unhex};
use crate::container::{Conditioning, Manifest, ModelRequirement, Wai};
use crate::jwp_delivery::GroupBuilder;
use serde_json::{json, Map};

const PUBLISHER: u8 = 0x11;
const HOLDER_A: u8 = 0x51;
const HOLDER_B: u8 = 0x52;
const HOLDER_C: u8 = 0x53;
const D1: &str = "did:web:publisher.example#d1";
const GROUP_START: &str = "2026-11-03T08:00:00Z";
const GROUP_END: &str = "2026-11-03T08:00:05Z";
/// The instant the records state their holdings at.
const T: u64 = 1_793_692_800_000;
/// An hour: when the records expire.
const HOUR: u64 = 3_600_000;

fn key(seed: u8) -> SigningKey {
    SigningKey::from_bytes(&[seed; 32])
}

/// A group of `n` objects of track `track`: its receipt.
fn group(track: u64, n: usize) -> GroupReceipt {
    let mut b = GroupBuilder::default();
    for i in 0..n {
        let payload = format!("holdings track {track} object {i}").into_bytes();
        let e = Wai::new(Manifest::new("binary", "improve", ModelRequirement::new("wai.text.zstd"), Conditioning::new("bytes")), payload)
            .to_bytes()
            .expect("a small envelope writes");
        b.add(crate::jwp_hash::content_hash(&e), 0, "", None);
    }
    b.finish(&key(PUBLISHER), D1, track, 1, GROUP_START, GROUP_END, None, false).expect("composes").0
}

/// The groups: of 1, 9 and 20 objects, and one of 4 no case holds the receipt of.
struct Groups {
    g1: GroupReceipt,
    g9: GroupReceipt,
    g20: GroupReceipt,
    unknown: GroupReceipt,
}

fn groups() -> Groups {
    Groups { g1: group(1, 1), g9: group(2, 9), g20: group(3, 20), unknown: group(4, 4) }
}

fn e(g: &GroupReceipt, held: &[u64]) -> Entry {
    Entry::of(g.link_hash(), g.leaf_count, held).expect("a case's entry holds something")
}

fn all(g: &GroupReceipt) -> Vec<u64> {
    (0..g.leaf_count).collect()
}

/// The records the cases are built from.
struct Records {
    /// A: g1 and g20 whole, objects 0 and 8 of g9 (its bitmap's last byte part used).
    a: Holdings,
    /// B: g9 whole, objects 3, 5 and 19 of g20.
    b: Holdings,
    /// C: object 5 of g20, and object 0 of a group no case holds the receipt of.
    c: Holdings,
    /// A a second later: object 0 of g9 alone (it evicted object 8).
    a_later: Holdings,
    /// A at that same instant, stating object 1 instead.
    a_later_other: Holdings,
}

fn records(g: &Groups) -> Records {
    let a = Holdings::seal(&key(HOLDER_A), T, T + HOUR, vec![e(&g.g1, &[0]), e(&g.g9, &[0, 8]), e(&g.g20, &all(&g.g20))]);
    let b = Holdings::seal(&key(HOLDER_B), T, T + HOUR, vec![e(&g.g9, &all(&g.g9)), e(&g.g20, &[3, 5, 19])]);
    let c = Holdings::seal(&key(HOLDER_C), T, T + HOUR, vec![e(&g.g20, &[5]), e(&g.unknown, &[0])]);
    let a_later = Holdings::seal(&key(HOLDER_A), T + 1_000, T + HOUR, vec![e(&g.g9, &[0])]);
    let a_later_other = Holdings::seal(&key(HOLDER_A), T + 1_000, T + HOUR, vec![e(&g.g9, &[1])]);
    Records { a, b, c, a_later, a_later_other }
}

fn read_outcome(h: &Holdings) -> Value {
    json!({ "outcome": "read", "digest": hex(&h.digest()), "holder": hex(&h.holder), "entries": h.entries.len() })
}

fn refuse(reason: &str) -> Value {
    json!({ "outcome": "refuse", "reason": reason })
}

/// A record with the given entries, sealed without its rules checked.
fn raw(as_of: u64, expires: u64, entries: Vec<Entry>) -> Vec<u8> {
    let mut h = Holdings::seal(&key(HOLDER_A), as_of, expires, Vec::new());
    h.entries = entries;
    h.sig = key(HOLDER_A).sign(&[DOMAIN, &h.signed_bytes()].concat()).to_bytes();
    h.to_bytes()
}

fn record_cases(g: &Groups, r: &Records) -> Vec<Value> {
    let case = |name: &str, description: &str, bytes: &[u8], outcome: Value| json!({ "name": name, "description": description, "bytes": hex(bytes), "outcome": outcome });
    let mut out = vec![
        case("whole_and_partial", "Two groups whole and the first and last of a group of nine.", &r.a.to_bytes(), read_outcome(&r.a)),
        case("several_groups", "A group of nine whole and three objects of twenty.", &r.b.to_bytes(), read_outcome(&r.b)),
        case("one_object", "One object of twenty, and one of a group of four.", &r.c.to_bytes(), read_outcome(&r.c)),
    ];
    let good = r.a.to_bytes();
    let mut mutated = |name: &str, description: &str, f: &dyn Fn(&mut Vec<u8>), reason: &str| {
        let mut b = good.clone();
        f(&mut b);
        out.push(case(name, description, &b, refuse(reason)));
    };
    mutated("magic", "WHD2.", &|b| b[3] = b'2', MALFORMED);
    mutated("version", "Version 2.", &|b| b[4] = 2, MALFORMED);
    mutated("short", "The last byte of the signature missing.", &|b| {
        b.pop();
    }, MALFORMED);
    mutated("trailing", "A byte after the signature.", &|b| b.push(0), MALFORMED);
    mutated("signature", "A bit of the signature flipped.", &|b| {
        let n = b.len() - 1;
        b[n] ^= 1;
    }, SIGNATURE_INVALID);
    mutated("signed_bytes_changed", "As of a millisecond later, the signature unchanged.", &|b| b[44] ^= 1, SIGNATURE_INVALID);
    {
        // The identity point: a key of small order, under which a signature proves nothing.
        let mut b = good.clone();
        b[5..37].copy_from_slice(&{
            let mut k = [0u8; 32];
            k[0] = 1;
            k
        });
        out.push(case("small_order_key", "The holder's key the identity point, of small order.", &b, refuse(SIGNATURE_INVALID)));
    }
    let mut ruled = |name: &str, description: &str, bytes: Vec<u8>| out.push(case(name, description, &bytes, refuse(MALFORMED)));
    ruled("expires_at_as_of", "expires_ms equal to as_of_ms.", raw(T, T, r.a.entries.clone()));
    ruled("expires_past_bound", "expires_ms of 2^53.", raw(T, 1 << 53, r.a.entries.clone()));
    ruled("no_entry", "No entry.", raw(T, T + HOUR, Vec::new()));
    let mut reversed = r.a.entries.clone();
    reversed.reverse();
    ruled("entries_out_of_order", "The entries in decreasing order of group.", raw(T, T + HOUR, reversed));
    ruled("entry_repeated", "One group twice.", raw(T, T + HOUR, vec![r.a.entries[0].clone(), r.a.entries[0].clone()]));
    let mut zero = e(&g.g9, &[0]);
    zero.leaf_count = 0;
    zero.held = Held::Whole;
    ruled("leaf_count_zero", "A group of no objects.", raw(T, T + HOUR, vec![zero]));
    let mut huge = e(&g.g9, &[0]);
    huge.leaf_count = 1 << 53;
    huge.held = Held::Whole;
    ruled("leaf_count_past_bound", "A group of 2^53 objects.", raw(T, T + HOUR, vec![huge]));
    let mut past = e(&g.g9, &[0]);
    past.held = Held::Bitmap(vec![0x80, 0x40]);
    ruled("bit_past_leaf_count", "Object 9 of a group of nine.", raw(T, T + HOUR, vec![past]));
    let mut none = e(&g.g9, &[0]);
    none.held = Held::Bitmap(vec![0, 0]);
    ruled("no_bit_set", "A bitmap that holds nothing.", raw(T, T + HOUR, vec![none]));
    let mut every = e(&g.g9, &[0]);
    every.held = Held::Bitmap(vec![0xff, 0x80]);
    ruled("every_bit_set", "A bitmap of every object: form 1's holding.", raw(T, T + HOUR, vec![every]));
    {
        let mut b = raw(T, T + HOUR, vec![e(&g.g20, &all(&g.g20))]);
        // The form byte: after magic, version, holder, two instants, n, group and leaf count.
        b[4 + 1 + 32 + 8 + 8 + 4 + 32 + 8] = 2;
        out.push(case("form_unknown", "Form 2.", &b, refuse(MALFORMED)));
    }
    out
}

/// What a verdict case states: each record's verdict, in order.
fn verdict_case(name: &str, description: &str, records: &[&Holdings], receipts: &[&GroupReceipt], at_ms: u64, labels: &[&str]) -> Value {
    json!({
        "name": name,
        "description": description,
        "records": records.iter().map(|h| hex(&h.to_bytes())).collect::<Vec<_>>(),
        "receipts": receipts.iter().map(|g| g.to_json()).collect::<Vec<_>>(),
        "at_ms": at_ms,
        "outcome": { "records": labels },
    })
}

fn verdict_cases(g: &Groups, r: &Records) -> Vec<Value> {
    let all_receipts = [&g.g1, &g.g9, &g.g20];
    let mismatch = {
        let mut x = e(&g.g9, &[0]);
        x.leaf_count = 10;
        Holdings::seal(&key(HOLDER_B), T, T + HOUR, vec![x])
    };
    let not_yet = format!("reject reason={NOT_YET}");
    let expired = format!("reject reason={EXPIRED}");
    let mismatched = format!("reject reason={LEAF_COUNT_MISMATCH}");
    let contradicted = format!("set-aside reason={CONTRADICTED}");
    vec![
        verdict_case("before_its_window", "A millisecond before as_of_ms.", &[&r.a], &all_receipts, T - 1, &[&not_yet]),
        verdict_case("at_as_of", "At as_of_ms.", &[&r.a], &all_receipts, T, &["stands"]),
        verdict_case("last_instant", "A millisecond before it expires.", &[&r.a], &all_receipts, T + HOUR - 1, &["stands"]),
        verdict_case("expired", "At expires_ms.", &[&r.a], &all_receipts, T + HOUR, &[&expired]),
        verdict_case("leaf_count_mismatch", "A group of nine stated as ten, its receipt held: the record refused whole.", &[&mismatch], &all_receipts, T, &[&mismatched]),
        verdict_case("mismatch_unchecked_without_receipt", "The same record with no receipt of that group held: kept.", &[&mismatch], &[&g.g1], T, &["stands"]),
        verdict_case("unknown_group_kept", "An entry for a group no receipt is held of: kept.", &[&r.c], &all_receipts, T, &["stands"]),
        verdict_case("later_record_stands", "A holder's record a second later: the earlier one superseded.", &[&r.a, &r.a_later], &all_receipts, T + 2_000, &["superseded", "stands"]),
        verdict_case("contradicted", "Two records of one holder at one instant that differ: both set aside.", &[&r.a_later, &r.a_later_other], &all_receipts, T + 2_000, &[&contradicted, &contradicted]),
        verdict_case("same_record_twice", "One record given twice: one record, standing.", &[&r.b, &r.b], &all_receipts, T, &["stands", "stands"]),
        verdict_case("superseded_by_a_refused_one", "A later record that is not yet: the earlier stands.", &[&r.a, &r.a_later], &all_receipts, T + 500, &["stands", &not_yet]),
    ]
}

fn query_cases(g: &Groups, r: &Records) -> Vec<Value> {
    let q = |name: &str, h: &Holdings, group: &GroupReceipt, i: u64, outcome: &str| {
        json!({ "name": name, "record": hex(&h.to_bytes()), "group": hex(&group.link_hash()), "index": i, "outcome": outcome })
    };
    vec![
        q("held_whole", &r.a, &g.g20, 19, "held"),
        q("held_bitmap_last_byte", &r.a, &g.g9, 8, "held"),
        q("not_held", &r.a, &g.g9, 7, OBJECT_NOT_HELD),
        q("out_of_range", &r.a, &g.g9, 9, OBJECT_OUT_OF_RANGE),
        q("out_of_range_whole", &r.a, &g.g1, 1, OBJECT_OUT_OF_RANGE),
        q("not_advertised", &r.b, &g.g1, 0, GROUP_NOT_ADVERTISED),
    ]
}

fn plan_cases(g: &Groups, r: &Records) -> Vec<Value> {
    let hx = |h: &Holdings| hex(&h.holder);
    let wanted = [(&g.g20, 5u64), (&g.g20, 3), (&g.g9, 8), (&g.g9, 4), (&g.g1, 0), (&g.unknown, 0), (&g.unknown, 1), (&g.g20, 7)];
    let (a, b, c) = (hx(&r.a), hx(&r.b), hx(&r.c));
    vec![json!({
        "name": "three_neighbours",
        "description": "C, B and A in the asker's order: objects held by all three, by two, by one, and by none.",
        "records": [hex(&r.c.to_bytes()), hex(&r.b.to_bytes()), hex(&r.a.to_bytes())],
        "receipts": [g.g1.to_json(), g.g9.to_json(), g.g20.to_json()],
        "at_ms": T + 10,
        "wanted": wanted.iter().map(|(grp, i)| json!({ "group": hex(&grp.link_hash()), "index": i })).collect::<Vec<_>>(),
        "outcome": {
            "records": ["stands", "stands", "stands"],
            "holders": [[c.clone(), b.clone(), a.clone()], [b.clone(), a.clone()], [b.clone(), a.clone()], [b.clone()], [a.clone()], [c.clone()], [], [a.clone()]],
            "from_origin": [6],
        },
    })]
}

/// The corpus: `vectors.json`, from fixed seeds.
pub fn generate() -> Vec<(String, String)> {
    let g = groups();
    let r = records(&g);
    let mut keys = Map::new();
    for (name, seed) in [("publisher", PUBLISHER), ("holder_a", HOLDER_A), ("holder_b", HOLDER_B), ("holder_c", HOLDER_C)] {
        keys.insert(name.into(), hex(&key(seed).verifying_key().to_bytes()).into());
    }
    let doc = json!({
        "about": "Conformance corpus of extensions/holdings.md (wai.delivery.holdings): holdings records (§4), their verdicts at \
                  an instant against group receipts (§5.1 to §5.3), queries and an asker's plan (§5.4). Every key is Ed25519 from \
                  a fixed 32-byte seed and attests nothing; envelopes carry opaque test payloads no case decodes. Each case states \
                  the outcome it was written to have, declared in the generator. Regenerate with `wai_holdings_vectors gen`.",
        "keys": keys,
        "records": record_cases(&g, &r),
        "verdicts": verdict_cases(&g, &r),
        "queries": query_cases(&g, &r),
        "plans": plan_cases(&g, &r),
    });
    let mut s = serde_json::to_string_pretty(&doc).expect("serialises");
    s.push('\n');
    vec![("vectors.json".into(), s)]
}

// ───────────────────────────── reading it back ─────────────────────────────

fn bytes_of(v: &Value) -> Result<Vec<u8>, String> {
    v.as_str().and_then(unhex).ok_or_else(|| format!("not hex: {v}"))
}

fn hash_of(v: &Value) -> Result<[u8; 32], String> {
    bytes_of(v)?.try_into().map_err(|_| "not 32 bytes".to_string())
}

fn record_outcome(b: &[u8]) -> Value {
    match Holdings::read(b) {
        Ok(h) => read_outcome(&h),
        Err(r) => refuse(r),
    }
}

fn verdict_of(c: &Value) -> Result<(Vec<RecordVerdict>, BTreeMap<usize, Holdings>), String> {
    let records: Vec<Vec<u8>> = c["records"].as_array().ok_or("records")?.iter().map(bytes_of).collect::<Result<_, _>>()?;
    let receipts = c["receipts"].as_array().ok_or("receipts")?.clone();
    Ok(verdict(&records, &receipts, c["at_ms"].as_u64().ok_or("at_ms")?))
}

fn labels(v: &[RecordVerdict]) -> Value {
    json!(v.iter().map(RecordVerdict::label).collect::<Vec<_>>())
}

fn plan_outcome(c: &Value) -> Result<Value, String> {
    let (v, standing) = verdict_of(c)?;
    let wanted: Vec<([u8; 32], u64)> = c["wanted"]
        .as_array()
        .ok_or("wanted")?
        .iter()
        .map(|w| Ok((hash_of(&w["group"])?, w["index"].as_u64().ok_or("index")?)))
        .collect::<Result<_, String>>()?;
    let order: Vec<&Holdings> = standing.values().collect();
    let (who, origin) = plan(&order, &wanted);
    Ok(json!({
        "records": labels(&v),
        "holders": who.iter().map(|w| w.iter().map(|k| hex(k)).collect::<Vec<_>>()).collect::<Vec<_>>(),
        "from_origin": origin,
    }))
}

fn strings(v: &Value, out: &mut Vec<String>) {
    match v {
        Value::String(s) => out.push(s.clone()),
        Value::Array(a) => a.iter().for_each(|x| strings(x, out)),
        Value::Object(m) => m.values().for_each(|x| strings(x, out)),
        _ => {}
    }
}

/// Check a corpus's files against this module: each case's outcome, and every reason of §7.
/// Returns the failures.
pub fn verify(files: &BTreeMap<String, String>) -> Vec<String> {
    let mut fails = Vec::new();
    let Some(text) = files.get("vectors.json") else { return vec!["no vectors.json".into()] };
    let doc: Value = match serde_json::from_str(text) {
        Ok(d) => d,
        Err(e) => return vec![format!("vectors.json: {e}")],
    };
    let empty = Vec::new();
    let mut stated = Vec::new();
    let mut check = |kind: &str, run: &dyn Fn(&Value) -> Result<Value, String>| {
        let cases = doc[kind].as_array().unwrap_or(&empty);
        if cases.is_empty() {
            fails.push(format!("no {kind} cases"));
        }
        for c in cases {
            let name = c["name"].as_str().unwrap_or("?");
            match run(c) {
                Err(e) => fails.push(format!("{name}: {e}")),
                Ok(got) if got != c["outcome"] => fails.push(format!("{name}: stated {} got {got}", c["outcome"])),
                Ok(_) => {}
            }
            strings(&c["outcome"], &mut stated);
        }
    };
    check("records", &|c| Ok(record_outcome(&bytes_of(&c["bytes"])?)));
    check("verdicts", &|c| Ok(json!({ "records": labels(&verdict_of(c)?.0) })));
    check("queries", &|c| {
        let h = Holdings::read(&bytes_of(&c["record"])?).map_err(|r| format!("the record: {r}"))?;
        Ok(json!(holds(&h, &hash_of(&c["group"])?, c["index"].as_u64().ok_or("index")?).map_or_else(|r| r.to_string(), |()| "held".into())))
    });
    check("plans", &plan_outcome);
    for code in CODES {
        if !stated.iter().any(|s| s == code || s.ends_with(&format!("reason={code}"))) {
            fails.push(format!("no case gives {code}"));
        }
    }
    fails
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn the_corpus_reaches_every_stated_outcome() {
        let files: BTreeMap<String, String> = generate().into_iter().collect();
        let fails = verify(&files);
        assert!(fails.is_empty(), "{}", fails.join("\n"));
    }

    #[test]
    fn a_changed_outcome_fails_verification() {
        let mut files: BTreeMap<String, String> = generate().into_iter().collect();
        let text = files["vectors.json"].replacen("\"superseded\"", "\"stands\"", 1);
        files.insert("vectors.json".into(), text);
        assert!(!verify(&files).is_empty());
    }
}
