//! The roster response, held against bytes captured from the live oracle.
//!
//! The fixture masks the two volatile fields (`Date:`, `Server:`) and records
//! the observed `Server` string separately, so this can assert the full byte
//! layout while still failing loudly if the oracle's Python version drifts away
//! from the adapter's `ORACLE_SERVER` constant.

use openfut_adapter_fifa17::roster::{self, Method};

const MASK: &str = "<MASKED>";

fn fixture() -> String {
    let path = format!("{}/fixtures/roster.json", env!("CARGO_MANIFEST_DIR"));
    std::fs::read_to_string(&path)
        .unwrap_or_else(|e| panic!("roster fixtures missing at {path}: {e}"))
}

/// Minimal extraction: the fixture is written by our own generator and is a
/// flat object, so a JSON dependency would be overkill in a crate that has none.
fn field(text: &str, key: &str) -> String {
    let needle = format!("\"{key}\"");
    let at = text
        .find(&needle)
        .unwrap_or_else(|| panic!("fixture has no {key}"));
    let rest = &text[at + needle.len()..];
    let colon = rest.find(':').expect("key: value");
    let open = rest[colon..].find('"').expect("value opens") + colon;
    let close = rest[open + 1..].find('"').expect("value closes");
    rest[open + 1..open + 1 + close].to_string()
}

fn expected(method: &str) -> Vec<u8> {
    // Scope the search to the responses object so a key never matches elsewhere.
    let text = fixture();
    let at = text.find("\"responses\"").expect("responses");
    let hex = field(&text[at..], method);
    (0..hex.len())
        .step_by(2)
        .map(|i| u8::from_str_radix(&hex[i..i + 2], 16).expect("hex"))
        .collect()
}

/// Re-apply the generator's masking so the comparison is like-for-like.
fn mask(raw: &[u8]) -> Vec<u8> {
    let text = String::from_utf8_lossy(raw);
    let masked: String = text
        .split("\r\n")
        .map(|line| {
            if line.starts_with("Date: ") {
                format!("Date: {MASK}")
            } else if line.starts_with("Server: ") {
                format!("Server: {MASK}")
            } else {
                line.to_string()
            }
        })
        .collect::<Vec<_>>()
        .join("\r\n");
    masked.into_bytes()
}

fn check(method: Method, name: &str) {
    let got = roster::roster_response(
        method,
        roster::ORACLE_SERVER,
        "Tue, 11 Aug 2026 05:15:13 GMT",
    );
    assert_eq!(
        String::from_utf8_lossy(&mask(&got)),
        String::from_utf8_lossy(&expected(name)),
        "{name} differs from the oracle"
    );
}

#[test]
fn get_matches_the_oracle() {
    check(Method::Get, "GET");
}

#[test]
fn head_matches_the_oracle() {
    check(Method::Head, "HEAD");
}

/// Including the quirk: headers advertising a body that never arrives.
#[test]
fn post_matches_the_oracle() {
    check(Method::Post, "POST");
}

/// If the container's Python changes, `ORACLE_SERVER` is stale and every
/// response this adapter builds is wrong in a byte the oracle would have got
/// right. The fixture records what was actually observed so that drift is a
/// test failure rather than a silent divergence.
#[test]
fn the_server_constant_still_matches_the_observed_oracle() {
    let observed = field(&fixture(), "observed_server");
    assert_eq!(
        roster::ORACLE_SERVER,
        observed,
        "roster::ORACLE_SERVER is stale — the oracle now sends {observed:?}. \
         Regenerate fixtures and update the constant."
    );
}

/// The masking must not be able to hide a real difference. If `Date:` were
/// dropped rather than masked, a response missing it entirely would still pass.
#[test]
fn masking_does_not_hide_a_missing_header() {
    let good = roster::roster_response(Method::Get, roster::ORACLE_SERVER, "X");
    let without_date: Vec<u8> = String::from_utf8_lossy(&good)
        .split("\r\n")
        .filter(|l| !l.starts_with("Date: "))
        .collect::<Vec<_>>()
        .join("\r\n")
        .into_bytes();
    assert_ne!(
        mask(&good),
        mask(&without_date),
        "masking collapsed a missing Date into a match"
    );
}
