//! Public surface, edge cases, and decoder leniency.

mod common;

#[test]
fn compress_and_decompress_are_callable() {
    let c = lzbase62::compress("x");
    assert_eq!(lzbase62::decompress(&c), "x");
}

#[test]
fn decompress_units_returns_raw_code_units() {
    assert_eq!(lzbase62::decompress_units(""), Vec::<u16>::new());
    assert_eq!(lzbase62::decompress_units("tR"), vec![0x61]);
    let c = lzbase62::compress("hello hello hello");
    let units: Vec<u16> = "hello hello hello".encode_utf16().collect();
    assert_eq!(lzbase62::decompress_units(&c), units);
}

#[test]
fn decompress_units_preserves_a_lone_surrogate() {
    // `decompress` flattens this to U+FFFD because a String cannot hold a lone
    // surrogate. The units path keeps the original code unit.
    assert_eq!(lzbase62::decompress_units("wmle"), vec![0xD83D]);
    assert_eq!(lzbase62::decompress("wmle").encode_utf16().count(), 1);
    assert_eq!(
        lzbase62::decompress("wmle").encode_utf16().next(),
        Some(0xFFFD)
    );
}

// A1, A2, A3: empty-ish guard.

#[test]
fn empty_input_returns_empty() {
    assert_eq!(lzbase62::compress(""), "");
    assert_eq!(lzbase62::decompress(""), "");
}

#[test]
fn empty_input_fires_no_callbacks() {
    let mut data_calls = 0;
    let mut end_calls = 0;
    let out = lzbase62::compress_with(
        "",
        lzbase62::Options::new()
            .on_data(|_| data_calls += 1)
            .on_end(|| end_calls += 1),
    );
    assert_eq!(out, "");
    assert_eq!(data_calls, 0);
    assert_eq!(end_calls, 0);
}

// A4..A8: short exact outputs across the encoder paths.

#[test]
fn short_exact_outputs() {
    let cases = [
        ("a", "tR"),             // single Latin literal
        ("ab", "tRS"),           // two Latin literals, carried index
        ("aaaa", "tRxBD"),       // short fixed-length match
        ("\u{3042}", "wMiV"),    // Unicode index path
        ("a\u{3042}", "tRwMiV"), // Latin then Unicode index switch
    ];
    for (input, expected) in cases {
        assert_eq!(lzbase62::compress(input), expected, "compress {input:?}");
        assert_eq!(
            lzbase62::decompress(expected),
            input,
            "decompress {expected:?}"
        );
    }
}

// A9: non-base62 chars are skipped, but junk inside a token shifts reads.

#[test]
fn unknown_chars_are_skipped_not_errors() {
    // Spaces inserted between every character of a valid stream. They no-op as
    // separators but also land inside multi-char tokens, shifting the reads.
    // The decoder must reproduce the same output the source produces, with no
    // error: "hello" then six spaces then a form feed.
    let interleaved: String = "tYVccfxGM"
        .chars()
        .map(|c| c.to_string())
        .collect::<Vec<_>>()
        .join(" ");
    let out = lzbase62::decompress(&interleaved);
    let codes: Vec<u32> = out.chars().map(|c| c as u32).collect();
    assert_eq!(
        codes,
        vec![104, 101, 108, 108, 111, 32, 32, 32, 32, 32, 32, 12]
    );
}

#[test]
fn all_unknown_chars_decode_to_empty() {
    assert_eq!(lzbase62::decompress("!!! ??? ..."), "");
}

// A10: fresh state per call.

#[test]
fn repeated_calls_are_independent() {
    let a = lzbase62::compress("hello hello hello");
    let b = lzbase62::compress("hello hello hello");
    assert_eq!(a, b);

    let x = lzbase62::decompress("tYVccfxGM");
    let y = lzbase62::decompress("tYVccfxGM");
    assert_eq!(x, y);
}

// A11: truncated input never panics.

#[test]
fn truncated_input_does_not_panic() {
    for prefix in ["t", "tR", "tY", "tYV", "x", "xY", "xYZ"] {
        let _ = lzbase62::decompress(prefix);
    }
}

#[test]
fn decompress_returns_full_string_when_streaming() {
    let compressed = lzbase62::compress("hello hello hello");
    let mut chunks = Vec::new();
    let returned = lzbase62::decompress_with(
        &compressed,
        lzbase62::Options::new().on_data(|c| chunks.push(c.to_string())),
    );
    assert_eq!(returned, "hello hello hello");
    assert_eq!(chunks.join(""), "hello hello hello");
}

#[test]
fn streaming_compress_returns_empty_string() {
    let returned = lzbase62::compress_with(
        "hello hello hello",
        lzbase62::Options::new().on_data(|_| {}),
    );
    assert_eq!(returned, "");
}

#[test]
fn every_golden_vector_round_trips_via_helpers() {
    // A cross-check that the helper-driven path agrees with the stored vectors.
    for v in common::load_vectors() {
        let input = common::units_to_string(&v.input_units);
        assert_eq!(
            lzbase62::decompress(&lzbase62::compress(&input)),
            input,
            "{}",
            v.name
        );
    }
}
