//! Unit tests of the steering module: the records round-trip and are refused by their rules,
//! the request reader and its RFC 9651 parser, the exact products at their limits, the margin
//! at its boundary, and the rule's guarantee that a pathway with no figure is never placed
//! first by its rank while a pathway with a figure is within the limits.

use super::*;

fn k(i: u8) -> SigningKey {
    SigningKey::from_bytes(&sha256(&[b"wai:energy-steering/test/key\x00", &[i]]))
}

fn pathway(id: &str, hop: u8) -> Pathway {
    Pathway {
        id: id.into(),
        base: NOT_CLONE,
        host: String::new(),
        params: vec![],
        top_tier: 2,
        latency_ms: 100,
        capacity: NO_CAPACITY,
        min_share_ppm: 0,
        hops: vec![(k(hop).verifying_key().to_bytes(), place::EDGE)],
    }
}

fn policy(pathways: Vec<Pathway>) -> Policy {
    Policy {
        room: [7; 32],
        signer: [0; 32],
        period_us: 10,
        window_us: 30,
        lag_us: 2,
        basis: basis::BYTES,
        floor: 1,
        latency_ceiling_ms: 500,
        min_buffer_ms: 1000,
        hysteresis_ppm: 100_000,
        ttl_s: 300,
        min_ttl_s: 30,
        flags: 0,
        initial: 0,
        tiers: vec![1_000, 2_000, 4_000],
        server_uri: "https://s.example/steer?a=1&b=2".into(),
        reload_uri: "https://s.example/steer".into(),
        decision_uri: "https://s.example/d/".into(),
        pathways,
        sig: [0; 64],
    }
    .sign(&k(0))
}

fn two() -> Policy {
    policy(vec![pathway("a", 1), pathway("b", 2)])
}

fn fig(class: u8, joules: u128, delivery: u128) -> Evidence {
    Evidence { status: status::FIGURE, class, joules, delivery }
}

const NO_FIGURE: Evidence = Evidence { status: status::STALE, class: 0, joules: 0, delivery: 0 };

#[test]
fn a_policy_round_trips_and_its_rules_refuse() {
    let p = two();
    let b = p.to_bytes();
    assert_eq!(Policy::read(&b).unwrap(), p);
    let reread = |f: &dyn Fn(&mut Policy)| {
        let mut q = two();
        f(&mut q);
        Policy::read(&q.to_bytes())
    };
    assert_eq!(reread(&|q| q.window_us = 25), Err(Reason::PolicyInvalid), "a window that is not whole periods");
    assert_eq!(reread(&|q| q.pathways[1].id = "a".into()), Err(Reason::PolicyInvalid), "two pathways of one id");
    assert_eq!(reread(&|q| q.pathways[1].hops = q.pathways[0].hops.clone()), Err(Reason::PolicyInvalid), "a hop in two pathways");
    assert_eq!(reread(&|q| q.reload_uri = "https://s.example/#x".into()), Err(Reason::PolicyInvalid), "a fragment");
    assert_eq!(reread(&|q| q.reload_uri = "https://s.example/%4".into()), Err(Reason::PolicyInvalid), "a broken percent-encoding");
    assert_eq!(reread(&|q| q.pathways[0].hops[0].1 = place::INTERIOR), Err(Reason::PolicyInvalid), "no edge hop");
    assert_eq!(reread(&|q| q.pathways[1].base = 1), Err(Reason::CloneUnknown), "a clone of itself");
    assert_eq!(reread(&|q| q.pathways[0].base = 1), Err(Reason::CloneUnknown), "a clone of a later pathway");
    assert_eq!(reread(&|q| q.pathways[1].host = "h.example".into()), Err(Reason::PolicyInvalid), "a host on a pathway that is no clone");
    assert!(reread(&|q| {
        q.pathways[1].base = 0;
        q.pathways[1].host = "h.example".into();
        q.pathways[1].params = vec![("x".into(), "%2F".into())];
    })
    .is_ok());
    let mut short = b.clone();
    short.truncate(b.len() - 1);
    assert_eq!(Policy::read(&short), Err(TRUNCATED));
    let mut long = b.clone();
    long.push(0);
    assert_eq!(Policy::read(&long), Err(MALFORMED));
}

#[test]
fn the_window_ends_on_the_last_period_past_the_lag() {
    let p = two();
    assert_eq!(window(&p, 0), (0, 0));
    assert_eq!(window(&p, 11), (0, 0));
    assert_eq!(window(&p, 12), (0, 10));
    assert_eq!(window(&p, 41), (0, 30));
    assert_eq!(window(&p, 52), (20, 50));
}

#[test]
fn the_request_reader_takes_what_each_protocol_sends() {
    let p = two();
    let hls = |t: &str| read_request(protocol::HLS, t.as_bytes(), None, &p);
    let dash = |t: &str| read_request(protocol::DASH, t.as_bytes(), None, &p);
    assert_eq!(hls("/s?_HLS_pathway=%22b%22&_HLS_throughput=4000"), Report::new(Some(1), Some(4000), None));
    assert_eq!(hls("/s?_HLS_pathway=b&_HLS_pathway=a"), Report::new(Some(1), None, None));
    assert_eq!(hls("/s?_HLS_pathway=%22z%22&_HLS_throughput=-1"), Report::default());
    assert_eq!(hls("/s?_HLS_throughput=4%3"), Report::default());
    assert_eq!(dash("/s?_DASH_pathway=%22z,b%22&_DASH_throughput=5,6"), Report::new(Some(1), Some(6), None));
    assert_eq!(dash("/s?_DASH_pathway=a&_DASH_throughput=5,6"), Report::new(Some(0), Some(5), None));
    let cmcd = read_request(protocol::HLS, b"/s?CMCD=bl%3D%2821300%3Bv%2013000%3Ba%29%2Cmtp%3D48000", None, &p);
    assert_eq!(cmcd, Report::new(None, Some(48_000_000), Some(13_000)));
    let header = read_request(protocol::HLS, b"/s", Some(b"bl=(900;v),mtp=(2;v 3;a)"), &p);
    assert_eq!(header, Report::new(None, Some(2_000), Some(900)));
}

#[test]
fn the_structured_field_parser_reads_rfc_9651_dictionaries() {
    use sf::{parse_dictionary as d, Bare, Member};
    let ok = d("a=1, b=?0, c=\"x\\\"y\", d=tok/en:1, e=:AQID:, f=-1.25;p=@12, g=%\"caf%c3%a9\", h, i=(1 2;q);r").unwrap();
    assert_eq!(ok.len(), 9);
    assert_eq!(ok[0].1, Member::Item(sf::Item { bare: Bare::Integer(1), params: vec![] }));
    assert_eq!(ok[5].1, Member::Item(sf::Item { bare: Bare::Decimal(-1250), params: vec![("p".into(), Bare::Date(12))] }));
    assert_eq!(ok[6].1, Member::Item(sf::Item { bare: Bare::Display("café".into()), params: vec![] }));
    assert_eq!(ok[7].1, Member::Item(sf::Item { bare: Bare::Boolean(true), params: vec![] }));
    assert!(matches!(&ok[8].1, Member::Inner(items, ps) if items.len() == 2 && ps.len() == 1));
    assert_eq!(d("").unwrap(), vec![]);
    for good in ["a=::", "a=:AQ:", "a=:AQ==:", "a=:AQI:", "a=:AQI=:", "a=:AQID:"] {
        assert!(d(good).is_some(), "{good} does not parse");
    }
    assert_eq!(d("a=1,a=2").unwrap().len(), 1, "a key given twice keeps one place");
    for bad in ["a=1,", "A=1", "a=(1", "a=1 b=2", "a=1234567890123456", "a=1.2345", "a=1.", "a=\"x", "a=?2", "a=%\"\u{e9}\"", "a=%\"%C3\"", "a=@1.5", "a=:a*b:", "a=:A:", "a=:AQ=:", "a=:A=Q=:", "a=:AQ===:"] {
        assert!(d(bad).is_none(), "{bad} parses");
    }
}

#[test]
fn products_are_exact_at_their_limits() {
    let m = u128::MAX;
    let a = fig(4, m, m - 1);
    let b = fig(4, m - 1, m);
    // J_a × D_b = m × m; J_b × D_a = (m − 1)²: a's figure is the higher.
    assert_eq!(cmp_figures(&a, &b), Ordering::Greater);
    assert_eq!(cmp_figures(&b, &a), Ordering::Less);
    assert_eq!(cmp_figures(&a, &a), Ordering::Equal);
    // 2^64 × 2^64 is 2^128: one limb set, two above the u128 range's limbs.
    let p = product(&[1u128 << 64, 1u128 << 64]);
    assert!(p.iter().enumerate().all(|(i, &l)| l == u64::from(i == 2)), "{p:?}");
    assert_eq!(cmp_limbs(&product(&[m, m, 3]), &product(&[m, m, 2])), Ordering::Greater);
    assert_eq!(cmp_limbs(&product(&[m, m, PPM as u128 + 1]), &product(&[m, m, PPM as u128])), Ordering::Greater);
}

#[test]
fn the_margin_is_strict() {
    // w's figure is exactly 10% below x's: it does not beat x by more than 10%.
    let x = fig(4, 110, 100);
    let w = fig(4, 100, 100);
    assert!(!beats(&w, &x, 100_000));
    assert!(beats(&w, &x, 99_999));
    assert!(!beats(&w, &NO_FIGURE, 0));
}

#[test]
fn the_order_compares_figures_only_within_a_class() {
    let p = policy(vec![pathway("a", 1), pathway("b", 2), pathway("c", 3), pathway("d", 4)]);
    // "a" is the cheapest but estimated, "b" counter-read, "c" calibrated, "d" unknown.
    let ev = [fig(3, 1, 100), fig(4, 50, 100), fig(5, 90, 100), NO_FIGURE];
    assert_eq!(order(&p, &ev), vec![2, 1, 0, 3]);
    let ev = [fig(4, 3, 1), fig(4, 6, 2), NO_FIGURE, NO_FIGURE];
    assert_eq!(order(&p, &ev), vec![0, 1, 2, 3], "equal figures fall to the ids");
}

/// Over every evidence of four pathways drawn from a small set, with and without a current
/// pathway: when some pathway with a figure is within the limits, the rule never places a
/// pathway with no figure first by its rank or by holding a client on it.
#[test]
fn a_pathway_with_no_figure_is_never_preferred_for_it() {
    let p = policy(vec![pathway("a", 1), pathway("b", 2), pathway("c", 3), pathway("d", 4)]);
    let set = [NO_FIGURE, fig(4, 1, 100), fig(4, 9, 100), fig(2, 1, 1000), Evidence { status: status::UNMETERED, ..NO_FIGURE }];
    let digest = p.digest();
    let session = [0u8; 16];
    let mut seen = 0;
    for i in 0..set.len().pow(4) {
        let ev: Vec<Evidence> = (0..4).map(|j| set[i / set.len().pow(j) % set.len()]).collect();
        for c in [None, Some(0), Some(3)] {
            let out = decide(&p, &digest, &session, &Report::new(c, Some(10_000), Some(5_000)), &[0; 4], &ev);
            let any_figure = ev.iter().any(Evidence::has_figure);
            if any_figure && matches!(out.reason, why::RANK | why::HOLD) {
                assert!(ev[out.priority[0] as usize].has_figure(), "{ev:?} with c {c:?} gives {out:?}");
                seen += 1;
            }
            // Every pathway is within the limits here, so after the first the priority is the
            // order: every pathway with a figure before every pathway with none.
            let tail: Vec<bool> = out.priority[1..].iter().map(|&x| ev[x as usize].has_figure()).collect();
            assert!(tail.windows(2).all(|w| w[0] >= w[1]), "a pathway with a figure after one with none: {ev:?} gives {out:?}");
        }
    }
    assert!(seen > 0);
}

#[test]
fn a_decision_round_trips_and_its_rules_refuse() {
    let p = two();
    let d = Decision {
        room: p.room,
        signer: [0; 32],
        policy: p.digest(),
        t: 52,
        session: [9; 16],
        draw: draw(&p.digest(), &[9; 16]),
        protocol: protocol::HLS,
        report: Report::new(Some(1), Some(5_000), None),
        pathways: vec![
            PathwayState { load: 1, limits: limit::ALL, status: status::FIGURE, class: 4, joules: 10, delivery: 20, evidence: vec![[1; 32]] },
            PathwayState { load: 2, limits: limit::ALL, status: status::STALE, class: 0, joules: 0, delivery: 0, evidence: vec![] },
        ],
        ttl: 300,
        reason: why::RANK,
        priority: vec![0, 1],
        sig: [0; 64],
    }
    .sign(&k(0));
    assert_eq!(Decision::read(&d.to_bytes()).unwrap(), d);
    let reread = |f: &dyn Fn(&mut Decision)| {
        let mut x = d.clone();
        f(&mut x);
        Decision::read(&x.to_bytes())
    };
    assert_eq!(reread(&|x| x.priority = vec![0, 0]), Err(Reason::DecisionInvalid));
    assert_eq!(reread(&|x| x.pathways[1].joules = 5), Err(Reason::DecisionInvalid), "a figure on a stale pathway");
    assert_eq!(reread(&|x| x.pathways[0].class = 0), Err(Reason::DecisionInvalid), "a figure with no class");
    assert_eq!(reread(&|x| x.pathways[1].evidence = vec![[1; 32]]), Err(Reason::DecisionInvalid), "a record named twice");
    assert_eq!(reread(&|x| x.report.throughput = 0), Ok(Decision { report: Report { throughput: 0, ..d.report }, ..d.clone() }));
    assert_eq!(reread(&|x| x.report.present &= !present::THROUGHPUT), Err(Reason::DecisionInvalid), "an absent value that is not 0");
    assert_eq!(reread(&|x| x.reason = 7), Err(Reason::DecisionInvalid));
}

#[test]
fn the_manifest_and_the_content_descriptions_are_fixed_bytes() {
    let mut p = two();
    p.pathways[1].base = 0;
    p.pathways[1].host = "h.example".into();
    p.pathways[1].params = vec![("k".into(), "v".into()), ("t".into(), "%2F".into())];
    let d = Decision {
        room: p.room,
        signer: [0; 32],
        policy: p.digest(),
        t: 0,
        session: [0xab; 16],
        draw: 0,
        protocol: protocol::DASH,
        report: Report::default(),
        pathways: vec![PathwayState::default(), PathwayState::default()],
        ttl: 30,
        reason: why::NONE,
        priority: vec![1, 0],
        sig: [0; 64],
    };
    let m = manifest(&p, &d);
    let want = format!(
        "{{\"VERSION\":1,\"TTL\":30,\"RELOAD-URI\":\"https://s.example/steer?wai_session={}\",\"PATHWAY-PRIORITY\":[\"b\",\"a\"],\"PATHWAY-CLONES\":[{{\"BASE-ID\":\"a\",\"ID\":\"b\",\"URI-REPLACEMENT\":{{\"HOST\":\"h.example\",\"PARAMS\":{{\"k\":\"v\",\"t\":\"%2F\"}}}}}}],\"science.transaction.wai-decision\":\"https://s.example/d/{}\"}}",
        "ab".repeat(16),
        hex(&d.digest())
    );
    assert_eq!(m, want);
    assert!(serde_json::from_str::<serde_json::Value>(&m).is_ok());
    assert_eq!(hls_tag(&p), "#EXT-X-CONTENT-STEERING:SERVER-URI=\"https://s.example/steer?a=1&b=2\",PATHWAY-ID=\"a\"");
    assert_eq!(dash_element(&p), "<ContentSteering defaultServiceLocation=\"a\" queryBeforeStart=\"false\">https://s.example/steer?a=1&amp;b=2</ContentSteering>");
}
