use motosan_index_core::core::metadata::{
    FilterCondition, FilterOperator, MetadataFilter, MetadataFilters, MetadataValue,
};
use motosan_index_core::core::node::{Node, NodeWithScore};
use motosan_index_core::traits::post_processor::PostProcessor;
use motosan_index_core::{DocumentId, NodeId};
use motosan_index_postprocessor::{MetadataFilterProcessor, SimilarityCutoff, TopN};

fn make_scored(content: &str, score: f32) -> NodeWithScore {
    NodeWithScore {
        node: Node::from_chunk(content, DocumentId::new(), 0),
        score,
    }
}

#[tokio::test]
async fn test_similarity_cutoff_filters_below_threshold() {
    let pp = SimilarityCutoff::new(0.5);
    let nodes = vec![
        make_scored("high", 0.9),
        make_scored("low", 0.3),
        make_scored("mid", 0.5),
    ];
    let result = pp.process(nodes).await.unwrap();
    assert_eq!(result.len(), 2);
    assert_eq!(result[0].node.content, "high");
    assert_eq!(result[1].node.content, "mid");
}

#[tokio::test]
async fn test_similarity_cutoff_empty_input() {
    let pp = SimilarityCutoff::new(0.5);
    let result = pp.process(vec![]).await.unwrap();
    assert!(result.is_empty());
}

#[tokio::test]
async fn test_top_n_truncates() {
    let pp = TopN::new(2);
    let nodes = vec![
        make_scored("a", 0.9),
        make_scored("b", 0.8),
        make_scored("c", 0.7),
    ];
    let result = pp.process(nodes).await.unwrap();
    assert_eq!(result.len(), 2);
    assert_eq!(result[0].node.content, "a");
    assert_eq!(result[1].node.content, "b");
}

#[tokio::test]
async fn test_top_n_fewer_than_n() {
    let pp = TopN::new(10);
    let nodes = vec![make_scored("only", 0.5)];
    let result = pp.process(nodes).await.unwrap();
    assert_eq!(result.len(), 1);
}

#[tokio::test]
async fn test_metadata_filter_eq() {
    let mut node_match = Node::from_chunk("match", DocumentId::new(), 0);
    node_match
        .metadata
        .insert("lang".into(), MetadataValue::String("rust".into()));
    let mut node_miss = Node::from_chunk("miss", DocumentId::new(), 0);
    node_miss
        .metadata
        .insert("lang".into(), MetadataValue::String("python".into()));

    let pp = MetadataFilterProcessor::new(MetadataFilters {
        filters: vec![MetadataFilter {
            key: "lang".into(),
            value: MetadataValue::String("rust".into()),
            operator: FilterOperator::Eq,
        }],
        condition: FilterCondition::And,
    });

    let result = pp
        .process(vec![
            NodeWithScore {
                node: node_match,
                score: 0.9,
            },
            NodeWithScore {
                node: node_miss,
                score: 0.8,
            },
        ])
        .await
        .unwrap();
    assert_eq!(result.len(), 1);
    assert_eq!(result[0].node.content, "match");
}

#[tokio::test]
async fn test_composing_two_processors() {
    let nodes = vec![
        make_scored("a", 0.9),
        make_scored("b", 0.3), // will be cut
        make_scored("c", 0.7),
        make_scored("d", 0.6),
    ];

    let cutoff = SimilarityCutoff::new(0.5);
    let top2 = TopN::new(2);

    let after_cutoff = cutoff.process(nodes).await.unwrap();
    assert_eq!(after_cutoff.len(), 3); // a, c, d

    let after_top2 = top2.process(after_cutoff).await.unwrap();
    assert_eq!(after_top2.len(), 2); // a, c
    assert_eq!(after_top2[0].node.content, "a");
    assert_eq!(after_top2[1].node.content, "c");
}

#[tokio::test]
async fn test_top_n_zero_returns_empty() {
    let pp = TopN::new(0);
    let nodes = vec![make_scored("a", 0.9), make_scored("b", 0.8)];
    let result = pp.process(nodes).await.unwrap();
    assert!(result.is_empty(), "TopN(0) should return empty result");
}

#[tokio::test]
async fn test_similarity_cutoff_above_one_filters_everything() {
    let pp = SimilarityCutoff::new(1.1);
    let nodes = vec![
        make_scored("a", 1.0),
        make_scored("b", 0.99),
        make_scored("c", 0.5),
    ];
    let result = pp.process(nodes).await.unwrap();
    assert!(
        result.is_empty(),
        "SimilarityCutoff(1.1) should filter out all nodes since max cosine similarity is 1.0"
    );
}
