//! No floating point on the integer decode paths (a test of the sources).
//!
//! The integer capabilities decode to the same bytes on every machine because no step of
//! their decode rounds: every value is an integer, and so is every operation on it. A
//! floating-point step on such a path is exact only by an argument someone must make and
//! keep, and a decoder compiled to a module that admits no floating point cannot hold one
//! at all. So these paths hold none in their source, and this test reads the source of
//! every module on them, and of the operator set they run their networks through
//! (`wai-ops`, every file), outside tests and model-checker harnesses, for a
//! floating-point type, literal or vector intrinsic: `f32` or `f64` (alone or in a name
//! such as `vaddq_f32` or `f32x4`), a numeric literal with a fraction or an exponent, or an
//! x86 intrinsic on packed or scalar floats (`_mm*_pd`, `_ps`, `_sd`, `_ss`, `_ph`).
//!
//! The one place a float may be named is a conversion from a float source by contract,
//! listed in [`BOUNDARIES`] with its reason: it reads its argument's bits, and every step
//! after is integer.
//!
//! It reads source, not a dependency's code, so a dependency could bring floating point
//! back unseen. The one that did is serde_json: its number parser is binary64 code, which
//! runs on a fraction, an exponent or an integer past `u64` before an integer read refuses
//! it. So the modules read here call no serde_json parser either (but on the lines in
//! [`JSON_BOUNDARIES`]): the parameter sets' JSON (`entropy.json`, a `WMB1` bundle's
//! tables) is read by [`crate::strict_json`], which is read here, and `int_synth.json` by
//! the operator set's own reader. The tools' JSON forms (`from_value` and the
//! `*_from_json` converters, over a `serde_json::Value`) are converters and debugging aids,
//! not decodes: their callers parse the text. Beyond these modules and the operator set, a
//! decode reaches the capability table's lookups (`codecs`), the pin checks and their
//! SHA-256 (`pin`, whose listing is text) and `std`, none of which reads JSON; this test
//! does not read them.
//!
//! The test fails when a file of the integer modules (`int_*.rs`, and the files under
//! `int_kernels/` and `int_payload/` but its tests) is not in [`SOURCES`], so a new one is
//! read from the start.
//!
//! Outside these paths, recorded and not read: `exact_tonemap`'s ST 2094-50 reader
//! (`read_metadata_set`) forms `Alternate::m_f64`, the slopes in binary64 (a tangent, and a
//! rational's quotient), for §6's measurement alone, beside the Q30 slopes its integer
//! derivation uses. The derivation's output does not depend on them, but its reader cannot
//! be built into a module with no floating point until they are formed apart from it.

/// The modules on the integer decode paths: the kernels, the operator-set bridge, the
/// payload formats and their decode, the integer codecs, the entropy coder and its
/// bucketing, and the strict reader the decoders read their parameter sets' JSON with
/// (and the splat codec its JSON form); and the other integer entropy decoders and the
/// integer decodes built on them: JPEG AI's mANS decoder, the residual refinement
/// (`WIR1`) and its step, the NNC tensor's arithmetic decoder and its tensor decode,
/// DRED's range decoder, payload, tables, weights, lowering and stream (all of
/// `dred-int/1` but its floating-point reference and its measurement), and the faces
/// from keypoints.
const SOURCES: &[(&str, &str)] = &[
    ("int_kernels.rs", include_str!("int_kernels.rs")),
    ("int_kernels/narrow.rs", include_str!("int_kernels/narrow.rs")),
    ("int_kernels/x86.rs", include_str!("int_kernels/x86.rs")),
    ("int_ops.rs", include_str!("int_ops.rs")),
    ("int_payload/wire.rs", include_str!("int_payload/wire.rs")),
    ("int_payload/mod.rs", include_str!("int_payload/mod.rs")),
    ("int_synth.rs", include_str!("int_synth.rs")),
    ("int_hyper_synth.rs", include_str!("int_hyper_synth.rs")),
    ("int_mlicpp.rs", include_str!("int_mlicpp.rs")),
    ("int_video.rs", include_str!("int_video.rs")),
    ("int_video_enc.rs", include_str!("int_video_enc.rs")),
    ("int_audio.rs", include_str!("int_audio.rs")),
    ("int_splat.rs", include_str!("int_splat.rs")),
    ("int_world.rs", include_str!("int_world.rs")),
    ("int_sr.rs", include_str!("int_sr.rs")),
    ("int_set_cache.rs", include_str!("int_set_cache.rs")),
    ("int_transform.rs", include_str!("int_transform.rs")),
    ("int_transformer.rs", include_str!("int_transformer.rs")),
    ("int_entropy.rs", include_str!("int_entropy.rs")),
    ("rans.rs", include_str!("rans.rs")),
    ("strict_json.rs", include_str!("strict_json.rs")),
    ("jpegai_mans.rs", include_str!("jpegai_mans.rs")),
    ("staged/wir1.rs", include_str!("staged/wir1.rs")),
    ("staged/wir1_arith.rs", include_str!("staged/wir1_arith.rs")),
    ("prior_wire/cabac.rs", include_str!("prior_wire/cabac.rs")),
    ("prior_wire/nnc.rs", include_str!("prior_wire/nnc.rs")),
    ("dred_exact/ec.rs", include_str!("dred_exact/ec.rs")),
    ("dred_exact/packet.rs", include_str!("dred_exact/packet.rs")),
    ("dred_exact/payload.rs", include_str!("dred_exact/payload.rs")),
    ("dred_exact/tables.rs", include_str!("dred_exact/tables.rs")),
    ("dred_exact/weights.rs", include_str!("dred_exact/weights.rs")),
    ("dred_exact/lower.rs", include_str!("dred_exact/lower.rs")),
    ("dred_exact/scenario.rs", include_str!("dred_exact/scenario.rs")),
    ("avatar_face.rs", include_str!("avatar_face.rs")),
];

/// The lines that may name a float, each a conversion from a float source by contract.
/// `int_entropy`'s two take a float model's scale table and a float hyper-decoder's σ
/// (codecs that stay `NeuralFloat`); each reads the binary32 bits and forms the
/// fixed-point value in integers.
const BOUNDARIES: &[(&str, &str)] = &[
    ("int_entropy.rs", "pub fn fixed_scale_edges(scale_table: &[f32], frac: u32) -> Vec<i64> {"),
    ("int_entropy.rs", "pub fn quantize_scale(sigma: f32, frac: u32) -> i64 {"),
];

/// The lines that may call a serde_json parser, each with its reason. `int_synth`'s is
/// the direct decode kept for the differential tests (`legacy`, built only for tests and
/// under the test-only `legacy_int`); a sink's decode reads `int_synth.json` through the
/// operator set. `avatar_face`'s is the conformance verifier's (§9) read of an object's
/// envelope: its manifest, the container's JSON (SPEC §2), read before any face is
/// decoded; the faces themselves (§7) read no JSON.
const JSON_BOUNDARIES: &[(&str, &str)] = &[
    ("int_synth.rs", "let spec: SynthSpec = serde_json::from_slice(spec_json)"),
    ("avatar_face.rs", "let m: Value = serde_json::from_slice(b.get(8..8 + n)?).ok()?;"),
];

/// `src` with every comment, string literal and character literal blanked (newlines
/// kept, so line numbers hold): what is left is code.
fn code_of(src: &str) -> String {
    let b = src.as_bytes();
    let mut out = String::with_capacity(src.len());
    let blank = |out: &mut String, s: &str| out.extend(s.chars().map(|c| if c == '\n' { '\n' } else { ' ' }));
    let ident = |i: usize| i > 0 && (b[i - 1].is_ascii_alphanumeric() || b[i - 1] == b'_');
    let mut i = 0;
    while i < b.len() {
        let rest = &src[i..];
        let end = if rest.starts_with("//") {
            i + rest.find('\n').unwrap_or(rest.len())
        } else if rest.starts_with("/*") {
            let (mut j, mut depth) = (i, 0);
            loop {
                if src[j..].starts_with("/*") {
                    depth += 1;
                    j += 2;
                } else if src[j..].starts_with("*/") {
                    depth -= 1;
                    j += 2;
                    if depth == 0 {
                        break j;
                    }
                } else {
                    j += 1;
                }
            }
        } else if let Some(q) = raw_string_open(rest).filter(|_| !ident(i)) {
            let close = format!("\"{}", "#".repeat(rest[..q].matches('#').count()));
            i + q + 1 + rest[q + 1..].find(&close).expect("a raw string closes") + close.len()
        } else if b[i] == b'"' || (!ident(i) && rest.starts_with("b\"")) {
            let mut j = i + rest.find('"').unwrap() + 1;
            while b[j] != b'"' {
                j += if b[j] == b'\\' { 2 } else { 1 };
            }
            j + 1
        } else if b[i] == b'\'' {
            // A character literal ('x', '\n', '\u{..}'), not a lifetime ('a).
            let mut cs = rest.char_indices().skip(1);
            match (cs.next(), cs.next()) {
                // The escaped character is at 2 (and may be a quote); the close follows.
                (Some((_, '\\')), _) => i + 3 + rest[3..].find('\'').unwrap() + 1,
                (Some(_), Some((k, '\''))) => i + k + 1,
                _ => i + 1,
            }
        } else {
            let c = rest.chars().next().unwrap();
            out.push(c);
            i += c.len_utf8();
            continue;
        };
        if b[i] == b'\'' && end == i + 1 {
            out.push('\'');
        } else {
            blank(&mut out, &src[i..end]);
        }
        i = end;
    }
    out
}

/// Where the opening quote of a raw string literal starting `rest` is (`r"`, `r#"`,
/// `br##"` and so on), or `None` (`r#ident`, a raw identifier, is not one).
fn raw_string_open(rest: &str) -> Option<usize> {
    let after = rest.strip_prefix("br").or_else(|| rest.strip_prefix('r'))?;
    let hashes = after.len() - after.trim_start_matches('#').len();
    after[hashes..].starts_with('"').then_some(rest.len() - after.len() + hashes)
}

/// The lines of `code`, numbered from 1, without the items a `#[cfg(test)]` or
/// `#[cfg(kani)]` (alone or under `all(..)`) keeps out of a decoder's build.
fn decoder_lines(code: &str) -> Vec<(usize, &str)> {
    let lines: Vec<&str> = code.lines().collect();
    let test_only = |l: &str| {
        let t = l.trim();
        ["#[cfg(test)]", "#[cfg(kani)]"].contains(&t) || t.starts_with("#[cfg(all(test,") || t.starts_with("#[cfg(all(kani,")
    };
    let mut out = Vec::new();
    let mut i = 0;
    while i < lines.len() {
        if !test_only(lines[i]) {
            out.push((i + 1, lines[i]));
            i += 1;
            continue;
        }
        // Skip the attributes, then the item: to its `;`, or its braces' close.
        let mut j = i;
        while j < lines.len() && (lines[j].trim().starts_with("#[") || lines[j].trim().is_empty()) {
            j += 1;
        }
        let (mut depth, mut opened) = (0i64, false);
        while j < lines.len() {
            let l = lines[j];
            depth += l.matches('{').count() as i64 - l.matches('}').count() as i64;
            opened |= l.contains('{');
            if (!opened && l.trim_end().ends_with(';')) || (opened && depth <= 0) {
                break;
            }
            j += 1;
        }
        i = j + 1;
    }
    out
}

/// The floating-point tokens of one line of code.
fn float_tokens(line: &str) -> Vec<&str> {
    let b = line.as_bytes();
    let mut out = Vec::new();
    let mut i = 0;
    while i < b.len() {
        let start = i;
        if b[i].is_ascii_alphabetic() || b[i] == b'_' {
            while i < b.len() && (b[i].is_ascii_alphanumeric() || b[i] == b'_') {
                i += 1;
            }
            let t = &line[start..i];
            let parts: Vec<&str> = t.split('_').collect();
            let float_part = parts.iter().any(|p| matches!(*p, "f32" | "f64") || p.starts_with("f32x") || p.starts_with("f64x") || p.starts_with("float"));
            let x86 = t.starts_with("_mm") && matches!(parts.last(), Some(&("pd" | "ps" | "sd" | "ss" | "ph")));
            if float_part || x86 {
                out.push(t);
            }
        } else if b[i].is_ascii_digit() {
            // A numeric literal; `x.0` (a field) is not one.
            let field = start > 0 && b[start - 1] == b'.';
            while i < b.len() && (b[i].is_ascii_alphanumeric() || b[i] == b'_') {
                i += 1;
            }
            if i + 1 < b.len() && b[i] == b'.' && b[i + 1].is_ascii_digit() {
                i += 1;
                while i < b.len() && (b[i].is_ascii_alphanumeric() || b[i] == b'_') {
                    i += 1;
                }
            }
            if i < b.len() && matches!(b[i], b'+' | b'-') && matches!(b[i - 1], b'e' | b'E') {
                i += 1;
                while i < b.len() && b[i].is_ascii_digit() {
                    i += 1;
                }
            }
            let t = &line[start..i];
            let based = t.starts_with("0x") || t.starts_with("0b") || t.starts_with("0o");
            let exp = !based && t.bytes().skip(1).zip(t.bytes().skip(2)).any(|(e, d)| matches!(e, b'e' | b'E') && (d.is_ascii_digit() || d == b'+' || d == b'-'));
            if !field && !based && (t.contains('.') || exp || t.ends_with("f32") || t.ends_with("f64")) {
                out.push(t);
            }
        } else {
            i += 1;
        }
    }
    out
}

/// The serde_json parsers one line of code names: `from_slice`, `from_str`,
/// `from_reader` or `Deserializer` on a line that names `serde_json`.
fn json_parsers(line: &str) -> Vec<&str> {
    if !line.contains("serde_json") {
        return Vec::new();
    }
    line.split(|c: char| !(c.is_ascii_alphanumeric() || c == '_'))
        .filter(|t| matches!(*t, "from_slice" | "from_str" | "from_reader" | "Deserializer"))
        .collect()
}

/// Every floating-point token and every serde_json parser on a decoder line of `src`, as
/// `line: token`, but those on a boundary line of `file` for each.
fn findings(file: &str, src: &str) -> Vec<String> {
    let code = code_of(src);
    let source: Vec<&str> = src.lines().collect();
    let on = |list: &[(&str, &str)], n: usize| list.iter().any(|&(f, l)| f == file && source[n - 1].trim() == l);
    let mut out = Vec::new();
    for (n, line) in decoder_lines(&code) {
        let floats = if on(BOUNDARIES, n) { Vec::new() } else { float_tokens(line) };
        let parsers = if on(JSON_BOUNDARIES, n) { Vec::new() } else { json_parsers(line) };
        for t in floats.into_iter().chain(parsers) {
            out.push(format!("{file}:{n}: {t}"));
        }
    }
    out
}

/// Every file under `root`, as its path from `root` with `/` between the parts.
fn files_under(root: &std::path::Path) -> Vec<String> {
    let mut out = Vec::new();
    let mut dirs = vec![String::new()];
    while let Some(d) = dirs.pop() {
        let dir = root.join(&d);
        for e in std::fs::read_dir(&dir).unwrap_or_else(|e| panic!("{}: {e}", dir.display())) {
            let e = e.expect("a directory entry");
            let name = e.file_name().into_string().expect("a UTF-8 file name");
            let rel = if d.is_empty() { name } else { format!("{d}/{name}") };
            if e.file_type().expect("a file type").is_dir() {
                dirs.push(rel);
            } else {
                out.push(rel);
            }
        }
    }
    out.sort();
    out
}

/// Whether `f`, a path under `src/`, is a file of the integer modules: an `int_*.rs`, or a
/// source file under `int_kernels/` or `int_payload/` but the payloads' tests.
fn integer_module(f: &str) -> bool {
    let top = !f.contains('/') && f.starts_with("int_");
    let under = f.starts_with("int_kernels/") || (f.starts_with("int_payload/") && f != "int_payload/tests.rs");
    f.ends_with(".rs") && (top || under)
}

/// The operator set's source files (`wai-ops/src`, every one), as `(name, text)`.
fn operator_set() -> Vec<(String, String)> {
    let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../wai-ops/src");
    files_under(&root).into_iter().filter(|f| f.ends_with(".rs"))
        .map(|f| {
            let text = std::fs::read_to_string(root.join(&f)).unwrap_or_else(|e| panic!("wai-ops/src/{f}: {e}"));
            (format!("wai-ops/src/{f}"), text)
        })
        .collect()
}

#[test]
fn the_integer_decode_paths_hold_no_floating_point() {
    let ops = operator_set();
    assert!(ops.iter().any(|(f, _)| f == "wai-ops/src/interp.rs"), "the operator set's source is read");
    let read = SOURCES.iter().map(|&(f, src)| (f, src)).chain(ops.iter().map(|(f, src)| (f.as_str(), src.as_str())));
    let all: Vec<String> = read.flat_map(|(f, src)| findings(f, src)).collect();
    assert!(all.is_empty(), "floating point, or a serde_json parser, on an integer decode path:\n{}", all.join("\n"));
    // Every boundary is a line that is there, so none outlives its code.
    for &(f, l) in BOUNDARIES.iter().chain(JSON_BOUNDARIES) {
        let src = SOURCES.iter().find(|&&(n, _)| n == f).expect("a listed source").1;
        assert!(src.lines().any(|s| s.trim() == l), "{f}: the boundary line is gone: {l}");
    }
    // Every file of the integer modules is read, so a new one is not passed over.
    let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
    let unread: Vec<String> = files_under(&src).into_iter()
        .filter(|f| integer_module(f) && !SOURCES.iter().any(|&(n, _)| n == f)).collect();
    assert!(unread.is_empty(), "files of the integer modules this test does not read (list them in SOURCES): {unread:?}");
}

/// The reader finds what it must and passes what it must: it is the test.
#[test]
fn the_reader_finds_floats_in_code_and_only_there() {
    let caught = "fn f(n: u64) -> u64 {\n    (n as f64).sqrt() as u64\n}\n\
                  fn g() { let x = 1.5; let y = 2e3; let z = 1u8 as f32; let w = 3f64; }\n\
                  unsafe fn h() { _mm512_sqrt_pd(a); _mm_cvtepi32_ps(b); vaddq_f32(c, d); let v: float32x4_t; }\n\
                  fn j(b: &[u8]) { let v: serde_json::Value = serde_json::from_slice(b).unwrap(); let w = serde_json::from_str::<u8>(\"1\"); }\n";
    let got: Vec<String> = findings("x.rs", caught).into_iter().map(|s| s.split(": ").nth(1).unwrap().to_string()).collect();
    assert_eq!(got, ["f64", "1.5", "2e3", "f32", "3f64", "_mm512_sqrt_pd", "_mm_cvtepi32_ps", "vaddq_f32", "float32x4_t", "from_slice", "from_str"]);
    let passed = "// (n as f64).sqrt()\n/* 1.5 */\nfn f() -> &'static str { let s = \"f64 1.5\"; let c = '.'; let r = r#\"f32\"#; t.0.1; 0x1e5; 1..2; s }\n\
                  #[cfg(test)]\nmod tests {\n    fn t() { let x = 1.0f64; }\n}\n\
                  #[cfg(kani)]\nfn k() { let y: f32 = 0.5; }\n\
                  fn ok<'a>(isqrt: u64, ph: usize, s: &'a str) -> u64 { let q = '\\''; let e = b'\"'; isqrt + 10u64 }\n\
                  // serde_json::from_slice(b)\n\
                  fn v(j: &serde_json::Value, s: &str) -> u32 { let t = \"serde_json::from_str\"; u32::from_str_radix(s, 16).unwrap() }\n\
                  #[cfg(test)]\nfn parse() { let v: serde_json::Value = serde_json::from_slice(b\"1\").unwrap(); }\n";
    assert_eq!(findings("x.rs", passed), Vec::<String>::new());
}
