//! Behavioral oracle: each generated JavaScript pattern matches every emoji
//! sequence in full.
//!
//! For every version it compiles the `javascript.txt` and `javascript-u.txt`
//! patterns and asserts that each sequence matches from index zero and consumes
//! the whole string.

mod common;

use std::collections::HashSet;

use common::{default_sorted_sequences, golden, js_u_to_rust, lcd_rgi_emoji, VERSIONS};
use emoji_test_regex_pattern::OutputKind;
use regex::Regex;

/// Map a UTF-16 code unit to a distinct scalar char.
///
/// Non-surrogate units map to themselves. Surrogate units (U+D800..=U+DFFF) map
/// into a private plane so no unit collides with a real BMP character used in a
/// pattern.
fn unit_to_char(unit: u16) -> char {
    let u = unit as u32;
    if (0xD800..=0xDFFF).contains(&u) {
        char::from_u32(0xF_0000 + (u - 0xD800)).unwrap()
    } else {
        char::from_u32(u).unwrap()
    }
}

/// Translate the no-flag JavaScript pattern into a Rust `regex` that matches on
/// remapped code units.
///
/// The no-flag pattern matches UTF-16 code units, including bare surrogate
/// escapes such as `\uD83C`. Rust `regex` cannot hold surrogates in a scalar.
/// Each `\uXXXX` escape is rewritten to the remapped scalar from
/// [`unit_to_char`]. Test strings are remapped the same way, so matching happens
/// on the exact code-unit sequence JavaScript would see.
fn js_noflag_to_rust(pattern: &str) -> String {
    let cs: Vec<char> = pattern.chars().collect();
    let mut out = String::with_capacity(pattern.len());
    let mut i = 0;
    while i < cs.len() {
        if cs[i] == '\\' && i + 5 < cs.len() && cs[i + 1] == 'u' && cs[i + 2] != '{' {
            let hex: String = cs[i + 2..i + 6].iter().collect();
            let unit = u16::from_str_radix(&hex, 16).unwrap();
            out.push_str(&format!("\\x{{{:X}}}", unit_to_char(unit) as u32));
            i += 6;
        } else {
            out.push(cs[i]);
            i += 1;
        }
    }
    out
}

/// Re-encode a string as remapped scalars through its UTF-16 units.
fn remap_string(s: &str) -> String {
    s.encode_utf16().map(unit_to_char).collect()
}

#[test]
fn u_flag_pattern_fully_matches_every_sequence() {
    for &dir in VERSIONS {
        let pattern = golden(dir, OutputKind::JavaScriptU);
        let re = Regex::new(&js_u_to_rust(pattern.trim())).expect("u-flag pattern compiles");
        for s in default_sorted_sequences(dir) {
            let m = re
                .find(&s)
                .unwrap_or_else(|| panic!("{dir}: no match for {s:?}"));
            assert_eq!(m.start(), 0, "{dir}: match not at start for {s:?}");
            assert_eq!(m.as_str(), s, "{dir}: partial match for {s:?}");
        }
    }
}

#[test]
fn no_flag_pattern_fully_matches_every_sequence() {
    for &dir in VERSIONS {
        let pattern = golden(dir, OutputKind::JavaScript);
        let re = Regex::new(&js_noflag_to_rust(pattern.trim())).expect("no-flag pattern compiles");
        for s in default_sorted_sequences(dir) {
            let target = remap_string(&s);
            let m = re
                .find(&target)
                .unwrap_or_else(|| panic!("{dir}: no match for {s:?}"));
            assert_eq!(m.start(), 0, "{dir}: match not at start for {s:?}");
            assert_eq!(m.as_str(), target, "{dir}: partial match for {s:?}");
        }
    }
}

#[test]
fn lcd_rgi_covered_by_every_version() {
    let lcd = lcd_rgi_emoji();
    for &dir in VERSIONS {
        let set: HashSet<String> = default_sorted_sequences(dir).into_iter().collect();
        for s in &lcd {
            assert!(set.contains(s), "{dir}: missing LCD RGI {s:?}");
        }
    }
}

#[test]
fn other_outputs_are_non_empty() {
    for &dir in VERSIONS {
        for kind in [
            OutputKind::JavaScriptV,
            OutputKind::Index,
            OutputKind::CppRe2,
            OutputKind::Css,
            OutputKind::Java,
        ] {
            assert!(
                !golden(dir, kind).trim().is_empty(),
                "{dir}/{} is empty",
                kind.file_name()
            );
        }
    }
}
