//! Signed-byte pins for every receipt family that seals `joules_micro`.
//!
//! Each family below carried its energy figure with no acquisition class
//! before it was given an optional one. A receipt sealed without a class must
//! keep exactly the signed bytes it had before: otherwise every receipt
//! already issued stops verifying. So each
//! test builds one fixed, unlabelled receipt with a non-zero figure and pins
//! two things computed from the code *before* the class was added:
//!
//! - `receipt_hash()`, which covers the whole signing payload and the
//!   signature, so any change to a signed byte changes it;
//! - where the family renders JSON, a BLAKE3 hash of `to_json()`, so the
//!   rendered form of a legacy receipt is byte-identical too.
//!
//! The hex values were produced by the pre-change code and must never be
//! edited to make a test pass. A failure here means legacy receipts broke.

use ed25519_dalek::SigningKey;

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

fn hx(b: &[u8]) -> String {
    b.iter().map(|x| format!("{x:02x}")).collect()
}

fn json_hash(j: &str) -> String {
    hx(blake3::hash(j.as_bytes()).as_bytes())
}

// ---------------------------------------------------------------------------
// One fixed, unlabelled receipt per family, with a non-zero figure.
// ---------------------------------------------------------------------------

#[cfg(feature = "world")]
fn world() -> crate::world::receipt::SessionReceipt {
    use crate::world::Checkpoint;
    let cps = vec![
        Checkpoint { tick: 100, hash: [0x11; 32] },
        Checkpoint { tick: 200, hash: [0x22; 32] },
    ];
    crate::world::receipt::SessionReceipt::build(&key(9), "did:key:pin", [0xab; 32], 3, &[], &cps, 12_345, 999, Some([0x44; 32]))
}

#[cfg(feature = "audio_scene")]
fn audio_scene() -> crate::audio_scene::SceneReceipt {
    use crate::audio_scene::{BlockHash, SceneReceipt};
    let blocks: Vec<BlockHash> = (0..3u64).map(|i| BlockHash { index: i, hash: [i as u8 + 7; 32] }).collect();
    SceneReceipt::build(&key(9), "did:web:pin#audio", [0x5a; 32], 1, blocks, 777, 4_321, None)
}

#[cfg(feature = "film")]
fn film() -> crate::film::FilmReceipt {
    use crate::film::{FilmReceipt, FrameHash};
    let frames: Vec<FrameHash> = (0..5u64).map(|i| FrameHash { index: i, hash: [i as u8 + 1; 32] }).collect();
    FilmReceipt::build(&key(9), "did:web:pin#film", [0xab; 32], 2, frames, 5, 8_080, Some([7; 32]))
}

#[cfg(feature = "haptic")]
fn haptic() -> crate::haptic::HapticReceipt {
    use crate::haptic::{ChannelHash, HapticReceipt};
    let ch: Vec<ChannelHash> = (0..2u64).map(|i| ChannelHash { index: i, hash: [i as u8 + 3; 32] }).collect();
    HapticReceipt::build(&key(9), "did:web:pin#haptic", [0x3c; 32], 0, ch, 960, 555, None)
}

#[cfg(feature = "score")]
fn score() -> crate::score::ScoreReceipt {
    use crate::score::{ScoreReceipt, SegmentHash};
    let seg: Vec<SegmentHash> = (0..4u64).map(|i| SegmentHash { index: i, hash: [i as u8 + 9; 32] }).collect();
    ScoreReceipt::build(&key(9), "did:web:pin#score", [0x2e; 32], 5, seg, 48_000, 2_468, Some([1; 32]))
}

#[cfg(feature = "splat4d")]
fn splat4d() -> crate::splat4d::Splat4dReceipt {
    use crate::splat4d::{FrameHash, Splat4dReceipt};
    let fr: Vec<FrameHash> = (0..3u64).map(|i| FrameHash { index: i, hash: [i as u8 + 0x40; 32] }).collect();
    Splat4dReceipt::build(&key(9), "did:web:pin#splat4d", [0x4d; 32], 0, fr, 3_000, 9_999, None)
}

#[cfg(feature = "ternary_receipt")]
fn ternary() -> crate::ternary_receipt::TernaryReceipt {
    use crate::ternary::encode_residual;
    let stream: Vec<i64> = (0..256).map(|i| ((i * 37) % 1400) as i64 - 700).collect();
    let stack = encode_residual(&stream, 8, 2, 4, 3);
    crate::ternary_receipt::TernaryReceipt::seal(&key(1), "did:key:pin", &stack, 4_200, Some([0x66; 32]))
}

#[cfg(feature = "provenance")]
fn provenance_steps() -> Vec<crate::provenance::ProvStep> {
    use crate::provenance::ProvStep;
    let raw = [0x10; 32];
    let png = [0x20; 32];
    let jpeg = [0x30; 32];
    vec![
        ProvStep { op: "capture".into(), input_hash: [0u8; 32], output_hash: raw, work: 12_000_000, joules_micro: 0, energy_provenance: None },
        ProvStep { op: "encode.png".into(), input_hash: raw, output_hash: png, work: 12_000_000, joules_micro: 4_200, energy_provenance: None },
        ProvStep { op: "encode.jpeg".into(), input_hash: png, output_hash: jpeg, work: 12_000_000, joules_micro: 90, energy_provenance: None },
    ]
}

#[cfg(feature = "provenance")]
fn provenance(steps: Vec<crate::provenance::ProvStep>) -> crate::provenance::AssetReceipt {
    crate::provenance::AssetReceipt::build(&key(9), "did:web:pin#prov", [0x30; 32], "image", "wai.image.jpeg", 0, steps, None)
}

// ---------------------------------------------------------------------------
// The legacy pins.
// ---------------------------------------------------------------------------

#[cfg(feature = "world")]
#[test]
fn world_session_legacy_bytes() {
    let r = world();
    assert!(r.verify());
    assert_eq!(hx(&r.receipt_hash()), "e9fad41501e4d009b436e4f9830fd4f72fd29af8dbb333a6cba919ad6d2a0e72");
    assert_eq!(json_hash(&r.to_json()), "f36e2358488181caac445253cb9c4df138cf174ce52f725ed550ed59d5178d55");
}

#[cfg(feature = "audio_scene")]
#[test]
fn audio_scene_legacy_bytes() {
    let r = audio_scene();
    assert!(r.verify());
    assert_eq!(hx(&r.receipt_hash()), "0b13c8d93ea2207461af14e5589402d389837839383cfdbad09081a21dd21ab5");
}

#[cfg(feature = "film")]
#[test]
fn film_legacy_bytes() {
    let r = film();
    assert!(r.verify());
    assert_eq!(hx(&r.receipt_hash()), "e92507a9c11b0b4d2a7f3b940d256fb8a7d22f49705991af358911401f4ebf62");
    assert_eq!(json_hash(&r.to_json()), "58516902783835bb7f36b175bbb1f37589971d3afa402864f26025039c74a030");
}

#[cfg(feature = "haptic")]
#[test]
fn haptic_legacy_bytes() {
    let r = haptic();
    assert!(r.verify());
    assert_eq!(hx(&r.receipt_hash()), "f06b8e9f5592be27fd953ce168b6d42ef50aa6bc65ee5fb5900543b9b24ccfec");
    assert_eq!(json_hash(&r.to_json()), "20a6aa26ae7cb62fedbe5064e329fedcf7aa5a60a3829a530a5fd0287824c623");
}

#[cfg(feature = "score")]
#[test]
fn score_legacy_bytes() {
    let r = score();
    assert!(r.verify());
    assert_eq!(hx(&r.receipt_hash()), "61069b0b9e2d341b855a5230c681ceae9358492e4e5f9c883407dbea206f11e9");
    assert_eq!(json_hash(&r.to_json()), "3fd5d6ff397d646efee0d625801706b4c90753973a80d0f494ea5f3c18b3e26d");
}

#[cfg(feature = "splat4d")]
#[test]
fn splat4d_legacy_bytes() {
    let r = splat4d();
    assert!(r.verify());
    assert_eq!(hx(&r.receipt_hash()), "fa9fb6e12674b6b9e5e662fa51cb99f98df5d2e57046e1436239a926ebbf4655");
    assert_eq!(json_hash(&r.to_json()), "1e2a9d497299e2f2d41eca827d845f568376a30bcbbcf0fc5001a8f9e8a4360a");
}

#[cfg(feature = "ternary_receipt")]
#[test]
fn ternary_legacy_bytes() {
    let r = ternary();
    assert!(r.verify());
    assert_eq!(hx(&r.receipt_hash()), "3e4d15dc85b6a223cbd7d409161bfaeeb660e625736ffc0aa86f54db8aa6bd41");
    assert_eq!(json_hash(&r.to_json()), "78112ecd5b127992955c1247e17ea3b8f93a8e73c76df4f79de9395c8769b734");
}

#[cfg(feature = "provenance")]
#[test]
fn provenance_legacy_bytes() {
    let r = provenance(provenance_steps());
    assert!(r.verify());
    assert_eq!(hx(&r.receipt_hash()), "d22c9b26323f3e529d13290c66c8af3cb1e7c4c39debc41650190acfab1340c5");
    assert_eq!(json_hash(&r.to_json()), "70c753ccf508dc652504147dee940131cfbed02e782c194d7e34174c00048c8e");
}

// ---------------------------------------------------------------------------
// The optional label, exercised identically on every family.
// ---------------------------------------------------------------------------

mod labels {
    use super::*;
    use crate::energy_class::{
        label_from_json, AcquisitionError, CounterFiltering, EnergyProvenance, Uncertainty,
    };

    /// Uniform access to the families' label machinery, for one generic check.
    pub(super) trait Family: Clone + std::fmt::Debug {
        fn ok(&self) -> bool;
        fn id(&self) -> [u8; 32];
        fn label(&mut self) -> &mut Option<EnergyProvenance>;
        fn figure(&mut self) -> &mut u64;
        fn relabel(self, k: &SigningKey, c: EnergyProvenance) -> Result<Self, AcquisitionError>;
        fn resign(&mut self, k: &SigningKey);
        fn json(&self) -> Option<String>;
    }

    macro_rules! family {
        ($t:ty, json) => {
            family!(@impl $t, |r: &$t| Some(r.to_json()));
        };
        ($t:ty, nojson) => {
            family!(@impl $t, |_r: &$t| None);
        };
        (@impl $t:ty, $json:expr) => {
            impl Family for $t {
                fn ok(&self) -> bool { self.verify() }
                fn id(&self) -> [u8; 32] { self.receipt_hash() }
                fn label(&mut self) -> &mut Option<EnergyProvenance> { &mut self.energy_provenance }
                fn figure(&mut self) -> &mut u64 { &mut self.joules_micro }
                fn relabel(self, k: &SigningKey, c: EnergyProvenance) -> Result<Self, AcquisitionError> {
                    self.with_energy_class(k, c)
                }
                fn resign(&mut self, k: &SigningKey) { self.resign_for_test(k) }
                fn json(&self) -> Option<String> { ($json)(self) }
            }
        };
    }

    #[cfg(feature = "world")]
    family!(crate::world::receipt::SessionReceipt, json);
    #[cfg(feature = "audio_scene")]
    family!(crate::audio_scene::SceneReceipt, nojson);
    #[cfg(feature = "film")]
    family!(crate::film::FilmReceipt, json);
    #[cfg(feature = "haptic")]
    family!(crate::haptic::HapticReceipt, json);
    #[cfg(feature = "score")]
    family!(crate::score::ScoreReceipt, json);
    #[cfg(feature = "splat4d")]
    family!(crate::splat4d::Splat4dReceipt, json);
    #[cfg(feature = "ternary_receipt")]
    family!(crate::ternary_receipt::TernaryReceipt, json);

    pub(super) fn chip(ppm: u32) -> EnergyProvenance {
        EnergyProvenance::OnChipCounter {
            uncertainty: Uncertainty { relative_ppm: ppm, window_us: 2_977_104 },
            filtering: CounterFiltering::Unknown,
        }
    }

    /// Everything the label must do, on one family's pinned legacy receipt
    /// sealed by `signer`.
    pub(super) fn exercise<R: Family>(legacy: R, signer: &SigningKey) {
        assert!(legacy.ok());
        let legacy_id = legacy.id();

        // Labelling re-signs, and the labelled receipt is a different receipt.
        let l = legacy.clone().relabel(signer, chip(15_403)).expect("a valid label");
        assert!(l.ok(), "a labelled receipt verifies");
        assert_ne!(l.id(), legacy_id);

        // The label is inside the signature: it cannot be stripped, added,
        // restated or promoted in transit.
        let mut stripped = l.clone();
        *stripped.label() = None;
        assert!(!stripped.ok(), "stripping the label breaks the signature");
        let mut added = legacy.clone();
        *added.label() = Some(chip(15_403));
        assert!(!added.ok(), "adding a label without the signer breaks the signature");
        let mut restated = l.clone();
        *restated.label() = Some(chip(1));
        assert!(!restated.ok(), "restating the declared uncertainty breaks the signature");
        let mut promoted = l.clone();
        *promoted.label() = Some(
            EnergyProvenance::calibrated_instrument(Uncertainty { relative_ppm: 15_403, window_us: 2_977_104 }, "cert")
                .unwrap(),
        );
        assert!(!promoted.ok(), "promoting the class breaks the signature");

        // Labelling refuses what verify() would refuse.
        #[allow(deprecated)]
        let hw = EnergyProvenance::HwShunt;
        assert_eq!(legacy.clone().relabel(signer, hw.clone()).unwrap_err(), AcquisitionError::LegacyLabel);
        assert_eq!(legacy.clone().relabel(&key(8), chip(15_403)).unwrap_err(), AcquisitionError::SignerMismatch);
        let mut zero = legacy.clone();
        *zero.figure() = 0;
        assert_eq!(zero.relabel(signer, chip(15_403)).unwrap_err(), AcquisitionError::NoFigure);
        let undeclared = EnergyProvenance::OnChipCounter {
            uncertainty: Uncertainty { relative_ppm: 0, window_us: 1 },
            filtering: CounterFiltering::Unknown,
        };
        assert_eq!(legacy.clone().relabel(signer, undeclared.clone()).unwrap_err(), AcquisitionError::NoUncertainty);

        // And verify() itself refuses them even when correctly signed.
        for bad in [Some(hw), Some(undeclared)] {
            let mut r = legacy.clone();
            *r.label() = bad;
            r.resign(signer);
            assert!(!r.ok(), "a correctly signed receipt with a refused label must not verify");
        }
        let mut zero = l.clone();
        *zero.figure() = 0;
        zero.resign(signer);
        assert!(!zero.ok(), "a class beside a zero figure must not verify");
        // An unlabelled zero is unmetered and fine, as it always was.
        let mut unmetered = legacy.clone();
        *unmetered.figure() = 0;
        unmetered.resign(signer);
        assert!(unmetered.ok());

        // JSON: unlabelled renders no class; labelled renders the class, its
        // evidence and the labelled receipt's own hash.
        if let (Some(lj), Some(j)) = (legacy.json(), l.json()) {
            assert!(!lj.contains("energy_provenance") && !lj.contains("energy_uncertainty"));
            let v: serde_json::Value = serde_json::from_str(&j).unwrap();
            assert_eq!(label_from_json(v.as_object().unwrap()), Ok(Some(chip(15_403))));
            assert_eq!(v["receipt_hash"], serde_json::Value::from(hx(&l.id())));
        }
    }

    #[cfg(feature = "world")]
    #[test]
    fn world_label() {
        exercise(world(), &key(9));
    }

    #[cfg(feature = "audio_scene")]
    #[test]
    fn audio_scene_label() {
        exercise(audio_scene(), &key(9));
    }

    #[cfg(feature = "film")]
    #[test]
    fn film_label() {
        exercise(film(), &key(9));
    }

    #[cfg(feature = "haptic")]
    #[test]
    fn haptic_label() {
        exercise(haptic(), &key(9));
    }

    #[cfg(feature = "score")]
    #[test]
    fn score_label() {
        exercise(score(), &key(9));
    }

    #[cfg(feature = "splat4d")]
    #[test]
    fn splat4d_label() {
        exercise(splat4d(), &key(9));
    }

    #[cfg(feature = "ternary_receipt")]
    #[test]
    fn ternary_label() {
        exercise(ternary(), &key(1));
        // The parser keeps the label, and refuses a malformed one rather than
        // dropping it.
        use crate::ternary_receipt::TernaryReceipt;
        let l = ternary().with_energy_class(&key(1), chip(15_403)).unwrap();
        let back = TernaryReceipt::from_json(&l.to_json()).expect("parse");
        assert_eq!(back, l);
        assert!(back.verify());
        let broken = l.to_json().replace("\"energy_provenance\":\"OnChipCounter\",", "");
        assert!(TernaryReceipt::from_json(&broken).is_none(), "evidence without a class is refused");
        let legacy = TernaryReceipt::from_json(&ternary().to_json()).unwrap();
        assert_eq!(legacy.energy_provenance, None);
        assert!(legacy.verify());
    }

    #[cfg(feature = "provenance")]
    #[test]
    fn provenance_step_labels() {
        use crate::provenance::AssetReceipt;
        let legacy = provenance(provenance_steps());
        // Label only the measured encode step; the capture step stays unmetered.
        let mut steps = provenance_steps();
        steps[1] = steps[1].clone().with_energy_class(chip(15_403)).unwrap();
        let l = provenance(steps.clone());
        assert!(l.verify());
        assert_ne!(l.receipt_hash(), legacy.receipt_hash());
        assert_ne!(l.steps[1].content_hash(), legacy.steps[1].content_hash(), "the label is in the leaf");
        assert_eq!(l.steps[2].content_hash(), legacy.steps[2].content_hash(), "an unlabelled step keeps its leaf");

        // Stripping, adding or restating a step label breaks the receipt.
        let mut stripped = l.clone();
        stripped.steps[1].energy_provenance = None;
        assert!(!stripped.verify());
        let mut added = legacy.clone();
        added.steps[2].energy_provenance = Some(chip(1));
        assert!(!added.verify());
        let mut restated = l.clone();
        restated.steps[1].energy_provenance = Some(chip(1));
        assert!(!restated.verify());

        // A step label is refused beside a zero figure or as legacy HwShunt,
        // both at labelling and in verify() when correctly signed.
        assert_eq!(steps[0].clone().with_energy_class(chip(15_403)).unwrap_err(), AcquisitionError::NoFigure);
        #[allow(deprecated)]
        let hw = EnergyProvenance::HwShunt;
        assert_eq!(steps[2].clone().with_energy_class(hw.clone()).unwrap_err(), AcquisitionError::LegacyLabel);
        let mut bad = l.clone();
        bad.steps[0].energy_provenance = Some(chip(15_403));
        bad.merkle_root = crate::world::receipt::merkle_root(
            &bad.steps.iter().map(|s| s.content_hash()).collect::<Vec<_>>(),
        );
        bad.resign_for_test(&key(9));
        assert!(!bad.verify(), "a labelled zero-figure step must not verify");
        let mut bad = l.clone();
        bad.steps[2].energy_provenance = Some(hw);
        bad.merkle_root = crate::world::receipt::merkle_root(
            &bad.steps.iter().map(|s| s.content_hash()).collect::<Vec<_>>(),
        );
        bad.resign_for_test(&key(9));
        assert!(!bad.verify(), "a legacy HwShunt step label must not verify");

        // JSON round trip keeps the step label; a legacy receipt parses unlabelled.
        let back = AssetReceipt::from_json(&l.to_json()).expect("parse");
        assert_eq!(back, l);
        assert!(back.verify());
        let legacy_back = AssetReceipt::from_json(&legacy.to_json()).unwrap();
        assert!(legacy_back.steps.iter().all(|s| s.energy_provenance.is_none()));
        assert!(legacy_back.verify());
    }
}

// ---------------------------------------------------------------------------
// Pins for the labelled forms (energy-measurement §2.8).
//
// The legacy pins above hold unlabelled receipts to their pre-label bytes.
// These hold one labelled receipt of every family to the bytes the label
// gave it when it was introduced, so a later change to a labelled encoding —
// a byte leaking into `write_signed` or `write_label`, or a changed domain —
// fails here instead of re-signing quietly. The hex was produced by the code
// as the labels were first introduced and must never be edited to make a
// test pass.
// ---------------------------------------------------------------------------

mod labelled_pins {
    use super::*;
    use super::labels::chip;

    #[cfg(feature = "world")]
    #[test]
    fn world_labelled_bytes() {
        let r = world().with_energy_class(&key(9), chip(15_403)).unwrap();
        assert!(r.verify());
        assert_eq!(hx(&r.receipt_hash()), "32b7ef840b7140c369ecdd5d04c0b9ed2f361980aacfa072c0342d3ad4eb1057");
        assert_eq!(json_hash(&r.to_json()), "6c8f53541d7f8a896a8a0e5e2873c076c5afab36c914a95d9e4bf3211b042855");
    }

    #[cfg(feature = "audio_scene")]
    #[test]
    fn audio_scene_labelled_bytes() {
        let r = audio_scene().with_energy_class(&key(9), chip(15_403)).unwrap();
        assert!(r.verify());
        assert_eq!(hx(&r.receipt_hash()), "2fe5b5771dfd818a050c020fe365522f5ac5bfbbd133191e0663c25ab3b1533e");
    }

    #[cfg(feature = "film")]
    #[test]
    fn film_labelled_bytes() {
        let r = film().with_energy_class(&key(9), chip(15_403)).unwrap();
        assert!(r.verify());
        assert_eq!(hx(&r.receipt_hash()), "9bd844ee24ea3903a273a79e509edaae3eef830509edb05a586482581d003e06");
        assert_eq!(json_hash(&r.to_json()), "d8644a2f82e407b14d1ff57a2f12b733f63e931c62c00b9526880d2af55e329e");
    }

    #[cfg(feature = "haptic")]
    #[test]
    fn haptic_labelled_bytes() {
        let r = haptic().with_energy_class(&key(9), chip(15_403)).unwrap();
        assert!(r.verify());
        assert_eq!(hx(&r.receipt_hash()), "e17f760cd90364feb9ac47e121a783c85035253c29bf640406c66cfc8cf3e487");
        assert_eq!(json_hash(&r.to_json()), "1fb81b2976b6e49a568d63a0a60a872946c29b6751de88f30b12a3fa99dfaa85");
    }

    #[cfg(feature = "score")]
    #[test]
    fn score_labelled_bytes() {
        let r = score().with_energy_class(&key(9), chip(15_403)).unwrap();
        assert!(r.verify());
        assert_eq!(hx(&r.receipt_hash()), "0e795647d7ef74bf400fee962e6bcf57292116e0a084da41a1c153de777a0bee");
        assert_eq!(json_hash(&r.to_json()), "856823c9874b10bb5d78c01b576318bc0cd44e1f2112fbcd56f72204db38b544");
    }

    #[cfg(feature = "splat4d")]
    #[test]
    fn splat4d_labelled_bytes() {
        let r = splat4d().with_energy_class(&key(9), chip(15_403)).unwrap();
        assert!(r.verify());
        assert_eq!(hx(&r.receipt_hash()), "410e26fdf5c60b8bba50f39d318a6e6e8c122f8a3807bb65f90dcf2a92c13a76");
        assert_eq!(json_hash(&r.to_json()), "5efa236fa7b818685fda50ef7e52c9037c48c76b32eeb0fbfc092888a255156d");
    }

    #[cfg(feature = "ternary_receipt")]
    #[test]
    fn ternary_labelled_bytes() {
        let r = ternary().with_energy_class(&key(1), chip(15_403)).unwrap();
        assert!(r.verify());
        assert_eq!(hx(&r.receipt_hash()), "3eaf95f6d3e7cbc2505f6bacfadac60f2cf7451041cf36c1c7307c1345120c3e");
        assert_eq!(json_hash(&r.to_json()), "321f0c9d739ce6fd6e4bf6e32d663ef3e14f25339ad75799fce9f17159052413");
    }

    #[cfg(feature = "provenance")]
    #[test]
    fn provenance_labelled_bytes() {
        let mut steps = provenance_steps();
        steps[1] = steps[1].clone().with_energy_class(chip(15_403)).unwrap();
        let r = provenance(steps);
        assert!(r.verify());
        assert_eq!(hx(&r.receipt_hash()), "db3979573fc3ff07a1c2d82256268c862d6a2d1737ea9636c876155ef9546ae1");
        assert_eq!(json_hash(&r.to_json()), "d1a52b06b30dece511e701a86b86eb176b5fa414ae2084520a35a1e004fdcee0");
        assert_eq!(hx(&r.steps[1].content_hash()), "8b94897f5b73b5b8e62b8328ee416368e8d087c65ee661fc188e4bcb5b97da42");
    }
}
