use super::*;
use crate::energy_class::WindowOpening;
use crate::energy_report::{BracketSummary, BracketValues};

fn s(n: u64, mean: u64, sd: u64) -> Summary {
    Summary { n, mean_uw: mean, sd_uw: sd, live_intervals: n, longest_live_run: n }
}

/// §4.3 by hand: 2, 4 and 6 W over 200 ms each: mean 4 W, sample sd 2 W exactly, and a
/// sd that is not whole rounds up.
#[test]
fn a_bracket_summary_is_its_intervals_in_integers() {
    let iv = [(400_000_000, 200_000), (800_000_000, 200_000), (1_200_000_000, 200_000)];
    let x = Summary::of_intervals(&iv).unwrap();
    assert_eq!((x.n, x.mean_uw, x.sd_uw, x.live_intervals, x.longest_live_run), (3, 4_000_000, 2_000_000, 3, 3));
    // 1, 2 µW: mean 1.5 rounds up to 2; sd √0.5 = 0.707… rounds up to 1.
    let y = Summary::of_intervals(&[(1, 1000), (2, 1000)]).unwrap();
    assert_eq!((y.mean_uw, y.sd_uw), (2, 1));
    // An interval with no advance breaks the run; one of no length is no reading.
    let z = Summary::of_intervals(&[(5, 10), (0, 10), (5, 10), (5, 10)]).unwrap();
    assert_eq!((z.live_intervals, z.longest_live_run), (3, 2));
    assert!(Summary::of_intervals(&[(5, 0)]).is_none());
}

/// A round's figure and least declaration are energy-measurement §6.2's: the same numbers
/// `energy_report`'s reference gives for a from-rest bracket, across a spread of brackets.
#[test]
fn a_round_agrees_with_the_reference_bracket_arithmetic() {
    let mut checked = 0;
    for (k, (idle_mean, idle_sd, marginal, busy_sd, window, c, n)) in [
        (2_000_000u64, 40_000u64, 3_000_000u64, 60_000u64, 2_800_000u64, 1_000u64, 8u64),
        (24_950_000, 230_000, 890_000, 300_000, 4_230_000, 1_525, 8),
        (5_000, 70, 4_900_000, 120_000, 1_000_000, 3, 1),
        (1_000_000, 1, 2, 1, 1_000_000, 1, 2),
    ]
    .into_iter()
    .enumerate()
    {
        let (idle, busy) = (s(10, idle_mean, idle_sd), s(14, idle_mean + marginal, busy_sd));
        let r = Round { idle, busy, window_us: window, completions: c };
        let mine = round_outcome(&r, n, 200_000, 1_000_000);
        let bs = |x: &Summary| BracketSummary { n: x.n, mean_uw: x.mean_uw, sd_uw: x.sd_uw, live_intervals: x.live_intervals, longest_live_run: x.longest_live_run };
        let v = BracketValues { idle: bs(&idle), busy: bs(&busy), window_us: window, completions: c, in_flight: n, opening: WindowOpening::FromRest };
        match (mine, v.figure()) {
            (Ok((f, p)), Some(g)) => {
                assert_eq!(f, g, "case {k}");
                assert_eq!(Some(p), v.least_relative_ppm(f), "case {k}");
                checked += 1;
            }
            (Err(why), g) => assert!(g.is_none() || why == "below_attribution_floor" || why == "not_declarable", "case {k}: {why} {g:?}"),
            (Ok(fp), None) => panic!("case {k}: {fp:?} where the reference gives none"),
        }
    }
    assert!(checked >= 3);
}

/// §4.5's rules in order on a round that breaks several: the first is the reason.
#[test]
fn a_round_is_refused_by_its_first_broken_rule() {
    let ok = Round { idle: s(10, 2_000_000, 40_000), busy: s(14, 5_000_000, 60_000), window_us: 2_800_000, completions: 100 };
    assert!(round_outcome(&ok, 8, 200_000, 1_000_000).is_ok());
    let mut r = ok;
    r.idle.n = 1;
    assert_eq!(round_outcome(&r, 8, 200_000, 1_000_000), Err("too_few_samples"));
    let mut r = ok;
    r.busy.sd_uw = 0;
    assert_eq!(round_outcome(&r, 8, 200_000, 1_000_000), Err("rail_not_live"));
    let mut r = ok;
    r.idle.live_intervals = 7; // 7 of 10 is below four of every five
    assert_eq!(round_outcome(&r, 8, 200_000, 1_000_000), Err("rail_not_live"));
    let mut r = ok;
    r.window_us = 900_000;
    assert_eq!(round_outcome(&r, 8, 200_000, 1_000_000), Err("window_short"));
    assert_eq!(round_outcome(&ok, 8, 20_000, 1_000_000), Err("rail_not_live"), "an idle bracket of 200 ms");
    let mut r = ok;
    r.completions = 0;
    assert_eq!(round_outcome(&r, 8, 200_000, 1_000_000), Err("empty_burst"));
    let mut r = ok;
    r.busy.mean_uw = r.idle.mean_uw;
    assert_eq!(round_outcome(&r, 8, 200_000, 1_000_000), Err("no_marginal"));
    let mut r = ok;
    r.busy.mean_uw = r.idle.mean_uw + 39_999;
    assert_eq!(round_outcome(&r, 8, 200_000, 1_000_000), Err("below_attribution_floor"));
    let mut r = ok;
    r.completions = 1_000_000_000;
    assert_eq!(round_outcome(&r, 8, 200_000, 1_000_000), Err("figure_zero"));
}

/// The median is the middle of the rounds ordered by (figure, index); the declaration
/// covers the spread; the verdict is the median's.
#[test]
fn a_row_seals_its_median_and_covers_its_spread() {
    let round = |marginal: u64| Round { idle: s(10, 2_000_000, 40_000), busy: s(14, 2_000_000 + marginal, 60_000), window_us: 2_800_000, completions: 1_000 };
    let rounds: Vec<Round> = [3_000_000, 3_100_000, 2_900_000, 3_000_000, 3_300_000, 2_700_000, 3_050_000].into_iter().map(round).collect();
    let figs: Vec<u64> = rounds.iter().map(|r| round_outcome(r, 8, 200_000, 1_000_000).unwrap().0).collect();
    let RowOutcome::Figure { clean, median, joules_micro, range, least_ppm, .. } = row_outcome(&rounds, 8, 200_000, 1_000_000) else { panic!() };
    assert!(clean);
    let mut sorted = figs.clone();
    sorted.sort();
    assert_eq!(joules_micro, sorted[3]);
    assert_eq!(figs[median], joules_micro);
    assert_eq!(median, 3, "two rounds tie at the median figure; ordered by index, the later is the middle one");
    assert_eq!(range, (sorted[0], sorted[6]));
    // q: 12·q²·F² ≥ 10¹²·Δ², by the real-valued formula Δ/(2√3·F).
    let q = (1e6 * (range.1 - range.0) as f64 / (2.0 * 3f64.sqrt() * joules_micro as f64)).ceil() as u32;
    assert!(least_ppm + 1 >= q && least_ppm <= (q + 1).max(round_outcome(&rounds[median], 8, 200_000, 1_000_000).unwrap().1));
    // The same rounds at a ratio between 3 and 10 are relative; between 1 and 3, noisy.
    let at = |m: u64| (0..7).map(|_| round(m)).collect::<Vec<_>>();
    assert!(matches!(row_outcome(&at(200_000), 8, 200_000, 1_000_000), RowOutcome::Figure { clean: false, .. }));
    assert_eq!(row_outcome(&at(100_000), 8, 200_000, 1_000_000), RowOutcome::Unmetered("noisy"));
}

/// §5.2: a bounded window pays for exactly one segment, so no completion-count term.
#[test]
fn a_bounded_segment_has_no_count_term() {
    let (idle, busy) = (s(10, 2_000_000, 40_000), s(10, 2_900_000, 60_000));
    let (f, p) = bounded_figure(&idle, &busy, 2_000_000).unwrap();
    assert_eq!(f, 1_800_000, "0.9 W for 2 s");
    // u_power alone, in the real numbers: sqrt(s_i²/n_i + s_b²/n_b)/M, plus the rounding.
    let u = ((0.04f64.powi(2) / 10.0 + 0.06f64.powi(2) / 10.0).sqrt() / 0.9 * f as f64).hypot(0.5 / 3f64.sqrt()) / f as f64;
    assert!((p as f64 - u * 1e6).abs() <= 1.0, "{p} vs {}", u * 1e6);
}

#[test]
fn the_estimators_and_shares_round_half_up() {
    assert_eq!(cpu_time_estimate(150_000, 3_500), 525_000);
    assert_eq!(cpu_time_estimate(1, 1), 0, "1 nJ rounds to 0 µJ");
    assert_eq!(cpu_time_estimate(1, 500), 1, "half a microjoule rounds up");
    assert_eq!(display_estimate(30_000, 200, 120_000_000, 250), Some(2_880_000_000));
    assert_eq!(display_estimate(1, 1, 1, 0), None);
    assert_eq!(table_figure(10_000, 48, 60), Some(8_000));
    assert_eq!(table_figure(1, 1, 2), Some(1));
    assert_eq!(share_figure(50_000_000, 3_582_000_000, 3_600_000_000), Some(49_750_000));
    // A display estimate past 128 bits in its numerator still divides exactly.
    let big = display_estimate(MAX_INTEGER, MAX_INTEGER, MAX_INTEGER, MAX_INTEGER);
    assert!(big.is_none() || big.unwrap() > 0);
}

#[test]
fn decoders_follow_their_grammar() {
    for ok in ["builtin:wai@1.2.3", "builtin:wai-rs_x@0.1.0+abc", "platform:media-framework", "platform:a"] {
        assert!(decoder_ok(ok), "{ok}");
    }
    for bad in ["builtin:wai@x1", "builtin:Wai@1", "platform:", "platform:Media", "wai@1", "builtin:wai", "platform:a b"] {
        assert!(!decoder_ok(bad), "{bad}");
    }
}

/// A record signs and verifies, its identity is over the body and the signature, and a
/// changed member breaks the signature.
#[test]
fn records_sign_and_their_identity_covers_the_signature() {
    let k = corpus::key("sink");
    let body = serde_json::json!({ "wai_sink_table": 1, "signer": hex(k.verifying_key().as_bytes()) });
    let r = sign(Kind::Table, body, &k);
    assert!(check_signature(Kind::Table, &r).is_ok());
    let id = identity(Kind::Table, &r).unwrap();
    let mut r2 = r.clone();
    r2["wai_sink_table"] = 2.into();
    assert_eq!(check_signature(Kind::Table, &r2).unwrap_err().reason, Reason::SignatureInvalid);
    assert_ne!(identity(Kind::Table, &r2).unwrap(), id);
    assert_ne!(identity(Kind::Segment, &r).unwrap(), id, "the identity is domain-separated");
}

/// The reasons are §8's, each once, and §8 lists exactly them.
#[test]
fn the_reasons_are_the_extensions() {
    let text = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/../extensions/sink-energy.md")).unwrap();
    let start = text.find("## 8. Reasons").unwrap();
    let end = text[start..].find("The **row reasons**").unwrap() + start;
    let listed: Vec<&str> = text[start..end].lines().filter_map(|l| l.strip_prefix("| `")).filter_map(|l| l.split('`').next()).collect();
    let mine: Vec<&str> = Reason::ALL.iter().map(|r| r.code()).collect();
    assert_eq!(listed, mine);
    let tail = &text[end..text[end..].find("## 9.").unwrap() + end];
    for r in ABSENT_REASONS.iter().chain(&ROUND_REASONS).chain(&UNMETERED_REASONS).chain(&["noisy"]) {
        assert!(tail.contains(&format!("`{r}`")), "§8 does not name {r}");
    }
}
