//! Runtime tests for the Picker and the matcher predicates.
//!
//! The core build suite never runs a matcher. These tests drive a Picker with
//! a small in-test DOM and a builder that compiles the decision tree into a
//! matcher. They cover pick1 tie-breaks, every attribute matcher in both
//! modes, the allow-listed pseudo-classes, and a non-treeify builder.

use std::cmp::Ordering;
use std::rc::Rc;

use selderee::{
    compare_specificity, pick1_from, AttrPredicate, DecisionTree, DecisionTreeNode,
    DecisionTreeOptions, Picker, Specificity, ValueContainer,
};

// ---------------------------------------------------------------------------
// A tiny DOM for driving matchers.
// ---------------------------------------------------------------------------

struct Node {
    name: String,
    attrs: Vec<(String, String)>,
    parent: Option<usize>,
    prev_elem: Option<usize>,
    next_elem: Option<usize>,
    has_children: bool,
}

struct Arena {
    nodes: Vec<Node>,
}

#[derive(Clone)]
struct ElemRef {
    arena: Rc<Arena>,
    id: usize,
}

impl ElemRef {
    fn node(&self) -> &Node {
        &self.arena.nodes[self.id]
    }
    fn attr(&self, name: &str) -> Option<&str> {
        self.node()
            .attrs
            .iter()
            .find(|(n, _)| n == name)
            .map(|(_, v)| v.as_str())
    }
    fn has_attr(&self, name: &str) -> bool {
        self.node().attrs.iter().any(|(n, _)| n == name)
    }
    fn parent(&self) -> Option<ElemRef> {
        self.node().parent.map(|id| ElemRef {
            arena: self.arena.clone(),
            id,
        })
    }
    fn prev(&self) -> Option<ElemRef> {
        self.node().prev_elem.map(|id| ElemRef {
            arena: self.arena.clone(),
            id,
        })
    }
}

// ---------------------------------------------------------------------------
// A builder that compiles the tree into a matcher over ElemRef.
// ---------------------------------------------------------------------------

/// A compiled matcher over the in-test DOM stack.
type Matcher<V> = Box<dyn Fn(&[ElemRef]) -> Vec<ValueContainer<V>>>;

fn build_matcher<V: Clone + 'static>(nodes: &[DecisionTreeNode<V>]) -> Matcher<V> {
    let compiled: Vec<Matcher<V>> = nodes.iter().map(compile_node).collect();
    Box::new(move |stack: &[ElemRef]| {
        let mut out = Vec::new();
        for m in &compiled {
            out.extend(m(stack));
        }
        out
    })
}

fn compile_node<V: Clone + 'static>(node: &DecisionTreeNode<V>) -> Matcher<V> {
    match node {
        DecisionTreeNode::Terminal { value_container } => {
            let vc = value_container.clone();
            Box::new(move |_stack| vec![vc.clone()])
        }
        DecisionTreeNode::TagName { variants } => {
            let table: Vec<(String, Matcher<V>)> = variants
                .iter()
                .map(|v| (v.value.clone(), build_matcher(&v.cont)))
                .collect();
            Box::new(move |stack| {
                let el = &stack[0];
                let name = el.node().name.clone();
                for (variant, cont) in &table {
                    if *variant == name {
                        return cont(stack);
                    }
                }
                Vec::new()
            })
        }
        DecisionTreeNode::AttrPresence { name, cont } => {
            let name = name.clone();
            let cont = build_matcher(cont);
            Box::new(move |stack| {
                if stack[0].has_attr(&name) {
                    cont(stack)
                } else {
                    Vec::new()
                }
            })
        }
        DecisionTreeNode::AttrValue { name, matchers } => {
            let name = name.clone();
            let compiled: Vec<(AttrPredicate, Matcher<V>)> = matchers
                .iter()
                .map(|m| (m.predicate.clone(), build_matcher(&m.cont)))
                .collect();
            Box::new(move |stack| {
                let el = &stack[0];
                match el.attr(&name) {
                    Some(actual) => {
                        let mut out = Vec::new();
                        for (pred, cont) in &compiled {
                            if pred.matches(actual) {
                                out.extend(cont(stack));
                            }
                        }
                        out
                    }
                    None => Vec::new(),
                }
            })
        }
        DecisionTreeNode::PseudoClass { name, cont } => {
            let name = name.clone();
            let cont = build_matcher(cont);
            Box::new(move |stack| {
                let el = &stack[0];
                let pass = match name.as_str() {
                    "empty" => !el.node().has_children,
                    "first-child" => el.prev().is_none(),
                    "last-child" => el.node().next_elem.is_none(),
                    "only-child" => el.prev().is_none() && el.node().next_elem.is_none(),
                    "any-link" => {
                        (el.node().name == "a" || el.node().name == "area") && el.has_attr("href")
                    }
                    _ => false,
                };
                if pass {
                    cont(stack)
                } else {
                    Vec::new()
                }
            })
        }
        DecisionTreeNode::PushElement { combinator, cont } => {
            let combinator = *combinator;
            let cont = build_matcher(cont);
            Box::new(move |stack| {
                let el = &stack[0];
                let pushed = if combinator == '+' {
                    el.prev()
                } else {
                    el.parent()
                };
                match pushed {
                    Some(p) => {
                        let mut new_stack = vec![p];
                        new_stack.extend_from_slice(stack);
                        cont(&new_stack)
                    }
                    None => Vec::new(),
                }
            })
        }
        DecisionTreeNode::PopElement { cont } => {
            let cont = build_matcher(cont);
            Box::new(move |stack| cont(&stack[1..]))
        }
    }
}

fn picker_from(input: &[(&str, &str)], opts: DecisionTreeOptions) -> Picker<ElemRef, String> {
    let owned: Vec<(String, String)> = input
        .iter()
        .map(|(s, v)| (s.to_string(), v.to_string()))
        .collect();
    let tree = DecisionTree::new(owned, opts).expect("selector should build");
    let matcher = tree.build(build_matcher);
    Picker::new(matcher)
}

fn single(name: &str, attrs: &[(&str, &str)], has_children: bool) -> ElemRef {
    let node = Node {
        name: name.to_string(),
        attrs: attrs
            .iter()
            .map(|(k, v)| (k.to_string(), v.to_string()))
            .collect(),
        parent: None,
        prev_elem: None,
        next_elem: None,
        has_children,
    };
    ElemRef {
        arena: Rc::new(Arena { nodes: vec![node] }),
        id: 0,
    }
}

// ---------------------------------------------------------------------------
// pick1 direct tests.
// ---------------------------------------------------------------------------

fn vc(index: usize, specificity: Specificity, value: &str) -> ValueContainer<String> {
    ValueContainer {
        index,
        specificity,
        value: value.to_string(),
    }
}

#[test]
fn pick1_empty_is_none() {
    let results: Vec<ValueContainer<String>> = vec![];
    assert_eq!(pick1_from(&results, false), None);
}

#[test]
fn pick1_single_is_fast_path() {
    let results = vec![vc(3, [0, 0, 1], "only")];
    assert_eq!(pick1_from(&results, false), Some("only".to_string()));
    assert_eq!(pick1_from(&results, true), Some("only".to_string()));
}

#[test]
fn pick1_picks_highest_specificity() {
    let results = vec![
        vc(0, [0, 0, 9], "tag"),
        vc(1, [0, 9, 0], "class"),
        vc(2, [1, 0, 0], "id"),
    ];
    // Per-level compare: id beats class beats tag, not a base-N reduction.
    assert_eq!(pick1_from(&results, false), Some("id".to_string()));
}

#[test]
fn pick1_tie_break_prefers_last_by_default() {
    let results = vec![vc(0, [0, 1, 0], "first"), vc(5, [0, 1, 0], "last")];
    assert_eq!(pick1_from(&results, false), Some("last".to_string()));
}

#[test]
fn pick1_tie_break_prefers_first_when_requested() {
    let results = vec![vc(0, [0, 1, 0], "first"), vc(5, [0, 1, 0], "last")];
    assert_eq!(pick1_from(&results, true), Some("first".to_string()));
}

// ---------------------------------------------------------------------------
// compare_specificity direct table.
// ---------------------------------------------------------------------------

#[test]
fn compare_specificity_lexicographic_levels() {
    use Ordering::{Equal, Greater, Less};
    let cases: &[(Specificity, Specificity, Ordering)] = &[
        ([0, 0, 0], [0, 0, 0], Equal),
        ([1, 0, 0], [0, 0, 0], Greater),
        ([0, 0, 0], [1, 0, 0], Less),
        ([0, 2, 0], [0, 1, 0], Greater),
        ([0, 1, 0], [0, 2, 0], Less),
        ([0, 0, 2], [0, 0, 1], Greater),
        ([0, 0, 1], [0, 0, 2], Less),
        ([0, 0, 5], [0, 0, 5], Equal),
        // id dominates regardless of lower levels, so this is not a base-N sum.
        ([1, 0, 0], [0, 9, 9], Greater),
        // class dominates tag.
        ([0, 1, 0], [0, 0, 9], Greater),
    ];
    for (a, b, expected) in cases {
        assert_eq!(
            compare_specificity(*a, *b),
            *expected,
            "compare_specificity({a:?}, {b:?})"
        );
    }
}

// ---------------------------------------------------------------------------
// pickAll and end-to-end matching.
// ---------------------------------------------------------------------------

#[test]
fn pick_all_returns_every_match() {
    let picker = picker_from(
        &[("[foo]", "a"), ("[bar]", "b")],
        DecisionTreeOptions::default(),
    );
    let el = single("div", &[("foo", "1"), ("bar", "2")], false);
    let all = picker.pick_all(el);
    let mut values: Vec<String> = all.iter().map(|v| v.value.clone()).collect();
    values.sort();
    assert_eq!(values, vec!["a".to_string(), "b".to_string()]);
}

#[test]
fn pick_all_preserves_dfs_order() {
    // Two [foo] selectors share one branch and emit before [bar], so the
    // order is a (index 0), c (index 2), then b (index 1). No sorting.
    let picker = picker_from(
        &[("[foo]", "a"), ("[bar]", "b"), ("[foo]", "c")],
        DecisionTreeOptions::default(),
    );
    let el = single("div", &[("foo", "1"), ("bar", "2")], false);
    let all = picker.pick_all(el);
    let order: Vec<(usize, String)> = all.iter().map(|v| (v.index, v.value.clone())).collect();
    assert_eq!(
        order,
        vec![
            (0, "a".to_string()),
            (2, "c".to_string()),
            (1, "b".to_string()),
        ]
    );
}

#[test]
fn pick1_end_to_end_uses_specificity() {
    let picker = picker_from(
        &[("div", "tag"), ("#main", "id")],
        DecisionTreeOptions::default(),
    );
    let el = single("div", &[("id", "main")], false);
    assert_eq!(picker.pick1(el, false), Some("id".to_string()));
}

// ---------------------------------------------------------------------------
// Attribute matcher execution, both modes.
// ---------------------------------------------------------------------------

fn matches_attr(selector: &str, attr_value: &str, opts: DecisionTreeOptions) -> bool {
    let picker = picker_from(&[(selector, "hit")], opts);
    let el = single("div", &[("a", attr_value)], false);
    !picker.pick_all(el).is_empty()
}

#[test]
fn matcher_equals() {
    assert!(matches_attr(
        "[a=foo]",
        "foo",
        DecisionTreeOptions::default()
    ));
    assert!(!matches_attr(
        "[a=foo]",
        "bar",
        DecisionTreeOptions::default()
    ));
    assert!(!matches_attr(
        "[a=foo]",
        "FOO",
        DecisionTreeOptions::default()
    ));
    assert!(matches_attr(
        "[a=foo i]",
        "FOO",
        DecisionTreeOptions::default()
    ));
}

#[test]
fn matcher_includes() {
    assert!(matches_attr(
        "[a~=foo]",
        "x foo y",
        DecisionTreeOptions::default()
    ));
    // split is on the full ASCII whitespace set
    assert!(matches_attr(
        "[a~=foo]",
        "x\tfoo",
        DecisionTreeOptions::default()
    ));
    assert!(!matches_attr(
        "[a~=foo]",
        "xfoo",
        DecisionTreeOptions::default()
    ));
    assert!(matches_attr(
        "[a~=foo i]",
        "X FOO",
        DecisionTreeOptions::default()
    ));
    // newline separates tokens too
    assert!(matches_attr(
        "[a~=foo]",
        "x\nfoo",
        DecisionTreeOptions::default()
    ));
}

#[test]
fn matcher_prefix() {
    assert!(matches_attr(
        "[a^=foo]",
        "foobar",
        DecisionTreeOptions::default()
    ));
    assert!(!matches_attr(
        "[a^=foo]",
        "barfoo",
        DecisionTreeOptions::default()
    ));
    assert!(matches_attr(
        "[a^=foo i]",
        "FOObar",
        DecisionTreeOptions::default()
    ));
}

#[test]
fn matcher_suffix() {
    assert!(matches_attr(
        "[a$=bar]",
        "foobar",
        DecisionTreeOptions::default()
    ));
    assert!(!matches_attr(
        "[a$=bar]",
        "barfoo",
        DecisionTreeOptions::default()
    ));
    assert!(matches_attr(
        "[a$=bar i]",
        "fooBAR",
        DecisionTreeOptions::default()
    ));
}

#[test]
fn matcher_substring() {
    assert!(matches_attr(
        "[a*=oob]",
        "foobar",
        DecisionTreeOptions::default()
    ));
    assert!(!matches_attr(
        "[a*=zzz]",
        "foobar",
        DecisionTreeOptions::default()
    ));
    assert!(matches_attr(
        "[a*=OOB i]",
        "foobar",
        DecisionTreeOptions::default()
    ));
}

#[test]
fn matcher_dash() {
    assert!(matches_attr(
        "[a|=en]",
        "en",
        DecisionTreeOptions::default()
    ));
    assert!(matches_attr(
        "[a|=en]",
        "en-US",
        DecisionTreeOptions::default()
    ));
    assert!(!matches_attr(
        "[a|=en]",
        "english",
        DecisionTreeOptions::default()
    ));
    assert!(!matches_attr(
        "[a|=en]",
        "fr",
        DecisionTreeOptions::default()
    ));
    assert!(matches_attr(
        "[a|=en i]",
        "EN-US",
        DecisionTreeOptions::default()
    ));
}

// ---------------------------------------------------------------------------
// Empty-value matcher edges. These follow string-membership and
// index-after-prefix rules that are easy to get subtly wrong.
// ---------------------------------------------------------------------------

#[test]
fn matcher_includes_empty_value() {
    let d = DecisionTreeOptions::default;
    // split yields [""], so only an empty actual holds the empty token.
    assert!(matches_attr("[a~=\"\"]", "", d()));
    assert!(!matches_attr("[a~=\"\"]", "x", d()));
    assert!(!matches_attr("[a~=\"\"]", "x y", d()));
}

#[test]
fn matcher_dash_empty_value() {
    let d = DecisionTreeOptions::default;
    assert!(matches_attr("[a|=\"\"]", "", d())); // exact match
    assert!(matches_attr("[a|=\"\"]", "-x", d())); // empty prefix then dash
    assert!(!matches_attr("[a|=\"\"]", "x", d()));
}

#[test]
fn matcher_substring_prefix_suffix_empty_value() {
    let d = DecisionTreeOptions::default;
    assert!(matches_attr("[a*=\"\"]", "", d()));
    assert!(matches_attr("[a*=\"\"]", "abc", d()));
    assert!(matches_attr("[a^=\"\"]", "abc", d()));
    assert!(matches_attr("[a$=\"\"]", "abc", d()));
}

#[test]
fn matcher_equals_empty_value() {
    let d = DecisionTreeOptions::default;
    assert!(matches_attr("[a=\"\"]", "", d()));
    assert!(!matches_attr("[a=\"\"]", "x", d()));
}

#[test]
fn matcher_includes_whitespace_edges() {
    let d = DecisionTreeOptions::default;
    // Leading and trailing separators still expose the token.
    assert!(matches_attr("[a~=foo]", " foo", d()));
    assert!(matches_attr("[a~=foo]", "foo ", d()));
}

// ---------------------------------------------------------------------------
// Allow-listed pseudo-classes that the snapshot suite never builds.
// ---------------------------------------------------------------------------

#[test]
fn any_link_builds_and_matches() {
    let picker = picker_from(&[(":any-link", "link")], DecisionTreeOptions::default());
    let anchor = single("a", &[("href", "/x")], false);
    assert_eq!(picker.pick1(anchor, false), Some("link".to_string()));

    let picker2 = picker_from(&[(":any-link", "link")], DecisionTreeOptions::default());
    let plain = single("a", &[], false);
    assert_eq!(picker2.pick_all(plain).len(), 0);
}

#[test]
fn last_child_builds_and_matches() {
    let picker = picker_from(&[(":last-child", "last")], DecisionTreeOptions::default());
    let el = single("li", &[], false);
    assert_eq!(picker.pick1(el, false), Some("last".to_string()));
}

// ---------------------------------------------------------------------------
// A builder generic over R proves build is not hard-wired to String.
// ---------------------------------------------------------------------------

#[test]
fn custom_builder_returns_count() {
    let owned = vec![
        ("foo".to_string(), "x".to_string()),
        ("[bar]".to_string(), "y".to_string()),
    ];
    let tree =
        DecisionTree::new(owned, DecisionTreeOptions::default()).expect("selector should build");
    let count = tree.build(|nodes: &[DecisionTreeNode<String>]| nodes.len());
    assert_eq!(count, 2);
}

// ---------------------------------------------------------------------------
// Default options keep listed-style attributes case-sensitive.
// ---------------------------------------------------------------------------

#[test]
fn default_options_keep_case_sensitive() {
    // No attributesWithNormalizedValues, so [type=Text] stays case-sensitive.
    assert!(matches_attr_named(
        "type",
        "[type=Text]",
        "Text",
        DecisionTreeOptions::default()
    ));
    assert!(!matches_attr_named(
        "type",
        "[type=Text]",
        "text",
        DecisionTreeOptions::default()
    ));
}

fn matches_attr_named(
    attr: &str,
    selector: &str,
    attr_value: &str,
    opts: DecisionTreeOptions,
) -> bool {
    let picker = picker_from(&[(selector, "hit")], opts);
    let el = single("input", &[(attr, attr_value)], false);
    !picker.pick_all(el).is_empty()
}
