//! This module test the search cutoff and ensure a few things:
//! 1. A basic test works and mark the search as degraded
//! 2. A test that ensure the filters are affectively applied even with a cutoff of 0
//! 3. A test that ensure the cutoff works well with the ranking scores

use std::collections::BTreeMap;
use std::sync::Arc;
use std::time::Duration;

use http_client::policy::IpPolicy;
use meili_snap::snapshot;

use crate::index::tests::TempIndex;
use crate::score_details::{ScoreDetails, ScoringStrategy};
use crate::search::facet::IndexFilter;
use crate::update::Setting;
use crate::vector::settings::EmbeddingSettings;
use crate::vector::{Embedder, EmbedderOptions};
use crate::{Criterion, Deadline, Filter, FilterableAttributesRule};

fn create_index() -> TempIndex {
    let index = TempIndex::new();

    index
        .update_settings(|s| {
            s.set_primary_key("id".to_owned());
            s.set_searchable_fields(vec!["text".to_owned()]);
            s.set_filterable_fields(vec![FilterableAttributesRule::Field("id".to_owned())]);
            s.set_criteria(vec![Criterion::Words, Criterion::Typo]);
        })
        .unwrap();

    // reverse the ID / insertion order so we see better what was sorted from what got the insertion order ordering
    index
        .add_documents(documents!([
            {
                "id": 4,
                "text": "hella puppo kefir",
            },
            {
                "id": 3,
                "text": "hella puppy kefir",
            },
            {
                "id": 2,
                "text": "hello",
            },
            {
                "id": 1,
                "text": "hello puppy",
            },
            {
                "id": 0,
                "text": "hello puppy kefir",
            },
        ]))
        .unwrap();
    index
}

#[test]
fn basic_degraded_search() {
    let index = create_index();
    let rtxn = index.read_txn().unwrap();

    let mut search = index.search(&rtxn);
    search.query("hello puppy kefir");
    search.limit(3);
    search.deadline(Deadline::from_budget(Duration::from_millis(0)));

    let result = search.execute().unwrap();
    assert!(result.degraded);
}

#[test]
fn degraded_search_cannot_skip_filter() {
    let index = create_index();
    let rtxn = index.read_txn().unwrap();

    let mut search = index.search(&rtxn);
    search.query("hello puppy kefir");
    search.limit(100);
    search.deadline(Deadline::from_budget(Duration::from_millis(0)));
    let filter_condition = Filter::from_str("id > 2").unwrap().unwrap();
    search.filter(IndexFilter::from(filter_condition));

    let result = search.execute().unwrap();
    assert!(result.degraded);
    snapshot!(format!("{:?}\n{:?}", result.candidates, result.documents_ids), @r###"
    RoaringBitmap<[0, 1]>
    [0, 1]
    "###);
}

#[test]
#[allow(clippy::format_collect)] // the test is already quite big
fn degraded_search_and_score_details() {
    let index = create_index();
    let rtxn = index.read_txn().unwrap();

    let mut search = index.search(&rtxn);
    search.query("hello puppy kefir");
    search.limit(4);
    search.scoring_strategy(ScoringStrategy::Detailed);
    search.deadline(Deadline::never());

    let result = search.execute().unwrap();
    snapshot!(format!("IDs: {:?}\nScores: {}\nScore Details:\n{:#?}", result.documents_ids, result.document_scores.iter().map(|scores| format!("{:.4} ", ScoreDetails::global_score(scores.iter()))).collect::<String>(), result.document_scores), @r###"
    IDs: [4, 1, 0, 3]
    Scores: 1.0000 0.9167 0.8333 0.6667 
    Score Details:
    [
        [
            Words(
                Words {
                    matching_words: 3,
                    max_matching_words: 3,
                },
            ),
            Typo(
                Typo {
                    typo_count: 0,
                    max_typo_count: 3,
                },
            ),
        ],
        [
            Words(
                Words {
                    matching_words: 3,
                    max_matching_words: 3,
                },
            ),
            Typo(
                Typo {
                    typo_count: 1,
                    max_typo_count: 3,
                },
            ),
        ],
        [
            Words(
                Words {
                    matching_words: 3,
                    max_matching_words: 3,
                },
            ),
            Typo(
                Typo {
                    typo_count: 2,
                    max_typo_count: 3,
                },
            ),
        ],
        [
            Words(
                Words {
                    matching_words: 2,
                    max_matching_words: 3,
                },
            ),
            Typo(
                Typo {
                    typo_count: 0,
                    max_typo_count: 2,
                },
            ),
        ],
    ]
    "###);

    // Do ONE loop iteration. Not much can be deduced, almost everyone matched the words first bucket.
    search.deadline(Deadline::never().with_stop_after(1));

    let result = search.execute().unwrap();
    snapshot!(format!("IDs: {:?}\nScores: {}\nScore Details:\n{:#?}", result.documents_ids, result.document_scores.iter().map(|scores| format!("{:.4} ", ScoreDetails::global_score(scores.iter()))).collect::<String>(), result.document_scores), @r###"
    IDs: [0, 1, 4, 2]
    Scores: 0.6667 0.6667 0.6667 0.0000 
    Score Details:
    [
        [
            Words(
                Words {
                    matching_words: 3,
                    max_matching_words: 3,
                },
            ),
            Skipped,
        ],
        [
            Words(
                Words {
                    matching_words: 3,
                    max_matching_words: 3,
                },
            ),
            Skipped,
        ],
        [
            Words(
                Words {
                    matching_words: 3,
                    max_matching_words: 3,
                },
            ),
            Skipped,
        ],
        [
            Skipped,
        ],
    ]
    "###);

    // Do TWO loop iterations. The first document should be entirely sorted
    search.deadline(Deadline::never().with_stop_after(2));

    let result = search.execute().unwrap();
    snapshot!(format!("IDs: {:?}\nScores: {}\nScore Details:\n{:#?}", result.documents_ids, result.document_scores.iter().map(|scores| format!("{:.4} ", ScoreDetails::global_score(scores.iter()))).collect::<String>(), result.document_scores), @r###"
    IDs: [4, 0, 1, 2]
    Scores: 1.0000 0.6667 0.6667 0.0000 
    Score Details:
    [
        [
            Words(
                Words {
                    matching_words: 3,
                    max_matching_words: 3,
                },
            ),
            Typo(
                Typo {
                    typo_count: 0,
                    max_typo_count: 3,
                },
            ),
        ],
        [
            Words(
                Words {
                    matching_words: 3,
                    max_matching_words: 3,
                },
            ),
            Skipped,
        ],
        [
            Words(
                Words {
                    matching_words: 3,
                    max_matching_words: 3,
                },
            ),
            Skipped,
        ],
        [
            Skipped,
        ],
    ]
    "###);

    // Do THREE loop iterations. The second document should be entirely sorted as well
    search.deadline(Deadline::never().with_stop_after(3));

    let result = search.execute().unwrap();
    snapshot!(format!("IDs: {:?}\nScores: {}\nScore Details:\n{:#?}", result.documents_ids, result.document_scores.iter().map(|scores| format!("{:.4} ", ScoreDetails::global_score(scores.iter()))).collect::<String>(), result.document_scores), @r###"
    IDs: [4, 1, 0, 2]
    Scores: 1.0000 0.9167 0.6667 0.0000 
    Score Details:
    [
        [
            Words(
                Words {
                    matching_words: 3,
                    max_matching_words: 3,
                },
            ),
            Typo(
                Typo {
                    typo_count: 0,
                    max_typo_count: 3,
                },
            ),
        ],
        [
            Words(
                Words {
                    matching_words: 3,
                    max_matching_words: 3,
                },
            ),
            Typo(
                Typo {
                    typo_count: 1,
                    max_typo_count: 3,
                },
            ),
        ],
        [
            Words(
                Words {
                    matching_words: 3,
                    max_matching_words: 3,
                },
            ),
            Skipped,
        ],
        [
            Skipped,
        ],
    ]
    "###);

    // Do FOUR loop iterations. The third document should be entirely sorted as well
    // The words bucket have still not progressed thus the last document doesn't have any info yet.
    search.deadline(Deadline::never().with_stop_after(4));

    let result = search.execute().unwrap();
    snapshot!(format!("IDs: {:?}\nScores: {}\nScore Details:\n{:#?}", result.documents_ids, result.document_scores.iter().map(|scores| format!("{:.4} ", ScoreDetails::global_score(scores.iter()))).collect::<String>(), result.document_scores), @r###"
    IDs: [4, 1, 0, 2]
    Scores: 1.0000 0.9167 0.8333 0.0000 
    Score Details:
    [
        [
            Words(
                Words {
                    matching_words: 3,
                    max_matching_words: 3,
                },
            ),
            Typo(
                Typo {
                    typo_count: 0,
                    max_typo_count: 3,
                },
            ),
        ],
        [
            Words(
                Words {
                    matching_words: 3,
                    max_matching_words: 3,
                },
            ),
            Typo(
                Typo {
                    typo_count: 1,
                    max_typo_count: 3,
                },
            ),
        ],
        [
            Words(
                Words {
                    matching_words: 3,
                    max_matching_words: 3,
                },
            ),
            Typo(
                Typo {
                    typo_count: 2,
                    max_typo_count: 3,
                },
            ),
        ],
        [
            Skipped,
        ],
    ]
    "###);

    // After FIVE loop iterations. The words ranking rule gave us a new bucket.
    search.deadline(Deadline::never().with_stop_after(5));

    let result = search.execute().unwrap();
    snapshot!(format!("IDs: {:?}\nScores: {}\nScore Details:\n{:#?}", result.documents_ids, result.document_scores.iter().map(|scores| format!("{:.4} ", ScoreDetails::global_score(scores.iter()))).collect::<String>(), result.document_scores), @r###"
    IDs: [4, 1, 0, 3]
    Scores: 1.0000 0.9167 0.8333 0.3333 
    Score Details:
    [
        [
            Words(
                Words {
                    matching_words: 3,
                    max_matching_words: 3,
                },
            ),
            Typo(
                Typo {
                    typo_count: 0,
                    max_typo_count: 3,
                },
            ),
        ],
        [
            Words(
                Words {
                    matching_words: 3,
                    max_matching_words: 3,
                },
            ),
            Typo(
                Typo {
                    typo_count: 1,
                    max_typo_count: 3,
                },
            ),
        ],
        [
            Words(
                Words {
                    matching_words: 3,
                    max_matching_words: 3,
                },
            ),
            Typo(
                Typo {
                    typo_count: 2,
                    max_typo_count: 3,
                },
            ),
        ],
        [
            Words(
                Words {
                    matching_words: 2,
                    max_matching_words: 3,
                },
            ),
            Skipped,
        ],
    ]
    "###);

    // After SIX loop iterations.
    // we finished
    search.deadline(Deadline::never().with_stop_after(6));

    let result = search.execute().unwrap();
    snapshot!(format!("IDs: {:?}\nScores: {}\nScore Details:\n{:#?}", result.documents_ids, result.document_scores.iter().map(|scores| format!("{:.4} ", ScoreDetails::global_score(scores.iter()))).collect::<String>(), result.document_scores), @r###"
    IDs: [4, 1, 0, 3]
    Scores: 1.0000 0.9167 0.8333 0.6667 
    Score Details:
    [
        [
            Words(
                Words {
                    matching_words: 3,
                    max_matching_words: 3,
                },
            ),
            Typo(
                Typo {
                    typo_count: 0,
                    max_typo_count: 3,
                },
            ),
        ],
        [
            Words(
                Words {
                    matching_words: 3,
                    max_matching_words: 3,
                },
            ),
            Typo(
                Typo {
                    typo_count: 1,
                    max_typo_count: 3,
                },
            ),
        ],
        [
            Words(
                Words {
                    matching_words: 3,
                    max_matching_words: 3,
                },
            ),
            Typo(
                Typo {
                    typo_count: 2,
                    max_typo_count: 3,
                },
            ),
        ],
        [
            Words(
                Words {
                    matching_words: 2,
                    max_matching_words: 3,
                },
            ),
            Typo(
                Typo {
                    typo_count: 0,
                    max_typo_count: 2,
                },
            ),
        ],
    ]
    "###);
}

#[test]
#[ignore] // See <https://github.com/meilisearch/meilisearch/pull/6116> for explanations
fn degraded_search_and_score_details_vector() {
    let index = create_index();

    index
        .add_documents(documents!([
            {
                "id": 4,
                "text": "hella puppo kefir",
                "_vectors": {
                    "default": [0.1, 0.1]
                }
            },
            {
                "id": 3,
                "text": "hella puppy kefir",
                "_vectors": {
                    "default": [-0.1, 0.1]
                }
            },
            {
                "id": 2,
                "text": "hello",
                "_vectors": {
                    "default": [0.1, -0.1]
                }
            },
            {
                "id": 1,
                "text": "hello puppy",
                "_vectors": {
                    "default": [-0.1, -0.1]
                }
            },
            {
                "id": 0,
                "text": "hello puppy kefir",
                "_vectors": {
                    "default": null
                }
            },
        ]))
        .unwrap();

    index
        .update_settings(|settings| {
            let mut embedders = BTreeMap::new();
            embedders.insert(
                "default".into(),
                Setting::Set(EmbeddingSettings {
                    source: Setting::Set(crate::vector::settings::EmbedderSource::UserProvided),
                    dimensions: Setting::Set(2),
                    ..Default::default()
                }),
            );
            settings.set_embedder_settings(embedders);
            settings.set_vector_store(crate::vector::VectorStoreBackend::Hannoy);
        })
        .unwrap();

    let rtxn = index.read_txn().unwrap();
    let mut search = index.search(&rtxn);

    let embedder = Arc::new(
        Embedder::new(
            EmbedderOptions::UserProvided(crate::vector::embedder::manual::EmbedderOptions {
                dimensions: 2,
                distribution: None,
            }),
            0,
            // NO DANGER: test code
            IpPolicy::danger_always_allow(),
        )
        .unwrap(),
    );

    search.semantic("default".into(), embedder, false, Some(vec![1., -1.]), None);

    search.limit(4);
    search.scoring_strategy(ScoringStrategy::Detailed);
    search.deadline(Deadline::never());

    let result = search.execute().unwrap();
    snapshot!(format!("IDs: {:?}\nScores: {}\nScore Details:\n{:#?}", result.documents_ids, result.document_scores.iter().map(|scores| format!("{:.4} ", ScoreDetails::global_score(scores.iter()))).collect::<String>(), result.document_scores), @r###"
    IDs: [2, 0, 3, 1]
    Scores: 1.0000 0.5000 0.5000 0.0000 
    Score Details:
    [
        [
            Vector(
                Vector {
                    similarity: Some(
                        1.0,
                    ),
                },
            ),
        ],
        [
            Vector(
                Vector {
                    similarity: Some(
                        0.5,
                    ),
                },
            ),
        ],
        [
            Vector(
                Vector {
                    similarity: Some(
                        0.5,
                    ),
                },
            ),
        ],
        [
            Vector(
                Vector {
                    similarity: Some(
                        0.0,
                    ),
                },
            ),
        ],
    ]
    "###);

    // Do ONE loop iteration. Not much can be deduced, almost everyone matched the words first bucket.
    search.deadline(Deadline::never().with_stop_after(1));

    let result = search.execute().unwrap();
    snapshot!(format!("IDs: {:?}\nScores: {}\nScore Details:\n{:#?}", result.documents_ids, result.document_scores.iter().map(|scores| format!("{:.4} ", ScoreDetails::global_score(scores.iter()))).collect::<String>(), result.document_scores), @r###"
    IDs: [0, 1, 2, 3]
    Scores: 0.5000 0.0000 0.0000 0.0000 
    Score Details:
    [
        [
            Vector(
                Vector {
                    similarity: Some(
                        0.5,
                    ),
                },
            ),
        ],
        [
            Skipped,
        ],
        [
            Skipped,
        ],
        [
            Skipped,
        ],
    ]
    "###);

    search.deadline(Deadline::never().with_stop_after(2));

    let result = search.execute().unwrap();
    snapshot!(format!("IDs: {:?}\nScores: {}\nScore Details:\n{:#?}", result.documents_ids, result.document_scores.iter().map(|scores| format!("{:.4} ", ScoreDetails::global_score(scores.iter()))).collect::<String>(), result.document_scores), @r###"
    IDs: [0, 1, 2, 3]
    Scores: 0.5000 0.0000 0.0000 0.0000 
    Score Details:
    [
        [
            Vector(
                Vector {
                    similarity: Some(
                        0.5,
                    ),
                },
            ),
        ],
        [
            Vector(
                Vector {
                    similarity: Some(
                        0.0,
                    ),
                },
            ),
        ],
        [
            Skipped,
        ],
        [
            Skipped,
        ],
    ]
    "###);

    search.deadline(Deadline::never().with_stop_after(3));

    let result = search.execute().unwrap();
    snapshot!(format!("IDs: {:?}\nScores: {}\nScore Details:\n{:#?}", result.documents_ids, result.document_scores.iter().map(|scores| format!("{:.4} ", ScoreDetails::global_score(scores.iter()))).collect::<String>(), result.document_scores), @r###"
    IDs: [2, 0, 1, 3]
    Scores: 1.0000 0.5000 0.0000 0.0000 
    Score Details:
    [
        [
            Vector(
                Vector {
                    similarity: Some(
                        1.0,
                    ),
                },
            ),
        ],
        [
            Vector(
                Vector {
                    similarity: Some(
                        0.5,
                    ),
                },
            ),
        ],
        [
            Vector(
                Vector {
                    similarity: Some(
                        0.0,
                    ),
                },
            ),
        ],
        [
            Skipped,
        ],
    ]
    "###);

    search.deadline(Deadline::never().with_stop_after(4));

    let result = search.execute().unwrap();
    snapshot!(format!("IDs: {:?}\nScores: {}\nScore Details:\n{:#?}", result.documents_ids, result.document_scores.iter().map(|scores| format!("{:.4} ", ScoreDetails::global_score(scores.iter()))).collect::<String>(), result.document_scores), @r###"
    IDs: [2, 0, 3, 1]
    Scores: 1.0000 0.5000 0.5000 0.0000 
    Score Details:
    [
        [
            Vector(
                Vector {
                    similarity: Some(
                        1.0,
                    ),
                },
            ),
        ],
        [
            Vector(
                Vector {
                    similarity: Some(
                        0.5,
                    ),
                },
            ),
        ],
        [
            Vector(
                Vector {
                    similarity: Some(
                        0.5,
                    ),
                },
            ),
        ],
        [
            Vector(
                Vector {
                    similarity: Some(
                        0.0,
                    ),
                },
            ),
        ],
    ]
    "###);

    search.deadline(Deadline::never().with_stop_after(5));

    let result = search.execute().unwrap();
    snapshot!(format!("IDs: {:?}\nScores: {}\nScore Details:\n{:#?}", result.documents_ids, result.document_scores.iter().map(|scores| format!("{:.4} ", ScoreDetails::global_score(scores.iter()))).collect::<String>(), result.document_scores), @r###"
    IDs: [2, 0, 3, 1]
    Scores: 1.0000 0.5000 0.5000 0.0000 
    Score Details:
    [
        [
            Vector(
                Vector {
                    similarity: Some(
                        1.0,
                    ),
                },
            ),
        ],
        [
            Vector(
                Vector {
                    similarity: Some(
                        0.5,
                    ),
                },
            ),
        ],
        [
            Vector(
                Vector {
                    similarity: Some(
                        0.5,
                    ),
                },
            ),
        ],
        [
            Vector(
                Vector {
                    similarity: Some(
                        0.0,
                    ),
                },
            ),
        ],
    ]
    "###);
}
