//! Tests for the compiled matcher API (`MatcherBuilder` / `CompiledMatcher`).
//!
//! Contract under test:
//! - compile validated patterns once, reuse across many queries;
//! - results are identical to the existing `find_matches` semantics;
//! - concurrent read-only use is safe;
//! - construction configuration is explicit and serialisable (no serialising
//!   of compiled implementation internals).

use std::thread;

use terraphim_automata::{
    CompiledMatcher, Matched, MatcherBuilder, MatcherOptions, find_matches, thesaurus_from_terms,
};
use terraphim_types::{NormalizedTerm, NormalizedTermValue, RoleName, Thesaurus};

fn thesaurus_sample() -> Thesaurus {
    let role = RoleName::new("Engineer");
    thesaurus_from_terms(
        &role,
        [
            "rust",
            "rust async",
            "async runtime",
            "knowledge graph",
            "machine learning",
        ],
    )
}

#[test]
fn compiled_matches_equal_find_matches() {
    let thesaurus = thesaurus_sample();
    let compiled = CompiledMatcher::from_thesaurus(&thesaurus, MatcherOptions::default()).unwrap();

    let text = "Rust and RUST ASYNC power the knowledge graph with an async runtime.";
    let expected = find_matches(text, &thesaurus, true).unwrap();
    let got = compiled.find_matches(text, true).unwrap();
    assert_eq!(got, expected);
}

#[test]
fn repeated_reuse_is_identical() {
    let thesaurus = thesaurus_sample();
    let compiled = CompiledMatcher::from_thesaurus(&thesaurus, MatcherOptions::default()).unwrap();

    let first = compiled
        .find_matches("rust knowledge graph machine learning", false)
        .unwrap()
        .len();
    assert_eq!(first, 3);
    for _ in 0..50 {
        let matches = compiled
            .find_matches("rust knowledge graph machine learning", false)
            .unwrap();
        assert_eq!(matches.len(), first);
    }
}

#[test]
fn builder_skips_invalid_patterns() {
    let mut builder = MatcherBuilder::new(MatcherOptions::default());
    let _ = builder.insert("rust".to_string(), sample_term("rust"));
    let short = builder.insert("a".to_string(), sample_term("a")).is_err();
    let empty = builder.insert("  ".to_string(), sample_term("  ")).is_err();
    assert!(short, "too-short pattern must be rejected");
    assert!(empty, "blank pattern must be rejected");

    let compiled = builder.build().unwrap();
    let matches = compiled.find_matches("rust", true).unwrap();
    assert_eq!(matches.len(), 1);
    assert_eq!(matches[0].term, "rust");
}

#[test]
fn builder_rejects_duplicate_patterns() {
    let mut builder = MatcherBuilder::new(MatcherOptions::default());
    builder
        .insert("rust".to_string(), sample_term("rust"))
        .unwrap();
    let dup = builder.insert("rust".to_string(), sample_term("rust"));
    assert!(dup.is_err(), "duplicate patterns must be rejected");
}

#[test]
fn empty_pattern_set_builds_and_matches_nothing() {
    let builder = MatcherBuilder::new(MatcherOptions::default());
    let compiled = builder.build().unwrap();
    assert!(compiled.is_empty());
    assert!(compiled.find_matches("rust", true).unwrap().is_empty());
}

#[test]
fn empty_query_returns_no_matches() {
    let compiled =
        CompiledMatcher::from_thesaurus(&thesaurus_sample(), MatcherOptions::default()).unwrap();
    assert!(compiled.find_matches("", true).unwrap().is_empty());
}

#[test]
fn leftmost_longest_semantics_preserved() {
    // "rust async" must win over the shorter "rust" prefix.
    let compiled =
        CompiledMatcher::from_thesaurus(&thesaurus_sample(), MatcherOptions::default()).unwrap();
    let matches = compiled.find_matches("rust async", true).unwrap();
    assert_eq!(matches.len(), 1);
    assert_eq!(matches[0].term, "rust async");
}

#[test]
fn multibyte_unicode_positions_are_byte_accurate() {
    let mut builder = MatcherBuilder::new(MatcherOptions::default());
    builder
        .insert("héllo".to_string(), sample_term("héllo"))
        .unwrap();
    let compiled = builder.build().unwrap();

    let text = "say héllo wörld";
    let expected = {
        let role = RoleName::new("T");
        let t = thesaurus_from_terms(&role, ["héllo"]);
        find_matches(text, &t, true).unwrap()
    };
    let got = compiled.find_matches(text, true).unwrap();
    assert_eq!(got.len(), expected.len());
    assert_eq!(got[0].term, expected[0].term);
    assert_eq!(got[0].pos, expected[0].pos);
    let (start, end) = got[0].pos.unwrap();
    assert_eq!(&text[start..end], "héllo");
}

#[test]
fn overlapping_matches_reported_like_reference() {
    let role = RoleName::new("T");
    let thesaurus = thesaurus_from_terms(&role, ["abc", "bcd"]);
    let compiled = CompiledMatcher::from_thesaurus(&thesaurus, MatcherOptions::default()).unwrap();

    // Neither `abc` nor `bcd` is a whole word inside `abcd`, so both are
    // rejected by the word-boundary rule. Previously `abc` was reported here,
    // which is the same class of defect as `ce` matching inside `concept`.
    let got = compiled.find_matches("abcd", true).unwrap();
    let expected = find_matches("abcd", &thesaurus, true).unwrap();
    assert_eq!(got, expected);
    assert!(got.is_empty(), "a term inside a longer word is not a match");

    // With real boundaries the overlap rule still applies: leftmost-longest
    // picks one match per position, and the compiled matcher agrees with the
    // reference implementation.
    let got = compiled.find_matches("abc bcd", true).unwrap();
    let expected = find_matches("abc bcd", &thesaurus, true).unwrap();
    assert_eq!(got, expected);
    assert_eq!(got.len(), 2, "both terms match as standalone words");
}

#[test]
fn concurrent_read_only_use_is_safe() {
    let compiled =
        CompiledMatcher::from_thesaurus(&thesaurus_sample(), MatcherOptions::default()).unwrap();

    let handles: Vec<_> = (0..8)
        .map(|i| {
            let compiled = compiled.clone();
            thread::spawn(move || {
                let text = format!("rust async knowledge graph number {i}");
                let matches = compiled.find_matches(&text, true).unwrap();
                assert_eq!(matches.len(), 2);
                matches
            })
        })
        .collect();
    for h in handles {
        let matches = h.join().unwrap();
        assert_eq!(matches[0].term, "rust async");
        assert_eq!(matches[1].term, "knowledge graph");
    }
}

#[test]
fn caller_buffer_path_avoids_reallocation() {
    let compiled =
        CompiledMatcher::from_thesaurus(&thesaurus_sample(), MatcherOptions::default()).unwrap();
    let mut buffer: Vec<Matched> = Vec::new();
    compiled.push_matches("rust async", &mut buffer).unwrap();
    assert!(!buffer.is_empty());
    buffer.clear();
    compiled.push_matches("nothing here", &mut buffer).unwrap();
    assert!(buffer.is_empty());
    assert!(buffer.capacity() >= 1, "buffer reuse is preserved");
}

#[test]
fn options_are_serialisable_construction_config() {
    let options = MatcherOptions::default();
    let json = serde_json::to_string(&options).unwrap();
    let back: MatcherOptions = serde_json::from_str(&json).unwrap();
    assert_eq!(options, back);
}

#[test]
fn min_pattern_length_option_is_enforced() {
    let options = MatcherOptions {
        min_pattern_length: 5,
        ..MatcherOptions::default()
    };
    let mut builder = MatcherBuilder::new(options);
    let rejected = builder.insert("rust".to_string(), sample_term("rust"));
    assert!(rejected.is_err());
    let accepted = builder.insert("runtime".to_string(), sample_term("runtime"));
    assert!(accepted.is_ok());
}

#[test]
fn case_insensitive_default_matches_reference() {
    let role = RoleName::new("T");
    let thesaurus = thesaurus_from_terms(&role, ["rust"]);
    let compiled = CompiledMatcher::from_thesaurus(&thesaurus, MatcherOptions::default()).unwrap();
    let got = compiled.find_matches("RUST rust Rust", true).unwrap();
    assert_eq!(got.len(), 3);
}

fn sample_term(value: &str) -> NormalizedTerm {
    NormalizedTerm::new(1u64, NormalizedTermValue::from(value))
}