//! Energy meters — where a receipt's `joules_micro` actually comes from.
//!
//! The receipts built on [`crate::energy_class`] seal a joule figure *beside
//! its acquisition class*, and reject the two out of step: an unlabelled
//! non-zero figure is uncomparable, a labelled zero claims a measurement that
//! did not happen. That discipline is only as good as the thing producing the
//! number, so this module makes the ladder **mechanical rather than
//! conventional**:
//!
//! - A [`CounterMeter`] reads a real hardware energy counter and is the only
//!   meter here that can ever yield a hardware class:
//!   [`EnergyProvenance::OnChipCounter`] when built with [`CounterMeter::new`]
//!   or [`CounterMeter::rapl`], and [`EnergyProvenance::CalibratedInstrument`]
//!   only when built with [`CounterMeter::calibrated_instrument`] and a
//!   calibration-certificate reference. Either way the reading carries the
//!   declared relative uncertainty the meter was configured with, combined
//!   with the `1/√6` µJ resolution term of a difference of two whole-microjoule
//!   register readings, and the integration window it
//!   actually measured over. It never yields the legacy `HwShunt`.
//! - A [`ModelMeter`] multiplies elapsed time by a calibrated power figure and
//!   can only ever yield [`EnergyProvenance::ModelBased`]: its figure is a
//!   model's output, labelled as one, and never presented as a measurement.
//! - When a meter cannot measure — no hardware, no counter, no span started, a
//!   counter whose rail was not live, a model that gives nothing —
//!   [`Meter::stop`] returns `None`. It never falls back to a fabricated
//!   figure, because a plausible number with the wrong label is worse than no
//!   number: the caller then either meters another way and says so, or seals
//!   the receipt with the unmetered marker (`0` with no class,
//!   `extensions/energy-measurement.md` §2). The marker is not a figure of zero
//!   joules, and no meter here returns a reading of zero.
//!
//! # A live counter
//!
//! A counter that is not being refreshed, or is refreshed in batches, can show
//! a delta between two reads that is no measurement of the span: nothing at
//! all, or one batch's worth. So a counter meter reads the counter during the
//! span as well as at its ends ([`CounterMeter::sample`]), and refuses the span
//! unless the counter advanced over it as a live rail does
//! (energy-measurement §3.1, [`crate::energy_report::Liveness`]): in two
//! consecutive intervals, in at least five, in at least four of every five,
//! over at least 300 ms. Two reads alone are one interval, and never enough.
//!
//! Both meters take their source injected — a counter reader, a clock — so the
//! delta and wraparound arithmetic is testable without hardware, and so the
//! module stays usable on targets with no `Instant` (wasm). The convenience
//! constructor [`CounterMeter::rapl`] wires the Linux powercap sysfs interface
//! and returns `None` anywhere it is absent.
//!
//! # What a counter meter can and cannot know
//!
//! A counter meter measures the delta and the window. It cannot know the
//! counter's uncertainty — that is a property of the interface, stated by its
//! vendor or evaluated by the operator — so the relative uncertainty is a
//! required constructor argument, typed [`NonZeroU32`] so an undeclared (zero)
//! uncertainty cannot be configured at all. It usually cannot know whether the
//! counter was filtering its output either (reading that state needs MSR
//! access an unprivileged process does not have), so the filtering state
//! defaults to [`CounterFiltering::Unknown`] and is set only by a caller that
//! has actually determined it.

use core::num::NonZeroU32;

use crate::energy_class::{
    AcquisitionError, AcquisitionKind, CounterFiltering, Uncertainty, REGISTER_DELTA_UNCERTAINTY_UJ,
};
use crate::energy_report::Liveness;
use crate::wai_video_manifest::EnergyProvenance;

/// A reading over a span: the energy, and its class — measured or modelled, as
/// the class states. Never zero: a span with no figure is `None` at
/// [`Meter::stop`], and the receipt carries the unmetered marker instead.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EnergyReading {
    pub joules_micro: u64,
    pub provenance: EnergyProvenance,
}

impl EnergyReading {
    /// The pair a receipt's `seal` wants: `(joules_micro, Some(provenance))`.
    pub fn as_seal_args(self) -> (u64, Option<EnergyProvenance>) {
        (self.joules_micro, Some(self.provenance))
    }

    /// What to seal when there was no measurement at all — `(0, None)`, the
    /// unmetered marker (energy-measurement §2): a `0` that means "not
    /// metered", never a figure of zero joules.
    pub fn unmetered() -> (u64, Option<EnergyProvenance>) {
        (0, None)
    }
}

/// Something that can measure the energy of a span of work.
pub trait Meter {
    /// Begin a span. Calling it again restarts the span.
    fn start(&mut self);
    /// End the span. `None` when this meter could not measure — never a
    /// fabricated figure.
    fn stop(&mut self) -> Option<EnergyReading>;
    /// The class this meter is capable of claiming, whatever happens. The
    /// evidence (window, and so the full [`EnergyProvenance`]) exists only once
    /// a span has been measured, so this is the class without it.
    fn kind(&self) -> AcquisitionKind;
}

/// What stands behind a counter meter's readings.
enum CounterSource {
    /// The measured silicon's own counter.
    OnChip { filtering: CounterFiltering },
    /// An external instrument with a current calibration.
    Instrument { calibration_ref: String },
}

/// A monotonic hardware energy counter, in microjoules, that wraps at
/// `wrap_at`. The read and the clock are injected, so the delta, wraparound and
/// window arithmetic is exercised in tests without needing the hardware present.
pub struct CounterMeter {
    read: Box<dyn FnMut() -> Option<u64> + Send>,
    clock_micros: Box<dyn FnMut() -> u64 + Send>,
    wrap_at: u64,
    relative_ppm: NonZeroU32,
    source: CounterSource,
    /// `(counter, clock)` at each read of the span: its start, every
    /// [`CounterMeter::sample`], and nothing yet of its stop. Empty with no span
    /// started.
    reads: Vec<(u64, u64)>,
    /// A read during the span failed, so the span cannot be measured.
    broken: bool,
}

impl CounterMeter {
    /// An on-chip counter. `read` returns the counter's current microjoule
    /// value, or `None` when it cannot be read; `clock_micros` a monotonic
    /// microsecond timestamp; `wrap_at` the counter's range (its maximum + 1);
    /// `relative_ppm` the standard relative uncertainty (k = 1) the operator
    /// declares for this counter at the windows it will be read over. The
    /// filtering state starts as [`CounterFiltering::Unknown`].
    ///
    /// A reading declares `relative_ppm` combined with the resolution of a
    /// delta of two whole-microjoule register readings
    /// ([`crate::energy_class::REGISTER_DELTA_UNCERTAINTY_UJ`]), relative to
    /// the delta — so a short span declares more than `relative_ppm`, and a
    /// long one about `relative_ppm`, rounded up. A register whose underlying
    /// hardware unit is coarser than a microjoule hides that from the meter;
    /// `relative_ppm` must cover it.
    pub fn new(
        read: impl FnMut() -> Option<u64> + Send + 'static,
        clock_micros: impl FnMut() -> u64 + Send + 'static,
        wrap_at: u64,
        relative_ppm: NonZeroU32,
    ) -> CounterMeter {
        CounterMeter {
            read: Box::new(read),
            clock_micros: Box::new(clock_micros),
            wrap_at,
            relative_ppm,
            source: CounterSource::OnChip { filtering: CounterFiltering::Unknown },
            reads: Vec::new(),
            broken: false,
        }
    }

    /// Read the counter during a span, as one interval's boundary. A span
    /// with too few of these is refused at [`Meter::stop`]: two reads are one
    /// interval, and the liveness rule needs at least five advancing ones
    /// (energy-measurement §3.1). Does nothing with no span started.
    pub fn sample(&mut self) {
        if self.reads.is_empty() || self.broken {
            return;
        }
        match (self.read)() {
            Some(c) => {
                let t = (self.clock_micros)();
                self.reads.push((c, t));
            }
            None => self.broken = true,
        }
    }

    /// Probe whether the counter is live before trusting it: a span of
    /// `intervals` intervals, `pause` between reads, that must pass the
    /// liveness rule. The probe's reading is discarded.
    pub fn probe(&mut self, intervals: u32, mut pause: impl FnMut()) -> bool {
        self.start();
        for _ in 1..intervals {
            pause();
            self.sample();
        }
        pause();
        self.stop().is_some()
    }

    /// The counter delta between two reads, across one wrap at `wrap_at`.
    fn delta(&self, from: u64, to: u64) -> u64 {
        if to >= from { to - from } else { self.wrap_at.saturating_sub(from).saturating_add(to) }
    }

    /// An external instrument that exposes a cumulative energy register and
    /// holds a current calibration. `calibration_ref` identifies the
    /// certificate (a URI, or the issuing laboratory's certificate number);
    /// `relative_ppm` is the standard uncertainty (k = 1) it states for the
    /// range and conditions of use — an expanded uncertainty `U` at coverage
    /// factor `k` is declared as `U / k`.
    ///
    /// Refuses an empty reference: without one, the "calibrated" in the class
    /// name is an assertion nobody can check.
    pub fn calibrated_instrument(
        read: impl FnMut() -> Option<u64> + Send + 'static,
        clock_micros: impl FnMut() -> u64 + Send + 'static,
        wrap_at: u64,
        relative_ppm: NonZeroU32,
        calibration_ref: impl Into<String>,
    ) -> Result<CounterMeter, AcquisitionError> {
        let calibration_ref = calibration_ref.into();
        if calibration_ref.trim().is_empty() {
            return Err(AcquisitionError::NoCalibrationRef);
        }
        let mut m = CounterMeter::new(read, clock_micros, wrap_at, relative_ppm);
        m.source = CounterSource::Instrument { calibration_ref };
        Ok(m)
    }

    /// Record the counter's filtering state, for a caller that has determined
    /// it (e.g. by reading `IA32_MISC_PACKAGE_CTLS[0]`, or because the platform
    /// runs a confidential-computing enclave that enables it). Has no effect on
    /// an external instrument, which has no counter filter.
    pub fn with_filtering(mut self, filtering: CounterFiltering) -> CounterMeter {
        if let CounterSource::OnChip { filtering: f } = &mut self.source {
            *f = filtering;
        }
        self
    }

    /// Wire the Linux powercap (RAPL) interface for a package domain, timed by
    /// the process monotonic clock. `None` when the interface is absent — which
    /// is every non-Linux target, and any Linux without powercap — so the caller
    /// must fall back explicitly rather than silently receive a made-up figure.
    ///
    /// The filtering state is left [`CounterFiltering::Unknown`]: powercap does
    /// not expose it, and an unprivileged reader cannot read the MSR that does.
    /// Where filtering may be active, the advisory's guidance states the added noise as typically
    /// under 2 % standard deviation over 1 s and 5–15 % over 100 ms
    /// (INTEL-SA-00389); `relative_ppm` should cover it at the windows used.
    ///
    /// The counter is probed before the meter is returned: six intervals of
    /// 60 ms, which must pass the liveness rule (energy-measurement §3.1). A
    /// counter that is present but frozen, or refreshed in batches, gives no
    /// meter either.
    #[cfg(not(target_arch = "wasm32"))]
    pub fn rapl(domain: &str, relative_ppm: NonZeroU32) -> Option<CounterMeter> {
        CounterMeter::rapl_with_probe(domain, relative_ppm, |m| {
            m.probe(6, || std::thread::sleep(std::time::Duration::from_millis(60)))
        })
    }

    /// [`CounterMeter::rapl`] with the liveness probe supplied by the caller.
    #[cfg(not(target_arch = "wasm32"))]
    pub fn rapl_with_probe(
        domain: &str,
        relative_ppm: NonZeroU32,
        probe: impl FnOnce(&mut CounterMeter) -> bool,
    ) -> Option<CounterMeter> {
        let base = format!("/sys/class/powercap/{domain}");
        let energy = format!("{base}/energy_uj");
        let range = format!("{base}/max_energy_range_uj");
        // Both files must be readable now, or this meter does not exist.
        let wrap_at = read_u64_file(&range)?.saturating_add(1);
        read_u64_file(&energy)?;
        let origin = std::time::Instant::now();
        let mut m = CounterMeter::new(
            move || read_u64_file(&energy),
            move || origin.elapsed().as_micros() as u64,
            wrap_at,
            relative_ppm,
        );
        probe(&mut m).then_some(m)
    }
}

#[cfg(not(target_arch = "wasm32"))]
fn read_u64_file(path: &str) -> Option<u64> {
    std::fs::read_to_string(path).ok()?.trim().parse::<u64>().ok()
}

impl Meter for CounterMeter {
    fn start(&mut self) {
        self.broken = false;
        let counter = (self.read)();
        let t = (self.clock_micros)();
        self.reads = counter.map(|c| vec![(c, t)]).unwrap_or_default();
    }

    fn stop(&mut self) -> Option<EnergyReading> {
        let mut reads = std::mem::take(&mut self.reads);
        if reads.is_empty() || std::mem::take(&mut self.broken) {
            return None;
        }
        let now = (self.read)()?;
        let t = (self.clock_micros)();
        reads.push((now, t));
        let window_us = t.saturating_sub(reads[0].1);
        // Energy counters are monotonic until they wrap at their range; each
        // interval is read across at most one wrap.
        let deltas: Vec<u64> = reads.windows(2).map(|w| self.delta(w[0].0, w[1].0)).collect();
        // The counter must have advanced as a live rail does over the span
        // (energy-measurement §3.1). One interval — a read at each end — never
        // passes, nor does a counter that is frozen or moves in one batch.
        if !Liveness::from_advances(deltas.iter().map(|d| *d > 0)).is_live_over(window_us) {
            return None;
        }
        let delta = deltas.iter().try_fold(0u64, |a, d| a.checked_add(*d))?;
        // A zero delta or a zero window is a failure to measure, not a
        // measurement of nothing; sealing either would be refused downstream
        // anyway, so it is refused here where the cause is known.
        if delta == 0 || window_us == 0 {
            return None;
        }
        // The declaration describes the figure sealed, the delta of two whole-
        // microjoule readings: the operator's relative uncertainty for the
        // counter, carried across in microjoules, and the readings' own
        // resolution (§2.3), divided by the delta. Without the second term a
        // small delta would declare less than its integer can carry, and the
        // receipts refuse that (`check_resolution`).
        let operator = self.relative_ppm.get() as f64 / 1e6;
        let rel = (operator * delta as f64).hypot(REGISTER_DELTA_UNCERTAINTY_UJ) / delta as f64;
        let uncertainty = Uncertainty::from_relative(rel, window_us).ok()?;
        let provenance = match &self.source {
            CounterSource::OnChip { filtering } => {
                EnergyProvenance::OnChipCounter { uncertainty, filtering: *filtering }
            }
            CounterSource::Instrument { calibration_ref } => EnergyProvenance::CalibratedInstrument {
                uncertainty,
                calibration_ref: calibration_ref.clone(),
            },
        };
        Some(EnergyReading { joules_micro: delta, provenance })
    }

    fn kind(&self) -> AcquisitionKind {
        match self.source {
            CounterSource::OnChip { .. } => AcquisitionKind::OnChipCounter,
            CounterSource::Instrument { .. } => AcquisitionKind::CalibratedInstrument,
        }
    }
}

/// Elapsed time multiplied by a calibrated power figure: a modelled figure,
/// labelled `ModelBased`, never a measurement. It cannot claim a hardware class
/// however it is configured, and it never yields a figure of zero: a model
/// that gives nothing — a zero power, a span too short to reach a microjoule —
/// or a product too large to represent gives `None`, never a labelled zero
/// (which the receipts refuse) or a saturated maximum (which is not the
/// model's output).
pub struct ModelMeter {
    clock_micros: Box<dyn FnMut() -> u64 + Send>,
    milliwatts: u64,
    t0: Option<u64>,
}

impl ModelMeter {
    /// `clock_micros` returns a monotonic microsecond timestamp; `milliwatts`
    /// is the calibrated marginal power of the work being measured.
    pub fn new(clock_micros: impl FnMut() -> u64 + Send + 'static, milliwatts: u64) -> ModelMeter {
        ModelMeter { clock_micros: Box::new(clock_micros), milliwatts, t0: None }
    }

    /// A model meter driven by the process monotonic clock.
    #[cfg(not(target_arch = "wasm32"))]
    pub fn wall_clock(milliwatts: u64) -> ModelMeter {
        let origin = std::time::Instant::now();
        ModelMeter::new(move || origin.elapsed().as_micros() as u64, milliwatts)
    }
}

impl Meter for ModelMeter {
    fn start(&mut self) {
        self.t0 = Some((self.clock_micros)());
    }

    fn stop(&mut self) -> Option<EnergyReading> {
        let t0 = self.t0.take()?;
        let dt_micros = (self.clock_micros)().saturating_sub(t0);
        // uJ = mW * us / 1000  (mW*us = nJ; /1000 -> uJ)
        let joules_micro = self.milliwatts.checked_mul(dt_micros)? / 1000;
        if joules_micro == 0 {
            return None;
        }
        Some(EnergyReading { joules_micro, provenance: EnergyProvenance::ModelBased })
    }

    fn kind(&self) -> AcquisitionKind {
        AcquisitionKind::ModelBased
    }
}

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

    /// A scripted counter: successive reads return successive values.
    fn scripted(values: Vec<Option<u64>>) -> impl FnMut() -> Option<u64> + Send {
        let mut q = values.into_iter();
        move || q.next().flatten()
    }

    fn scripted_clock(values: Vec<u64>) -> impl FnMut() -> u64 + Send {
        let mut q = values.into_iter();
        move || q.next().unwrap_or(0)
    }

    /// Every meter here is `Send`, so a host hands one to a worker thread that runs the
    /// work while another thread reads the counter (the package receipt's meter slot is
    /// `Box<dyn Meter + Send>`).
    #[test]
    fn every_meter_crosses_threads() {
        fn send<T: Send>(_: &T) {}
        let counter = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
        let c = std::sync::Arc::clone(&counter);
        let mut m = CounterMeter::new(move || Some(c.load(std::sync::atomic::Ordering::SeqCst)), scripted_clock((0..64).map(|i| i * 100_000).collect()), u64::MAX, NonZeroU32::new(20_000).unwrap());
        send(&m);
        let model = ModelMeter::new(scripted_clock(vec![0, 1_000_000]), 500);
        send(&model);
        // The span runs on a worker thread; the counter advances from this one.
        m.start();
        let worker = std::thread::spawn(move || {
            for _ in 0..8 {
                m.sample();
            }
            m
        });
        counter.store(1_000_000, std::sync::atomic::Ordering::SeqCst);
        let mut m = worker.join().unwrap();
        let boxed: Box<dyn Meter + Send> = Box::new(model);
        let back = std::thread::spawn(move || boxed).join().unwrap();
        assert_eq!(back.kind(), AcquisitionKind::ModelBased);
        assert_eq!(m.kind(), AcquisitionKind::OnChipCounter);
        let _ = m.stop();
    }

    fn ppm(n: u32) -> NonZeroU32 {
        NonZeroU32::new(n).unwrap()
    }

    /// A span over `n_reads` reads: the first at start, the last at stop, the
    /// rest as samples.
    fn span(m: &mut CounterMeter, n_reads: usize) -> Option<EnergyReading> {
        m.start();
        for _ in 2..n_reads {
            m.sample();
        }
        m.stop()
    }

    /// A meter over the counter values `counter`, read every `step_us`.
    fn meter(counter: &[u64], step_us: u64, wrap_at: u64, p: u32) -> CounterMeter {
        let clock: Vec<u64> = (0..counter.len() as u64).map(|i| 10 + i * step_us).collect();
        CounterMeter::new(scripted(counter.iter().copied().map(Some).collect()), scripted_clock(clock), wrap_at, ppm(p))
    }

    /// A span over `counter` read every `step_us`.
    fn measured(counter: &[u64], step_us: u64, wrap_at: u64, p: u32) -> Option<EnergyReading> {
        span(&mut meter(counter, step_us, wrap_at, p), counter.len())
    }

    /// Six reads 200 ms apart, advancing by `per` µJ each: five live intervals
    /// over a second.
    fn live(start: u64, per: u64) -> Vec<u64> {
        (0..6).map(|i| start + i * per).collect()
    }

    #[test]
    fn counter_measures_a_real_delta_as_an_on_chip_reading_with_its_uncertainty() {
        let mut m = meter(&live(1_000, 700), 200_000, 1 << 32, 20_000);
        let r = span(&mut m, 6).unwrap();
        assert_eq!(r.joules_micro, 3_500);
        // 2 % of 3,500 µJ is 70 µJ; with the 1/√6 µJ resolution of a register
        // delta in quadrature, 70.0012 µJ, which is 20,000.34 ppm, rounded up.
        assert_eq!(
            r.provenance,
            EnergyProvenance::OnChipCounter {
                uncertainty: Uncertainty { relative_ppm: 20_001, window_us: 1_000_000 },
                filtering: CounterFiltering::Unknown,
            },
            "the declared uncertainty travels with the reading, over the window actually measured"
        );
        assert_eq!(m.kind(), AcquisitionKind::OnChipCounter);
        assert!(!r.provenance.is_legacy(), "a meter never produces the legacy label");
    }

    #[test]
    fn a_known_filtering_state_is_recorded_and_the_default_is_unknown() {
        let mut m = meter(&live(0, 2), 80_000, 1 << 32, 150_000).with_filtering(CounterFiltering::On);
        match span(&mut m, 6).unwrap().provenance {
            EnergyProvenance::OnChipCounter { filtering, uncertainty } => {
                assert_eq!(filtering, CounterFiltering::On);
                assert_eq!(uncertainty.window_us, 400_000);
            }
            other => panic!("unexpected class {other:?}"),
        }
    }

    #[test]
    fn a_calibrated_instrument_needs_its_certificate_and_says_so() {
        assert!(matches!(
            CounterMeter::calibrated_instrument(scripted(vec![]), scripted_clock(vec![]), 1 << 32, ppm(5_000), " "),
            Err(AcquisitionError::NoCalibrationRef)
        ));
        let reads = live(100, 120);
        let clock: Vec<u64> = (0..6).map(|i| i * 12_000_000).collect();
        let mut m = CounterMeter::calibrated_instrument(
            scripted(reads.iter().copied().map(Some).collect()),
            scripted_clock(clock),
            1 << 32,
            ppm(5_000),
            "lab-cert-0412",
        )
        .unwrap()
        .with_filtering(CounterFiltering::On); // no effect on an instrument
        assert_eq!(m.kind(), AcquisitionKind::CalibratedInstrument);
        let r = span(&mut m, 6).unwrap();
        assert_eq!(
            r.provenance,
            EnergyProvenance::CalibratedInstrument {
                uncertainty: Uncertainty { relative_ppm: 5_047, window_us: 60_000_000 },
                calibration_ref: "lab-cert-0412".into(),
            },
            "0.5 % of 600 µJ with the register delta's resolution: 5,046.08 ppm, rounded up"
        );
    }

    #[test]
    fn a_short_span_declares_its_register_resolution() {
        // A 5 µJ delta, the least a live span can show (one µJ in each of five
        // intervals): the operator's 2 % is 0.1 µJ, below the 1/√6 µJ the two
        // whole-microjoule end readings leave. Declaring 2 % alone would state
        // less than the integer can carry, and the receipts refuse that.
        let r = measured(&live(10, 1), 100_000, 1 << 32, 20_000).unwrap();
        assert_eq!(r.joules_micro, 5);
        let u = r.provenance.uncertainty().unwrap();
        assert_eq!(u.relative_ppm, 84_064);
        assert_eq!(crate::energy_class::check_resolution(r.joules_micro, &r.provenance), Ok(()));
        assert!(crate::energy_class::figure_and_class_cohere(r.joules_micro, Some(&r.provenance)));
        // Every span meets the floor, whatever the operator declared.
        for per in [1u64, 2, 5, 17, 288, 289, 1_000, 288_676, 10_000_000] {
            let r = measured(&live(0, per), 100_000, u64::MAX, 1).unwrap();
            assert_eq!(crate::energy_class::check_resolution(r.joules_micro, &r.provenance), Ok(()), "per {per}");
        }
    }

    #[test]
    fn counter_handles_wraparound() {
        // Range 10_000 (wrap_at 10_000): 9_800 -> 200 crosses the wrap in the
        // third interval; every interval advances by 100 µJ.
        assert_eq!(measured(&[9_700, 9_800, 9_900, 0, 100, 200], 100_000, 10_000, 1).unwrap().joules_micro, 500);
    }

    #[test]
    fn a_meter_that_cannot_read_reports_nothing_not_a_guess() {
        // No hardware: the read fails. The meter must yield None so the caller
        // falls back explicitly rather than sealing a fabricated hardware figure.
        let mut m = CounterMeter::new(scripted(vec![None, None]), scripted_clock(vec![0, 5]), 1 << 32, ppm(1));
        m.start();
        assert!(m.stop().is_none());
        // And a stop with no start is likewise nothing.
        let mut m2 = CounterMeter::new(scripted(vec![Some(5)]), scripted_clock(vec![0]), 1 << 32, ppm(1));
        assert!(m2.stop().is_none());
        // A read that fails during the span breaks the span.
        let mut m3 = CounterMeter::new(
            scripted(vec![Some(0), Some(1), None, Some(3), Some(4), Some(5)]),
            scripted_clock((0..6).map(|i| i * 100_000).collect()),
            1 << 32,
            ppm(1),
        );
        assert!(span(&mut m3, 6).is_none());
    }

    #[test]
    fn a_zero_delta_or_a_zero_window_is_not_a_measurement() {
        assert!(measured(&[7; 6], 200_000, 1 << 32, 1).is_none(), "no energy counted is a failure to measure");
        assert!(measured(&live(7, 2), 0, 1 << 32, 1).is_none(), "no window is not an integration");
    }

    // ── fault injection: a counter whose rail is not live (§3.1) ──

    /// Two reads are one interval: the delta a counter shows between them is
    /// no evidence that it was refreshed in between. Before the liveness rule
    /// this sealed a 3,500 µJ OnChipCounter reading.
    #[test]
    fn two_reads_are_never_a_live_span() {
        let mut m = CounterMeter::new(scripted(vec![Some(1_000), Some(4_500)]), scripted_clock(vec![10, 1_000_010]), 1 << 32, ppm(20_000));
        m.start();
        assert!(m.stop().is_none());
    }

    #[test]
    fn a_frozen_counter_is_refused() {
        assert!(measured(&[5_000; 11], 100_000, 1 << 32, 20_000).is_none());
    }

    #[test]
    fn a_single_refresh_lump_is_refused() {
        assert!(measured(&[5_000, 5_000, 5_000, 9_000, 9_000, 9_000, 9_000, 9_000, 9_000, 9_000, 9_000], 100_000, 1 << 32, 20_000).is_none());
    }

    #[test]
    fn a_two_step_refresh_is_refused() {
        // Two consecutive advances, and nothing else in ten intervals.
        assert!(measured(&[5_000, 5_000, 5_000, 7_000, 9_000, 9_000, 9_000, 9_000, 9_000, 9_000, 9_000], 100_000, 1 << 32, 20_000).is_none());
    }

    #[test]
    fn a_live_counter_over_too_short_a_span_is_refused() {
        assert!(measured(&live(0, 100), 59_000, 1 << 32, 20_000).is_none(), "five live intervals over 295 ms");
        assert!(measured(&live(0, 100), 60_000, 1 << 32, 20_000).is_some(), "over 300 ms");
    }

    #[test]
    fn the_probe_refuses_a_counter_that_is_not_live() {
        let mut frozen = meter(&[1; 6], 60_000, 1 << 32, 20_000);
        assert!(!frozen.probe(5, || {}), "five intervals: six reads");
        let mut ok = meter(&live(0, 3), 60_000, 1 << 32, 20_000);
        assert!(ok.probe(5, || {}));
        // The probe's span is discarded: a stop after it measures nothing.
        assert!(ok.stop().is_none());
        // An absent interface gives no meter, whatever the probe would say.
        assert!(CounterMeter::rapl_with_probe("no-such-domain", ppm(20_000), |_| true).is_none());
    }

    #[test]
    fn model_meter_is_derived_and_says_so() {
        // 2000 mW over 5_000 us = 10_000 uJ.
        let mut m = ModelMeter::new(scripted_clock(vec![1_000, 6_000]), 2_000);
        m.start();
        let r = m.stop().unwrap();
        assert_eq!(r.joules_micro, 10_000);
        assert_eq!(r.provenance, EnergyProvenance::ModelBased);
        // The ladder is mechanical: this meter cannot claim a measurement.
        assert_eq!(m.kind(), AcquisitionKind::ModelBased);
    }

    #[test]
    fn model_meter_never_yields_a_labelled_zero() {
        // A span too short to reach a microjoule, a zero power, a zero span:
        // a model that gives nothing is None, never a ModelBased zero.
        for (clock, mw) in [(vec![0, 400], 2), (vec![0, 1_000_000], 0), (vec![5, 5], 2_000)] {
            let mut m = ModelMeter::new(scripted_clock(clock), mw);
            m.start();
            assert_eq!(m.stop(), None);
        }
    }

    #[test]
    fn model_meter_refuses_overflow() {
        let mut m = ModelMeter::new(scripted_clock(vec![0, u64::MAX]), 2);
        m.start();
        assert_eq!(m.stop(), None, "a product past u64 is refused, not saturated to u64::MAX / 1000");
        let mut m = ModelMeter::new(scripted_clock(vec![0, u64::MAX / 2]), 2);
        m.start();
        assert_eq!(m.stop().unwrap().joules_micro, (u64::MAX / 2 * 2) / 1000);
    }

    #[test]
    fn model_meter_is_never_hardware() {
        let meters = [ModelMeter::new(scripted_clock(vec![0, 1_000_000]), 1_000), ModelMeter::wall_clock(1_000)];
        for mut m in meters {
            assert_eq!(m.kind(), AcquisitionKind::ModelBased);
            m.start();
            if let Some(r) = m.stop() {
                assert_eq!(r.provenance, EnergyProvenance::ModelBased);
                assert!(!r.provenance.is_hardware());
            }
        }
    }

    #[test]
    fn seal_args_round_trip_to_the_receipt_contract() {
        let p = crate::energy_class::test_on_chip();
        let r = EnergyReading { joules_micro: 42, provenance: p.clone() };
        assert_eq!(r.as_seal_args(), (42, Some(p)));
        // The unmetered marker the receipts accept.
        assert_eq!(EnergyReading::unmetered(), (0, None));
    }

    #[test]
    fn rapl_is_absent_here_and_says_so_rather_than_pretending() {
        // This box has no Linux powercap interface; the constructor must decline
        // rather than hand back a meter that invents hardware figures.
        if !std::path::Path::new("/sys/class/powercap/intel-rapl:0").exists() {
            assert!(CounterMeter::rapl("intel-rapl:0", ppm(20_000)).is_none());
        }
    }
}
