use std::time::{Duration, Instant};

use selene_core::{
    CancellationChecker, CancellationToken, GraphId, LabelDiff, LabelSet, NodeId, PropertyDiff,
    PropertyMap, Value,
};

use super::*;
use crate::SharedGraph;

fn db_string(value: &str) -> DbString {
    selene_core::db_string(value).expect("test string fits DB string cap")
}

fn props(key: &DbString, value: Value) -> PropertyMap {
    PropertyMap::from_pairs([(key.clone(), value)]).expect("test property map is valid")
}

#[test]
fn exact_text_search_ranks_labelled_string_nodes() {
    let graph = SharedGraph::new(GraphId::new(431_001));
    let doc = db_string("TextDoc");
    let other = db_string("OtherDoc");
    let body = db_string("body");
    {
        let mut txn = graph.begin_write();
        let mut mutator = txn.mutator();
        mutator
            .create_node(
                LabelSet::single(doc.clone()),
                props(
                    &body,
                    Value::String(db_string("graph memory graph retrieval")),
                ),
            )
            .unwrap();
        mutator
            .create_node(
                LabelSet::single(doc.clone()),
                props(
                    &body,
                    Value::String(db_string("vector retrieval retrieval")),
                ),
            )
            .unwrap();
        mutator
            .create_node(
                LabelSet::single(doc.clone()),
                props(&body, Value::String(db_string("graph search"))),
            )
            .unwrap();
        mutator
            .create_node(LabelSet::single(doc.clone()), props(&body, Value::Int(7)))
            .unwrap();
        mutator
            .create_node(
                LabelSet::single(other),
                props(&body, Value::String(db_string("graph retrieval"))),
            )
            .unwrap();
        txn.commit().unwrap();
    }

    let hits = graph
        .exact_text_search_nodes(&doc, &body, "Graph retrieval", 3)
        .unwrap();

    assert_eq!(
        hits.iter().map(|hit| hit.node_id).collect::<Vec<_>>(),
        vec![NodeId::new(1), NodeId::new(2), NodeId::new(3)]
    );
    assert!(hits[0].score > hits[1].score);
    assert!(hits[1].score > hits[2].score);
}

#[test]
fn exact_text_search_tokenizes_case_and_punctuation() {
    let graph = SharedGraph::new(GraphId::new(431_002));
    let doc = db_string("TextTokenDoc");
    let body = db_string("body");
    {
        let mut txn = graph.begin_write();
        txn.mutator()
            .create_node(
                LabelSet::single(doc.clone()),
                props(
                    &body,
                    Value::String(db_string("Agentic-memory, Graph_Retrieval!")),
                ),
            )
            .unwrap();
        txn.commit().unwrap();
    }

    let hits = graph
        .exact_text_search_nodes(&doc, &body, "agentic graph retrieval", 10)
        .unwrap();

    assert_eq!(hits.len(), 1);
    assert_eq!(hits[0].node_id, NodeId::new(1));
    assert!(hits[0].score > 0.0);
}

#[test]
fn tokenizer_borrows_lowercase_and_handles_multichar_lowercase() {
    assert!(matches!(
        tokenize_borrowed("graph").next(),
        Some(std::borrow::Cow::Borrowed("graph"))
    ));
    assert_eq!(
        tokenize_borrowed("İstanbul IĞDIR graph")
            .map(std::borrow::Cow::into_owned)
            .collect::<Vec<_>>(),
        vec!["i\u{307}stanbul", "iğdir", "graph"]
    );
}

#[test]
fn tokenizer_anchors_offsets_after_skipped_separators() {
    assert_eq!(
        tokenize_borrowed("alpha, graph beta")
            .map(std::borrow::Cow::into_owned)
            .collect::<Vec<_>>(),
        vec!["alpha", "graph", "beta"]
    );
    assert_eq!(
        tokenize_borrowed("Distilled summary covering Aionforge, Alice, NYC, Rust.")
            .map(std::borrow::Cow::into_owned)
            .collect::<Vec<_>>(),
        vec![
            "distilled",
            "summary",
            "covering",
            "aionforge",
            "alice",
            "nyc",
            "rust"
        ]
    );
}

#[test]
fn exact_text_search_parallel_matches_serial_and_index_ordering() {
    let graph = SharedGraph::new(GraphId::new(431_007));
    let doc = db_string("TextParallelDoc");
    let body = db_string("body");
    {
        let mut txn = graph.begin_write();
        let mut mutator = txn.mutator();
        for idx in 0..24 {
            let topic = match idx % 4 {
                0 => "gql",
                1 => "vector",
                2 => "memory",
                _ => "planner",
            };
            let state = if idx % 2 == 0 { "current" } else { "stale" };
            let text = format!("{topic} {state} retrieval evidence fact{}", idx % 5);
            mutator
                .create_node(
                    LabelSet::single(doc.clone()),
                    props(&body, Value::String(db_string(&text))),
                )
                .unwrap();
        }
        txn.commit().unwrap();
    }

    let token = CancellationToken::new();
    let serial_pool = rayon::ThreadPoolBuilder::new()
        .num_threads(1)
        .build()
        .expect("single-thread rayon pool builds");
    let serial = serial_pool
        .install(|| {
            graph.exact_text_search_nodes_checked(
                &doc,
                &body,
                "gql current retrieval",
                8,
                CancellationChecker::new(Some(&token), None),
            )
        })
        .unwrap();
    let checked = graph
        .exact_text_search_nodes_checked(
            &doc,
            &body,
            "gql current retrieval",
            8,
            CancellationChecker::new(Some(&token), None),
        )
        .unwrap();
    let indexed = graph
        .build_text_index(&doc, &body)
        .unwrap()
        .search("gql current retrieval", 8);

    assert_eq!(checked, serial);
    assert_eq!(checked, indexed);
}

#[test]
fn exact_text_search_tracks_update_and_delete_visibility() {
    let graph = SharedGraph::new(GraphId::new(431_003));
    let doc = db_string("TextMutableDoc");
    let body = db_string("body");
    let stale;
    let fresh;
    {
        let mut txn = graph.begin_write();
        let mut mutator = txn.mutator();
        stale = mutator
            .create_node(
                LabelSet::single(doc.clone()),
                props(&body, Value::String(db_string("stale memory"))),
            )
            .unwrap();
        fresh = mutator
            .create_node(
                LabelSet::single(doc.clone()),
                props(&body, Value::String(db_string("fresh memory"))),
            )
            .unwrap();
        txn.commit().unwrap();
    }
    assert_eq!(
        graph
            .exact_text_search_nodes(&doc, &body, "fresh", 10)
            .unwrap()
            .iter()
            .map(|hit| hit.node_id)
            .collect::<Vec<_>>(),
        vec![fresh]
    );

    {
        let mut txn = graph.begin_write();
        txn.mutator()
            .update_node(
                stale,
                LabelDiff::new([], []).unwrap(),
                PropertyDiff::new(
                    [(body.clone(), Value::String(db_string("fresh updated")))],
                    [],
                )
                .unwrap(),
            )
            .unwrap();
        txn.commit().unwrap();
    }
    assert_eq!(
        graph
            .exact_text_search_nodes(&doc, &body, "updated", 10)
            .unwrap()
            .iter()
            .map(|hit| hit.node_id)
            .collect::<Vec<_>>(),
        vec![stale]
    );

    {
        let mut txn = graph.begin_write();
        txn.mutator().delete_node(stale).unwrap();
        txn.commit().unwrap();
    }
    assert_eq!(
        graph
            .exact_text_search_nodes(&doc, &body, "fresh", 10)
            .unwrap()
            .iter()
            .map(|hit| hit.node_id)
            .collect::<Vec<_>>(),
        vec![fresh]
    );
}

#[test]
fn exact_text_search_empty_query_and_zero_k_are_empty() {
    let graph = SharedGraph::new(GraphId::new(431_004));
    let doc = db_string("TextEmptyDoc");
    let body = db_string("body");
    {
        let mut txn = graph.begin_write();
        txn.mutator()
            .create_node(
                LabelSet::single(doc.clone()),
                props(&body, Value::String(db_string("graph memory"))),
            )
            .unwrap();
        txn.commit().unwrap();
    }

    assert!(
        graph
            .exact_text_search_nodes(&doc, &body, "!!!", 10)
            .unwrap()
            .is_empty()
    );
    assert!(
        graph
            .exact_text_search_nodes(&doc, &body, "graph", 0)
            .unwrap()
            .is_empty()
    );
}

#[test]
fn exact_text_search_checked_observes_cancelled_token_before_scan() {
    let graph = SharedGraph::new(GraphId::new(431_005));
    let doc = db_string("TextCancelDoc");
    let body = db_string("body");
    let token = CancellationToken::new();
    token.cancel();
    let checker = CancellationChecker::new(Some(&token), None);

    let err = graph
        .exact_text_search_nodes_checked(&doc, &body, "graph", 10, checker)
        .expect_err("cancelled token should stop search");

    assert!(matches!(err, TextSearchError::Cancelled));
}

#[test]
fn exact_text_search_checked_observes_elapsed_deadline_before_scan() {
    let graph = SharedGraph::new(GraphId::new(431_006));
    let doc = db_string("TextTimeoutDoc");
    let body = db_string("body");
    let checker = CancellationChecker::new(None, Some(Instant::now() - Duration::from_secs(1)));

    let err = graph
        .exact_text_search_nodes_checked(&doc, &body, "graph", 10, checker)
        .expect_err("expired deadline should stop search");

    assert!(matches!(err, TextSearchError::Timeout { .. }));
}
