Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/monetize

crates/monetize-ui-web/src/fmt.rs

//! Pure formatting and validation — no `web-sys`, tested on the host.
//!
//! Numbers stay integers in state; only the rendered string carries a decimal.

use std::collections::BTreeMap;

// The arithmetic of a daily counter and of "how long since we saw them" lives
// in the product crate, beside the `Usage` it reads, and is tested there. This
// file decides the WORDS; it does not own a second copy of the fold.
use monetize_product::flow;

use crate::model::{
    ActivityDayJson, CloudJson, CloudOwnedJson, FleetJson, FleetMeterJson, HoldingJson, MeterInfo,
    PriceListJson, QuoteJson, RefusalJson, RowJson, SaleJson, SignupJson, SpendJson, TenantRow,
    TrendJson,
};

const KIB: u64 = 1 << 10;
const MIB: u64 = 1 << 20;
const GIB: u64 = 1 << 30;
const TIB: u64 = 1 << 40;
const DAY_MS: u64 = 86_400_000;

/// A refusal, as the console shows it: the status, monetize's code, the
/// server's words — never reworded.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Fail {
    /// The HTTP status, or 0 when the request never reached the server.
    pub status: u16,
    pub code: String,
    pub message: String,
}

impl Fail {
    pub fn summary(&self) -> &'static str {
        match (self.status, self.code.as_str()) {
            (_, NO_PASSKEY) => "No passkey answered.",
            (_, STALE_PASSKEY) => "This passkey is not registered on this box.",
            (_, ALREADY_ENROLLED) => "This key is already enrolled.",
            (_, BROWSER_PASSKEY) => "The browser did not create a passkey.",
            (0, _) => "The console could not reach its server.",
            (401, _) => "Your session is no longer valid.",
            (403, "stepup") => {
                "This moves money or keys: touch your passkey again to confirm it is you."
            }
            (403, _) => "Your role does not allow this.",
            // **A sale that cannot be DELIVERED is not a malformed request.**
            // `failed_precondition` is what an order gets when the product
            // itself says no — before the reserve, so nothing has been spent. The detail below carries the product's own sentence, and on
            // a sealed gunnar that sentence says a bought disk would be billed
            // and invisible. "Invalid request" over that paragraph sends an
            // operator to look for a typo instead of reading it.
            (400, "failedprecondition") => {
                "This cannot be delivered, so monetize refused it before any money moved."
            }
            (400, _) => "monetize refused the request as invalid.",
            (404, _) => "It does not exist.",
            (409, _) => "That already exists.",
            (502, _) => "monetize answered with an error.",
            (503, _) => "monetize is not responding right now.",
            (504, _) => "monetize did not answer in time.",
            _ => "The request failed.",
        }
    }

    /// The server's step-up refusal (`auth::STEP_UP_CODE`): answered with a
    /// fresh passkey touch and the same request again.
    pub fn wants_step_up(&self) -> bool {
        self.status == 403 && self.code == "stepup"
    }

    pub fn worth_retrying(&self) -> bool {
        !matches!(
            self.code.as_str(),
            NO_PASSKEY | ALREADY_ENROLLED | BROWSER_PASSKEY
        ) && matches!(self.status, 0 | 502 | 503 | 504)
    }

    /// The footer under the words: the HTTP status and monetize's code.
    pub fn footer(&self) -> Option<String> {
        if matches!(
            self.code.as_str(),
            NO_PASSKEY | STALE_PASSKEY | ALREADY_ENROLLED | BROWSER_PASSKEY
        ) {
            return None;
        }
        Some(if self.code.is_empty() {
            format!("HTTP {}", self.status)
        } else {
            format!("HTTP {} · monetize: {}", self.status, self.code)
        })
    }

    /// **The browser's passkey call failed** (NotAllowedError, no passkey answered): the
    /// server was never asked. `host` is the console's own (`location.hostname`).
    pub fn no_passkey(host: &str, browser: &str) -> Fail {
        let _ = browser;
        Fail { status: 0, code: NO_PASSKEY.into(), message: format!("No passkey for {host} answered on this device. Try another device, or ask the operator for a break-glass reset.") }
    }

    /// **A login the server refused**, in the words a person acts on: a passkey this box does
    /// not know (`credential is not registered here`) names the host and the way out.
    pub fn login_refused(self, host: &str) -> Fail {
        if self.status == 401 && self.message.contains("not registered here") {
            return Fail { status: 401, code: STALE_PASSKEY.into(), message: format!("This passkey is not registered on this box — pick another passkey for {host} or delete the stale one.") };
        }
        self
    }
}

/// The code a browser-side passkey failure carries (never sent by a server).
pub const NO_PASSKEY: &str = "no-passkey";
/// The code a passkey the box does not know carries, once the console said it.
pub const STALE_PASSKEY: &str = "stale-passkey";

/// The four states anything in this console can be in, plus "not a status".
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Tone {
    Pass,
    Warn,
    Bad,
    Live,
    Plain,
}

impl Tone {
    pub fn class(self) -> &'static str {
        match self {
            Tone::Pass => "badge pass",
            Tone::Warn => "badge warn",
            Tone::Bad => "badge bad",
            Tone::Live => "badge live",
            Tone::Plain => "badge plain",
        }
    }

    /// The bare colour word, for a dot that is not a badge.
    pub fn word(self) -> &'static str {
        match self {
            Tone::Pass => "pass",
            Tone::Warn => "warn",
            Tone::Bad => "bad",
            Tone::Live => "live",
            Tone::Plain => "plain",
        }
    }
}

/// The ladder's colour. Free is not a status: nothing was paid for.
pub fn state_tone(state: &str) -> Tone {
    match state {
        "paid" => Tone::Pass,
        "grace" => Tone::Warn,
        "suspended" | "retention" => Tone::Bad,
        _ => Tone::Plain,
    }
}

pub fn state_label(state: &str) -> &'static str {
    match state {
        "free" => "Free",
        "paid" => "Paid",
        "grace" => "Grace",
        "suspended" => "Suspended",
        "retention" => "Retention",
        _ => "Unknown",
    }
}

/// **Bytes in the largest unit that still leaves a digit in front of the
/// decimal point**: `10.0 GiB`, `64.0 MiB`, `999 B`.
///
/// Re-exported, not written here. This file had its own copy that divided by
/// 1 GiB unconditionally, which is a lie about small numbers rather than a
/// rounding nicety: a real 64 MiB `pack_bytes` cap printed as `0.0 GiB`, and
/// `0.0` beside a quota is read as *there is no quota*. Core and the gunnar
/// plugin each had the same six lines with the same bug, so the fix is one
/// writer rather than three that agree.
pub use monetize_product::bytes;

/// A meter's value in its own unit: `*_bytes` meters as GiB, the rest as counts.
pub fn meter_value(name: &str, value: u64) -> String {
    if name.ends_with("_bytes") {
        bytes(value)
    } else {
        value.to_string()
    }
}

/// **The word beside a USING row** (`docs/v2/07-console.md` rule 3, the tenant screen): what
/// a cap on this meter IS to the tenant, in plain words, decided from two
/// facts and nothing invented — whether the product polices it, and whether
/// the deployment's price list charges for it.
///
/// * `wall` — [`Enforcement`] says `enforced`: the product refuses over it,
///   whatever the list says. A wall is a wall whether or not it is billed.
/// * `billed` — priced above zero on the list, and not a wall: counted, and
///   the customer pays for it.
/// * `not billed` — priced at zero, or not on the list at all, or a meter
///   with no unit. The open-source deployment's answer for everything.
/// * `still costs` — `tombstoned_bytes` when it is priced: bytes a tenant
///   deleted and has not purged, which the list charges for all the same.
///
/// There is no billing attribute on a meter; **the price list IS it**. A
/// console that said `billed` from a word of its own would be saying money
/// moves where the ledger would show none.
pub fn billing_word(meter: &str, enforcement: &str, unit_price: Option<u64>) -> &'static str {
    if enforcement == "enforced" {
        return "wall";
    }
    match unit_price {
        Some(p) if p > 0 => {
            if meter == "tombstoned_bytes" {
                "still costs"
            } else {
                "billed"
            }
        }
        _ => "not billed",
    }
}

/// The list's unit price for `meter`: what the tenant's market charges per
/// unit-month under the meter's [`MeterInfo::unit`]. `None` when the meter has
/// no unit at all (a count) or there is no list (free); `Some(0)` when the list
/// exists and does not charge for it. Both read `not billed`.
pub fn unit_price(meter: &MeterInfo, list: Option<&PriceListJson>) -> Option<u64> {
    if meter.unit.is_empty() {
        return None;
    }
    Some(list?.units.get(&meter.unit).copied().unwrap_or(0))
}

/// **The list the order form and the USING words read**: the product's list
/// for `market`, or its first list when none is named, or nothing — free.
pub fn list_for<'a>(
    prices: &'a [PriceListJson],
    product: &str,
    market: Option<&str>,
) -> Option<&'a PriceListJson> {
    let mine = prices.iter().filter(|l| l.product == product);
    match market {
        Some(m) => mine.clone().find(|l| l.market.eq_ignore_ascii_case(m)),
        None => None,
    }
    .or_else(|| prices.iter().find(|l| l.product == product))
}

/// **A customer price, in words**: `free` at zero — FREE is a kind of
/// deployment, not a price of nothing — else `1980.00 SEK`.
pub fn price_words(minor: u64, currency: &str) -> String {
    if minor == 0 {
        return "free".to_owned();
    }
    money(minor, currency)
}

/// **The provider's cost, in words**: what we pay for the delta's iron.
/// Zero with iron behind it is the box's own disk — served from what was
/// already paid for — and zero with none is an order that buys no iron.
pub fn cost_words(cost_minor: u64, currency: &str, buys_iron: bool) -> String {
    match (cost_minor, buys_iron) {
        (0, true) => "this box · nothing to buy".to_owned(),
        (0, false) => "no iron bought".to_owned(),
        (minor, _) => money(minor, currency),
    }
}

/// **Can it be served?** The badge and the sentence: `yes`, or `no` with
/// monetize's reason whole — the sentence an operator reads back to the
/// customer, never shortened here.
pub fn can_serve_words(q: &QuoteJson) -> (Tone, String) {
    if q.can_serve {
        (Tone::Pass, "can serve: yes".to_owned())
    } else {
        (Tone::Bad, format!("can serve: no — {}", q.reason))
    }
}

/// **The order form's number for a meter**: whole GiB for a bytes meter
/// (rounded UP, so what the form shows is never less than what the tenant
/// holds), the count itself otherwise.
pub fn form_value(meter: &str, bytes: u64) -> u64 {
    if meter.ends_with("_bytes") {
        bytes.div_ceil(GIB)
    } else {
        bytes
    }
}

/// The inverse of [`form_value`]: what a typed number means in the meter's
/// own unit.
pub fn form_bytes(meter: &str, typed: u64) -> u64 {
    if meter.ends_with("_bytes") {
        typed.saturating_mul(GIB)
    } else {
        typed
    }
}

/// **The caps an order sends**: only the meters whose typed value differs
/// from what the tenant holds — a meter left alone is left out, and the server
/// carries it forward. Pure, so the arithmetic that decides what is asked for
/// is testable on its own.
pub fn order_caps(
    current: &BTreeMap<String, u64>,
    typed: &BTreeMap<String, u64>,
) -> BTreeMap<String, u64> {
    typed
        .iter()
        .map(|(m, v)| (m.clone(), form_bytes(m, *v)))
        .filter(|(m, v)| current.get(m).copied().unwrap_or(0) != *v)
        .collect()
}

/// `meter=value,meter=value` — the `caps=` query `GET /api/quote` takes.
pub fn caps_query(caps: &BTreeMap<String, u64>) -> String {
    caps.iter()
        .map(|(k, v)| format!("{k}={v}"))
        .collect::<Vec<_>>()
        .join(",")
}

/// Minor units to `49.00 SEK`. Never a float.
/// **The line behind a sale's price**: what the customer was charged, what the iron
/// cost, and — when the vendor said — what it kept and what actually arrived.
///
/// The net is folded here rather than carried: `price − fee` is the same kind of
/// number as the margin, and `docs/v2/06-data-model.md` §6.1 rule 3 keeps those out of storage.
///
/// A sale nobody reported a fee for reads exactly as it did before. Rendering
/// "vendor kept 0" for a vendor that never told us would turn an absence into a
/// claim — and the absence is the common case: a free sale never reached a vendor,
/// `invoice` has none, `klarna` settles in a portal.
pub fn sale_money_line(sale: &SaleJson) -> String {
    let mut line = format!(
        "price {} · cost {}",
        price_words(sale.price_minor, &sale.currency),
        price_words(sale.cost_minor, &sale.cost_currency)
    );
    if let Some(fee) = sale.fee_minor {
        line.push_str(&format!(
            " · vendor kept {} · net {}",
            money(fee, &sale.fee_currency),
            money(sale.price_minor.saturating_sub(fee), &sale.currency)
        ));
    }
    line
}

/// **The VAT per order** (owner 2026-09-28), for the row: `VAT 55.00 SEK
/// (25 %)`, or `(as the vendor charged)` when the figure is the vendor's own —
/// led by the buyer's country and the vendor's rate for it when the row names
/// one (`VAT 1.90 EUR · DE 19 % (as the vendor charged)`).
/// `None` when the row carries no tax — nothing is said rather than a zero.
pub fn sale_tax_line(sale: &SaleJson) -> Option<String> {
    let minor = sale.tax_minor?;
    let currency = if sale.tax_currency.is_empty() {
        sale.currency.as_str()
    } else {
        sale.tax_currency.as_str()
    };
    let bp = sale.vat_basis_points;
    let rate = if bp % 100 == 0 {
        format!("{} %", bp / 100)
    } else {
        format!(
            "{} %",
            format!("{:.2}", bp as f64 / 100.0)
                .trim_end_matches('0')
                .trim_end_matches('.')
        )
    };
    let country = sale.customer_country.trim();
    // The buyer's country (owner 2026-10-11: Stripe decides per country, each
    // payment is a row) leads the rate when the vendor said it.
    Some(match (country.is_empty(), sale.tax_from_vendor) {
        (true, true) => format!("VAT {} (as the vendor charged)", money(minor, currency)),
        (true, false) => format!("VAT {} ({rate})", money(minor, currency)),
        (false, true) => format!(
            "VAT {} · {country} {rate} (as the vendor charged)",
            money(minor, currency)
        ),
        (false, false) => format!("VAT {} · {country} ({rate})", money(minor, currency)),
    })
}

/// `path?estate=<label>` (percent-encoded) for a site; `path` alone for none.
pub fn site_path(path: &str, estate: Option<&str>) -> String {
    match estate.filter(|e| !e.is_empty()) {
        Some(e) => format!("{path}?estate={}", percent_encode(e)),
        None => path.to_owned(),
    }
}

/// **What a page says when the box answered from its kept state** (PROD-4, owner 2026-10-26:
/// the Overview and the Guardian page spun for ever). `None`: the state is fresh.
pub fn stale_note(stale_secs: Option<u64>) -> Option<String> {
    let s = stale_secs?;
    let age = if s < 90 {
        format!("{s} s")
    } else if s < 5400 {
        format!("{} min", s / 60)
    } else {
        format!("{} h", s / 3600)
    };
    Some(format!("The box did not answer within 2 s: this is its state from {age} ago. It is asked again in the background; reload to see it."))
}

#[cfg(test)]
mod site_path_tests {
    #[test]
    fn a_sites_money_panels_ask_for_that_site_only() {
        assert_eq!(
            super::site_path("/api/signups", Some("test2-gunnar-rs")),
            "/api/signups?estate=test2-gunnar-rs"
        );
        assert_eq!(super::site_path("/api/owed-back", None), "/api/owed-back");
    }
}

pub fn money(minor: u64, currency: &str) -> String {
    format!("{} {currency}", money_amount(minor))
}

/// Minor units to `49.00`, for a figure whose currency the wire may not carry
/// (`CloudJson::monthly_minor` from a server older than its `currency` field).
/// Still never a float.
pub fn money_amount(minor: u64) -> String {
    format!("{}.{:02}", minor / 100, minor % 100)
}

/// Percent of `cap` used, clamped to 100. `None` without a cap.
pub fn percent(value: u64, cap: Option<u64>) -> Option<u8> {
    let cap = cap.filter(|c| *c > 0)?;
    Some(((value.min(cap) as u128 * 100) / cap as u128) as u8)
}

/// [`percent`] rounded UP: for money against a ceiling, where drawing spend
/// short is the direction that costs (`SpendCap::reconcile_total` corrects
/// upward only, for the same reason). `226 of 1000` is 23, not 22.
pub fn percent_up(value: u64, cap: Option<u64>) -> Option<u8> {
    let cap = cap.filter(|c| *c > 0)?;
    Some((value.min(cap) as u128 * 100).div_ceil(cap as u128) as u8)
}

fn civil_from_days(z: i64) -> (i64, u32, u32) {
    let z = z + 719_468;
    let era = z.div_euclid(146_097);
    let doe = z.rem_euclid(146_097);
    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
    let mp = (5 * doy + 2) / 153;
    let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
    let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
    (yoe + era * 400 + i64::from(m <= 2), m, d)
}

fn days_from_civil(y: i64, m: u32, d: u32) -> i64 {
    let y = if m <= 2 { y - 1 } else { y };
    let era = y.div_euclid(400);
    let yoe = y.rem_euclid(400);
    let mp = if m > 2 { m - 3 } else { m + 9 } as i64;
    let doy = (153 * mp + 2) / 5 + i64::from(d) - 1;
    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
    era * 146_097 + doe - 719_468
}

/// `YYYY-MM-DD` (UTC) of a millisecond timestamp.
pub fn date(unix_ms: u64) -> String {
    let (y, m, d) = civil_from_days((unix_ms / DAY_MS) as i64);
    format!("{y:04}-{m:02}-{d:02}")
}

/// `YYYY-MM-DD` → the millisecond timestamp of that midnight (UTC).
pub fn parse_date_ms(s: &str) -> Option<u64> {
    let mut it = s.trim().split('-');
    let y: i64 = it.next()?.parse().ok()?;
    let m: u32 = it.next()?.parse().ok()?;
    let d: u32 = it.next()?.parse().ok()?;
    if it.next().is_some() || !(1..=12).contains(&m) || !(1..=31).contains(&d) {
        return None;
    }
    u64::try_from(days_from_civil(y, m, d))
        .ok()
        .map(|days| days * DAY_MS)
}

/// How long ago, in the coarsest unit that is not zero.
pub fn since(then_ms: u64, now_ms: u64) -> String {
    since_ms(now_ms.saturating_sub(then_ms))
}

/// [`since`] over an AGE that has already been worked out — the words alone,
/// with no second subtraction.
///
/// Split out for [`last_seen_line`], whose age comes from
/// `monetize_product::flow::quiet_for`: that function owns the arm this file
/// must not re-derive (`0` is NEVER, a clock that runs ahead is "just now"),
/// and a `since(seen, now)` beside it would be a second, disagreeing
/// subtraction of the same two numbers.
pub fn since_ms(age_ms: u64) -> String {
    let s = age_ms / 1000;
    match s {
        0..=59 => "just now".into(),
        60..=3599 => format!("{} min ago", s / 60),
        3600..=86_399 => format!("{} h ago", s / 3600),
        _ => format!("{} d ago", s / 86_400),
    }
}

/// An order may only cap a meter its product declares. Names the meter.
pub fn validate_caps(
    product: &str,
    declared: &[String],
    caps: &BTreeMap<String, u64>,
) -> Result<(), String> {
    match caps.keys().find(|k| !declared.contains(*k)) {
        Some(m) => Err(format!(
            "cap on {m:?}: product {product} does not declare that meter"
        )),
        None => Ok(()),
    }
}

/// The caps editor's text: one `meter = value` per line, where value is an
/// integer with an optional `GiB` / `MiB` suffix (`pack_bytes = 10 GiB`).
pub fn parse_caps(text: &str) -> Result<BTreeMap<String, u64>, String> {
    let mut caps = BTreeMap::new();
    for line in text.lines().map(str::trim).filter(|l| !l.is_empty()) {
        let (name, value) = line
            .split_once('=')
            .ok_or_else(|| format!("{line:?}: expected `meter = value`"))?;
        let value = value.trim();
        let (digits, unit) = match value.find(|c: char| !c.is_ascii_digit()) {
            Some(i) => (&value[..i], value[i..].trim()),
            None => (value, ""),
        };
        let n: u64 = digits
            .parse()
            .map_err(|_| format!("{line:?}: {value:?} is not a number"))?;
        let scaled = match unit {
            "" => n,
            // The same ladder [`bytes`] prints in, so a number the console
            // showed can be typed straight back into this box.
            "TiB" => n
                .checked_mul(TIB)
                .ok_or_else(|| format!("{line:?}: too large"))?,
            "GiB" => n
                .checked_mul(GIB)
                .ok_or_else(|| format!("{line:?}: too large"))?,
            "MiB" => n
                .checked_mul(MIB)
                .ok_or_else(|| format!("{line:?}: too large"))?,
            "KiB" => n
                .checked_mul(KIB)
                .ok_or_else(|| format!("{line:?}: too large"))?,
            other => {
                return Err(format!(
                    "{line:?}: unit {other:?} is not KiB, MiB, GiB or TiB"
                ))
            }
        };
        caps.insert(name.trim().to_owned(), scaled);
    }
    Ok(caps)
}

/// The inverse of [`parse_caps`], for the editor: the largest unit that divides
/// the value exactly, and raw bytes when none does.
///
/// It only offered GiB, so `rehearsal-free`'s 64 MiB cap came back as
/// `pack_bytes = 67108864` — a number an operator has to do arithmetic on before
/// they can tell whether it is the one they meant.
pub fn caps_text(caps: &BTreeMap<String, u64>) -> String {
    caps.iter()
        .map(|(k, v)| {
            match [("TiB", TIB), ("GiB", GIB), ("MiB", MIB), ("KiB", KIB)]
                .iter()
                .find(|(_, per)| *v > 0 && v % per == 0)
            {
                Some((unit, per)) => format!("{k} = {} {unit}", v / per),
                None => format!("{k} = {v}"),
            }
        })
        .collect::<Vec<_>>()
        .join("\n")
}

/// Percent-encode a query value.
/// **One estate's part of a product** (MAP-SEG 1): `/api/products/<p>/tenants?estate=<e>`;
/// no estate: the rows of no ONE estate.
pub fn tenants_path(product: &str, estate: &str) -> String {
    let base = format!("/api/products/{}/tenants", percent_encode(product));
    if estate.is_empty() {
        base
    } else {
        format!("{base}?estate={}", percent_encode(estate))
    }
}

pub fn percent_encode(s: &str) -> String {
    s.bytes()
        .map(|b| match b {
            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
                (b as char).to_string()
            }
            other => format!("%{other:02X}"),
        })
        .collect()
}

/// **What the serving badge says when the health read FAILED**, decided by the
/// HTTP status alone.
///
/// It exists because the badge used to render ONE label for every `Err` —
/// "console server unreachable" — and `/api/health` sits behind the auth gate. So
/// an operator who had simply not signed in yet was told, in red, that the server
/// was down. Seen on the live console 2026-09-04, minutes after the name went
/// public, while `/api/health` was answering `serving: true, products:
/// ["gunnar"]` to any request that carried a credential.
///
/// `status == 0` is the console never reaching the server at all (`api::Fail`
/// documents that), and it is the ONLY shape entitled to the word "unreachable".
/// Every other status already has an honest sentence from `Fail::summary`; this
/// only has to stop overwriting it — and it carries the number, because one word
/// for nine causes is not something an operator can act on.
///
/// Red is a claim about the SYSTEM. A login state is not a fault, so it is not red.
pub fn health_failure_badge(status: u16, summary: &str) -> (Tone, String) {
    match status {
        0 => (Tone::Bad, "console server unreachable".to_owned()),
        401 => (Tone::Plain, "sign in to see monetize".to_owned()),
        403 => (Tone::Warn, "your session may not read this".to_owned()),
        _ => (Tone::Bad, format!("console server: {summary} ({status})")),
    }
}

/// ★ **A 401 on a page is not a page's error — it is the login's cue.**
///
/// Seen on monetize.vetra.se 2026-09-08: with no session the navigation is
/// shown (so a visitor can see what the console is), and clicking any entry
/// answered a red "Your session is no longer valid" box in the middle of the
/// page. Red is a claim about the system, and nothing is wrong with the
/// system — the visitor is simply not logged in. The ruling: send them
/// to the login and flash a red bulb THERE. So a 401 anywhere but `/login`
/// itself becomes this destination, carrying `why=session` so the login page
/// knows it was reached by a refusal and not by the menu. `None` means "stay
/// and render the failure as before": a 401 on `/login` would loop, and every
/// other status is a real answer the page should show.
pub fn login_redirect_for(status: u16, current_path: &str) -> Option<&'static str> {
    (status == 401 && current_path != "/login").then_some("/login?why=session")
}

/// The one line the login page adds when it was reached through
/// [`login_redirect_for`], keyed on the `why` it carried. `None` for a visit
/// from the menu, where a warning would be a warning about nothing.
pub fn login_prompt(why: Option<&str>) -> Option<&'static str> {
    match why {
        Some("session") => Some("That page needs a login."),
        _ => None,
    }
}

/// **What to say beside a cap**, given whether the product enforces it.
///
/// `None` only when there is no cap: the number is a reading and nothing
/// bounds it. Every cap gets a word, because `pack_bytes = 10 GiB` and
/// `cpu_millicores = 2000` look like the same kind of promise as numbers and
/// only one of them is a wall — and `docs/v2/07-console.md` rule 3 wants the wall SAID, in
/// that word, rather than left as the unmarked default.
pub fn cap_note(has_cap: bool, enforcement: &str) -> Option<&'static str> {
    if !has_cap {
        return None;
    }
    match enforcement {
        "enforced" => Some("wall"),
        "provisioned" => Some("provisioned"),
        "measured" => Some("not enforced"),
        _ => Some("unknown"),
    }
}

/// **What a usage bar is actually SAYING**, from the reading, the cap and
/// whether the product polices that cap.
///
/// It exists because colour on this page is a claim about whether a tenant is in
/// trouble, and the bar used to make that claim from the percentage alone. Two
/// consequences, both wrong at 3am:
///
/// * a `measured` meter at 97% went RED. Nothing refuses on it — the tenant is
///   not in trouble, they are using a thing that is counted. An operator who
///   learns that red sometimes means nothing stops reading red.
/// * a meter with NO cap was drawn against the largest number on the page, so
///   the tenant's biggest uncapped meter was always a FULL bar. A full bar is
///   the strongest thing this page can say and it was being said about a
///   quantity nobody had set a limit on.
///
/// So the escalation belongs to `enforced` meters and only to them: those are
/// the caps that are walls, where 100% means the next push is refused.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BarState {
    /// No cap (or a cap of zero, which is how a product spells "unlimited").
    /// Not 0%, not 100% — there is no percentage, and the bar must not draw one.
    Uncapped,
    /// A wall, with room.
    Ok,
    /// A wall, and the tenant is close to it.
    Near,
    /// A wall, and the tenant is AT it: the product refuses now.
    AtLimit,
    /// Counted and billed, never refused. Fills honestly and does not escalate.
    Metered,
    /// Bought at the provider. The number is what was BOUGHT, so the bar is a
    /// picture of a purchase, not of a budget being consumed.
    Provisioned,
    /// This monetize did not say whether the cap is enforced. Said, not guessed.
    Unknown,
}

/// Where the escalation starts, for a cap that is a wall.
pub const NEAR_PERCENT: u8 = 80;

/// The bar's state. Pure, and the ONE place the decision is made — `ui::Bars`
/// renders whatever this says.
pub fn bar_state(value: u64, cap: Option<u64>, enforcement: &str) -> BarState {
    bar_state_at(percent(value, cap), enforcement)
}

/// [`bar_state`] from an already-computed percentage, so a bar drawn from a
/// differently-rounded figure ([`percent_up`]) escalates at the width it draws,
/// never a point behind it.
pub fn bar_state_at(pct: Option<u8>, enforcement: &str) -> BarState {
    let Some(pct) = pct else {
        return BarState::Uncapped;
    };
    match enforcement {
        "enforced" if pct >= 100 => BarState::AtLimit,
        "enforced" if pct >= NEAR_PERCENT => BarState::Near,
        "enforced" => BarState::Ok,
        "measured" => BarState::Metered,
        "provisioned" => BarState::Provisioned,
        _ => BarState::Unknown,
    }
}

impl BarState {
    /// The fill's class. `uncapped` gets its own so the track can be drawn as a
    /// track with no reading in it rather than as an empty or a full bar.
    pub fn class(self) -> &'static str {
        match self {
            BarState::Uncapped => "fill uncapped",
            BarState::Ok => "fill ok",
            BarState::Near => "fill near",
            BarState::AtLimit => "fill at-limit",
            BarState::Metered => "fill metered",
            BarState::Provisioned => "fill provisioned",
            BarState::Unknown => "fill unknown",
        }
    }

    /// How wide to draw the fill, given the percentage of the cap. An uncapped
    /// meter has no percentage at all — drawing one against the biggest number
    /// on the page is what made every tenant's largest meter look full.
    pub fn width_percent(self, pct: Option<u8>) -> u8 {
        match self {
            BarState::Uncapped => 0,
            _ => pct.unwrap_or(0),
        }
    }

    /// What the state means, for the bar's `title`. An operator hovering a
    /// colour must be able to find out what it claims.
    pub fn meaning(self) -> &'static str {
        match self {
            BarState::Uncapped => "no cap: this is a reading, and nothing bounds it",
            BarState::Ok => "the product refuses over this cap; there is room",
            BarState::Near => "the product refuses over this cap, and the tenant is close to it",
            BarState::AtLimit => "at the cap: the product is refusing now",
            BarState::Metered => "counted and billed; the product never refuses on it",
            BarState::Provisioned => {
                "bought at the provider; this is what the tenant has, not a budget being spent"
            }
            BarState::Unknown => "this monetize did not say whether the cap is enforced",
        }
    }
}

#[cfg(test)]
mod tests {

    #[test]
    fn a_kept_state_is_said_with_its_age_and_a_fresh_one_says_nothing() {
        assert_eq!(stale_note(None), None);
        assert!(stale_note(Some(12)).unwrap().contains("from 12 s ago"));
        assert!(stale_note(Some(600)).unwrap().contains("from 10 min ago"));
        assert!(stale_note(Some(7200)).unwrap().contains("from 2 h ago"));
    }
    use super::*;

    /// ★ **A passkey the browser did not give is not a server that cannot be reached**
    /// (owner 2026-10-17): the console says which host's passkey did not answer and what to do,
    /// never "could not reach its server … HTTP 0"; a passkey this box does not know is said
    /// as such, with the host.
    #[test]
    fn a_passkey_that_did_not_answer_or_is_stale_is_said_in_words_never_http_0() {
        let f = Fail::no_passkey(
            "monetize.ignalina.dk",
            "The operation either timed out or was not allowed.",
        );
        assert_eq!(f.message, "No passkey for monetize.ignalina.dk answered on this device. Try another device, or ask the operator for a break-glass reset.");
        assert_eq!(f.code, NO_PASSKEY);
        assert!(
            !f.summary().contains("could not reach its server"),
            "{}",
            f.summary()
        );
        assert_eq!(f.footer(), None, "no HTTP 0: the server was never asked");
        assert!(
            !f.worth_retrying(),
            "trying the same device again is not the way out"
        );
        let refused = Fail {
            status: 401,
            code: "unauthenticated".into(),
            message: "this credential is not registered here".into(),
        }
        .login_refused("monetize.ignalina.dk");
        assert_eq!(refused.message, "This passkey is not registered on this box — pick another passkey for monetize.ignalina.dk or delete the stale one.");
        assert_eq!(refused.code, STALE_PASSKEY);
        assert_eq!(refused.footer(), None);
        // Any other refusal is said as the server said it.
        let other = Fail {
            status: 401,
            code: "unauthenticated".into(),
            message: "the ceremony expired".into(),
        };
        assert_eq!(other.clone().login_refused("monetize.ignalina.dk"), other);
        assert_eq!(
            other.footer().as_deref(),
            Some("HTTP 401 · monetize: unauthenticated")
        );
    }

    /// ★ **A step-up is not a role refusal.** The server's `403 stepup` is
    /// the one refusal the console answers by asking for a passkey touch and
    /// sending the request again; an ordinary 403 is shown as it came.
    #[test]
    fn a_step_up_refusal_asks_for_a_touch_and_a_role_refusal_does_not() {
        let step = Fail {
            status: 403,
            code: "stepup".into(),
            message: "touch your passkey".into(),
        };
        assert!(step.wants_step_up());
        assert!(step.summary().contains("passkey"), "{}", step.summary());
        assert!(
            !step.worth_retrying(),
            "a step-up is answered with a touch, not a blind retry"
        );
        let role = Fail {
            status: 403,
            code: String::new(),
            message: "this needs the admin role".into(),
        };
        assert!(!role.wants_step_up());
        assert_eq!(role.summary(), "Your role does not allow this.");
        assert!(
            !Fail {
                status: 400,
                code: "stepup".into(),
                message: String::new()
            }
            .wants_step_up(),
            "only a 403 is a step-up"
        );
    }

    #[test]
    fn a_401_off_the_login_page_is_sent_to_the_login_with_a_reason() {
        assert_eq!(
            login_redirect_for(401, "/tenants"),
            Some("/login?why=session")
        );
        assert_eq!(login_redirect_for(401, "/"), Some("/login?why=session"));
        // On the login page itself a redirect would loop.
        assert_eq!(login_redirect_for(401, "/login"), None);
        // Every other status is an answer the page shows.
        for status in [0u16, 400, 403, 404, 409, 503] {
            assert_eq!(login_redirect_for(status, "/tenants"), None, "{status}");
        }
    }

    #[test]
    fn the_login_page_flashes_only_when_it_was_reached_by_a_refusal() {
        assert_eq!(
            login_prompt(Some("session")),
            Some("That page needs a login.")
        );
        assert_eq!(login_prompt(None), None);
        assert_eq!(login_prompt(Some("anything-else")), None);
    }

    /// RED before the fix: every failed health read rendered `Tone::Bad`
    /// "console server unreachable", so a 401 on the login page accused a healthy
    /// server of being down.
    #[test]
    fn only_a_request_that_never_arrived_may_say_unreachable() {
        // The one true unreachable: the request never left.
        assert_eq!(
            health_failure_badge(0, "The console could not reach its server."),
            (Tone::Bad, "console server unreachable".to_owned())
        );

        // Not signed in. Not a fault in anything, so not red, and it must not
        // claim anything about the server.
        let (tone, label) = health_failure_badge(401, "Your session is no longer valid.");
        assert_eq!(
            tone,
            Tone::Plain,
            "a login state is not a system fault: {label}"
        );
        assert!(!label.contains("unreachable"), "{label}");

        // Signed in, wrong role: a warning about the SESSION.
        let (tone, label) = health_failure_badge(403, "Your role does not allow this.");
        assert_eq!(tone, Tone::Warn);
        assert!(!label.contains("unreachable"), "{label}");

        // A real server-side failure keeps its own sentence AND its number.
        let (tone, label) = health_failure_badge(503, "monetize is not responding right now.");
        assert_eq!(tone, Tone::Bad);
        assert!(
            label.contains("503")
                && label.contains("not responding")
                && !label.contains("unreachable"),
            "{label}"
        );

        // And an unmapped status still says something specific rather than a guess.
        let (_, label) = health_failure_badge(418, "The request failed.");
        assert!(label.contains("418"), "{label}");
    }

    /// **A meter reading that is not zero must never render as zero.**
    ///
    /// RED before the fix: this crate's own `gib` divided by 1 GiB and printed
    /// one decimal, full stop. `rehearsal-free` caps `pack_bytes` at 67108864 —
    /// a real 64 MiB quota, small enough that one push crosses it — and the
    /// Plans page rendered it `0.0 GiB`, which an operator reads as *no quota at
    /// all*. The same formatter draws the tenant page's meter bars, so a tenant
    /// sitting on 300 MiB of a 10 GiB cap read as using nothing.
    ///
    /// The formatter itself is `monetize_product::bytes` and is tested there,
    /// over its whole range. What is asserted here is that the meter renderer
    /// this console actually calls goes through it.
    #[test]
    fn a_meter_reading_that_exists_never_renders_as_zero() {
        for n in [1u64, 512, 1 << 20, 67_108_864, GIB - 1] {
            let s = meter_value("pack_bytes", n);
            assert!(
                !s.starts_with("0.0 ") && !s.starts_with("0 "),
                "{n} bytes rendered as {s:?}, which reads as nothing"
            );
        }
        // The rehearsal cap, in the unit an operator can act on.
        assert_eq!(meter_value("pack_bytes", 67_108_864), "64.0 MiB");
        assert_eq!(
            meter_value("pack_bytes", 0),
            "0 B",
            "only a genuine zero says zero"
        );
        assert_eq!(meter_value("pack_bytes", 2 << 30), "2.0 GiB");
        assert_eq!(
            meter_value("pushes", 42),
            "42",
            "a count is not bytes and gets no unit"
        );
    }

    #[test]
    fn money_and_percent_stay_integer_arithmetic() {
        assert_eq!(money(4900, "SEK"), "49.00 SEK");
        assert_eq!(money(5, "EUR"), "0.05 EUR");
        assert_eq!(percent(3 << 30, Some(10 << 30)), Some(30));
        assert_eq!(percent(20 << 30, Some(10 << 30)), Some(100));
        assert_eq!(percent(1, None), None);
    }

    /// **The USING word is read off the price list, not invented.** Four arms,
    /// on a fixture where the cache is unpriced and the tombstoned bytes are
    /// priced — the pair that tells `not billed` from `still costs`.
    ///
    /// RED first: the console had no billing word at all, so the browser
    /// matrix's `screen3-plain-words-on-the-surface` row could not find
    /// `not billed` or `still costs` on the tenant screen.
    #[test]
    fn the_using_word_is_wall_billed_not_billed_or_still_costs_from_the_price() {
        let list = PriceListJson {
            product: "gunnar".into(),
            market: "SE".into(),
            currency: "SEK".into(),
            units: [
                ("disk_gib_month".to_string(), 2200u64),
                ("cpu_core_month".to_string(), 0),
            ]
            .into(),
        };
        let meter = |name: &str, enforcement: &str, unit: &str| MeterInfo {
            name: name.into(),
            meaning: String::new(),
            enforcement: enforcement.into(),
            scope: "tenant".into(),
            unit: unit.into(),
        };
        let word = |m: &MeterInfo, list: Option<&PriceListJson>| {
            billing_word(&m.name, &m.enforcement, unit_price(m, list))
        };

        // A wall is a wall, priced or not.
        assert_eq!(
            word(
                &meter("pack_bytes", "enforced", "disk_gib_month"),
                Some(&list)
            ),
            "wall"
        );
        assert_eq!(
            word(&meter("pack_bytes", "enforced", "disk_gib_month"), None),
            "wall"
        );
        // Priced above zero and not a wall: billed.
        assert_eq!(
            word(
                &meter("lfs_bytes", "measured", "disk_gib_month"),
                Some(&list)
            ),
            "billed"
        );
        // The cache has no unit: not billed, whatever the list says.
        assert_eq!(
            word(&meter("cache_bytes", "measured", ""), Some(&list)),
            "not billed"
        );
        // A zero unit on the list is free, said as not billed.
        assert_eq!(
            word(
                &meter("cpu_millicores", "provisioned", "cpu_core_month"),
                Some(&list)
            ),
            "not billed"
        );
        // A unit the list does not name is free too.
        assert_eq!(
            word(
                &meter("ram_bytes", "provisioned", "ram_gib_month"),
                Some(&list)
            ),
            "not billed"
        );
        // Deleted-not-purged bytes the list charges for: still costs.
        assert_eq!(
            word(
                &meter("tombstoned_bytes", "measured", "disk_gib_month"),
                Some(&list)
            ),
            "still costs"
        );
        // …and NOT when there is no list: the open-source deployment bills nothing.
        assert_eq!(
            word(
                &meter("tombstoned_bytes", "measured", "disk_gib_month"),
                None
            ),
            "not billed"
        );
        assert_eq!(
            unit_price(
                &meter("tombstoned_bytes", "measured", "disk_gib_month"),
                Some(&list)
            ),
            Some(2200)
        );
        assert_eq!(
            unit_price(&meter("repos", "measured", ""), Some(&list)),
            None,
            "a count has no unit"
        );

        // The list picked for a product: the market's, else its first, else none.
        let fi = PriceListJson {
            product: "gunnar".into(),
            market: "FI".into(),
            currency: "EUR".into(),
            units: [("disk_gib_month".to_string(), 200u64)].into(),
        };
        let other = PriceListJson {
            product: "skade".into(),
            ..fi.clone()
        };
        let all = vec![list.clone(), fi.clone(), other];
        assert_eq!(
            list_for(&all, "gunnar", Some("fi")).map(|l| l.currency.as_str()),
            Some("EUR")
        );
        assert_eq!(
            list_for(&all, "gunnar", None).map(|l| l.market.as_str()),
            Some("SE"),
            "the first of the product's lists"
        );
        assert_eq!(
            list_for(&all, "gunnar", Some("NO")).map(|l| l.market.as_str()),
            Some("SE"),
            "an unknown market falls back rather than pricing nothing"
        );
        assert!(list_for(&all, "idun", None).is_none(), "no list is free");
    }

    /// **The form's arithmetic**: GiB up, GiB back, only what changed is sent.
    /// 10 GiB held, 100 typed: one cap of 100 GiB; the cache typed unchanged is
    /// left out, and the server carries it forward.
    #[test]
    fn the_order_form_sends_only_what_changed_in_bytes() {
        assert_eq!(form_value("pack_bytes", 10 * GIB), 10);
        assert_eq!(
            form_value("pack_bytes", 10 * GIB + 1),
            11,
            "rounded UP: never less than the tenant holds"
        );
        assert_eq!(form_value("repos", 26), 26, "a count is itself");
        assert_eq!(form_bytes("pack_bytes", 100), 100 * GIB);
        assert_eq!(form_bytes("repos", 50), 50);
        let current: BTreeMap<String, u64> = [
            ("pack_bytes".to_string(), 10 * GIB),
            ("cache_bytes".to_string(), 20 * GIB),
        ]
        .into();
        let typed: BTreeMap<String, u64> = [
            ("pack_bytes".to_string(), 100u64),
            ("cache_bytes".to_string(), 20),
        ]
        .into();
        let caps = order_caps(&current, &typed);
        assert_eq!(
            caps,
            [("pack_bytes".to_string(), 100 * GIB)].into(),
            "the unchanged cache is not re-ordered"
        );
        assert_eq!(caps_query(&caps), format!("pack_bytes={}", 100 * GIB));
        assert!(
            order_caps(&current, &[("pack_bytes".to_string(), 10u64)].into()).is_empty(),
            "nothing changed, nothing sent"
        );
        // A typed downgrade IS sent — the server refuses it by name, and the
        // console shows that sentence rather than silently dropping the field.
        assert_eq!(
            order_caps(&current, &[("pack_bytes".to_string(), 1u64)].into())["pack_bytes"],
            GIB
        );

        // The words on the quote.
        assert_eq!(
            price_words(0, "SEK"),
            "free",
            "free is a kind of deployment, not 0.00"
        );
        assert_eq!(price_words(198_000, "SEK"), "1980.00 SEK");
        assert_eq!(cost_words(0, "EUR", true), "this box · nothing to buy");
        assert_eq!(cost_words(0, "EUR", false), "no iron bought");
        assert_eq!(cost_words(2_070, "EUR", true), "20.70 EUR");
        let yes = QuoteJson {
            can_serve: true,
            ..QuoteJson::default()
        };
        assert_eq!(can_serve_words(&yes), (Tone::Pass, "can serve: yes".into()));
        let no = QuoteJson { can_serve: false, reason: "short 5 GiB. Serving more needs a fresh install onto a larger set, not another disk".into(), ..QuoteJson::default() };
        let (tone, line) = can_serve_words(&no);
        assert_eq!(tone, Tone::Bad);
        assert!(
            line.starts_with("can serve: no — ") && line.ends_with("not another disk"),
            "the reason, whole: {line}"
        );
    }

    #[test]
    fn the_ladder_maps_to_colour_and_label() {
        assert_eq!(
            (state_tone("paid"), state_label("paid")),
            (Tone::Pass, "Paid")
        );
        assert_eq!(
            (state_tone("grace"), state_label("grace")),
            (Tone::Warn, "Grace")
        );
        assert_eq!(state_tone("suspended"), Tone::Bad);
        assert_eq!(state_tone("retention"), Tone::Bad);
        assert_eq!(
            (state_tone("free"), state_label("free")),
            (Tone::Plain, "Free")
        );
        assert_eq!(state_label("banana"), "Unknown");
        assert_eq!(Tone::Warn.class(), "badge warn");
    }

    #[test]
    fn dates_round_trip_and_since_picks_the_coarsest_unit() {
        assert_eq!(date(0), "1970-01-01");
        assert_eq!(
            date(1_788_436_800_000),
            "2026-09-03",
            "monetize's own test fixture"
        );
        assert_eq!(parse_date_ms("2026-09-03"), Some(1_788_393_600_000));
        assert_eq!(date(parse_date_ms("2024-02-29").unwrap()), "2024-02-29");
        assert_eq!(parse_date_ms("2026-13-01"), None);
        assert_eq!(parse_date_ms("soon"), None);
        assert_eq!(since(1_000, 30_000), "just now");
        assert_eq!(since(0, 5 * 60_000), "5 min ago");
        assert_eq!(since(0, 3 * 3_600_000), "3 h ago");
        assert_eq!(since(0, 2 * DAY_MS), "2 d ago");
    }

    #[test]
    fn caps_parse_validate_and_name_the_undeclared_meter() {
        let caps = parse_caps("pack_bytes = 10 GiB\nlfs_bytes=5GiB\n\npushes = 100").unwrap();
        assert_eq!(caps["pack_bytes"], 10 << 30);
        assert_eq!(caps["lfs_bytes"], 5 << 30);
        assert_eq!(caps["pushes"], 100);
        assert_eq!(
            caps_text(&caps),
            "lfs_bytes = 5 GiB\npack_bytes = 10 GiB\npushes = 100"
        );
        assert!(parse_caps("pack_bytes 10").is_err());
        assert!(parse_caps("pack_bytes = 10 GB")
            .unwrap_err()
            .contains("KiB, MiB, GiB or TiB"));

        // **The editor round-trips a cap that is not a whole GiB.** It used to
        // print `pack_bytes = 67108864` for the 64 MiB rehearsal cap, so the one
        // plan whose number is easy to get wrong was the one shown in raw bytes.
        let small = parse_caps("pack_bytes = 64 MiB").unwrap();
        assert_eq!(small["pack_bytes"], 67_108_864);
        assert_eq!(caps_text(&small), "pack_bytes = 64 MiB");
        assert_eq!(
            parse_caps(&caps_text(&small)).unwrap(),
            small,
            "what the editor shows must parse back to what it was given"
        );
        // Every unit the display can produce is a unit the editor accepts.
        for (text, want) in [("a = 2 TiB", 2 << 40), ("a = 3 KiB", 3 << 10), ("a = 7", 7)] {
            let one = parse_caps(text).unwrap();
            assert_eq!(one["a"], want, "{text}");
            assert_eq!(parse_caps(&caps_text(&one)).unwrap(), one, "{text}");
        }
        let declared = vec!["pack_bytes".to_string(), "lfs_bytes".to_string()];
        assert!(validate_caps("gunnar", &declared, &caps)
            .unwrap_err()
            .contains("\"pushes\""));
        let mut ok = caps.clone();
        ok.remove("pushes");
        assert!(validate_caps("gunnar", &declared, &ok).is_ok());
        assert_eq!(percent_encode("team/sub x"), "team%2Fsub%20x");
    }
}

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

    /// A cap is only a limit when something enforces it, and the console must
    /// say which — otherwise two identical-looking numbers make two different
    /// promises and the operator cannot tell.
    #[test]
    fn a_cap_says_whether_it_is_a_wall() {
        // No cap: nothing to qualify.
        assert_eq!(cap_note(false, "enforced"), None);
        assert_eq!(cap_note(false, "provisioned"), None);
        // Enforced: the number IS the limit, and `docs/v2/07-console.md` rule 3 wants that
        // said in a plain word. (It used to be None — "the number speaks for
        // itself" — which left the one cap that matters as the unmarked one.)
        assert_eq!(cap_note(true, "enforced"), Some("wall"));
        // The two that would otherwise read as walls.
        assert_eq!(cap_note(true, "provisioned"), Some("provisioned"));
        assert_eq!(cap_note(true, "measured"), Some("not enforced"));
        // A server older than the field, or a value this console does not know:
        // say so rather than defaulting to the reassuring answer.
        assert_eq!(cap_note(true, ""), Some("unknown"));
        assert_eq!(cap_note(true, "something-new"), Some("unknown"));
    }
}

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

    const GIB: u64 = 1 << 30;

    /// **Only a WALL may escalate.** A cap the product does not police is not a
    /// cliff a tenant is walking towards, and colouring it as one teaches an
    /// operator that red means nothing.
    #[test]
    fn colour_escalates_only_where_something_actually_refuses() {
        // Enforced: the three rungs, checked AT the boundary and one below it.
        assert_eq!(bar_state(0, Some(10 * GIB), "enforced"), BarState::Ok);
        assert_eq!(
            bar_state(7 * GIB, Some(10 * GIB), "enforced"),
            BarState::Ok,
            "70% is not a warning"
        );
        assert_eq!(
            bar_state(8 * GIB, Some(10 * GIB), "enforced"),
            BarState::Near,
            "80% is where it starts"
        );
        assert_eq!(
            bar_state(10 * GIB - 1, Some(10 * GIB), "enforced"),
            BarState::Near,
            "one byte short is not at the limit"
        );
        assert_eq!(
            bar_state(10 * GIB, Some(10 * GIB), "enforced"),
            BarState::AtLimit
        );
        assert_eq!(
            bar_state(99 * GIB, Some(10 * GIB), "enforced"),
            BarState::AtLimit,
            "over the cap is still at the cap"
        );

        // Measured: full, and STILL not an alarm. This is the one the old
        // percentage-only rule got wrong.
        for used in [0, 8 * GIB, 10 * GIB, 99 * GIB] {
            assert_eq!(
                bar_state(used, Some(10 * GIB), "measured"),
                BarState::Metered,
                "at {used} bytes"
            );
        }
        // Provisioned: the number is what was bought.
        assert_eq!(
            bar_state(2000, Some(2000), "provisioned"),
            BarState::Provisioned
        );
        // A word this console does not know is reported AS unknown rather than
        // defaulted to the reassuring answer.
        assert_eq!(bar_state(1, Some(2), ""), BarState::Unknown);
        assert_eq!(bar_state(1, Some(2), "something-new"), BarState::Unknown);
    }

    /// **Uncapped is its own state, at every enforcement.** It is not 0% and it
    /// is not 100%, and a zero cap is how a product spells "unlimited"
    /// (gunnar's `explode_budget_bytes`), so it is uncapped too.
    #[test]
    fn a_meter_with_no_cap_is_uncapped_and_draws_no_fill() {
        for enforcement in ["enforced", "measured", "provisioned", ""] {
            assert_eq!(
                bar_state(5 * GIB, None, enforcement),
                BarState::Uncapped,
                "{enforcement}"
            );
            assert_eq!(
                bar_state(5 * GIB, Some(0), enforcement),
                BarState::Uncapped,
                "a cap of 0 is unlimited: {enforcement}"
            );
        }
        // Even with a percentage handed to it, an uncapped bar draws nothing —
        // the old code drew it against the biggest meter on the page, so the
        // largest number a tenant had was always a FULL bar.
        assert_eq!(BarState::Uncapped.width_percent(Some(100)), 0);
        assert_eq!(BarState::AtLimit.width_percent(Some(100)), 100);
        assert_eq!(BarState::Metered.width_percent(Some(95)), 95);
        assert_eq!(BarState::Ok.width_percent(None), 0);
    }

    /// Every state has its own class and its own sentence, so a colour can
    /// always be asked what it claims — and no two states are the same colour.
    #[test]
    fn every_state_says_something_different() {
        let all = [
            BarState::Uncapped,
            BarState::Ok,
            BarState::Near,
            BarState::AtLimit,
            BarState::Metered,
            BarState::Provisioned,
            BarState::Unknown,
        ];
        let mut classes: Vec<&str> = all.iter().map(|s| s.class()).collect();
        let n = classes.len();
        classes.sort();
        classes.dedup();
        assert_eq!(
            classes.len(),
            n,
            "two states share a class and are therefore the same colour"
        );
        let mut meanings: Vec<&str> = all.iter().map(|s| s.meaning()).collect();
        meanings.sort();
        meanings.dedup();
        assert_eq!(meanings.len(), n);
        assert!(all
            .iter()
            .all(|s| s.class().starts_with("fill ") && !s.meaning().is_empty()));
    }
}

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

    /// **A sale that cannot be DELIVERED is not an invalid request**, and the
    /// heading over the refusal must not say it is.
    ///
    /// An order is refused with `failed_precondition` when the product says it
    /// cannot absorb the caps — before the reserve, so nothing has been spent. This BFF maps that to 400, which used to put "monetize refused the
    /// request as invalid." above a paragraph explaining that gunnar's data set
    /// cannot grow. The operator's next move differs entirely between the two:
    /// one is a typo, the other is a conversation with a customer about a
    /// reinstall.
    #[test]
    fn a_refusal_to_deliver_does_not_read_as_a_malformed_request() {
        let cannot_deliver = Fail {
            status: 400,
            code: "failedprecondition".into(),
            message: "gunnar can serve the pack_bytes cap of 100.0 GiB, but … AND THIS SET CANNOT GROW WHILE \
                      GUNNAR RUNS: … a disk … would be BILLED AND INVISIBLE. Serving more needs a fresh install \
                      onto a larger set, not another disk"
                .into(),
        };
        let malformed = Fail {
            status: 400,
            code: "invalidargument".into(),
            message: "tenant is required".into(),
        };

        assert_ne!(
            cannot_deliver.summary(),
            malformed.summary(),
            "a product that cannot deliver reads as a malformed request, and the operator is sent to fix the form"
        );
        assert!(
            !cannot_deliver.summary().contains("invalid"),
            "{}",
            cannot_deliver.summary()
        );
        assert!(
            malformed.summary().contains("invalid"),
            "{}",
            malformed.summary()
        );

        // The message itself is NEVER reworded — it is what the operator reads
        // back to the customer, and the remediation is inside it.
        assert!(
            cannot_deliver.message.contains("fresh install"),
            "the sentence must survive whole"
        );
    }

    /// A refusal that cannot be delivered is not worth retrying: nothing about
    /// it will change by asking again, and a "Try again" button beside it is an
    /// invitation to spend the operator's afternoon.
    #[test]
    fn only_a_transport_or_upstream_fault_offers_a_retry() {
        for status in [0, 502, 503, 504] {
            assert!(
                Fail {
                    status,
                    code: String::new(),
                    message: String::new()
                }
                .worth_retrying(),
                "{status}"
            );
        }
        for status in [400, 401, 403, 404, 409] {
            assert!(
                !Fail {
                    status,
                    code: "failedprecondition".into(),
                    message: String::new()
                }
                .worth_retrying(),
                "{status}"
            );
        }
    }
}

// ── The screens (`docs/v2/07-console.md`) ───────────────────────────────────────────────
//
// Every decision a screen makes is a function here, over the shapes in
// `crate::model`, and has a host-run test. The DOM layer only calls them.

/// **The dot on screen 1.** `(tone, glyph, word)`; the word is empty for a
/// product that answers, because the row then carries its numbers instead.
///
/// `unknown` is NOT down. A server that has not finished its first poll tick
/// has asked nobody, and painting that red would be a claim nobody made.
pub fn reach_dot(state: &str) -> (Tone, &'static str, &'static str) {
    match state {
        "up" => (Tone::Pass, "●", ""),
        "down" => (Tone::Bad, "○", "not connected"),
        "silent" => (Tone::Warn, "○", "not reporting"),
        "not-installed" => (Tone::Plain, "○", "not installed"),
        _ => (Tone::Plain, "○", "not checked yet"),
    }
}

/// **A product line on a SITE's page**: the site's own fleet reports decide it
/// (owner 2026-10-12: "gunnar — not connected" while test2's main reported to
/// the ONE box every 30 s). The ONE box dials no estate, so the product's own
/// reachability (`down`: nothing dialled) says nothing about a site. `reached`
/// / `installed` are the site summary's; `None` (not read yet, or no site)
/// keeps the product's word.
pub fn site_reach(product_state: &str, reached: Option<bool>, installed: Option<bool>) -> &str {
    match (reached, installed) {
        (Some(true), _) => "up",
        (Some(false), Some(false)) => "not-installed",
        (Some(false), _) => "silent",
        (None, _) => product_state,
    }
}

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

    /// ★ test2's main reports in: the line is up, never "not connected".
    #[test]
    fn a_sites_product_line_follows_its_fleet_reports() {
        assert_eq!(
            reach_dot(site_reach("down", Some(true), Some(true))).2,
            "",
            "reporting: up, no word"
        );
        assert_eq!(
            reach_dot(site_reach("down", Some(false), Some(true))).2,
            "not reporting"
        );
        assert_eq!(
            reach_dot(site_reach("down", Some(false), Some(false))).2,
            "not installed"
        );
        assert_eq!(
            reach_dot(site_reach("down", None, None)).2,
            "not connected",
            "no site read: the product's own word"
        );
    }
}

/// **The meter a product's screens lead with.** When the BFF sent a trend, its
/// `meter` is the authority (the server chose it, and the rate is OF it);
/// otherwise the same rule the BFF applies — the first `enforced` meter, the
/// first cap that is a wall. `None` for a product that declares no wall: no
/// headline rather than a headline about whichever meter sorted first.
pub fn primary_meter<'a>(trend: Option<&'a TrendJson>, meters: &'a [MeterInfo]) -> Option<&'a str> {
    match trend {
        Some(t) if !t.meter.is_empty() => Some(t.meter.as_str()),
        _ => meters
            .iter()
            .find(|m| m.enforcement == "enforced")
            .map(|m| m.name.as_str()),
    }
}

/// **The fleet's headline number — including the two shapes that are not a
/// number.** `docs/v2/06-data-model.md` §6.1 §console 3: an unanswered capacity question is a
/// measurement that did not happen, never a full disk and never an empty one.
#[derive(Debug, Clone, PartialEq)]
pub enum Figure {
    /// `used` of `capacity`; `is_a_floor` when some tenant went unmeasured.
    Measured {
        used: u64,
        capacity: u64,
        is_a_floor: bool,
    },
    /// The tenants' sum is known; what the box can serve is not.
    Unmeasured {
        used: u64,
        is_a_floor: bool,
        why: String,
    },
    /// The product declares no per-tenant meter to lead with.
    NoMeter,
}

pub fn fleet_figure(fleet: &FleetJson, meter: Option<&str>) -> Figure {
    let Some(m) = meter.and_then(|name| fleet.meters.iter().find(|m| m.meter == name)) else {
        return Figure::NoMeter;
    };
    match m.capacity {
        Some(capacity) => Figure::Measured {
            used: m.used,
            capacity,
            is_a_floor: m.is_a_floor,
        },
        None => Figure::Unmeasured {
            used: m.used,
            is_a_floor: m.is_a_floor,
            why: if m.oversell_reason.is_empty() {
                fleet.servable_reason.clone()
            } else {
                m.oversell_reason.clone()
            },
        },
    }
}

impl Figure {
    /// The words: `184.0 GiB of 300.0 GiB`, or the honest unmeasured state.
    pub fn text(&self) -> String {
        let floor = |f: bool| if f { "at least " } else { "" };
        match self {
            Figure::Measured {
                used,
                capacity,
                is_a_floor,
            } => format!(
                "{}{} of {}",
                floor(*is_a_floor),
                bytes(*used),
                bytes(*capacity)
            ),
            // Owner 2026-10-12 ("at least 0 B · capacity unmeasured"): nothing
            // until there is data, then one short line.
            Figure::Unmeasured { used: 0, .. } => String::new(),
            Figure::Unmeasured {
                used, is_a_floor, ..
            } => format!("{}{} used", floor(*is_a_floor), bytes(*used)),
            Figure::NoMeter => "no per-tenant wall declared".to_owned(),
        }
    }

    /// The bar, when there is a denominator: the same [`bar_state`] the tenant
    /// bars use, as a wall — a full disk is the one cap the product cannot
    /// help refusing on. `None` draws no bar at all, never an empty one.
    pub fn bar(&self) -> Option<(BarState, u8)> {
        match self {
            Figure::Measured { used, capacity, .. } => {
                // An exhausted box with nothing on it has capacity 0; that is
                // full, not uncapped.
                let cap = Some((*capacity).max(1));
                let state = bar_state(*used, cap, "enforced");
                Some((state, state.width_percent(percent(*used, cap))))
            }
            _ => None,
        }
    }

    /// The reason there is no denominator, for the row's title.
    pub fn why(&self) -> &str {
        match self {
            Figure::Unmeasured { why, .. } => why,
            _ => "",
        }
    }
}

/// **A rate as one phrase, or nothing.** `▲ 12.0 GiB/wk`, `▼ ~2.0 GiB/wk`.
/// `None` when the rate is not known — a screen then shows NOTHING where the
/// rate would be, never a flat line: "not enough history" is not zero.
pub fn rate_line(trend: Option<&TrendJson>) -> Option<String> {
    let t = trend?;
    let per_week = t.per_week.filter(|_| t.known)?;
    let arrow = if per_week < 0.0 { "▼" } else { "▲" };
    let magnitude = per_week.abs();
    let amount = if t.meter.ends_with("_bytes") {
        bytes(magnitude.round() as u64)
    } else {
        format!("{magnitude:.0}")
    };
    Some(format!("{arrow} {}{amount}/wk", t.marker))
}

/// Whole weeks until `used` reaches `capacity` at `per_week`, rounded UP.
/// `None` when there is no capacity, or the rate is not positive — a shrinking
/// fleet never fills. `Some(0)` when it is already there.
pub fn weeks_until_full(used: u64, capacity: Option<u64>, per_week: f64) -> Option<u64> {
    let capacity = capacity?;
    if used >= capacity {
        return Some(0);
    }
    if per_week <= 0.0 {
        return None;
    }
    Some(((capacity - used) as f64 / per_week).ceil() as u64)
}

/// **The trend as ONE line** (`docs/v2/07-console.md` rule 4), or `None` when the product has
/// no meter to lead with.
///
/// A projection is only rendered when the server said it may be
/// (`may_project`), and when it is rendered it ALWAYS carries `~` — git
/// storage is bursty, and a linear fit stated as a plain date is a confident
/// wrong day someone plans a re-image around (`docs/v2/06-data-model.md` §6.1 "never grow").
/// Everything else renders the rate and says, in words, why there is no date.
pub fn trend_line(trend: Option<&TrendJson>, used: u64, capacity: Option<u64>) -> Option<String> {
    let t = trend?;
    if !t.known {
        // Nothing until there is history to say something about.
        return None;
    }
    let rate = rate_line(Some(t))?;
    let per_week = t.per_week?;
    let window = format!("last {} d", t.window_ms / DAY_MS);
    let line = match (capacity, t.may_project) {
        (None, _) => format!("{rate} ({window}) · capacity unmeasured, no projection"),
        (Some(c), _) if used >= c => format!("At capacity · {rate} ({window})"),
        (Some(_), false) => format!("{rate} ({window}), bursty — no projection"),
        (Some(c), true) => match weeks_until_full(used, Some(c), per_week) {
            Some(w) if w <= 1 => format!("Full within a week ({rate}, {window})"),
            Some(w) => format!("Full in ~{w} weeks ({rate}, {window})"),
            None => format!("{rate} ({window}) · not filling"),
        },
    };
    let floor = if t.unmeasured_tenants > 0 {
        format!(
            " · a floor: {} of {} tenants unmeasured",
            t.unmeasured_tenants,
            t.measured_tenants + t.unmeasured_tenants
        )
    } else {
        String::new()
    };
    Some(line + &floor)
}

#[cfg(test)]
mod screen_one_tests {
    use super::*;
    use crate::model::FleetMeterJson;

    const GIB: u64 = 1 << 30;

    fn trend(known: bool, per_week: f64, confidence: &str) -> TrendJson {
        TrendJson {
            meter: "pack_bytes".into(),
            known,
            per_week: known.then_some(per_week),
            confidence: confidence.into(),
            marker: if confidence == "firm" { "" } else { "~" }.into(),
            may_project: matches!(confidence, "firm" | "approximate"),
            readings: 20,
            span_ms: 10 * DAY_MS,
            needed_span_ms: 3 * DAY_MS + DAY_MS / 2,
            window_ms: 30 * DAY_MS,
            ..TrendJson::default()
        }
    }

    fn fleet(capacity: Option<u64>, is_a_floor: bool) -> FleetJson {
        FleetJson {
            meters: vec![FleetMeterJson {
                meter: "pack_bytes".into(),
                tenants: 9,
                used: 184 * GIB,
                capacity,
                is_a_floor,
                oversell: if capacity.is_some() {
                    "fits"
                } else {
                    "unmeasured"
                }
                .into(),
                oversell_reason: if capacity.is_some() {
                    String::new()
                } else {
                    "no Admin.Capacity RPC".into()
                },
                ..FleetMeterJson::default()
            }],
            servable_measured: capacity.is_some(),
            ..FleetJson::default()
        }
    }

    /// **Unknown is not down.** RED against the two-state shortcut: a server on
    /// its first tick would have shown every product as unreachable.
    #[test]
    fn the_dot_has_three_states_and_only_down_is_red() {
        assert_eq!(reach_dot("up"), (Tone::Pass, "●", ""));
        assert_eq!(reach_dot("down"), (Tone::Bad, "○", "not connected"));
        let (tone, _, word) = reach_dot("unknown");
        assert_eq!(tone, Tone::Plain, "nobody asked yet is not a fault");
        assert_ne!(word, "not connected", "and must not be reported as one");
        assert_eq!(
            reach_dot(""),
            reach_dot("unknown"),
            "an older server that sent nothing is the same silence"
        );
    }

    /// The trend's meter is the authority; without one, the first wall; without
    /// a wall, nothing — never the first meter that happened to sort.
    #[test]
    fn the_headline_meter_is_the_trends_or_the_first_wall_or_nothing() {
        let meters = vec![
            MeterInfo {
                name: "lfs_bytes".into(),
                enforcement: "measured".into(),
                ..MeterInfo::default()
            },
            MeterInfo {
                name: "pack_bytes".into(),
                enforcement: "enforced".into(),
                ..MeterInfo::default()
            },
        ];
        assert_eq!(
            primary_meter(None, &meters),
            Some("pack_bytes"),
            "the first WALL, not the first meter"
        );
        let t = TrendJson {
            meter: "cache_bytes".into(),
            ..TrendJson::default()
        };
        assert_eq!(
            primary_meter(Some(&t), &meters),
            Some("cache_bytes"),
            "the server's choice wins"
        );
        let no_wall = vec![MeterInfo {
            name: "artifacts".into(),
            enforcement: "measured".into(),
            ..MeterInfo::default()
        }];
        assert_eq!(
            primary_meter(None, &no_wall),
            None,
            "holger declares no wall: no headline"
        );
    }

    /// **An unmeasured capacity is not a full disk and not an empty one.** RED
    /// against rendering `capacity.unwrap_or(0)`: the bar would have been full.
    #[test]
    fn the_figure_says_unmeasured_and_draws_no_bar_without_a_denominator() {
        let measured = fleet_figure(&fleet(Some(300 * GIB), false), Some("pack_bytes"));
        assert_eq!(measured.text(), "184.0 GiB of 300.0 GiB");
        let (state, width) = measured.bar().expect("a denominator draws a bar");
        assert_eq!((state, width), (BarState::Ok, 61));

        let unmeasured = fleet_figure(&fleet(None, true), Some("pack_bytes"));
        assert_eq!(unmeasured.text(), "at least 184.0 GiB used");
        // ★ Owner 2026-10-12: no "at least 0 B · capacity unmeasured" — nothing until there is data.
        let empty = Figure::Unmeasured {
            used: 0,
            is_a_floor: true,
            why: String::new(),
        };
        assert_eq!(empty.text(), "");
        assert_eq!(
            unmeasured.bar(),
            None,
            "no denominator, no bar — not an empty one, not a full one"
        );
        assert!(
            unmeasured.why().contains("Admin.Capacity"),
            "the product's reason travels: {}",
            unmeasured.why()
        );

        // A floor is SAID, on a measured figure too.
        assert_eq!(
            fleet_figure(&fleet(Some(300 * GIB), true), Some("pack_bytes")).text(),
            "at least 184.0 GiB of 300.0 GiB"
        );
        // The wall's escalation applies to the fleet: at capacity is red.
        let full = Figure::Measured {
            used: 300 * GIB,
            capacity: 300 * GIB,
            is_a_floor: false,
        };
        assert_eq!(full.bar(), Some((BarState::AtLimit, 100)));
        // An exhausted, empty box is full, not uncapped.
        assert_eq!(
            Figure::Measured {
                used: 0,
                capacity: 0,
                is_a_floor: false
            }
            .bar()
            .map(|b| b.0),
            Some(BarState::Ok)
        );
        // No meter to lead with: said, and no bar.
        assert_eq!(
            fleet_figure(&fleet(Some(GIB), false), None),
            Figure::NoMeter
        );
        assert_eq!(
            fleet_figure(&fleet(Some(GIB), false), Some("lfs_bytes")),
            Figure::NoMeter,
            "a meter the fleet did not fold is no figure"
        );
        assert_eq!(Figure::NoMeter.bar(), None);
    }

    /// **Not enough history is nothing, not zero.** RED against
    /// `per_week.unwrap_or(0.0)`: the row would have read `▲ 0 B/wk`.
    #[test]
    fn the_rate_is_a_phrase_with_its_marker_or_nothing_at_all() {
        assert_eq!(rate_line(None), None);
        assert_eq!(
            rate_line(Some(&trend(false, 0.0, ""))),
            None,
            "unknown renders nothing"
        );
        assert_eq!(
            rate_line(Some(&trend(true, 12.0 * GIB as f64, "firm"))).as_deref(),
            Some("▲ 12.0 GiB/wk")
        );
        assert_eq!(
            rate_line(Some(&trend(true, 12.0 * GIB as f64, "approximate"))).as_deref(),
            Some("▲ ~12.0 GiB/wk")
        );
        assert_eq!(
            rate_line(Some(&trend(true, -2.0 * GIB as f64, "bursty"))).as_deref(),
            Some("▼ ~2.0 GiB/wk")
        );
        let count = TrendJson {
            meter: "repos".into(),
            ..trend(true, 3.4, "firm")
        };
        assert_eq!(
            rate_line(Some(&count)).as_deref(),
            Some("▲ 3/wk"),
            "a count is not bytes"
        );
        // A known rate whose number the server nevertheless withheld renders nothing.
        let withheld = TrendJson {
            per_week: None,
            ..trend(true, 1.0, "firm")
        };
        assert_eq!(rate_line(Some(&withheld)), None);
    }

    #[test]
    fn weeks_until_full_rounds_up_and_a_shrinking_fleet_never_fills() {
        assert_eq!(
            weeks_until_full(184 * GIB, Some(300 * GIB), 12.9 * GIB as f64),
            Some(9),
            "116 / 12.9 = 8.99 → 9"
        );
        assert_eq!(
            weeks_until_full(184 * GIB, Some(300 * GIB), 116.0 * GIB as f64),
            Some(1)
        );
        assert_eq!(weeks_until_full(300 * GIB, Some(300 * GIB), 1.0), Some(0));
        assert_eq!(
            weeks_until_full(301 * GIB, Some(300 * GIB), 1.0),
            Some(0),
            "over is at"
        );
        assert_eq!(weeks_until_full(1, Some(300 * GIB), 0.0), None);
        assert_eq!(weeks_until_full(1, Some(300 * GIB), -5.0), None);
        assert_eq!(weeks_until_full(1, None, 5.0), None);
    }

    /// **A projection is never a date without `~`, and never rendered when the
    /// server said it may not be.** RED against two shortcuts: rendering
    /// `Full in 9 weeks` for a firm series (a plain number nobody should plan
    /// around), and rendering a date for a bursty one at all.
    #[test]
    fn the_trend_line_projects_only_when_allowed_and_always_with_its_uncertainty() {
        let cap = Some(300 * GIB);
        let firm = trend_line(
            Some(&trend(true, 12.9 * GIB as f64, "firm")),
            184 * GIB,
            cap,
        )
        .unwrap();
        assert_eq!(firm, "Full in ~9 weeks (▲ 12.9 GiB/wk, last 30 d)");
        assert!(
            firm.contains('~'),
            "a firm fit is still a projection: {firm}"
        );

        let approx = trend_line(
            Some(&trend(true, 12.9 * GIB as f64, "approximate")),
            184 * GIB,
            cap,
        )
        .unwrap();
        assert!(
            approx.starts_with("Full in ~9 weeks (▲ ~12.9 GiB/wk"),
            "{approx}"
        );

        let bursty = trend_line(
            Some(&trend(true, 12.9 * GIB as f64, "bursty")),
            184 * GIB,
            cap,
        )
        .unwrap();
        assert!(
            !bursty.contains("Full in"),
            "bursty forbids a date: {bursty}"
        );
        assert!(
            bursty.contains("▲ ~12.9 GiB/wk") && bursty.contains("no projection"),
            "but the rate is still said: {bursty}"
        );

        // ★ Owner 2026-10-12: "not enough history yet: 0 d of 3 d measured" is noise —
        // nothing is said until there is history (and never a rate of zero).
        assert_eq!(
            trend_line(Some(&trend(false, 0.0, "")), 184 * GIB, cap),
            None
        );

        let no_cap = trend_line(
            Some(&trend(true, 12.9 * GIB as f64, "firm")),
            184 * GIB,
            None,
        )
        .unwrap();
        assert!(
            no_cap.contains("capacity unmeasured") && !no_cap.contains("Full in"),
            "{no_cap}"
        );

        let full = trend_line(
            Some(&trend(true, 12.9 * GIB as f64, "firm")),
            300 * GIB,
            cap,
        )
        .unwrap();
        assert!(full.starts_with("At capacity"), "{full}");

        let shrinking =
            trend_line(Some(&trend(true, -(GIB as f64), "firm")), 184 * GIB, cap).unwrap();
        assert!(
            shrinking.contains("not filling") && !shrinking.contains("Full in"),
            "{shrinking}"
        );

        let soon = trend_line(
            Some(&trend(true, 200.0 * GIB as f64, "firm")),
            184 * GIB,
            cap,
        )
        .unwrap();
        assert!(soon.starts_with("Full within a week"), "{soon}");

        // A floor is said on the line, not hidden in the number.
        let floor = TrendJson {
            measured_tenants: 7,
            unmeasured_tenants: 2,
            ..trend(true, 12.9 * GIB as f64, "firm")
        };
        assert!(trend_line(Some(&floor), 184 * GIB, cap)
            .unwrap()
            .ends_with("a floor: 2 of 9 tenants unmeasured"));

        assert_eq!(
            trend_line(None, 1, cap),
            None,
            "no meter to lead with: no line"
        );
    }

    #[test]
    fn a_tones_word_is_its_class_without_the_badge() {
        for t in [Tone::Pass, Tone::Warn, Tone::Bad, Tone::Live, Tone::Plain] {
            assert_eq!(t.class(), format!("badge {}", t.word()));
        }
    }
}

// ── Screen 2: a product ─────────────────────────────────────────────────

/// **One tenant's standing on ONE meter**: the reading, the cap, and what the
/// bar may claim about them. Absence is kept as absence — a tenant the poller
/// has not read is not at zero, and a fact naming no cap is not a cap of zero.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Standing {
    /// The meter, so the text can format bytes as bytes and counts as counts.
    pub meter: String,
    pub used: Option<u64>,
    pub cap: Option<u64>,
    pub state: BarState,
    pub width: u8,
    /// [`cap_note`]'s word for the cap, or `None` without one.
    pub note: Option<&'static str>,
}

/// A tenant row's standing on `meter`. Reads what the row carries and nothing
/// else — no reading is `None`, and a cap of zero is no cap.
pub fn standing(row: &TenantRow, meter: &str, enforcement: &str) -> Standing {
    standing_of(
        meter,
        row.meters.get(meter).copied(),
        row.caps.get(meter).copied(),
        enforcement,
    )
}

/// The same decision from bare parts, for the bars on screen 3 — one writer of
/// "what does a reading against a cap claim" (one implementation).
pub fn standing_of(
    meter: &str,
    used: Option<u64>,
    cap: Option<u64>,
    enforcement: &str,
) -> Standing {
    let cap = cap.filter(|c| *c > 0);
    let state = match used {
        Some(u) => bar_state(u, cap, enforcement),
        None => BarState::Uncapped,
    };
    let width = match used {
        Some(u) => state.width_percent(percent(u, cap)),
        None => 0,
    };
    Standing {
        meter: meter.to_owned(),
        used,
        cap,
        state,
        width,
        note: cap_note(cap.is_some(), enforcement),
    }
}

impl Standing {
    /// `10.2 GiB of 10.0 GiB` · `10.2 GiB` (no cap) · `26 of 50` for a count ·
    /// `not measured yet` — never a zero for a reading that did not happen.
    pub fn text(&self) -> String {
        match (self.used, self.cap) {
            (Some(u), Some(c)) => format!(
                "{} of {}",
                meter_value(&self.meter, u),
                meter_value(&self.meter, c)
            ),
            (Some(u), None) => meter_value(&self.meter, u),
            (None, _) => "not measured yet".to_owned(),
        }
    }

    /// The word beside the bar: `AT CAP` when a WALL is being refused on, a
    /// percentage otherwise, and the two absences said as absences. A measured
    /// (not enforced) meter at 100 % is `100 %`, never `AT CAP` — the note
    /// beside it says "not enforced", and nothing is being refused.
    pub fn word(&self) -> String {
        match (self.used, self.cap) {
            (None, _) => "not read yet".into(),
            (Some(_), None) => "no cap".into(),
            (Some(u), Some(c)) => match self.state {
                BarState::AtLimit => "AT CAP".into(),
                _ => format!("{} %", percent(u, Some(c)).unwrap_or(0)),
            },
        }
    }

    /// **Sort key: lower first.** Capped and measured tenants first, closest to
    /// their cap on top; then measured tenants nobody capped; then tenants
    /// nobody has read. `docs/v2/07-console.md` rule 5 — the tenant at their cap is the first
    /// row, and a tenant with no percentage cannot outrank one with one.
    pub fn attention_key(&self) -> (u8, u8) {
        match (self.used, self.cap) {
            (Some(u), Some(c)) => (0, 100 - percent(u, Some(c)).unwrap_or(0)),
            (Some(_), None) => (1, 0),
            (None, _) => (2, 0),
        }
    }
}

/// Tenants in attention order, stable within a rank so the server's order
/// (alphabetical by tenant) breaks ties.
pub fn sort_closest(rows: &mut [TenantRow], meter: &str, enforcement: &str) {
    rows.sort_by_key(|r| standing(r, meter, enforcement).attention_key());
}

/// How many tenants screen 2 shows before `+ N more`.
pub const SHOWN_TENANTS: usize = 5;
/// How many outcome lines screen 2's RECENTLY BOUGHT shows before `all activity →`.
pub const SHOWN_SALES: usize = 5;

/// `(shown, hidden)` for a list of `total` folded at `shown_max`. Hidden is 0
/// rather than 1 when everything fits: a fold that hides one row is a fold
/// that costs a click to save nothing.
pub fn fold(total: usize, shown_max: usize) -> (usize, usize) {
    if total <= shown_max {
        (total, 0)
    } else {
        (shown_max, total - shown_max)
    }
}

/// **The growth badge**: `⚠ cannot grow` for a SEALED box, and a plain word for
/// a box that grows — or nothing. Only `sealed` earns the WARNING: a box that
/// can be grown by an operator is not a refusal, and one that could not tell
/// (`unknown`) must not be sold as sealed — a reinstall planned on a guess is
/// the wrong kind of certainty. `at_runtime` says the set grows in place (the
/// Data set panel is the way), `at_restart` that it grows at the next boot.
/// The empty state (never measured) is nothing.
pub fn growth_badge(state: &str) -> Option<(&'static str, Tone)> {
    match state {
        "sealed" => Some(("⚠ cannot grow", Tone::Warn)),
        "at_runtime" => Some(("grows in place at runtime", Tone::Pass)),
        "at_restart" => Some(("grows at restart", Tone::Plain)),
        _ => None,
    }
}

/// **`oversold by 4.0 GiB — promised 16.0 GiB, can serve 12.0 GiB`**, or
/// nothing. The one number that stops you selling air: the sum of what the
/// tenants were promised against what the box can serve (`docs/v2/07-console.md` "What must
/// be BUILT"). Only the `oversold` arm draws it — `fits` is headroom, not a
/// task; `unmeasured` already reads as such in the big number; `not_storage`
/// promises no bytes at all.
///
/// Keyed on the WORD, never on `oversell_bytes`: the same field carries a
/// `fits` fleet's headroom, and a reader that took the number alone would
/// announce 94 GiB of spare room as 94 GiB oversold.
pub fn oversell_line(m: &FleetMeterJson) -> Option<String> {
    if m.oversell != "oversold" {
        return None;
    }
    // The comparand is the capacity the server subtracted from; an oversold
    // verdict always carries one, and the derivation below is the same
    // subtraction run backwards, so an older wire without it still reads true.
    let can_serve = m
        .capacity
        .unwrap_or(m.allowance.saturating_sub(m.oversell_bytes));
    Some(format!(
        "oversold by {} — promised {}, can serve {}",
        bytes(m.oversell_bytes),
        bytes(m.allowance),
        bytes(can_serve)
    ))
}

/// Whether the ladder state is one an operator must not scroll past.
pub fn needs_attention_state(state: &str) -> bool {
    matches!(state, "grace" | "suspended" | "retention")
}

// ── Screen 3: a tenant ──────────────────────────────────────────────────

/// **Which meters a tenant screen lists.** A `product`-scoped meter (gunnar's
/// CPU and RAM) belongs on the product screen once and in no per-tenant list —
/// a tenant does not own cores. `unknown` (an older server) stays listed: this
/// console hides nothing on a guess.
pub fn on_tenant_screen(scope: &str) -> bool {
    scope != "product"
}

/// **BACKED BY, one line per thing the tenant stands on** — `docs/v2/06-data-model.md` §6.1
/// rule 4 made visible: what was BOUGHT, distinct from what is allowed.
/// `bought` is iron monetize paid for and a purge gives back; `on hand` is
/// capacity the pool already had, real to the tenant and not monetize's to
/// return. A kind this console does not know is shown as itself.
pub fn holding_line(h: &HoldingJson) -> String {
    let how = if h.provisioned { "bought" } else { "on hand" };
    format!("{} {} · {how}", h.detail, h.kind)
}

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

    /// Sold is not bought: the line says which of the two a holding is.
    #[test]
    fn a_holding_says_whether_monetize_bought_it() {
        let bought = HoldingJson {
            reference: "gunnar/alice/2026-09-04".into(),
            kind: "disk".into(),
            detail: "10 GiB on gunnar-se-sto1".into(),
            provisioned: true,
            id: "upcloud:abc".into(),
        };
        assert_eq!(
            holding_line(&bought),
            "10 GiB on gunnar-se-sto1 disk · bought"
        );
        let pool = HoldingJson {
            provisioned: false,
            id: String::new(),
            ..bought.clone()
        };
        assert_eq!(
            holding_line(&pool),
            "10 GiB on gunnar-se-sto1 disk · on hand"
        );
        assert_ne!(
            holding_line(&bought),
            holding_line(&pool),
            "the same iron, two different claims about who paid"
        );
    }

    #[test]
    fn a_product_scoped_meter_is_not_in_a_tenants_list() {
        assert!(on_tenant_screen("tenant"));
        assert!(!on_tenant_screen("product"), "a tenant does not own cores");
        assert!(
            on_tenant_screen("unknown") && on_tenant_screen(""),
            "an older server's silence hides nothing"
        );
    }
}

// ── FLOW: what MOVED, as against what is held (screens 2 and 3) ─────────
//
// Every meter USING draws is a STOCK reading — how much is on the disk — and
// it reads the same for a tenant who pushed this morning and one who stopped
// in March. The meters below are the other kind, and the arithmetic over them
// is `monetize_product::flow`'s, not this file's: `quiet_for` owns "0 means
// NEVER", `last_seen` owns "a fetch counts as being seen", and `daily_totals`
// owns "a `*_today` counter is read once per day however often it is polled".
// Nothing here re-derives one of those three.

/// **A meter that belongs to the Activity panel and not to USING.**
///
/// The flow meters arrive in the same `Usage` as the stock ones and are
/// declared in the same list, so USING drew them the moment the product
/// gained them: `bytes_in_today` as a bar with no cap, and — the defect that
/// makes this function exist — `last_push_unix_ms` as a thirteen-digit COUNT
/// with `no cap` beside it. A Unix millisecond is not a quantity a tenant is
/// using any amount of, and a bar is a claim that it is.
///
/// Keyed on the two suffixes the product declares them under (`*_today` for a
/// daily counter, `*_unix_ms` for an instant) rather than on a list of gunnar's
/// seven names, so a second product's flow meters land in the right panel
/// without this console learning their names.
pub fn is_flow_meter(name: &str) -> bool {
    name.ends_with("_today") || name.ends_with("_unix_ms")
}

/// **The four day series the Activity panel draws, in order**: the product's
/// wire name, and the word on the page.
///
/// In one place because the panel draws them, the titles name them and the
/// axis labels them — three surfaces that must not come to disagree about
/// whether the second row is "out" or "bytes out". The product's own day keys
/// are richer than this (`lfs_bytes_in`, `auth_failures`, `distinct_principals`
/// and the `refused_*` reasons); those are not four bars, they are the detail,
/// and the panel does not draw them.
pub const ACTIVITY_SERIES: [(&str, &str); 4] = [
    ("bytes_in", "in"),
    ("bytes_out", "out"),
    ("pushes", "pushes"),
    ("fetches", "fetches"),
];

/// The day meter that marks a day RED: the product said no that day.
pub const REFUSED_METER: &str = "refused";

/// The page word for a day meter, or the wire name when the panel does not
/// have one — never an empty label.
pub fn series_word(meter: &str) -> &str {
    ACTIVITY_SERIES
        .iter()
        .find(|(name, _)| *name == meter)
        .map(|(_, word)| *word)
        .unwrap_or(meter)
}

/// **A day meter's value in its own unit.** Not [`meter_value`]: the stock
/// meters are named `*_bytes` and the day meters are named `bytes_in`, so the
/// suffix rule reads every one of them as a count and would print 4 294 967 296
/// where the panel means `4.0 GiB`.
pub fn day_meter_value(meter: &str, value: u64) -> String {
    if meter.contains("bytes") {
        bytes(value)
    } else {
        value.to_string()
    }
}

/// One day's bar in one series.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DayBar {
    pub day_unix_ms: u64,
    /// `Sep 4` — the x-axis label, from [`month_day`]. One date formatter.
    pub label: String,
    /// **`None` when the product did not count this meter that day.** Absence,
    /// not zero: a product that stopped reporting and a day with no traffic
    /// are different facts, and only the second is a quiet day.
    pub value: Option<u64>,
    /// 0..=100, against the WINDOW's own maximum.
    pub width: u8,
    /// This day had refusals. The count is in [`DayBar::title`]; the flag is
    /// what the mark is drawn from.
    pub refused: bool,
    /// The whole row in words, for the bar's `title` — the only place a
    /// thirty-bar strip can say what any one bar is.
    pub title: String,
}

/// **A window of days as bars, scaled against its OWN maximum.**
///
/// Two things it refuses to do, both of which a naive scaling does:
///
/// * **A window whose maximum is 0 draws NO bars.** Dividing by the maximum
///   makes every day of a zero window full width (`0/0`, or the `max(1)`
///   spelling of it), so a customer who moved nothing for a month would be
///   drawn as a full, busy strip — the exact opposite of what happened, in the
///   one panel that exists to show that it happened.
/// * **A day the product did not count is not a bar of zero.** It draws
///   nothing and says so in its title.
///
/// A non-zero day is rounded UP to at least one percent, because a real day
/// that rounds to 0 % and a day that did not happen are the same empty column
/// on the screen.
pub fn day_bars(days: &[ActivityDayJson], meter: &str) -> Vec<DayBar> {
    let word = series_word(meter);
    let peak = days
        .iter()
        .filter_map(|d| d.meters.get(meter).copied())
        .max()
        .unwrap_or(0);
    days.iter()
        .map(|day| {
            let value = day.meters.get(meter).copied();
            let refused = day.meters.get(REFUSED_METER).copied().unwrap_or(0);
            let label = month_day(day.day_unix_ms);
            let width = match (value, peak) {
                // Nothing in the window: no bar is drawn at all. See above.
                (_, 0) | (None, _) => 0,
                (Some(0), _) => 0,
                (Some(v), p) => (v as u128 * 100).div_ceil(p as u128).min(100) as u8,
            };
            let title = match value {
                Some(v) => format!("{label} · {word} {}", day_meter_value(meter, v)),
                None => format!("{label} · {word} not counted"),
            };
            let title = match refused {
                0 => title,
                1 => format!("{title} · 1 refused"),
                n => format!("{title} · {n} refused"),
            };
            DayBar {
                day_unix_ms: day.day_unix_ms,
                label,
                value,
                width,
                refused: refused > 0,
                title,
            }
        })
        .collect()
}

/// One row of the Activity panel: a meter, its bars, and the window's total.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DaySeries {
    pub meter: &'static str,
    pub word: &'static str,
    pub bars: Vec<DayBar>,
    /// The window's total, added over the days the product COUNTED. Adding
    /// days is right here and only here: the product already folded each day
    /// (`monetize_product::flow::daily_totals`), so these are day totals and
    /// not repeated readings of one counter.
    pub total: u64,
}

impl DaySeries {
    /// The total in its own unit — bytes as bytes, events as a count.
    pub fn total_text(&self) -> String {
        day_meter_value(self.meter, self.total)
    }

    /// Did anything at all move in this series' window? A series of nothing
    /// draws its row and no bars, rather than being dropped: a missing row
    /// reads as a meter the product does not have.
    pub fn moved(&self) -> bool {
        self.total > 0
    }
}

/// The four rows the Activity panel draws, in [`ACTIVITY_SERIES`] order.
pub fn activity_series(days: &[ActivityDayJson]) -> Vec<DaySeries> {
    ACTIVITY_SERIES
        .iter()
        .map(|(meter, word)| DaySeries {
            meter,
            word,
            bars: day_bars(days, meter),
            total: days
                .iter()
                .filter_map(|d| d.meters.get(*meter).copied())
                .fold(0u64, |acc, n| acc.saturating_add(n)),
        })
        .collect()
}

/// **`last seen 3 d ago`, or `never seen`.**
///
/// The two `last_*_unix_ms` meters as one line. Both decisions belong to
/// `monetize_product::flow` and neither is made here: which of the two counts
/// (`last_seen` — a customer who clones every morning and never pushes is
/// using the product) and what `0` means (`quiet_for` — NEVER, and rendering
/// it as a date would put 1970 on a console beside real dates, which reads as
/// a very old customer rather than as one nobody has ever used).
pub fn last_seen_line(last_push_unix_ms: u64, last_fetch_unix_ms: u64, now_unix_ms: u64) -> String {
    let usage = monetize_product::Usage {
        meters: BTreeMap::from([
            ("last_push_unix_ms".to_owned(), last_push_unix_ms),
            ("last_fetch_unix_ms".to_owned(), last_fetch_unix_ms),
        ]),
        measured_at_unix_ms: now_unix_ms,
    };
    match flow::quiet_for(flow::last_seen(&usage), now_unix_ms) {
        // Unreachable from the tenant page, which does not call this at all
        // when neither meter is present — but the arm is real and is answered
        // with the true sentence rather than with `never seen`, so that a
        // second caller cannot reintroduce the claim by forgetting the guard.
        flow::Quiet::NoData => "no data".to_owned(),
        flow::Quiet::Never => "never seen".to_owned(),
        flow::Quiet::For(ms) => format!("last seen {}", since_ms(ms)),
    }
}

/// **The line naming the tenants nobody measured**, under the SILENT panel.
///
/// The third fact on that panel and the one the page used to tell as the first:
/// a product that reports neither `last_*_unix_ms` meter says nothing about
/// whether its tenants are being used, and folding that into SILENT published
/// `never seen` about real customers on the strength of a gap in a plugin.
///
/// They are NAMED rather than counted-and-dropped. An operator who reads "3
/// tenants" has to go and find out which; an operator who reads the names knows
/// immediately whether it is one odd account or a whole product build that
/// stopped reporting — which is the difference between an afternoon and a
/// glance.
///
/// `None` when the list is empty: a panel that says "no data for 0 tenants"
/// every time is a line that gets stopped being read.
pub fn no_flow_data_line(tenants: &[String]) -> Option<String> {
    match tenants {
        [] => None,
        [one] => Some(format!(
            "NO DATA for {one} — this product does not report flow, so nothing here says whether \
             it is being used. Not counted as silent."
        )),
        many => {
            // The names, capped, because a product with 400 unreporting tenants
            // must not push the measured findings off the screen.
            const SHOWN: usize = 6;
            let head = many
                .iter()
                .take(SHOWN)
                .cloned()
                .collect::<Vec<_>>()
                .join(", ");
            let rest = many.len().saturating_sub(SHOWN);
            let names = if rest > 0 {
                format!("{head} and {rest} more")
            } else {
                head
            };
            Some(format!(
                "NO DATA for {} tenants ({names}) — this product does not report flow, so nothing \
                 here says whether they are being used. Not counted as silent.",
                many.len()
            ))
        }
    }
}

/// **The words on a SILENT row.** `None` is `never seen` and is never a
/// number: the wire sends `null` for a tenant nobody has ever used, and
/// "quiet 0 d" is the most reassuring thing this console could put beside the
/// least reassuring row on the page.
pub fn quiet_line(quiet_days: Option<u64>) -> String {
    match quiet_days {
        None => "never seen".to_owned(),
        // Not a row the server puts on this list, and rendered honestly if it
        // ever is: a tenant seen today is not quiet for no days.
        Some(0) => "seen today".to_owned(),
        Some(1) => "quiet 1 d".to_owned(),
        Some(n) => format!("quiet {n} d"),
    }
}

/// **Why the Activity panel is empty**, which is two different facts and never
/// one sentence for both.
///
/// * The product remembers the whole window: nothing moved, and that is a
///   finding about the customer.
/// * The product remembers less than the window: the panel asked for thirty
///   days and this box keeps seven, so an empty strip says nothing about the
///   weeks before that and must not be read as if it did.
/// * `retained_days == 0`: the product would not say. Not a memory of no days
///   — a refusal to answer, and the line says so rather than picking whichever
///   of the two above happens to be more comfortable.
pub fn activity_empty_line(days_asked: u64, retained_days: u64) -> String {
    if retained_days == 0 {
        return format!("Nothing moved in the last {days_asked} days. This product does not say how far back it remembers, so an empty window here may be a short memory rather than a quiet month.");
    }
    if retained_days < days_asked {
        return format!("Nothing moved in the last {retained_days} days. This product remembers no further back than that; the panel asked for {days_asked}.");
    }
    format!("Nothing moved in the last {days_asked} days.")
}

/// **The words on a FRICTION row**: `4 refusals in the last 7 days`.
pub fn friction_line(refused: u64, days: u64) -> String {
    let window = if days == 1 {
        "the last day".to_owned()
    } else {
        format!("the last {days} days")
    };
    match refused {
        // Not a row the server sends — it drops a tenant with no refusals —
        // and said plainly if one ever arrives, rather than as "0 refusals",
        // which reads as a measurement of nothing.
        0 => format!("nothing refused in {window}"),
        1 => format!("1 refusal in {window}"),
        n => format!("{n} refusals in {window}"),
    }
}

/// **Whether the fleet page draws one of the two flow panels at all.**
///
/// The window is the SERVER's, and `0` is the one value it never sends: it is
/// what a `#[serde(default)]` leaves behind when the field is not on the wire
/// at all — a monetize older than the lists. Drawn anyway, that panel would
/// say "every tenant has been seen inside the last 0 days", which is an
/// all-clear about a question nobody asked. No window, no panel.
pub fn shows_flow_panel(window_days: u64) -> bool {
    window_days > 0
}

/// The SILENT panel's caption: the threshold, from the wire.
pub fn silent_window_note(after_days: u64) -> String {
    format!("no push and no fetch for {after_days} days")
}

/// The FRICTION panel's one short line — and it says what this page
/// deliberately does NOT do.
///
/// The fleet page folds refusals from the readings the server already holds,
/// never by asking the product once per tenant: a page that made N calls would
/// hang on the slowest box in the estate. The cost of that choice is that the
/// REASON breakdown is not here, and the line names where it is instead of
/// leaving an operator to wonder why a count has no cause beside it.
pub fn friction_window_note(days: u64) -> String {
    format!(
        "refused in the last {days} days · the reason breakdown is on each tenant's Activity panel"
    )
}

/// The SILENT panel with nothing in it: an answer, not a blank.
pub fn silent_empty_line(after_days: u64) -> String {
    format!("Every tenant has been pushed to or fetched from inside the last {after_days} days. Nothing to act on.")
}

/// The FRICTION panel with nothing in it.
pub fn friction_empty_line(days: u64) -> String {
    format!("No tenant has been refused in the last {days} days. Nothing to act on.")
}

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

    const GIB: u64 = 1 << 30;
    const DAY: u64 = 86_400_000;
    const NOON: u64 = 1_789_041_600_000;

    fn day(n: u64, meters: &[(&str, u64)]) -> ActivityDayJson {
        ActivityDayJson {
            day_unix_ms: NOON - (n * DAY),
            meters: meters.iter().map(|(k, v)| ((*k).to_string(), *v)).collect(),
        }
    }

    /// ★ **A month of nothing draws NO bars.**
    ///
    /// RED before the `(_, 0)` arm: scaling against the window's maximum with
    /// the usual `max(1)` guard makes every day of an all-zero window full
    /// width, so the one panel that exists to show a customer stopped would
    /// have drawn them as busy — thirty full bars for thirty empty days.
    #[test]
    fn a_window_that_holds_nothing_draws_no_bars_at_all() {
        let quiet: Vec<ActivityDayJson> = (0..5).map(|n| day(n, &[("pushes", 0)])).collect();
        let bars = day_bars(&quiet, "pushes");
        assert_eq!(bars.len(), 5);
        assert!(
            bars.iter().all(|b| b.width == 0),
            "a window whose maximum is 0 must draw nothing: {bars:?}"
        );
        assert!(
            bars.iter().all(|b| b.value == Some(0)),
            "the days were measured, and measured zero"
        );
    }

    /// ★ **Scaled against the window's own maximum, and a real day is never
    /// rounded away.**
    #[test]
    fn the_tallest_day_is_the_scale_and_the_smallest_still_draws() {
        let days = vec![
            day(2, &[("bytes_in", 10 * GIB)]),
            day(1, &[("bytes_in", 5 * GIB)]),
            day(0, &[("bytes_in", 1)]),
        ];
        let bars = day_bars(&days, "bytes_in");
        assert_eq!(bars[0].width, 100, "the peak is the scale");
        assert_eq!(bars[1].width, 50);
        assert_eq!(
            bars[2].width, 1,
            "one byte against ten gibibytes is a hair, not nothing: a real day must draw"
        );
    }

    /// **A day the product did not count is absent, not a zero.** The bar is
    /// empty either way; the TITLE is what keeps the two facts apart, and
    /// `value` is `Option` so nothing downstream can add them together.
    #[test]
    fn a_day_the_product_did_not_count_is_not_a_day_of_none() {
        let days = vec![day(1, &[("pushes", 4)]), day(0, &[("fetches", 9)])];
        let bars = day_bars(&days, "pushes");
        assert_eq!(
            bars[1].value, None,
            "the second day carries no `pushes` at all"
        );
        assert_eq!(bars[1].width, 0);
        assert!(
            bars[1].title.ends_with("pushes not counted"),
            "{:?}",
            bars[1].title
        );
        assert!(
            bars[0].title.starts_with(&month_day(days[0].day_unix_ms)),
            "the label is the x-axis label, one date formatter: {:?}",
            bars[0].title
        );
        assert_eq!(
            activity_series(&days)
                .iter()
                .find(|s| s.meter == "pushes")
                .unwrap()
                .total,
            4,
            "an uncounted day adds nothing rather than zero"
        );
    }

    /// ★ **A day the product said no on is marked, and the title carries the
    /// count.** The mark is a flag and not the number, because a red tick is
    /// legible at one bar in thirty and a number is not.
    #[test]
    fn a_day_with_refusals_is_marked_on_every_series() {
        let days = vec![
            day(1, &[("pushes", 3), ("refused", 2)]),
            day(0, &[("pushes", 3)]),
        ];
        for meter in ["pushes", "bytes_in"] {
            let bars = day_bars(&days, meter);
            assert!(
                bars[0].refused,
                "{meter}: the refusal is a fact about the DAY, not about one series"
            );
            assert!(!bars[1].refused);
        }
        assert!(
            day_bars(&days, "pushes")[0].title.ends_with("· 2 refused"),
            "{:?}",
            day_bars(&days, "pushes")[0].title
        );
        assert!(
            day_bars(&[day(0, &[("pushes", 1), ("refused", 1)])], "pushes")[0]
                .title
                .ends_with("· 1 refused"),
            "one refusal is not `1 refusals`"
        );
    }

    /// **Bytes are bytes and events are counts**, on names that carry neither
    /// suffix `meter_value` keys on. RED against reusing `meter_value` here:
    /// `bytes_in` does not end in `_bytes`, so 4 GiB of traffic printed as
    /// `4294967296`.
    #[test]
    fn a_day_meter_says_bytes_in_bytes_and_events_in_counts() {
        assert_eq!(day_meter_value("bytes_in", 4 * GIB), "4.0 GiB");
        assert_eq!(day_meter_value("lfs_bytes_out", 64 * (1 << 20)), "64.0 MiB");
        assert_eq!(day_meter_value("pushes", 12), "12");
        assert_eq!(day_meter_value("refused", 0), "0");
        assert_eq!(
            meter_value("bytes_in", 4 * GIB),
            "4294967296",
            "the stock rule keys on the SUFFIX and cannot read a day meter"
        );
    }

    /// The four rows are named once, in order, and the words are the page's.
    #[test]
    fn the_four_series_are_declared_in_one_place_and_keep_their_order() {
        let series = activity_series(&[day(
            0,
            &[
                ("bytes_in", 2),
                ("bytes_out", 3),
                ("pushes", 4),
                ("fetches", 5),
            ],
        )]);
        assert_eq!(
            series.iter().map(|s| s.word).collect::<Vec<_>>(),
            ["in", "out", "pushes", "fetches"]
        );
        assert_eq!(
            series.iter().map(|s| s.total).collect::<Vec<_>>(),
            [2, 3, 4, 5]
        );
        assert_eq!(series[0].total_text(), "2 B", "the byte rows read as bytes");
        assert_eq!(series[2].total_text(), "4");
        assert_eq!(series_word("bytes_in"), "in");
        assert_eq!(
            series_word("auth_failures"),
            "auth_failures",
            "a meter the panel has no word for keeps its own name rather than going blank"
        );
        assert!(series[0].moved(), "a series with a total moved");
        // A series the tenant never used keeps its ROW: a missing row reads as
        // a meter the product does not have, which is a different claim.
        let one_sided = activity_series(&[day(0, &[("fetches", 5)])]);
        assert_eq!(one_sided.len(), 4);
        assert!(
            !one_sided[2].moved() && one_sided[2].bars.iter().all(|b| b.width == 0),
            "nothing was pushed, so nothing is drawn"
        );
        assert!(one_sided[3].moved());
    }

    /// ★ **`never seen` is never a date, and a fetch counts as being seen.**
    ///
    /// Both decisions are `monetize_product::flow`'s; this test is here to
    /// prove the console asks it rather than re-deriving them. `0` read as a
    /// timestamp is 1970, which on a console beside real dates reads as a very
    /// old customer rather than as one nobody has ever used.
    #[test]
    fn the_last_seen_line_says_never_rather_than_nineteen_seventy() {
        assert_eq!(last_seen_line(0, 0, NOON), "never seen");
        assert_eq!(last_seen_line(NOON - 3 * DAY, 0, NOON), "last seen 3 d ago");
        assert_eq!(
            last_seen_line(0, NOON - 3 * DAY, NOON),
            "last seen 3 d ago",
            "a customer who only ever clones is using the product"
        );
        assert_eq!(
            last_seen_line(NOON - 40 * DAY, NOON - 2 * DAY, NOON),
            "last seen 2 d ago",
            "the LATER of the two"
        );
        assert_eq!(last_seen_line(NOON, 0, NOON), "last seen just now");
        assert_eq!(
            last_seen_line(NOON + DAY, 0, NOON),
            "last seen just now",
            "a clock that runs ahead is a disagreement, not a visit that has not happened"
        );
    }

    /// ★ **A tenant nobody has ever used never reads as `quiet 0 d`.**
    ///
    /// RED against a defaulted `0` on the wire field: `null` is what the
    /// server sends for never, and the reassuring number is exactly the wrong
    /// thing to print on the least reassuring row on the page.
    #[test]
    fn the_silent_row_says_never_seen_rather_than_a_number_of_days() {
        assert_eq!(quiet_line(None), "never seen");
        assert_eq!(quiet_line(Some(21)), "quiet 21 d");
        assert_eq!(quiet_line(Some(1)), "quiet 1 d");
        assert_eq!(
            quiet_line(Some(0)),
            "seen today",
            "not a row the server sends, and not rendered as quiet for no time either"
        );
        assert_ne!(
            quiet_line(None),
            quiet_line(Some(0)),
            "never and today are the two ends of this list, not the same words"
        );
    }

    /// ★ **A tenant nobody MEASURED reads as NO DATA, by name — never as zero
    /// and never as `never seen`.**
    ///
    /// The third fact on the SILENT panel. `never seen` is a MEASUREMENT: the
    /// product counts flow and counted none. NO DATA is a GAP — an older
    /// product build, the `Admin.StorageInfo` fallback, a tenant the poller has
    /// never read — and it says nothing at all about the customer.
    ///
    /// Until 2026-09-10 the fleet page could not tell them apart:
    /// `flow::last_seen` folded an absent meter into a meter reading `0`, so an
    /// unreporting tenant was drawn `never seen` AND sorted to the top of the
    /// page by `quiet_for_ms`'s `u64::MAX`. The most alarming row on the screen
    /// was manufactured from a plugin gap. `flow::Quiet::NoData` now keeps the
    /// two apart in the type and `silent_tenants` cannot list one.
    ///
    /// The names are ON the line: an operator who reads a count has to go and
    /// find out which tenants, and one who reads the names can see at a glance
    /// whether it is one odd account or a whole product that stopped reporting.
    #[test]
    fn no_data_is_named_and_is_not_never_seen_and_is_not_zero() {
        // Nothing to say, so nothing is said.
        assert_eq!(
            no_flow_data_line(&[]),
            None,
            "a panel must not say `no data for 0 tenants` every time"
        );

        let one = no_flow_data_line(&["acme".to_owned()]).expect("a line");
        assert!(one.contains("NO DATA"), "the fact is named: {one}");
        assert!(one.contains("acme"), "and the tenant is named: {one}");
        assert!(
            one.contains("Not counted as silent"),
            "and it says where it is NOT: {one}"
        );
        assert!(
            !one.contains('0'),
            "NO DATA must never be rendered as a zero: {one}"
        );
        assert!(
            !one.contains("never seen"),
            "a gap is not a measurement: {one}"
        );

        // Many are named too, and capped so they cannot push the measured
        // findings off the screen.
        let names: Vec<String> = (0..9).map(|n| format!("t{n}")).collect();
        let many = no_flow_data_line(&names).expect("a line");
        assert!(many.contains("9 tenants"), "{many}");
        assert!(
            many.contains("t0, t1, t2, t3, t4, t5"),
            "the first six by name: {many}"
        );
        assert!(many.contains("and 3 more"), "and the rest counted: {many}");
        assert!(!many.contains("t8"), "the list is capped: {many}");

        // THE CONFLATION THIS EXISTS TO PREVENT: three different sentences for
        // three different facts, and no two of them equal.
        let no_data = one;
        let never = quiet_line(None);
        let idle = quiet_line(Some(0));
        assert_ne!(no_data, never);
        assert_ne!(never, idle);
        assert_ne!(no_data, idle);

        // And the one-line renderer answers the arm rather than falling through
        // to `never seen`, so a second caller cannot reintroduce the claim.
        let unmeasured = monetize_product::Usage {
            meters: BTreeMap::new(),
            measured_at_unix_ms: NOON,
        };
        assert!(monetize_product::flow::last_seen(&unmeasured).is_none());
        assert_eq!(
            monetize_product::flow::quiet_for(monetize_product::flow::last_seen(&unmeasured), NOON),
            monetize_product::flow::Quiet::NoData
        );
    }

    /// ★ **An empty window says WHICH silence it is.**
    ///
    /// *Nothing moved in thirty days* is a finding about a customer.
    /// *This box keeps seven days* is a fact about the box, and reading the
    /// second as the first is how a console reports a quiet month that nobody
    /// measured.
    #[test]
    fn an_empty_activity_window_tells_a_quiet_month_from_a_short_memory() {
        let whole = activity_empty_line(30, 400);
        assert_eq!(whole, "Nothing moved in the last 30 days.");
        let short = activity_empty_line(30, 7);
        assert!(
            short.contains("last 7 days") && short.contains("asked for 30"),
            "{short:?}"
        );
        assert_ne!(short, whole);
        let silent = activity_empty_line(30, 0);
        assert!(
            silent.contains("does not say how far back"),
            "0 retained days is a refusal to answer, not a memory of no days: {silent:?}"
        );
        assert_ne!(silent, short);
        assert_ne!(silent, whole);
    }

    /// **The friction row counts refusals and names its window** — one is not
    /// `1 refusals`, and a count with no window is a number nobody can act on.
    #[test]
    fn the_friction_line_counts_refusals_and_states_the_window_it_counted_over() {
        assert_eq!(friction_line(4, 7), "4 refusals in the last 7 days");
        assert_eq!(friction_line(1, 7), "1 refusal in the last 7 days");
        assert_eq!(friction_line(1, 1), "1 refusal in the last day");
        assert_eq!(
            friction_line(0, 7),
            "nothing refused in the last 7 days",
            "not `0 refusals`, which reads as a measurement of nothing"
        );
    }

    /// **An empty list says there is nothing to act on**, and both captions
    /// carry the window the SERVER applied rather than a number this console
    /// picked.
    #[test]
    fn an_empty_list_says_nothing_to_act_on_and_the_captions_carry_the_servers_window() {
        assert!(
            silent_empty_line(14).contains("14 days")
                && silent_empty_line(14).ends_with("Nothing to act on.")
        );
        assert!(
            friction_empty_line(7).contains("7 days")
                && friction_empty_line(7).ends_with("Nothing to act on.")
        );
        assert!(
            silent_window_note(21).contains("21"),
            "the threshold is the wire's, so a server that changes it changes the caption"
        );
        let note = friction_window_note(7);
        assert!(note.contains("7 days") && note.contains("Activity panel"), "the fleet page must say where the reasons are, since it deliberately does not fetch them: {note:?}");
    }

    /// ★ **A server too old to have the lists draws no panel, rather than an
    /// all-clear over a window of no days.**
    ///
    /// RED against drawing the panels unconditionally: the window arrives
    /// `#[serde(default)]`, so an older monetize leaves 0 behind, and the
    /// empty state would then have read "every tenant has been pushed to or
    /// fetched from inside the last 0 days" — a reassurance nobody measured.
    #[test]
    fn a_window_of_no_days_is_a_missing_field_and_draws_no_panel() {
        assert!(shows_flow_panel(14) && shows_flow_panel(7) && shows_flow_panel(1));
        assert!(
            !shows_flow_panel(0),
            "0 is the absent field, never a threshold"
        );
    }

    /// ★ **A flow meter is not a bar in USING.**
    ///
    /// RED before `is_flow_meter`: the seven flow meters are declared beside
    /// the stock ones and arrive in the same reading, so USING drew
    /// `last_push_unix_ms` as a thirteen-digit count with `no cap` beside it —
    /// a bar claiming a tenant is using 1 789 041 600 000 of something.
    #[test]
    fn a_flow_meter_belongs_to_the_activity_panel_and_not_to_using() {
        for flow in [
            "bytes_in_today",
            "bytes_out_today",
            "pushes_today",
            "fetches_today",
            "refused_today",
            "last_push_unix_ms",
            "last_fetch_unix_ms",
        ] {
            assert!(
                is_flow_meter(flow),
                "{flow} is flow and must not be a USING bar"
            );
        }
        for stock in [
            "pack_bytes",
            "cache_bytes",
            "cold_bytes",
            "lfs_bytes",
            "tombstoned_bytes",
            "repos",
            "objects",
            "open_store_ram_bytes",
            "cpu_millicores",
            "ram_bytes",
            "concurrent_transfers",
        ] {
            assert!(
                !is_flow_meter(stock),
                "{stock} is a stock meter and must keep its bar"
            );
        }
    }
}

// ── Sales: one attempt as one line (screens 2 and 4) ────────────────────

const MONTHS: [&str; 12] = [
    "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
];

/// `Sep 4` — the day, as a row reads it. Year-less on purpose: the ledger's
/// full date is in the disclosure, and a list of outcomes is read for recency.
pub fn month_day(unix_ms: u64) -> String {
    let (_, m, d) = civil_from_days((unix_ms / DAY_MS) as i64);
    format!("{} {d}", MONTHS[(m - 1) as usize])
}

/// What a sale promised, in one phrase: `10.0 GiB` for a plan capping bytes,
/// `10.0 GiB + 50 repos` when it caps more, `no caps` when it caps nothing.
pub fn caps_phrase(caps: &BTreeMap<String, u64>) -> String {
    if caps.is_empty() {
        return "no caps".to_owned();
    }
    caps.iter()
        .map(|(k, v)| {
            if k.ends_with("_bytes") {
                bytes(*v)
            } else {
                format!("{v} {}", plain_meter(k).to_lowercase())
            }
        })
        .collect::<Vec<_>>()
        .join(" + ")
}

/// **The outcome line** (`docs/v2/07-console.md` rule 6): `Sold bob 90.0 GiB`,
/// `Refunded zz-visibility 1.0 GiB`, `Declined bob 10.0 GiB`, `Open: bob
/// 10.0 GiB`. The verb is the outcome; the steps live behind the disclosure.
/// What is named is the DELTA — what was sold — not the tenant's whole
/// allowance afterwards; a monetize too old to send one is read by its caps.
pub fn sale_line(s: &SaleJson) -> String {
    let what = caps_phrase(if s.delta.is_empty() {
        &s.caps
    } else {
        &s.delta
    });
    match s.outcome.as_str() {
        "granted" => format!("Sold {} {what}", s.tenant),
        "refunded" => format!("Refunded {} {what}", s.tenant),
        "declined" => format!("Declined {} {what}", s.tenant),
        "open" => format!("Open: {} {what}", s.tenant),
        other => format!("{other}: {} {what}", s.tenant),
    }
}

/// The mark at the end of the row, and its colour. **Declined is not a
/// refund**: nothing was locked, and rendering it with the refund's arrow
/// invents money that moved. A refund that never drew is said so too.
pub fn sale_mark(outcome: &str, drawn: bool) -> (Tone, &'static str) {
    match (outcome, drawn) {
        ("granted", _) => (Tone::Pass, "✓"),
        ("refunded", true) => (Tone::Warn, "↩ refunded"),
        ("refunded", false) => (Tone::Warn, "↩ released, nothing drawn"),
        ("declined", _) => (Tone::Bad, "✗ declined"),
        ("open", _) => (Tone::Live, "open"),
        _ => (Tone::Plain, "unknown outcome"),
    }
}

/// **NEEDS YOU's line**: the tenant and the server's reason, or the one
/// sentence that is true of every open attempt when the server gave none.
pub fn needs_you_line(s: &SaleJson) -> String {
    let why = if s.reason.is_empty() {
        "money reserved, not yet captured or released"
    } else {
        s.reason.as_str()
    };
    format!("{} — {why}", s.tenant)
}

/// Open attempts first — the only rows that are tasks — then history. Both
/// halves keep the server's order (newest first).
pub fn split_needs_you(sales: Vec<SaleJson>) -> (Vec<SaleJson>, Vec<SaleJson>) {
    sales.into_iter().partition(|s| s.needs_you)
}

// ── Screen 4: activity ──────────────────────────────────────────────────

/// **A ledger step in plain words** (`docs/v2/07-console.md` rule 3): `reserved → bought →
/// charged → granted`. The `Step` variant name stays in the title. A kind this
/// console does not know is shown as itself — visible, never guessed.
pub fn plain_step(kind: &str) -> String {
    match kind {
        "Reserved" => "reserved",
        "Declined" => "declined",
        "Ensured" => "bought",
        "Destroyed" => "gave back",
        "Captured" => "charged",
        "Released" => "refunded",
        "EntitlementPushed" => "granted",
        "Reclaimed" => "reclaimed",
        // The data set enlarged in place by the outside actor: a standing
        // cost, neither bought nor given back.
        "Grown" => "grew the set",
        other => other,
    }
    .to_owned()
}

/// The step's colour: money drawn is pass, money released is warn, a refusal
/// is bad, everything else is a fact with no colour.
pub fn step_tone(kind: &str) -> Tone {
    match kind {
        "Captured" | "EntitlementPushed" => Tone::Pass,
        "Released" | "Destroyed" | "Reclaimed" => Tone::Warn,
        "Declined" => Tone::Bad,
        _ => Tone::Plain,
    }
}

/// **The steps behind one outcome line**, out of the ledger's recent rows, in
/// the order they were written. Empty when the attempt is older than the
/// recent window — the disclosure then fetches them by reference instead.
pub fn steps_of(recent: &[RowJson], reference: &str) -> Vec<RowJson> {
    let mut rows: Vec<RowJson> = recent
        .iter()
        .filter(|r| r.reference == reference)
        .cloned()
        .collect();
    rows.sort_by_key(|r| r.seq);
    rows
}

/// Activity scoped to one tenant (`History →` on screen 3), or everything.
pub fn sales_for(sales: Vec<SaleJson>, tenant: Option<&str>) -> Vec<SaleJson> {
    match tenant.filter(|t| !t.is_empty()) {
        Some(t) => sales.into_iter().filter(|s| s.tenant == t).collect(),
        None => sales,
    }
}

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

    fn row(reference: &str, seq: u32, kind: &str) -> RowJson {
        RowJson {
            reference: reference.into(),
            seq,
            at_unix_ms: 1_000 + u64::from(seq),
            kind: kind.into(),
            json: "{}".into(),
        }
    }

    /// **The six steps read as a sentence, not as variant names.** RED against
    /// an identity mapping: `EntitlementPushed` would sit on the surface.
    #[test]
    fn every_ledger_step_has_plain_words_and_an_unknown_one_keeps_its_name() {
        let timeline: Vec<String> = ["Reserved", "Ensured", "Captured", "EntitlementPushed"]
            .iter()
            .map(|k| plain_step(k))
            .collect();
        assert_eq!(
            timeline.join(" → "),
            "reserved → bought → charged → granted"
        );
        for kind in [
            "Reserved",
            "Declined",
            "Ensured",
            "Destroyed",
            "Captured",
            "Released",
            "EntitlementPushed",
            "Reclaimed",
            "Grown",
        ] {
            let word = plain_step(kind);
            assert_ne!(word, kind, "{kind} still shows its variant name");
            assert_eq!(word, word.to_lowercase(), "{kind} → {word} is not plain");
        }
        assert_eq!(
            plain_step("SomethingNew"),
            "SomethingNew",
            "unmapped is shown as itself"
        );
        assert_eq!(
            (
                step_tone("Captured"),
                step_tone("Released"),
                step_tone("Declined"),
                step_tone("Reserved")
            ),
            (Tone::Pass, Tone::Warn, Tone::Bad, Tone::Plain)
        );
    }

    /// The disclosure shows THIS attempt's rows, in write order, whatever
    /// order the recent list arrived in — and nothing from its neighbours.
    #[test]
    fn the_steps_behind_a_line_are_its_own_in_write_order() {
        let recent = vec![
            row("b", 1, "Captured"),
            row("a", 2, "Captured"),
            row("b", 0, "Reserved"),
            row("a", 0, "Reserved"),
            row("a", 1, "Ensured"),
        ];
        let a: Vec<(u32, String)> = steps_of(&recent, "a")
            .into_iter()
            .map(|r| (r.seq, r.kind))
            .collect();
        assert_eq!(
            a,
            [
                (0, "Reserved".to_string()),
                (1, "Ensured".to_string()),
                (2, "Captured".to_string())
            ]
        );
        assert_eq!(steps_of(&recent, "b").len(), 2);
        assert!(
            steps_of(&recent, "zzz").is_empty(),
            "older than the window: nothing, and the page fetches by reference"
        );
    }

    #[test]
    fn history_for_one_tenant_keeps_only_that_tenants_attempts() {
        let sale = |t: &str| SaleJson {
            tenant: t.into(),
            ..SaleJson::default()
        };
        let all = vec![sale("a"), sale("b"), sale("a")];
        assert_eq!(sales_for(all.clone(), Some("a")).len(), 2);
        assert_eq!(sales_for(all.clone(), None).len(), 3);
        assert_eq!(
            sales_for(all.clone(), Some("")).len(),
            3,
            "an empty query is no filter"
        );
        assert!(sales_for(all, Some("nobody")).is_empty());
    }
}

/// **Plain words on the surface** (`docs/v2/07-console.md` rule 3): the meter's name goes in
/// the title, this goes on the page. A meter this console has no word for is
/// shown by its raw name — said, not hidden, and never a guess.
pub fn plain_meter(name: &str) -> String {
    match name {
        "pack_bytes" => "Repository data",
        "cache_bytes" => "Cache",
        "cold_bytes" => "Cold tier",
        "lfs_bytes" => "Large files",
        "tombstoned_bytes" => "Deleted, not purged",
        "repos" => "Repositories",
        "open_store_ram_bytes" => "RAM for open stores",
        "concurrent_transfers" => "Simultaneous transfers",
        "cpu_millicores" => "CPU",
        "ram_bytes" => "RAM",
        "artifacts" => "Artifacts",
        "archive_bytes" => "Archive",
        "bytes_served" => "Bytes served",
        "requests" => "Requests",
        "proxy_upstream_hits" => "Upstream fetches",
        other => other,
    }
    .to_owned()
}

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

    const GIB: u64 = 1 << 30;

    fn row(tenant: &str, used: Option<u64>, cap: Option<u64>) -> TenantRow {
        TenantRow {
            tenant: tenant.into(),
            meters: used
                .map(|u| [("pack_bytes".to_string(), u)].into())
                .unwrap_or_default(),
            caps: cap
                .map(|c| [("pack_bytes".to_string(), c)].into())
                .unwrap_or_default(),
            measured_at_unix_ms: used.map(|_| 1),
            ..TenantRow::default()
        }
    }

    /// **The tenant at their cap is the first row.** RED against alphabetical
    /// (the server's order): `bob` would sit above `alice`. And RED against
    /// sorting by bytes used: an uncapped tenant with 50 GiB would outrank the
    /// one being refused at 10.
    #[test]
    fn tenants_sort_by_closeness_to_cap_and_the_unmeasured_go_last() {
        let mut rows = vec![
            row("aaa-unread", None, Some(10 * GIB)),
            row("big-uncapped", Some(50 * GIB), None),
            row("bob", Some(3 * GIB + GIB / 8), Some(10 * GIB)),
            row("alice", Some(10 * GIB + GIB / 4), Some(10 * GIB)),
            row("near", Some(9 * GIB), Some(10 * GIB)),
        ];
        sort_closest(&mut rows, "pack_bytes", "enforced");
        let order: Vec<&str> = rows.iter().map(|r| r.tenant.as_str()).collect();
        assert_eq!(
            order,
            ["alice", "near", "bob", "big-uncapped", "aaa-unread"]
        );
        // Ties keep the server's order rather than reshuffling.
        let mut tie = vec![
            row("b", Some(GIB), Some(10 * GIB)),
            row("a", Some(GIB), Some(10 * GIB)),
        ];
        sort_closest(&mut tie, "pack_bytes", "enforced");
        assert_eq!(tie[0].tenant, "b");
    }

    /// The standing's words at every shape: over the cap is AT CAP, a measured
    /// (not enforced) cap at 100 % is NOT, no cap is said, no reading is said.
    #[test]
    fn a_standing_says_at_cap_only_for_a_wall_and_never_invents_a_zero() {
        let at = standing(
            &row("r", Some(10 * GIB + GIB / 4), Some(10 * GIB)),
            "pack_bytes",
            "enforced",
        );
        assert_eq!(
            (at.text().as_str(), at.word().as_str(), at.state, at.width),
            ("10.2 GiB of 10.0 GiB", "AT CAP", BarState::AtLimit, 100)
        );
        let part = standing(
            &row("h", Some(3 * GIB + GIB / 8), Some(10 * GIB)),
            "pack_bytes",
            "enforced",
        );
        assert_eq!(
            (part.text().as_str(), part.word().as_str(), part.width),
            ("3.1 GiB of 10.0 GiB", "31 %", 31)
        );
        let metered = standing(
            &row("m", Some(10 * GIB), Some(10 * GIB)),
            "pack_bytes",
            "measured",
        );
        assert_eq!(metered.word(), "100 %", "full and NOT refused");
        assert_eq!(
            metered.note,
            Some("not enforced"),
            "the note beside it says why 100 % is not AT CAP"
        );
        assert_ne!(metered.state, BarState::AtLimit);
        assert_eq!(at.note, Some("wall"), "and a wall is called one");
        // A count formats as a count, through the same formatter the bars use.
        let repos = standing_of("repos", Some(26), Some(50), "measured");
        assert_eq!(
            (repos.text().as_str(), repos.word().as_str()),
            ("26 of 50", "52 %")
        );
        assert_eq!(
            standing_of("repos", Some(26), None, "measured").text(),
            "26"
        );
        let uncapped = standing(&row("u", Some(5 * GIB), None), "pack_bytes", "enforced");
        assert_eq!(
            (
                uncapped.text().as_str(),
                uncapped.word().as_str(),
                uncapped.state
            ),
            ("5.0 GiB", "no cap", BarState::Uncapped)
        );
        let zero_cap = standing(&row("z", Some(5 * GIB), Some(0)), "pack_bytes", "enforced");
        assert_eq!(
            zero_cap.cap, None,
            "a cap of 0 is how a product spells unlimited"
        );
        let unread = standing(&row("n", None, Some(10 * GIB)), "pack_bytes", "enforced");
        assert_eq!(
            (unread.text().as_str(), unread.word().as_str(), unread.width),
            ("not measured yet", "not read yet", 0)
        );
        assert!(!unread.text().contains("0 B"), "no reading is not zero");
    }

    #[test]
    fn the_fold_never_hides_a_single_row() {
        assert_eq!(fold(9, 5), (5, 4));
        assert_eq!(fold(5, 5), (5, 0));
        assert_eq!(
            fold(6, 5),
            (5, 1),
            "six rows: five and one more — the fold saves a screen, not a click"
        );
        assert_eq!(fold(0, 5), (0, 0));
        assert_eq!(fold(2, 5), (2, 0));
    }

    /// **Only SEALED earns the badge.** RED against `state != ""`: UNKNOWN would
    /// have read as "cannot grow", and an operator would plan a reinstall on a
    /// box nobody measured.
    #[test]
    fn only_a_sealed_box_says_cannot_grow() {
        assert_eq!(growth_badge("sealed"), Some(("⚠ cannot grow", Tone::Warn)));
        assert_eq!(
            growth_badge("unknown"),
            None,
            "not a promise either way, so not a refusal either"
        );
        assert_eq!(
            growth_badge("operator_only"),
            None,
            "it can be grown, by hand"
        );
        assert_eq!(growth_badge(""), None, "never measured");
    }

    /// **A box that grows at runtime does not wear `cannot grow`.** RED on the
    /// live front 2026-09-15: the warning sat above a Data set panel offering
    /// Grow. The words follow the growth word the product reported.
    #[test]
    fn a_box_that_grows_at_runtime_says_so_and_warns_nothing() {
        let (word, tone) = growth_badge("at_runtime").expect("growth at runtime is worth saying");
        assert!(!word.contains("cannot"), "{word}");
        assert!(word.contains("runtime"), "{word}");
        assert_eq!(tone, Tone::Pass);
        let (restart, tone) = growth_badge("at_restart").unwrap();
        assert!(
            !restart.contains("cannot") && tone != Tone::Warn,
            "{restart}"
        );
    }

    /// **Only the OVERSOLD arm draws the line, and it draws the number.** RED
    /// against keying on `oversell_bytes > 0`: the same field carries a `fits`
    /// fleet's headroom, and 94 GiB of spare room rendered as "oversold by
    /// 94.0 GiB" — the wrong task, stated with confidence. The string is
    /// asserted whole: `is_some()` cannot tell `promised` from `can serve`.
    #[test]
    fn only_an_oversold_fleet_says_so_and_it_says_the_number() {
        let meter = |oversell: &str, oversell_bytes: u64, capacity: Option<u64>| FleetMeterJson {
            meter: "store_bytes".into(),
            tenants: 2,
            used: 8 * GIB + 800 * (1 << 20),
            allowance: 16 * GIB,
            oversell: oversell.into(),
            oversell_bytes,
            capacity,
            ..FleetMeterJson::default()
        };
        assert_eq!(
            oversell_line(&meter("oversold", 4 * GIB, Some(12 * GIB))).as_deref(),
            Some("oversold by 4.0 GiB — promised 16.0 GiB, can serve 12.0 GiB"),
            "two tenants promised 16 GiB on a 12 GiB box"
        );
        assert_eq!(
            oversell_line(&meter("fits", 94 * GIB, Some(110 * GIB))),
            None,
            "headroom is not a task, and `oversell_bytes` is the headroom here"
        );
        assert_eq!(
            oversell_line(&meter("unmeasured", 0, None)),
            None,
            "the big number already says capacity unmeasured"
        );
        assert_eq!(
            oversell_line(&meter("not_storage", 0, None)),
            None,
            "a meter that promises no bytes cannot oversell them"
        );
    }

    #[test]
    fn the_attention_states_are_the_middle_rungs() {
        assert!(
            needs_attention_state("grace")
                && needs_attention_state("suspended")
                && needs_attention_state("retention")
        );
        assert!(
            !needs_attention_state("paid")
                && !needs_attention_state("free")
                && !needs_attention_state("")
        );
    }
}

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

    const GIB: u64 = 1 << 30;

    /// **What a sale actually netted, and what an unreported fee must not become.**
    #[test]
    fn the_money_line_folds_the_net_and_stays_quiet_when_no_fee_was_reported() {
        let quiet = SaleJson {
            price_minor: 12_500,
            currency: "SEK".into(),
            cost_minor: 1_000,
            cost_currency: "EUR".into(),
            ..SaleJson::default()
        };
        assert_eq!(sale_money_line(&quiet), "price 125.00 SEK · cost 10.00 EUR");

        // Stripe kept 5.74 of 125.00 — measured, not computed from a rate card.
        let charged = SaleJson {
            fee_minor: Some(574),
            fee_currency: "SEK".into(),
            ..quiet.clone()
        };
        assert_eq!(
            sale_money_line(&charged),
            "price 125.00 SEK · cost 10.00 EUR · vendor kept 5.74 SEK · net 119.26 SEK"
        );

        // A vendor that says it kept nothing is not the same as one that said
        // nothing, and the line has to show the difference.
        let free = SaleJson {
            fee_minor: Some(0),
            fee_currency: "SEK".into(),
            ..quiet.clone()
        };
        assert!(
            sale_money_line(&free).contains("vendor kept 0.00 SEK"),
            "{}",
            sale_money_line(&free)
        );
        assert!(!sale_money_line(&quiet).contains("vendor kept"));
    }

    /// **The VAT per order** (owner 2026-09-28): the figure, its currency and
    /// the rate; the vendor's own figure says so; a sale that never carried
    /// tax (declined) shows nothing rather than a zero.
    #[test]
    fn a_sale_says_its_vat_and_whose_figure_it_is() {
        let base = SaleJson {
            price_minor: 22_000,
            currency: "SEK".into(),
            ..SaleJson::default()
        };
        assert_eq!(
            sale_tax_line(&base),
            None,
            "no tax on the row: nothing is said"
        );
        let folded = SaleJson {
            tax_minor: Some(5_500),
            tax_currency: "SEK".into(),
            vat_basis_points: 2_500,
            ..base.clone()
        };
        assert_eq!(
            sale_tax_line(&folded).as_deref(),
            Some("VAT 55.00 SEK (25 %)")
        );
        let stripe = SaleJson {
            tax_minor: Some(5_499),
            tax_from_vendor: true,
            ..folded.clone()
        };
        assert_eq!(
            sale_tax_line(&stripe).as_deref(),
            Some("VAT 54.99 SEK (as the vendor charged)")
        );
        let odd = SaleJson {
            vat_basis_points: 1_250,
            ..folded
        };
        assert_eq!(
            sale_tax_line(&odd).as_deref(),
            Some("VAT 55.00 SEK (12.5 %)")
        );
        // Stripe decides per country, each payment a row (owner 2026-10-11): the
        // buyer's country and the rate Stripe computed for it are on the row.
        let de = SaleJson {
            tax_minor: Some(190),
            tax_currency: "EUR".into(),
            vat_basis_points: 1_900,
            tax_from_vendor: true,
            customer_country: "DE".into(),
            ..base.clone()
        };
        assert_eq!(
            sale_tax_line(&de).as_deref(),
            Some("VAT 1.90 EUR · DE 19 % (as the vendor charged)")
        );
        let se = SaleJson {
            customer_country: "SE".into(),
            tax_from_vendor: false,
            ..odd
        };
        assert_eq!(
            sale_tax_line(&se).as_deref(),
            Some("VAT 55.00 SEK · SE (12.5 %)")
        );
    }

    fn sale(tenant: &str, outcome: &str, drawn: bool, needs_you: bool) -> SaleJson {
        SaleJson {
            reference: format!("gunnar/{tenant}/2026-09-04"),
            tenant: tenant.into(),
            price_minor: 22_000,
            currency: "SEK".into(),
            caps: [("pack_bytes".to_string(), 10 * GIB)].into(),
            at_unix_ms: 1_788_868_800_000,
            outcome: outcome.into(),
            drawn,
            needs_you,
            ..SaleJson::default()
        }
    }

    /// **Outcomes, not steps** — and DECLINED is not a refund. RED against a
    /// two-arm mark (`granted` → ✓, else ↩): a vendor's "no" would have shown
    /// the refund arrow over money that was never locked.
    #[test]
    fn a_sale_is_one_line_and_declined_is_not_a_refund() {
        assert_eq!(
            sale_line(&sale("bob", "granted", true, false)),
            "Sold bob 10.0 GiB"
        );
        // The DELTA is what was sold: 90 GiB, on a tenant now allowed 100.
        let more = SaleJson {
            caps: [("pack_bytes".to_string(), 100 * GIB)].into(),
            delta: [("pack_bytes".to_string(), 90 * GIB)].into(),
            ..sale("alice", "granted", true, false)
        };
        assert_eq!(
            sale_line(&more),
            "Sold alice 90.0 GiB",
            "the delta, never the hundred"
        );
        assert_eq!(
            sale_line(&sale("zz-visibility", "refunded", true, false)),
            "Refunded zz-visibility 10.0 GiB"
        );
        assert_eq!(
            sale_line(&sale("bob", "declined", false, false)),
            "Declined bob 10.0 GiB"
        );
        assert_eq!(
            sale_line(&sale("bob", "open", false, true)),
            "Open: bob 10.0 GiB"
        );
        assert_eq!(
            sale_line(&sale("x", "banana", false, false)),
            "banana: x 10.0 GiB",
            "an outcome this console does not know is shown, not guessed"
        );

        assert_eq!(sale_mark("granted", true), (Tone::Pass, "✓"));
        assert_eq!(sale_mark("refunded", true), (Tone::Warn, "↩ refunded"));
        let (tone, word) = sale_mark("refunded", false);
        assert_eq!(tone, Tone::Warn);
        assert!(
            word.contains("nothing drawn"),
            "a released reservation moved no money: {word}"
        );
        let (tone, word) = sale_mark("declined", false);
        assert_eq!(tone, Tone::Bad);
        assert!(
            !word.contains('↩'),
            "declined must not wear the refund arrow: {word}"
        );
        assert_eq!(sale_mark("open", false).0, Tone::Live);
        assert_eq!(sale_mark("weird", false).0, Tone::Plain);
    }

    #[test]
    fn needs_you_floats_the_open_attempts_and_keeps_the_servers_order() {
        let (needs, rest) = split_needs_you(vec![
            sale("a", "granted", true, false),
            sale("b", "open", false, true),
            sale("c", "refunded", true, false),
            sale("d", "open", false, true),
        ]);
        assert_eq!(
            needs.iter().map(|s| s.tenant.as_str()).collect::<Vec<_>>(),
            ["b", "d"]
        );
        assert_eq!(
            rest.iter().map(|s| s.tenant.as_str()).collect::<Vec<_>>(),
            ["a", "c"]
        );
        assert_eq!(
            needs_you_line(&sale("b", "open", false, true)),
            "b — money reserved, not yet captured or released"
        );
        let mut said = sale("b", "open", false, true);
        said.reason = "paid, never delivered".into();
        assert_eq!(
            needs_you_line(&said),
            "b — paid, never delivered",
            "the server's words win when it gave any"
        );
    }

    #[test]
    fn the_day_and_the_caps_read_as_a_row() {
        assert_eq!(month_day(1_788_868_800_000), "Sep 8");
        assert_eq!(month_day(0), "Jan 1");
        assert_eq!(month_day(parse_date_ms("2026-12-31").unwrap()), "Dec 31");
        assert_eq!(
            caps_phrase(&[("pack_bytes".to_string(), 10 * GIB)].into()),
            "10.0 GiB"
        );
        assert_eq!(
            caps_phrase(
                &[
                    ("pack_bytes".to_string(), 10 * GIB),
                    ("repos".to_string(), 50)
                ]
                .into()
            ),
            "10.0 GiB + 50 repositories"
        );
        assert_eq!(caps_phrase(&BTreeMap::new()), "no caps");
    }

    /// **Raw meter names leave the surface.** Every meter gunnar and holger
    /// declare has a word; a name nobody mapped comes back as itself, so it is
    /// visible rather than swallowed. RED against a mapping that returned the
    /// name for `pack_bytes` — the row would still say `pack_bytes`.
    #[test]
    fn every_declared_meter_has_plain_words_and_an_unknown_one_keeps_its_name() {
        assert_eq!(plain_meter("pack_bytes"), "Repository data");
        assert_eq!(plain_meter("cache_bytes"), "Cache");
        assert_eq!(plain_meter("tombstoned_bytes"), "Deleted, not purged");
        assert_eq!(plain_meter("repos"), "Repositories");
        for name in [
            "pack_bytes",
            "cache_bytes",
            "cold_bytes",
            "lfs_bytes",
            "tombstoned_bytes",
            "repos",
            "open_store_ram_bytes",
            "concurrent_transfers",
            "cpu_millicores",
            "ram_bytes",
            "artifacts",
            "archive_bytes",
            "bytes_served",
            "requests",
            "proxy_upstream_hits",
        ] {
            let word = plain_meter(name);
            assert_ne!(word, name, "{name} still shows its wire name");
            assert!(!word.contains('_'), "{name} → {word} is still jargon");
        }
        assert_eq!(
            plain_meter("something_new"),
            "something_new",
            "unmapped is shown as itself, never guessed"
        );
    }
}

// ── Screen 2: IRON — what we actually own out there, and the box itself ──

/// **The panel's title: `Iron · <provider>`.** It said `Cloud ·` until
/// 2026-09-05, when the `server` provider arrived — the iron a self-hoster
/// already has is not in any cloud, and a panel that called the box "cloud"
/// would be naming a vendor that is not there. Iron is what both are.
pub fn cloud_title(c: &CloudJson) -> String {
    format!("Iron · {}", c.provider)
}

/// **The three states the IRON panel can be in.** `Hidden` is a box with no
/// cloud at all — the server sent no block, or the registry's `none` stub is
/// the only "provider" — and the panel does not render. `Unavailable` is a
/// cloud that exists and could not be listed: the sentence renders where the
/// list would, because absence is not zero. `Show` is a listing.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CloudPanel {
    Hidden,
    Unavailable(String),
    Show,
}

pub fn cloud_panel_state(cloud: Option<&CloudJson>) -> CloudPanel {
    match cloud {
        // No block, a server older than the field (the BFF writes `provider: ""`),
        // or the registry's `none` stub: this box has no cloud.
        None => CloudPanel::Hidden,
        Some(c) if c.provider.is_empty() || c.provider == "none" => CloudPanel::Hidden,
        Some(c) if !c.available => CloudPanel::Unavailable(if c.error.is_empty() {
            format!("{} could not be listed", c.provider)
        } else {
            format!("{} could not be listed: {}", c.provider, c.error)
        }),
        Some(_) => CloudPanel::Show,
    }
}

/// **The panel's one line**: `10.0 GiB owned · ~2.26 EUR/month`. `unpriced > 0`
/// makes the money a floor and the line says `at least`. The currency is the
/// provider's own, as the wire carries it; a server older than the field sends
/// none, and the amount then stands alone rather than beside a guessed word.
pub fn cloud_summary_line(c: &CloudJson) -> String {
    if c.owned.is_empty() {
        return "nothing owned".to_owned();
    }
    let floor = if c.unpriced > 0 { "at least " } else { "" };
    let cost = if c.currency.is_empty() {
        money_amount(c.monthly_minor)
    } else {
        money(c.monthly_minor, &c.currency)
    };
    format!("{} owned · {floor}~{cost}/month", bytes(c.total_bytes))
}

/// One owned resource, as a row: every field is a string the page can place.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CloudRow {
    pub id: String,
    pub size: String,
    pub zone: String,
    /// `on <server>`, or `detached` — the proto's own word for an empty
    /// `attached_to`.
    pub attached: String,
    pub tenant: String,
    pub reference: String,
    /// `provenance == "sandbox"`: fake iron, and the row wears a badge so a
    /// tenant on it is distinguishable forever.
    pub play_iron: bool,
    /// `provenance == "server"`: the box itself — the product's own data set,
    /// never bought, never an orphan. Wears `this box` instead of a play badge.
    pub this_box: bool,
}

/// A row the provider lists as a SERVER (MAP-SEG 2): a machine, not a volume.
pub fn is_server(o: &CloudOwnedJson) -> bool {
    o.kind == "server"
}

fn cloud_row(o: &CloudOwnedJson) -> CloudRow {
    // MAP-SEG 2 (owner 2026-10-26: "server/… 0 B detached"): a server has no size of its
    // own and is attached to nothing — it IS what disks attach to. It reads as a server.
    let server = is_server(o);
    CloudRow {
        id: o.id.clone(),
        size: if server {
            "server".to_owned()
        } else {
            bytes(o.gib << 30)
        },
        zone: o.zone.clone(),
        attached: if server {
            String::new()
        } else if o.attached_to.is_empty() {
            "detached".to_owned()
        } else {
            format!("on {}", o.attached_to)
        },
        tenant: o.tenant.clone(),
        reference: o.reference.clone(),
        play_iron: o.provenance == "sandbox",
        this_box: o.provenance == "server",
    }
}

/// **One row per OWNED resource** — at the provider AND in this product's
/// ledger. Orphans are not here: they are tasks, and `cloud_needs_you` has them.
/// The servers first, then the disks (MAP-SEG 2): a machine, then what it holds.
pub fn cloud_rows(c: &CloudJson) -> Vec<CloudRow> {
    c.owned
        .iter()
        .filter(|o| is_server(o))
        .chain(c.owned.iter().filter(|o| !is_server(o)))
        .map(cloud_row)
        .collect()
}

/// **Where a row stands**: `se-sto1 · on <server>` for a disk, the zone alone for a server.
pub fn cloud_where(r: &CloudRow) -> String {
    if r.attached.is_empty() {
        r.zone.clone()
    } else {
        format!("{} · {}", r.zone, r.attached)
    }
}

/// **A cloud task**: one of the two bugs that cost money, in plain words.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NeedsYou {
    /// `orphan` | `missing`.
    pub what: &'static str,
    /// The resource id for an orphan; the tenant for a missing resource.
    pub subject: String,
    /// `10.0 GiB se-sto1` for an orphan; the fingerprint for a missing one.
    pub detail: String,
    pub why: &'static str,
}

impl NeedsYou {
    pub fn line(&self) -> String {
        format!(
            "{} {} {} — {}",
            self.what, self.subject, self.detail, self.why
        )
    }
}

/// **Orphans, then missing.** An orphan is at the provider and in no order:
/// we pay for it and nothing owns it. A missing resource is in the ledger and
/// not at the provider: sold, and not there. Both are tasks, not history.
pub fn cloud_needs_you(c: &CloudJson) -> Vec<NeedsYou> {
    // MAP-3: an orphan BACKUP (its disk gone, or itself an orphan) says it is one, by its title.
    let orphans = c.orphans.iter().map(|o| {
        let backup = o.kind == "backup";
        let title = if o.title.is_empty() {
            String::new()
        } else {
            format!(" {:?}", o.title)
        };
        NeedsYou {
            what: "orphan",
            subject: o.id.clone(),
            detail: format!(
                "{}{} {}{title}",
                if backup { "backup " } else { "" },

The file goes on; the console shows all of it.