use indoc::indoc;
use liwe::graph::Graph;
use liwe::model::config::MarkdownOptions;
use liwe::query::prelude::{
    and, eq, filter, find, included_by, includes, key_eq, key_in, key_ne, key_nin, nor, or,
    referenced_by, references,
};
use liwe::query::{execute, FindOp, InclusionAnchor, Outcome, ReferenceAnchor};
use liwe::state::from_indoc;

fn run_find_keys(docs: &str, op: FindOp) -> Vec<String> {
    let graph = Graph::import(&from_indoc(docs), MarkdownOptions::default(), None);
    match execute(&find(op), &graph) {
        Outcome::Find { matches } => matches.into_iter().map(|m| m.key.to_string()).collect(),
        other => panic!("expected Find, got {:?}", other),
    }
}

fn assert_keys(docs: &str, op: FindOp, expected: &[&str]) {
    let mut actual = run_find_keys(docs, op);
    actual.sort();
    let mut expected: Vec<String> = expected.iter().map(|s| s.to_string()).collect();
    expected.sort();
    assert_eq!(actual, expected);
}

#[test]
fn key_eq_selects_one() {
    assert_keys(
        indoc! {"
            # A
            _
            # B
            _
            # C
        "},
        filter(key_eq("2")),
        &["2"],
    );
}

#[test]
fn key_in_selects_subset() {
    assert_keys(
        indoc! {"
            # A
            _
            # B
            _
            # C
        "},
        filter(key_in(&["1", "3"])),
        &["1", "3"],
    );
}

#[test]
fn key_ne_excludes() {
    assert_keys(
        indoc! {"
            # A
            _
            # B
            _
            # C
        "},
        filter(key_ne("2")),
        &["1", "3"],
    );
}

#[test]
fn included_by_direct() {
    assert_keys(
        indoc! {"
            [b](2)
            _
            [c](3)
            _
            # C
        "},
        filter(included_by(InclusionAnchor::with_max("1", 1))),
        &["2"],
    );
}

#[test]
fn included_by_transitive() {
    assert_keys(
        indoc! {"
            [b](2)
            _
            [c](3)
            _
            # C
        "},
        filter(included_by(InclusionAnchor::with_max("1", 5))),
        &["2", "3"],
    );
}

#[test]
fn included_by_range_excludes_direct() {
    assert_keys(
        indoc! {"
            [b](2)
            _
            [c](3)
            _
            # C
        "},
        filter(included_by(InclusionAnchor::new("1", 2, 5))),
        &["3"],
    );
}

#[test]
fn includes_outbound() {
    assert_keys(
        indoc! {"
            [b](2)
            _
            [c](3)
            _
            # C
        "},
        filter(includes(InclusionAnchor::with_max("3", 5))),
        &["1", "2"],
    );
}

#[test]
fn anchor_excluded_from_walk_results() {
    assert_keys(
        indoc! {"
            [b](2)
            _
            [c](3)
            _
            # C
        "},
        filter(included_by(InclusionAnchor::with_max("1", 5))),
        &["2", "3"],
    );
}

#[test]
fn multi_anchor_intersects() {
    assert_keys(
        indoc! {"
            [c](3)
            _
            [c](3)
            _
            # C
        "},
        filter(and(vec![
            included_by(InclusionAnchor::with_max("1", 5)),
            included_by(InclusionAnchor::with_max("2", 5)),
        ])),
        &["3"],
    );
}

#[test]
fn references_by_inline_link_in_text() {
    assert_keys(
        indoc! {"
            # A

            See [other](2) for details.
            _
            # B
        "},
        filter(references(ReferenceAnchor::with_max("2", 1))),
        &["1"],
    );
}

#[test]
fn referenced_by_inline() {
    assert_keys(
        indoc! {"
            # A

            See [other](2) for details.
            _
            # B
        "},
        filter(referenced_by(ReferenceAnchor::with_max("1", 1))),
        &["2"],
    );
}

#[test]
fn missing_anchor_returns_empty() {
    assert_keys(
        indoc! {"
            # A
        "},
        filter(included_by(InclusionAnchor::with_max("does-not-exist", 5))),
        &[],
    );
}

#[test]
fn empty_corpus_returns_empty() {
    let graph = Graph::import(
        &std::collections::HashMap::new(),
        MarkdownOptions::default(),
        None,
    );
    let op = filter(key_eq("anything"));
    match execute(&find(op), &graph) {
        Outcome::Find { matches } => assert!(matches.is_empty()),
        other => panic!("{:?}", other),
    }
}

#[test]
fn graph_op_combines_with_frontmatter_predicate() {
    assert_keys(
        indoc! {"
            ---
            status: draft
            ---
            [b](2)
            _
            ---
            status: published
            ---
            # B
        "},
        filter(and(vec![
            included_by(InclusionAnchor::with_max("1", 5)),
            eq("status", "published"),
        ])),
        &["2"],
    );
}

#[test]
fn key_nin_excludes_listed() {
    assert_keys(
        indoc! {"
            # A
            _
            # B
            _
            # C
            _
            # D
        "},
        filter(key_nin(&["2", "4"])),
        &["1", "3"],
    );
}

#[test]
fn key_eq_against_missing_returns_empty() {
    assert_keys(
        indoc! {"
            # A
            _
            # B
        "},
        filter(key_eq("missing")),
        &[],
    );
}

#[test]
fn included_by_chain_at_depth_3() {
    assert_keys(
        indoc! {"
            [b](2)
            _
            [c](3)
            _
            [d](4)
            _
            # D
        "},
        filter(included_by(InclusionAnchor::new("1", 3, 3))),
        &["4"],
    );
}

#[test]
fn includes_outbound_chain() {
    assert_keys(
        indoc! {"
            [b](2)
            _
            [c](3)
            _
            [d](4)
            _
            # D
        "},
        filter(includes(InclusionAnchor::with_max("4", 5))),
        &["1", "2", "3"],
    );
}

#[test]
fn included_by_polyhierarchy_set_semantics() {
    assert_keys(
        indoc! {"
            [c](3)
            _
            [c](3)
            _
            # C
        "},
        filter(and(vec![
            included_by(InclusionAnchor::with_max("1", 5)),
            included_by(InclusionAnchor::with_max("2", 5)),
        ])),
        &["3"],
    );
}

#[test]
fn references_multi_hop() {
    assert_keys(
        indoc! {"
            See [b](2).
            _
            See [c](3).
            _
            # C
        "},
        filter(references(ReferenceAnchor::with_max("3", 2))),
        &["1", "2"],
    );
}

#[test]
fn referenced_by_multi_hop() {
    assert_keys(
        indoc! {"
            See [b](2).
            _
            See [c](3).
            _
            # C
        "},
        filter(referenced_by(ReferenceAnchor::with_max("1", 2))),
        &["2", "3"],
    );
}

#[test]
fn reference_self_link_excluded_from_walk() {
    assert_keys(
        indoc! {"
            See [self](1).
        "},
        filter(references(ReferenceAnchor::with_max("1", 1))),
        &[],
    );
}

#[test]
fn referenced_by_range_excludes_direct() {
    assert_keys(
        indoc! {"
            See [b](2).
            _
            See [c](3).
            _
            # C
        "},
        filter(referenced_by(ReferenceAnchor::new("1", 2, 3))),
        &["3"],
    );
}

#[test]
fn or_of_two_graph_ops() {
    assert_keys(
        indoc! {"
            [b](2)
            _
            [c](3)
            _
            # C
        "},
        filter(or(vec![
            key_eq("1"),
            included_by(InclusionAnchor::with_max("2", 1)),
        ])),
        &["1", "3"],
    );
}

#[test]
fn nor_wraps_walk() {
    assert_keys(
        indoc! {"
            [b](2)
            _
            # B
            _
            # C
        "},
        filter(nor(vec![included_by(InclusionAnchor::with_max("1", 5))])),
        &["1", "3"],
    );
}

#[test]
fn nor_excludes_union_of_children() {
    assert_keys(
        indoc! {"
            # A
            _
            # B
            _
            # C
        "},
        filter(nor(vec![key_eq("1"), key_eq("3")])),
        &["2"],
    );
}

#[test]
fn combined_three_predicates_hub_under_anchor() {
    assert_keys(
        indoc! {"
            ---
            status: draft
            ---
            [hub](2)
            _
            ---
            status: draft
            ---
            [a](3)

            [b](4)

            [c](5)
            _
            # A
            _
            # B
            _
            # C
        "},
        filter(and(vec![
            eq("status", "draft"),
            included_by(InclusionAnchor::with_max("1", 5)),
        ])),
        &["2"],
    );
}

#[test]
fn or_anchor_or_descendants() {
    assert_keys(
        indoc! {"
            [b](2)
            _
            [c](3)
            _
            # C
        "},
        filter(or(vec![
            key_eq("1"),
            included_by(InclusionAnchor::with_max("1", 5)),
        ])),
        &["1", "2", "3"],
    );
}

#[test]
fn exclude_inside_descendants() {
    assert_keys(
        indoc! {"
            [b](2)

            [c](3)
            _
            # B
            _
            # C
        "},
        filter(and(vec![
            included_by(InclusionAnchor::with_max("1", 5)),
            key_ne("3"),
        ])),
        &["2"],
    );
}

#[test]
fn disconnected_components() {
    assert_keys(
        indoc! {"
            [b](2)
            _
            # B
            _
            [d](4)
            _
            # D
        "},
        filter(included_by(InclusionAnchor::with_max("1", 100))),
        &["2"],
    );
}

#[test]
fn sort_after_graph_filter() {
    use liwe::query::prelude::exists;
    use liwe::query::Sort;
    assert_keys(
        indoc! {"
            ---
            created: 2026-03-01
            ---
            [b](2)
            _
            ---
            created: 2026-01-01
            ---
            [d](4)
            _
            # B
            _
            # D
        "},
        FindOp::new()
            .filter(exists("created", true))
            .sort(Sort::asc("created"))
            .limit(2),
        &["1", "2"],
    );
}

#[test]
fn included_by_match_anchors_by_frontmatter() {
    assert_keys(
        indoc! {"
            ---
            kind: project
            ---
            [c](3)
            _
            ---
            kind: project
            ---
            [d](4)
            _
            # C
            _
            # D
        "},
        filter(included_by(InclusionAnchor::with_match(
            eq("kind", "project"),
            1,
            5,
        ))),
        &["3", "4"],
    );
}

#[test]
fn included_by_match_anchors_by_key_in() {
    assert_keys(
        indoc! {"
            [c](3)
            _
            [d](4)
            _
            [e](5)
            _
            # D
            _
            # E
        "},
        filter(included_by(InclusionAnchor::with_match(
            key_in(&["1", "2"]),
            1,
            5,
        ))),
        &["3", "4", "5"],
    );
}

#[test]
fn included_by_match_excludes_anchor_set() {
    assert_keys(
        indoc! {"
            ---
            kind: project
            ---
            [b](2)
            _
            ---
            kind: project
            ---
            [c](3)
            _
            # C
        "},
        filter(included_by(InclusionAnchor::with_match(
            eq("kind", "project"),
            1,
            5,
        ))),
        &["3"],
    );
}
