//! Property-based round-trip and robustness fuzz tests for every DMS
//! value type. Generates pseudo-random valid values, formats them as
//! DMS text, parses them back, and asserts equality (or bit-equality
//! for floats). Also feeds random bytes to the parser and asserts it
//! never panics.
//!
//! Iteration counts are tuned so the suite finishes in <1s in release
//! mode. Bump them locally if hunting a regression.

use dms::{decode, Value};
use unicode_normalization::UnicodeNormalization;

const ITERS_PER_TYPE: u32 = 2_000;
const ROBUSTNESS_ITERS: u32 = 5_000;

// ---------- mini PRNG (xorshift64) -------------------------------------

struct Rng(u64);

impl Rng {
    fn new(seed: u64) -> Self {
        Self(if seed == 0 { 0x9E37_79B9_7F4A_7C15 } else { seed })
    }
    fn next_u64(&mut self) -> u64 {
        let mut x = self.0;
        x ^= x << 13;
        x ^= x >> 7;
        x ^= x << 17;
        self.0 = x;
        x
    }
    fn gen_range(&mut self, lo: u64, hi: u64) -> u64 {
        // [lo, hi)
        debug_assert!(hi > lo);
        lo + (self.next_u64() % (hi - lo))
    }
    fn gen_i64(&mut self) -> i64 {
        self.next_u64() as i64
    }
    fn gen_f64(&mut self) -> f64 {
        f64::from_bits(self.next_u64())
    }
    fn gen_bool(&mut self) -> bool {
        self.next_u64() & 1 == 1
    }
}

// ---------- per-type generators + formatters ---------------------------

fn fmt_int_random_base(n: i64, rng: &mut Rng) -> String {
    // Bases 2/8/16 only emit the magnitude; sign goes in front.
    let base = rng.gen_range(0, 4);
    let (sign, mag) = if n < 0 { ("-", (n as i128).unsigned_abs() as u64) } else { ("", n as u64) };
    let body = match base {
        0 => format!("{}", mag),                              // decimal
        1 => format!("0x{:X}", mag),                          // hex (uppercase digits ok)
        2 => format!("0o{:o}", mag),                          // octal
        _ => format!("0b{:b}", mag),                          // binary
    };
    // Sometimes sprinkle underscores between digit pairs (only for digit runs ≥ 4).
    let body = if rng.gen_bool() { sprinkle_underscores(&body, rng) } else { body };
    format!("{}{}", sign, body)
}

fn sprinkle_underscores(s: &str, rng: &mut Rng) -> String {
    // Find the prefix end (after `0x`/`0o`/`0b` or empty).
    let prefix_end = if s.len() > 2 && s.starts_with("0") && matches!(s.as_bytes()[1], b'x' | b'X' | b'o' | b'b') { 2 } else { 0 };
    let (prefix, body) = s.split_at(prefix_end);
    if body.len() < 4 {
        return s.to_string();
    }
    let mut out = String::from(prefix);
    let bytes = body.as_bytes();
    for i in 0..bytes.len() {
        // Insert at random interior boundaries only — not at start/end and not adjacent to existing underscore.
        if i > 0 && i < bytes.len() - 1 && bytes[i].is_ascii_alphanumeric() && bytes[i - 1].is_ascii_alphanumeric() && rng.gen_range(0, 4) == 0 {
            out.push('_');
        }
        out.push(bytes[i] as char);
    }
    out
}

fn fmt_finite_float(f: f64) -> String {
    debug_assert!(f.is_finite());
    // Use ryu so the parsed back value bit-equals the input (since the parser
    // accepts the canonical decimal form ryu produces).
    let mut buf = ryu::Buffer::new();
    let s = buf.format(f).to_string();
    // ryu emits `1e2` style; DMS requires digit-on-both-sides for the
    // mantissa. ryu always emits a `.` so this is fine for finite values.
    // But ryu does emit `1e0` for whole numbers — that's a valid DMS float
    // (no `.` required when exponent present)? Actually spec says "decimal
    // float requires '.'". Re-check.
    // Spec: "Decimal floats require at least one digit on each side of the
    // decimal point. `1.` and `.5` are parse errors; write `1.0` and `0.5`."
    // ryu emits `1.0` for 1.0, `1e100` for that magnitude, and `inf`/`nan`
    // for special values (we exclude those here).
    // Actually ryu emits `1e0` for whole numbers? Let's verify by testing.
    // For safety, if there's no `.` in the mantissa portion, append `.0`.
    let e_pos = s.find(|c: char| c == 'e' || c == 'E');
    let mantissa = match e_pos { Some(i) => &s[..i], None => &s[..] };
    if !mantissa.contains('.') {
        // ryu shouldn't emit a `.`-less mantissa for finite values, but be defensive.
        let mantissa_with_dot = format!("{}.0", mantissa);
        return match e_pos { Some(i) => format!("{}{}", mantissa_with_dot, &s[i..]), None => mantissa_with_dot };
    }
    s
}

fn random_unicode_char(rng: &mut Rng) -> char {
    // Choose from BMP to avoid surrogate pairs, but include ASCII, Latin,
    // CJK, emojis-via-BMP, etc.
    loop {
        let cp = rng.gen_range(0x20, 0x10000) as u32;
        // Skip surrogate range.
        if (0xD800..=0xDFFF).contains(&cp) { continue; }
        if let Some(c) = char::from_u32(cp) {
            return c;
        }
    }
}

fn gen_random_string(rng: &mut Rng, max_len: usize) -> String {
    let len = rng.gen_range(0, max_len as u64 + 1) as usize;
    let raw: String = (0..len).map(|_| random_unicode_char(rng)).collect();
    // Pre-NFC: the parser normalises every string to NFC at the source
    // level (per SPEC §UTF-8 only, NFC-normalized). Random BMP
    // codepoints occasionally include combining marks that compose
    // (e.g. U+0F71 + U+0F80 → U+0F81; e + U+0301 → é), which would
    // make a raw-input round-trip compare fail not because of a
    // round-trip bug but because the parser's input differs from its
    // output by NFC. Pre-normalising fixes the comparison without
    // exempting any values from the round-trip contract — NFC is
    // idempotent.
    raw.nfc().collect()
}

fn fmt_basic_string(s: &str) -> String {
    // Escape for `"..."` form.
    let mut out = String::with_capacity(s.len() + 2);
    out.push('"');
    for c in s.chars() {
        match c {
            '"' => out.push_str("\\\""),
            '\\' => out.push_str("\\\\"),
            '\n' => out.push_str("\\n"),
            '\r' => out.push_str("\\r"),
            '\t' => out.push_str("\\t"),
            '\u{08}' => out.push_str("\\b"),
            '\u{0C}' => out.push_str("\\f"),
            c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04X}", c as u32)),
            c => out.push(c),
        }
    }
    out.push('"');
    out
}

fn fmt_literal_string(s: &str) -> Option<String> {
    // Literal '...' allows no escapes. Reject if string contains the delimiter
    // or any control character including newline/cr/tab — those can't be
    // expressed in a single-line literal.
    if s.contains('\'') { return None; }
    if s.chars().any(|c| (c as u32) < 0x20 || c == '\u{7f}') { return None; }
    Some(format!("'{}'", s))
}

fn pick_string_form(s: &str, rng: &mut Rng) -> String {
    // Try literal first sometimes; fall back to basic if literal can't represent it.
    if rng.gen_bool() {
        if let Some(lit) = fmt_literal_string(s) { return lit; }
    }
    fmt_basic_string(s)
}

fn fmt_heredoc_basic(s: &str) -> String {
    // Use label `EOF` (collision-resistant since we wrap user data line by line).
    // Body lines must not contain `EOF` as their entire trimmed content.
    // Strategy: write body with an indent of 4 spaces, then split on newlines.
    // Apply backslash escapes for control characters.
    // For our fuzzer we use a unique label derived from a counter to avoid collisions.
    let label = "FUZZHEREDOC";
    let mut body = String::new();
    for line in s.split('\n') {
        body.push_str("    ");
        for c in line.chars() {
            match c {
                '\\' => body.push_str("\\\\"),
                '\r' => body.push_str("\\r"),
                '\t' => body.push_str("\\t"),
                '\u{08}' => body.push_str("\\b"),
                '\u{0C}' => body.push_str("\\f"),
                c if (c as u32) < 0x20 => body.push_str(&format!("\\u{:04X}", c as u32)),
                c => body.push(c),
            }
        }
        body.push('\n');
    }
    format!("\"\"\"{label}\n{body}    {label}", label = label, body = body)
}

fn gen_valid_date_str(rng: &mut Rng) -> String {
    let y = rng.gen_range(1, 10000);
    let m = rng.gen_range(1, 13);
    let d = rng.gen_range(1, 29); // safe across all months/years
    format!("{:04}-{:02}-{:02}", y, m, d)
}

fn gen_valid_time_str(rng: &mut Rng) -> String {
    let h = rng.gen_range(0, 24);
    let mi = rng.gen_range(0, 60);
    let s = rng.gen_range(0, 60);
    if rng.gen_bool() {
        let frac_digits = rng.gen_range(1, 10) as usize;
        let frac: u64 = rng.gen_range(0, 10u64.pow(frac_digits as u32));
        format!("{:02}:{:02}:{:02}.{:0width$}", h, mi, s, frac, width = frac_digits)
    } else {
        format!("{:02}:{:02}:{:02}", h, mi, s)
    }
}

fn gen_offset_suffix(rng: &mut Rng) -> String {
    let kind = rng.gen_range(0, 3);
    match kind {
        0 => "Z".to_string(),
        1 => {
            let h = rng.gen_range(0, 14);
            let m = rng.gen_range(0, 60);
            format!("+{:02}:{:02}", h, m)
        }
        _ => {
            let h = rng.gen_range(0, 14);
            let m = rng.gen_range(0, 60);
            format!("-{:02}:{:02}", h, m)
        }
    }
}

// ---------- tests ------------------------------------------------------

fn parse_value(text: &str) -> Value {
    // Wrap as `k: <text>\n`, decode, return the inner value.
    let src = format!("k: {}\n", text);
    match decode(&src) {
        Ok(Value::Table(t)) => t.get("k").cloned().expect("missing k"),
        Ok(_) => panic!("expected table root"),
        Err(e) => panic!("decode error for `{}`: {}", src.trim_end(), e),
    }
}

#[test]
fn fuzz_bool_roundtrip() {
    let mut rng = Rng::new(0x42);
    for _ in 0..ITERS_PER_TYPE {
        let b = rng.gen_bool();
        let text = if b { "true" } else { "false" };
        match parse_value(text) {
            Value::Bool(parsed) => assert_eq!(parsed, b),
            other => panic!("expected Bool, got {:?}", other),
        }
    }
}

#[test]
fn fuzz_integer_roundtrip() {
    let mut rng = Rng::new(0x1337);
    for _ in 0..ITERS_PER_TYPE {
        let n = rng.gen_i64();
        let text = fmt_int_random_base(n, &mut rng);
        match parse_value(&text) {
            Value::Integer(parsed) => assert_eq!(parsed, n, "input was `{}`", text),
            other => panic!("expected Integer for `{}`, got {:?}", text, other),
        }
    }
}

#[test]
fn fuzz_float_special_values() {
    // Deterministic edge cases — special values can't be hit by xorshift
    // over 64 random bits with any meaningful probability.
    match parse_value("inf") {
        Value::Float(p) => assert_eq!(p, f64::INFINITY),
        other => panic!("expected +inf, got {:?}", other),
    }
    match parse_value("+inf") {
        Value::Float(p) => assert_eq!(p, f64::INFINITY),
        other => panic!("expected +inf for `+inf`, got {:?}", other),
    }
    match parse_value("-inf") {
        Value::Float(p) => assert_eq!(p, f64::NEG_INFINITY),
        other => panic!("expected -inf, got {:?}", other),
    }
    match parse_value("nan") {
        Value::Float(p) => assert!(p.is_nan(), "expected NaN, got {:?}", p),
        other => panic!("expected NaN, got {:?}", other),
    }
}

#[test]
fn fuzz_float_finite_roundtrip() {
    let mut rng = Rng::new(0xFEED);
    let mut tested = 0u32;
    for _ in 0..ITERS_PER_TYPE {
        let f = rng.gen_f64();
        if !f.is_finite() { continue; }
        tested += 1;
        let text = fmt_finite_float(f);
        match parse_value(&text) {
            Value::Float(p) => assert_eq!(p.to_bits(), f.to_bits(), "input `{}` ({:?}) round-tripped to {:?}", text, f, p),
            Value::Integer(_) => panic!("float `{}` ({}) parsed as Integer", text, f),
            other => panic!("expected Float for `{}`, got {:?}", text, other),
        }
    }
    // Sanity: the vast majority of random u64 bit patterns produce finite f64.
    assert!(tested > ITERS_PER_TYPE * 9 / 10, "expected most iters to produce finite floats, got {}/{}", tested, ITERS_PER_TYPE);
}

#[test]
fn fuzz_string_roundtrip_basic_and_literal() {
    let mut rng = Rng::new(0xCAFE);
    for _ in 0..ITERS_PER_TYPE {
        let s = gen_random_string(&mut rng, 32);
        let text = pick_string_form(&s, &mut rng);
        match parse_value(&text) {
            Value::String(parsed) => assert_eq!(parsed, s, "input was `{}`", text),
            other => panic!("expected String for `{}`, got {:?}", text, other),
        }
    }
}

#[test]
fn fuzz_string_roundtrip_heredoc_basic() {
    let mut rng = Rng::new(0xDEAD);
    for _ in 0..ITERS_PER_TYPE / 4 {
        // Smaller iter count — heredocs are bigger inputs.
        let mut s = gen_random_string(&mut rng, 24);
        // Skip strings that contain our literal label to avoid premature termination.
        if s.contains("FUZZHEREDOC") { continue; }
        // Strip trailing newline; default heredoc emit appends a newline per
        // line, and the spec joins lines with `\n` without a final trailing `\n`.
        // So an input ending in `\n` would round-trip with one extra `\n`.
        // For this fuzzer we trim trailing newlines from the input.
        while s.ends_with('\n') { s.pop(); }
        let text = fmt_heredoc_basic(&s);
        let src = format!("k: {}\n", text);
        match decode(&src) {
            Ok(Value::Table(t)) => match t.get("k").cloned() {
                Some(Value::String(parsed)) => {
                    assert_eq!(parsed, s, "input src was:\n{}\n--\nexpected: {:?}\ngot: {:?}", src, s, parsed);
                }
                other => panic!("expected String, got {:?}", other),
            },
            Ok(other) => panic!("expected Table root, got {:?}", other),
            Err(e) => panic!("decode error: {}\nsource:\n{}", e, src),
        }
    }
}

#[test]
fn fuzz_local_date_roundtrip() {
    let mut rng = Rng::new(0xD00D);
    for _ in 0..ITERS_PER_TYPE {
        let s = gen_valid_date_str(&mut rng);
        match parse_value(&s) {
            Value::LocalDate(parsed) => assert_eq!(parsed, s),
            other => panic!("expected LocalDate for `{}`, got {:?}", s, other),
        }
    }
}

#[test]
fn fuzz_local_time_roundtrip() {
    let mut rng = Rng::new(0xBEEF);
    for _ in 0..ITERS_PER_TYPE {
        let s = gen_valid_time_str(&mut rng);
        match parse_value(&s) {
            Value::LocalTime(parsed) => assert_eq!(parsed, s),
            other => panic!("expected LocalTime for `{}`, got {:?}", s, other),
        }
    }
}

#[test]
fn fuzz_local_datetime_roundtrip() {
    let mut rng = Rng::new(0xACE);
    for _ in 0..ITERS_PER_TYPE {
        let d = gen_valid_date_str(&mut rng);
        let t = gen_valid_time_str(&mut rng);
        let s = format!("{}T{}", d, t);
        match parse_value(&s) {
            Value::LocalDateTime(parsed) => assert_eq!(parsed, s),
            other => panic!("expected LocalDateTime for `{}`, got {:?}", s, other),
        }
    }
}

#[test]
fn fuzz_offset_datetime_roundtrip() {
    let mut rng = Rng::new(0xFADE);
    for _ in 0..ITERS_PER_TYPE {
        let d = gen_valid_date_str(&mut rng);
        let t = gen_valid_time_str(&mut rng);
        let off = gen_offset_suffix(&mut rng);
        let s = format!("{}T{}{}", d, t, off);
        match parse_value(&s) {
            Value::OffsetDateTime(parsed) => assert_eq!(parsed, s),
            other => panic!("expected OffsetDateTime for `{}`, got {:?}", s, other),
        }
    }
}

#[test]
fn fuzz_flow_array_roundtrip_of_ints() {
    let mut rng = Rng::new(0xF10);
    for _ in 0..ITERS_PER_TYPE / 4 {
        let n = rng.gen_range(0, 8) as usize;
        let nums: Vec<i64> = (0..n).map(|_| rng.gen_i64()).collect();
        let formatted = nums.iter().map(|n| fmt_int_random_base(*n, &mut rng)).collect::<Vec<_>>();
        let text = format!("[{}]", formatted.join(", "));
        match parse_value(&text) {
            Value::List(items) => {
                assert_eq!(items.len(), n, "input `{}`", text);
                for (i, item) in items.iter().enumerate() {
                    match item {
                        Value::Integer(parsed) => assert_eq!(*parsed, nums[i]),
                        other => panic!("expected Integer at index {}, got {:?}", i, other),
                    }
                }
            }
            other => panic!("expected List for `{}`, got {:?}", text, other),
        }
    }
}

#[test]
fn fuzz_flow_table_roundtrip_of_bools() {
    let mut rng = Rng::new(0xBAB);
    for _ in 0..ITERS_PER_TYPE / 4 {
        let n = rng.gen_range(0, 6) as usize;
        let mut keys = Vec::new();
        let mut vals = Vec::new();
        for i in 0..n {
            keys.push(format!("k{}", i));
            vals.push(rng.gen_bool());
        }
        let pairs: Vec<String> = keys.iter().zip(&vals).map(|(k, v)| format!("{}: {}", k, v)).collect();
        let text = format!("{{{}}}", pairs.join(", "));
        match parse_value(&text) {
            Value::Table(t) => {
                assert_eq!(t.len(), n);
                for (k, v) in keys.iter().zip(&vals) {
                    match t.get(k) {
                        Some(Value::Bool(parsed)) => assert_eq!(parsed, v),
                        other => panic!("missing or wrong type for {}: {:?}", k, other),
                    }
                }
            }
            other => panic!("expected Table for `{}`, got {:?}", text, other),
        }
    }
}

// ---------- robustness fuzz: random bytes, no panic --------------------

#[test]
fn fuzz_robustness_random_bytes() {
    let mut rng = Rng::new(0xDEADBEEF_CAFEBABE);
    for _ in 0..ROBUSTNESS_ITERS {
        let len = rng.gen_range(0, 256) as usize;
        let mut bytes = Vec::with_capacity(len);
        for _ in 0..len {
            bytes.push((rng.next_u64() & 0x7F) as u8); // ASCII-only to keep utf-8 valid
        }
        let s = String::from_utf8(bytes).unwrap();
        // Only assertion: no panic. Any Ok or Err is fine.
        let _ = decode(&s);
    }
}

#[test]
fn fuzz_robustness_random_full_utf8() {
    let mut rng = Rng::new(0x1234_5678_9ABC_DEF0);
    for _ in 0..ROBUSTNESS_ITERS / 2 {
        // Full unicode (BMP) input — exercises decoder paths.
        let len = rng.gen_range(0, 128) as usize;
        let s: String = (0..len).map(|_| random_unicode_char(&mut rng)).collect();
        let _ = decode(&s);
    }
}

// ---------- targeted smoke tests for the generators themselves ---------

#[test]
fn smoke_fmt_int_random_base() {
    let mut rng = Rng::new(1);
    // Edge cases: 0, MIN, MAX, ±1
    for &n in &[0i64, 1, -1, i64::MAX, i64::MIN, 42, -42, 12345678] {
        let s = fmt_int_random_base(n, &mut rng);
        match parse_value(&s) {
            Value::Integer(p) => assert_eq!(p, n, "n={} text=`{}`", n, s),
            other => panic!("expected Integer for n={}, got {:?}", n, other),
        }
    }
}
