use meili_snap::*;
use meilisearch::Opt;
use tempfile::TempDir;

use super::test_settings_documents_indexing_swapping_and_search;
use crate::common::{
    default_settings, shared_does_not_exists_index, Server, DOCUMENTS, NESTED_DOCUMENTS,
};
use crate::json;

#[actix_rt::test]
async fn search_unexisting_index() {
    let index = shared_does_not_exists_index().await;
    let expected_response = json!({
        "message": "Index `DOES_NOT_EXISTS` not found.",
        "code": "index_not_found",
        "type": "invalid_request",
        "link": "https://docs.meilisearch.com/errors#index_not_found"
    });

    index
        .search(json!({"q": "hello"}), |response, code| {
            assert_eq!(code, 404);
            assert_eq!(response, expected_response);
        })
        .await;
}

#[actix_rt::test]
async fn search_unexisting_parameter() {
    let server = Server::new_shared();
    let index = server.unique_index();

    index
        .search(json!({"marin": "hello"}), |response, code| {
            assert_eq!(code, 400, "{}", response);
            assert_eq!(response["code"], "bad_request");
        })
        .await;
}

#[actix_rt::test]
async fn search_bad_q() {
    let server = Server::new_shared();
    let index = server.unique_index();

    let (response, code) = index.search_post(json!({"q": ["doggo"]})).await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Invalid value type at `.q`: expected a string, but found an array: `[\"doggo\"]`",
      "code": "invalid_search_q",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_q"
    }
    "###);
    // Can't make the `q` fail with a get search since it'll accept anything as a string.
}

#[actix_rt::test]
async fn search_bad_offset() {
    let server = Server::new_shared();
    let index = server.unique_index();

    let (response, code) = index.search_post(json!({"offset": "doggo"})).await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Invalid value type at `.offset`: expected a positive integer, but found a string: `\"doggo\"`",
      "code": "invalid_search_offset",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_offset"
    }
    "###);

    let (response, code) = index.search_get("?offset=doggo").await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Invalid value in parameter `offset`: could not parse `doggo` as a positive integer",
      "code": "invalid_search_offset",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_offset"
    }
    "###);
}

#[actix_rt::test]
async fn search_bad_limit() {
    let server = Server::new_shared();
    let index = server.unique_index();

    let (response, code) = index.search_post(json!({"limit": "doggo"})).await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Invalid value type at `.limit`: expected a positive integer, but found a string: `\"doggo\"`",
      "code": "invalid_search_limit",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_limit"
    }
    "###);

    let (response, code) = index.search_get("?limit=doggo").await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Invalid value in parameter `limit`: could not parse `doggo` as a positive integer",
      "code": "invalid_search_limit",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_limit"
    }
    "###);
}

#[actix_rt::test]
async fn search_bad_page() {
    let server = Server::new_shared();
    let index = server.unique_index();

    let (response, code) = index.search_post(json!({"page": "doggo"})).await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Invalid value type at `.page`: expected a positive integer, but found a string: `\"doggo\"`",
      "code": "invalid_search_page",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_page"
    }
    "###);

    let (response, code) = index.search_get("?page=doggo").await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Invalid value in parameter `page`: could not parse `doggo` as a positive integer",
      "code": "invalid_search_page",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_page"
    }
    "###);
}

#[actix_rt::test]
async fn search_bad_hits_per_page() {
    let server = Server::new_shared();
    let index = server.unique_index();

    let (response, code) = index.search_post(json!({"hitsPerPage": "doggo"})).await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Invalid value type at `.hitsPerPage`: expected a positive integer, but found a string: `\"doggo\"`",
      "code": "invalid_search_hits_per_page",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_hits_per_page"
    }
    "###);

    let (response, code) = index.search_get("?hitsPerPage=doggo").await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Invalid value in parameter `hitsPerPage`: could not parse `doggo` as a positive integer",
      "code": "invalid_search_hits_per_page",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_hits_per_page"
    }
    "###);
}

#[actix_rt::test]
async fn search_bad_attributes_to_retrieve() {
    let server = Server::new_shared();
    let index = server.unique_index();

    let (response, code) = index.search_post(json!({"attributesToRetrieve": "doggo"})).await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Invalid value type at `.attributesToRetrieve`: expected an array, but found a string: `\"doggo\"`",
      "code": "invalid_search_attributes_to_retrieve",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_attributes_to_retrieve"
    }
    "###);
    // Can't make the `attributes_to_retrieve` fail with a get search since it'll accept anything as an array of strings.
}

#[actix_rt::test]
async fn search_bad_retrieve_vectors() {
    let server = Server::new_shared();
    let index = server.unique_index();

    let (response, code) = index.search_post(json!({"retrieveVectors": "doggo"})).await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Invalid value type at `.retrieveVectors`: expected a boolean, but found a string: `\"doggo\"`",
      "code": "invalid_search_retrieve_vectors",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_retrieve_vectors"
    }
    "###);

    let (response, code) = index.search_post(json!({"retrieveVectors": [true]})).await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Invalid value type at `.retrieveVectors`: expected a boolean, but found an array: `[true]`",
      "code": "invalid_search_retrieve_vectors",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_retrieve_vectors"
    }
    "###);

    let (response, code) = index.search_get("?retrieveVectors=").await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Invalid value in parameter `retrieveVectors`: could not parse `` as a boolean, expected either `true` or `false`",
      "code": "invalid_search_retrieve_vectors",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_retrieve_vectors"
    }
    "###);

    let (response, code) = index.search_get("?retrieveVectors=doggo").await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Invalid value in parameter `retrieveVectors`: could not parse `doggo` as a boolean, expected either `true` or `false`",
      "code": "invalid_search_retrieve_vectors",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_retrieve_vectors"
    }
    "###);
}

#[actix_rt::test]
async fn search_bad_attributes_to_crop() {
    let server = Server::new_shared();
    let index = server.unique_index();

    let (response, code) = index.search_post(json!({"attributesToCrop": "doggo"})).await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Invalid value type at `.attributesToCrop`: expected an array, but found a string: `\"doggo\"`",
      "code": "invalid_search_attributes_to_crop",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_attributes_to_crop"
    }
    "###);
    // Can't make the `attributes_to_crop` fail with a get search since it'll accept anything as an array of strings.
}

#[actix_rt::test]
async fn search_bad_crop_length() {
    let server = Server::new_shared();
    let index = server.unique_index();

    let (response, code) = index.search_post(json!({"cropLength": "doggo"})).await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Invalid value type at `.cropLength`: expected a positive integer, but found a string: `\"doggo\"`",
      "code": "invalid_search_crop_length",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_crop_length"
    }
    "###);

    let (response, code) = index.search_get("?cropLength=doggo").await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Invalid value in parameter `cropLength`: could not parse `doggo` as a positive integer",
      "code": "invalid_search_crop_length",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_crop_length"
    }
    "###);
}

#[actix_rt::test]
async fn search_bad_attributes_to_highlight() {
    let server = Server::new_shared();
    let index = server.unique_index();

    let (response, code) = index.search_post(json!({"attributesToHighlight": "doggo"})).await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Invalid value type at `.attributesToHighlight`: expected an array, but found a string: `\"doggo\"`",
      "code": "invalid_search_attributes_to_highlight",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_attributes_to_highlight"
    }
    "###);
    // Can't make the `attributes_to_highlight` fail with a get search since it'll accept anything as an array of strings.
}

#[actix_rt::test]
async fn search_bad_filter() {
    // Since a filter is deserialized as a json Value it will never fail to deserialize.
    // Thus the error message is not generated by deserr but written by us.
    let server = Server::new_shared();
    let index = server.unique_index();
    // Also, to trigger the error message we need to effectively create the index or else it'll throw an
    // index does not exist error.
    let (response, _code) = index.create(None).await;
    server.wait_task(response.uid()).await.succeeded();

    let (response, code) = index.search_post(json!({ "filter": true })).await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Invalid syntax for the filter parameter: `expected String, Array, found: true`.",
      "code": "invalid_search_filter",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_filter"
    }
    "###);
    // Can't make the `filter` fail with a get search since it'll accept anything as a strings.
}

#[actix_rt::test]
async fn search_bad_sort() {
    let server = Server::new_shared();
    let index = server.unique_index();

    let (response, code) = index.search_post(json!({"sort": "doggo"})).await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Invalid value type at `.sort`: expected an array, but found a string: `\"doggo\"`",
      "code": "invalid_search_sort",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_sort"
    }
    "###);
    // Can't make the `sort` fail with a get search since it'll accept anything as a strings.
}

#[actix_rt::test]
async fn search_bad_show_matches_position() {
    let server = Server::new_shared();
    let index = server.unique_index();

    let (response, code) = index.search_post(json!({"showMatchesPosition": "doggo"})).await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Invalid value type at `.showMatchesPosition`: expected a boolean, but found a string: `\"doggo\"`",
      "code": "invalid_search_show_matches_position",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_show_matches_position"
    }
    "###);

    let (response, code) = index.search_get("?showMatchesPosition=doggo").await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Invalid value in parameter `showMatchesPosition`: could not parse `doggo` as a boolean, expected either `true` or `false`",
      "code": "invalid_search_show_matches_position",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_show_matches_position"
    }
    "###);
}

#[actix_rt::test]
async fn search_bad_facets() {
    let server = Server::new_shared();
    let index = server.unique_index();

    let (response, code) = index.search_post(json!({"facets": "doggo"})).await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Invalid value type at `.facets`: expected an array, but found a string: `\"doggo\"`",
      "code": "invalid_search_facets",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_facets"
    }
    "###);
    // Can't make the `attributes_to_highlight` fail with a get search since it'll accept anything as an array of strings.
}

#[actix_rt::test]
async fn search_bad_threshold() {
    let server = Server::new_shared();
    let index = server.unique_index();

    let (response, code) = index.search_post(json!({"rankingScoreThreshold": "doggo"})).await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Invalid value type at `.rankingScoreThreshold`: expected a number, but found a string: `\"doggo\"`",
      "code": "invalid_search_ranking_score_threshold",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_ranking_score_threshold"
    }
    "###);
}

#[actix_rt::test]
async fn search_invalid_threshold() {
    let server = Server::new_shared();
    let index = server.unique_index();

    let (response, code) = index.search_post(json!({"rankingScoreThreshold": 42})).await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Invalid value at `.rankingScoreThreshold`: the value of `rankingScoreThreshold` is invalid, expected a float between `0.0` and `1.0`.",
      "code": "invalid_search_ranking_score_threshold",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_ranking_score_threshold"
    }
    "###);
}

#[actix_rt::test]
async fn search_non_filterable_facets() {
    let server = Server::new_shared();
    let index = server.unique_index();
    let (response, _code) = index.update_settings(json!({"filterableAttributes": ["title"]})).await;
    // Wait for the settings update to complete
    server.wait_task(response.uid()).await.succeeded();

    let (response, code) = index.search_post(json!({"facets": ["doggo"]})).await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Invalid facet distribution: Attribute `doggo` is not filterable. Available filterable attributes patterns are: `title`.",
      "code": "invalid_search_facets",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_facets"
    }
    "###);

    let (response, code) = index.search_get("?facets=doggo").await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Invalid facet distribution: Attribute `doggo` is not filterable. Available filterable attributes patterns are: `title`.",
      "code": "invalid_search_facets",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_facets"
    }
    "###);
}

#[actix_rt::test]
async fn search_non_filterable_facets_multiple_filterable() {
    let server = Server::new_shared();
    let index = server.unique_index();
    let (response, _code) =
        index.update_settings(json!({"filterableAttributes": ["title", "genres"]})).await;
    server.wait_task(response.uid()).await.succeeded();

    let (response, code) = index.search_post(json!({"facets": ["doggo"]})).await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Invalid facet distribution: Attribute `doggo` is not filterable. Available filterable attributes patterns are: `genres, title`.",
      "code": "invalid_search_facets",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_facets"
    }
    "###);

    let (response, code) = index.search_get("?facets=doggo").await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Invalid facet distribution: Attribute `doggo` is not filterable. Available filterable attributes patterns are: `genres, title`.",
      "code": "invalid_search_facets",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_facets"
    }
    "###);
}

#[actix_rt::test]
async fn search_non_filterable_facets_no_filterable() {
    let server = Server::new_shared();
    let index = server.unique_index();
    let (response, _code) = index.update_settings(json!({"filterableAttributes": []})).await;
    server.wait_task(response.uid()).await.succeeded();

    let (response, code) = index.search_post(json!({"facets": ["doggo"]})).await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Invalid facet distribution: Attribute `doggo` is not filterable. This index does not have configured filterable attributes.",
      "code": "invalid_search_facets",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_facets"
    }
    "###);

    let (response, code) = index.search_get("?facets=doggo").await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Invalid facet distribution: Attribute `doggo` is not filterable. This index does not have configured filterable attributes.",
      "code": "invalid_search_facets",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_facets"
    }
    "###);
}

#[actix_rt::test]
async fn search_non_filterable_facets_multiple_facets() {
    let server = Server::new_shared();
    let index = server.unique_index();
    let (response, _uid) =
        index.update_settings(json!({"filterableAttributes": ["title", "genres"]})).await;
    server.wait_task(response.uid()).await.succeeded();

    let (response, code) = index.search_post(json!({"facets": ["doggo", "neko"]})).await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Invalid facet distribution: Attributes `doggo, neko` are not filterable. Available filterable attributes patterns are: `genres, title`.",
      "code": "invalid_search_facets",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_facets"
    }
    "###);

    let (response, code) = index.search_get("?facets=doggo,neko").await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Invalid facet distribution: Attributes `doggo, neko` are not filterable. Available filterable attributes patterns are: `genres, title`.",
      "code": "invalid_search_facets",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_facets"
    }
    "###);
}

#[actix_rt::test]
async fn search_bad_highlight_pre_tag() {
    let server = Server::new_shared();
    let index = server.unique_index();

    let (response, code) = index.search_post(json!({"highlightPreTag": ["doggo"]})).await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Invalid value type at `.highlightPreTag`: expected a string, but found an array: `[\"doggo\"]`",
      "code": "invalid_search_highlight_pre_tag",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_highlight_pre_tag"
    }
    "###);
    // Can't make the `highlight_pre_tag` fail with a get search since it'll accept anything as a strings.
}

#[actix_rt::test]
async fn search_bad_highlight_post_tag() {
    let server = Server::new_shared();
    let index = server.unique_index();

    let (response, code) = index.search_post(json!({"highlightPostTag": ["doggo"]})).await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Invalid value type at `.highlightPostTag`: expected a string, but found an array: `[\"doggo\"]`",
      "code": "invalid_search_highlight_post_tag",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_highlight_post_tag"
    }
    "###);
    // Can't make the `highlight_post_tag` fail with a get search since it'll accept anything as a strings.
}

#[actix_rt::test]
async fn search_bad_crop_marker() {
    let server = Server::new_shared();
    let index = server.unique_index();

    let (response, code) = index.search_post(json!({"cropMarker": ["doggo"]})).await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Invalid value type at `.cropMarker`: expected a string, but found an array: `[\"doggo\"]`",
      "code": "invalid_search_crop_marker",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_crop_marker"
    }
    "###);
    // Can't make the `crop_marker` fail with a get search since it'll accept anything as a strings.
}

#[actix_rt::test]
async fn search_bad_matching_strategy() {
    let server = Server::new_shared();
    let index = server.unique_index();

    let (response, code) = index.search_post(json!({"matchingStrategy": "doggo"})).await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Unknown value `doggo` at `.matchingStrategy`: expected one of `last`, `all`, `frequency`",
      "code": "invalid_search_matching_strategy",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_matching_strategy"
    }
    "###);

    let (response, code) = index.search_post(json!({"matchingStrategy": {"doggo": "doggo"}})).await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Invalid value type at `.matchingStrategy`: expected a string, but found an object: `{\"doggo\":\"doggo\"}`",
      "code": "invalid_search_matching_strategy",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_matching_strategy"
    }
    "###);

    let (response, code) = index.search_get("?matchingStrategy=doggo").await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Unknown value `doggo` for parameter `matchingStrategy`: expected one of `last`, `all`, `frequency`",
      "code": "invalid_search_matching_strategy",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_matching_strategy"
    }
    "###);
}

#[actix_rt::test]
async fn filter_invalid_syntax_object() {
    test_settings_documents_indexing_swapping_and_search(
        &DOCUMENTS,
        &json!({"filterableAttributes": ["title"]}),
        &json!({"filter": "title & Glass"}),
        |response, code| {
            snapshot!(response, @r#"
            {
              "message": "Was expecting an operation `=`, `!=`, `>=`, `>`, `<=`, `<`, `IN`, `NOT IN`, `TO`, `EXISTS`, `NOT EXISTS`, `IS NULL`, `IS NOT NULL`, `IS EMPTY`, `IS NOT EMPTY`, `CONTAINS`, `NOT CONTAINS`, `STARTS WITH`, `NOT STARTS WITH`, `_geoRadius`, `_geoBoundingBox` or `_geoPolygon` at `title & Glass`.\n1:14 title & Glass",
              "code": "invalid_search_filter",
              "type": "invalid_request",
              "link": "https://docs.meilisearch.com/errors#invalid_search_filter"
            }
            "#);
            snapshot!(code, @"400 Bad Request");
        },
    )
    .await;
}

#[actix_rt::test]
async fn filter_invalid_syntax_array() {
    test_settings_documents_indexing_swapping_and_search(
        &DOCUMENTS,
        &json!({"filterableAttributes": ["title"]}),
        &json!({"filter": ["title & Glass"]}),
        |response, code| {
            snapshot!(response, @r#"
            {
              "message": "Was expecting an operation `=`, `!=`, `>=`, `>`, `<=`, `<`, `IN`, `NOT IN`, `TO`, `EXISTS`, `NOT EXISTS`, `IS NULL`, `IS NOT NULL`, `IS EMPTY`, `IS NOT EMPTY`, `CONTAINS`, `NOT CONTAINS`, `STARTS WITH`, `NOT STARTS WITH`, `_geoRadius`, `_geoBoundingBox` or `_geoPolygon` at `title & Glass`.\n1:14 title & Glass",
              "code": "invalid_search_filter",
              "type": "invalid_request",
              "link": "https://docs.meilisearch.com/errors#invalid_search_filter"
            }
            "#);
            snapshot!(code, @"400 Bad Request");
        },
    )
    .await;
}

#[actix_rt::test]
async fn filter_invalid_syntax_string() {
    test_settings_documents_indexing_swapping_and_search(
        &DOCUMENTS,
        &json!({"filterableAttributes": ["title"]}),
        &json!({"filter": "title = Glass XOR title = Glass"}),
        |response, code| {
            snapshot!(response, @r###"
            {
              "message": "Found unexpected characters at the end of the filter: `XOR title = Glass`. You probably forgot an `OR` or an `AND` rule.\n15:32 title = Glass XOR title = Glass",
              "code": "invalid_search_filter",
              "type": "invalid_request",
              "link": "https://docs.meilisearch.com/errors#invalid_search_filter"
            }
            "###);
            snapshot!(code, @"400 Bad Request");
        },
    )
    .await;
}

#[actix_rt::test]
async fn filter_invalid_attribute_array() {
    test_settings_documents_indexing_swapping_and_search(
        &DOCUMENTS,
        &json!({"filterableAttributes": ["title"]}),
        &json!({"filter": ["many = Glass"]}),
        |response, code| {
            snapshot!(response, @r###"
            {
              "message": "Index `[uuid]`: Attribute `many` is not filterable. Available filterable attribute patterns are: `title`.\n1:5 many = Glass",
              "code": "invalid_search_filter",
              "type": "invalid_request",
              "link": "https://docs.meilisearch.com/errors#invalid_search_filter"
            }
            "###);
            snapshot!(code, @"400 Bad Request");
        },
    )
    .await;
}

#[actix_rt::test]
async fn filter_invalid_attribute_string() {
    test_settings_documents_indexing_swapping_and_search(
        &DOCUMENTS,
        &json!({"filterableAttributes": ["title"]}),
        &json!({"filter": "many = Glass"}),
        |response, code| {
            snapshot!(response, @r###"
            {
              "message": "Index `[uuid]`: Attribute `many` is not filterable. Available filterable attribute patterns are: `title`.\n1:5 many = Glass",
              "code": "invalid_search_filter",
              "type": "invalid_request",
              "link": "https://docs.meilisearch.com/errors#invalid_search_filter"
            }
            "###);
            snapshot!(code, @"400 Bad Request");
        },
    )
    .await;
}

#[actix_rt::test]
async fn filter_reserved_geo_attribute_array() {
    test_settings_documents_indexing_swapping_and_search(
        &DOCUMENTS,
        &json!({"filterableAttributes": ["title"]}),
        &json!({"filter": ["_geo = Glass"]}),
        |response, code| {
            snapshot!(response, @r###"
            {
              "message": "`_geo` is a reserved keyword and thus can't be used as a filter expression. Use the `_geoRadius(latitude, longitude, distance)` or `_geoBoundingBox([latitude, longitude], [latitude, longitude])` built-in rules to filter on `_geo` coordinates.\n1:13 _geo = Glass",
              "code": "invalid_search_filter",
              "type": "invalid_request",
              "link": "https://docs.meilisearch.com/errors#invalid_search_filter"
            }
            "###);
            snapshot!(code, @"400 Bad Request");
        },
    )
    .await;
}

#[actix_rt::test]
async fn filter_reserved_geo_attribute_string() {
    test_settings_documents_indexing_swapping_and_search(
        &DOCUMENTS,
        &json!({"filterableAttributes": ["title"]}),
        &json!({"filter": "_geo = Glass"}),
        |response, code| {
            snapshot!(response, @r###"
            {
              "message": "`_geo` is a reserved keyword and thus can't be used as a filter expression. Use the `_geoRadius(latitude, longitude, distance)` or `_geoBoundingBox([latitude, longitude], [latitude, longitude])` built-in rules to filter on `_geo` coordinates.\n1:13 _geo = Glass",
              "code": "invalid_search_filter",
              "type": "invalid_request",
              "link": "https://docs.meilisearch.com/errors#invalid_search_filter"
            }
            "###);
            snapshot!(code, @"400 Bad Request");
        },
    )
    .await;
}

#[actix_rt::test]
async fn filter_reserved_attribute_array() {
    test_settings_documents_indexing_swapping_and_search(
        &DOCUMENTS,
        &json!({"filterableAttributes": ["title"]}),
        &json!({"filter": ["_geoDistance = Glass"]}),
        |response, code| {
            snapshot!(response, @r###"
            {
              "message": "`_geoDistance` is a reserved keyword and thus can't be used as a filter expression. Use the `_geoRadius(latitude, longitude, distance)` or `_geoBoundingBox([latitude, longitude], [latitude, longitude])` built-in rules to filter on `_geo` coordinates.\n1:21 _geoDistance = Glass",
              "code": "invalid_search_filter",
              "type": "invalid_request",
              "link": "https://docs.meilisearch.com/errors#invalid_search_filter"
            }
            "###);
            snapshot!(code, @"400 Bad Request");
        },
    )
    .await;
}

#[actix_rt::test]
async fn filter_reserved_attribute_string() {
    test_settings_documents_indexing_swapping_and_search(
        &DOCUMENTS,
        &json!({"filterableAttributes": ["title"]}),
        &json!({"filter": "_geoDistance = Glass"}),
        |response, code| {
            snapshot!(response, @r###"
            {
              "message": "`_geoDistance` is a reserved keyword and thus can't be used as a filter expression. Use the `_geoRadius(latitude, longitude, distance)` or `_geoBoundingBox([latitude, longitude], [latitude, longitude])` built-in rules to filter on `_geo` coordinates.\n1:21 _geoDistance = Glass",
              "code": "invalid_search_filter",
              "type": "invalid_request",
              "link": "https://docs.meilisearch.com/errors#invalid_search_filter"
            }
            "###);
            snapshot!(code, @"400 Bad Request");
        },
    )
    .await;
}

#[actix_rt::test]
async fn filter_reserved_geo_point_array() {
    test_settings_documents_indexing_swapping_and_search(
        &DOCUMENTS,
        &json!({"filterableAttributes": ["title"]}),
        &json!({"filter": ["_geoPoint = Glass"]}),
        |response, code| {
            snapshot!(response, @r###"
            {
              "message": "`_geoPoint` is a reserved keyword and thus can't be used as a filter expression. Use the `_geoRadius(latitude, longitude, distance)` or `_geoBoundingBox([latitude, longitude], [latitude, longitude])` built-in rules to filter on `_geo` coordinates.\n1:18 _geoPoint = Glass",
              "code": "invalid_search_filter",
              "type": "invalid_request",
              "link": "https://docs.meilisearch.com/errors#invalid_search_filter"
            }
            "###);
            snapshot!(code, @"400 Bad Request");
        },
    )
    .await;
}

#[actix_rt::test]
async fn filter_reserved_geo_point_string() {
    test_settings_documents_indexing_swapping_and_search(
        &DOCUMENTS,
        &json!({"filterableAttributes": ["title"]}),
        &json!({"filter": "_geoPoint = Glass"}),
        |response, code| {
            snapshot!(response, @r###"
            {
              "message": "`_geoPoint` is a reserved keyword and thus can't be used as a filter expression. Use the `_geoRadius(latitude, longitude, distance)` or `_geoBoundingBox([latitude, longitude], [latitude, longitude])` built-in rules to filter on `_geo` coordinates.\n1:18 _geoPoint = Glass",
              "code": "invalid_search_filter",
              "type": "invalid_request",
              "link": "https://docs.meilisearch.com/errors#invalid_search_filter"
            }
            "###);
            snapshot!(code, @"400 Bad Request");
        },
    )
    .await;
}

#[actix_rt::test]
async fn search_with_pattern_filter_settings_errors() {
    // Check if the Equality filter works with patterns
    test_settings_documents_indexing_swapping_and_search(
        &NESTED_DOCUMENTS,
        &json!({"filterableAttributes": [{
            "attributePatterns": ["cattos","doggos.age"],
            "features": {
                "facetSearch": false,
                "filter": {"equality": false, "comparison": true}
            }
        }]}),
        &json!({
            "filter": "cattos = pésti"
        }),
        |response, code| {
            snapshot!(code, @"400 Bad Request");
            snapshot!(json_string!(response), @r#"
            {
              "message": "Index `[uuid]`: Filter operator `=` is not allowed for the attribute `cattos`.\n  - Note: allowed operators: OR, AND, NOT, <, >, <=, >=, TO, IS EMPTY, IS NULL, EXISTS.\n  - Note: field `cattos` matched rule #0 in `filterableAttributes`\n  - Hint: enable equality in rule #0 by modifying the features.filter object\n  - Hint: prepend another rule matching `cattos` with appropriate filter features before rule #0",
              "code": "invalid_search_filter",
              "type": "invalid_request",
              "link": "https://docs.meilisearch.com/errors#invalid_search_filter"
            }
            "#);
        },
    )
    .await;

    test_settings_documents_indexing_swapping_and_search(
    &NESTED_DOCUMENTS,
    &json!({"filterableAttributes": [{
        "attributePatterns": ["cattos","doggos.age"],
        "features": {
            "facetSearch": false,
            "filter": {"equality": false, "comparison": true}
        }
    }]}),
    &json!({
        "filter": "cattos IN [pésti, simba]"
    }),
    |response, code| {
        snapshot!(code, @"400 Bad Request");
        snapshot!(json_string!(response), @r#"
        {
          "message": "Index `[uuid]`: Filter operator `=` is not allowed for the attribute `cattos`.\n  - Note: allowed operators: OR, AND, NOT, <, >, <=, >=, TO, IS EMPTY, IS NULL, EXISTS.\n  - Note: field `cattos` matched rule #0 in `filterableAttributes`\n  - Hint: enable equality in rule #0 by modifying the features.filter object\n  - Hint: prepend another rule matching `cattos` with appropriate filter features before rule #0",
          "code": "invalid_search_filter",
          "type": "invalid_request",
          "link": "https://docs.meilisearch.com/errors#invalid_search_filter"
        }
        "#);
    },
)
.await;

    // Check if the Comparison filter works with patterns
    test_settings_documents_indexing_swapping_and_search(
        &NESTED_DOCUMENTS,
        &json!({"filterableAttributes": [{"attributePatterns": ["cattos","doggos.age"]}]}),
        &json!({
            "filter": "doggos.age > 2"
        }),
        |response, code| {
            snapshot!(code, @"400 Bad Request");
            snapshot!(json_string!(response), @r#"
            {
              "message": "Index `[uuid]`: Filter operator `>` is not allowed for the attribute `doggos.age`.\n  - Note: allowed operators: OR, AND, NOT, =, !=, IN, IS EMPTY, IS NULL, EXISTS.\n  - Note: field `doggos.age` matched rule #0 in `filterableAttributes`\n  - Hint: enable comparison in rule #0 by modifying the features.filter object\n  - Hint: prepend another rule matching `doggos.age` with appropriate filter features before rule #0",
              "code": "invalid_search_filter",
              "type": "invalid_request",
              "link": "https://docs.meilisearch.com/errors#invalid_search_filter"
            }
            "#);
        },
    )
    .await;

    test_settings_documents_indexing_swapping_and_search(
        &NESTED_DOCUMENTS,
        &json!({"filterableAttributes": [{
            "attributePatterns": ["cattos","doggos.age"],
            "features": {
                "facetSearch": false,
                "filter": {"equality": true, "comparison": false}
            }
        }]}),
        &json!({
            "filter": "doggos.age > 2"
        }),
        |response, code| {
            snapshot!(code, @"400 Bad Request");
            snapshot!(json_string!(response), @r#"
            {
              "message": "Index `[uuid]`: Filter operator `>` is not allowed for the attribute `doggos.age`.\n  - Note: allowed operators: OR, AND, NOT, =, !=, IN, IS EMPTY, IS NULL, EXISTS.\n  - Note: field `doggos.age` matched rule #0 in `filterableAttributes`\n  - Hint: enable comparison in rule #0 by modifying the features.filter object\n  - Hint: prepend another rule matching `doggos.age` with appropriate filter features before rule #0",
              "code": "invalid_search_filter",
              "type": "invalid_request",
              "link": "https://docs.meilisearch.com/errors#invalid_search_filter"
            }
            "#);
        },
    )
    .await;

    test_settings_documents_indexing_swapping_and_search(
        &NESTED_DOCUMENTS,
        &json!({"filterableAttributes": [{
            "attributePatterns": ["cattos","doggos.age"],
            "features": {
                "facetSearch": false,
                "filter": {"equality": true, "comparison": false}
            }
        }]}),
        &json!({
            "filter": "doggos.age 2 TO 4"
        }),
        |response, code| {
            snapshot!(code, @"400 Bad Request");
            snapshot!(json_string!(response), @r#"
            {
              "message": "Index `[uuid]`: Filter operator `TO` is not allowed for the attribute `doggos.age`.\n  - Note: allowed operators: OR, AND, NOT, =, !=, IN, IS EMPTY, IS NULL, EXISTS.\n  - Note: field `doggos.age` matched rule #0 in `filterableAttributes`\n  - Hint: enable comparison in rule #0 by modifying the features.filter object\n  - Hint: prepend another rule matching `doggos.age` with appropriate filter features before rule #0",
              "code": "invalid_search_filter",
              "type": "invalid_request",
              "link": "https://docs.meilisearch.com/errors#invalid_search_filter"
            }
            "#);
        },
    )
    .await;
}

#[actix_rt::test]
async fn sort_geo_reserved_attribute() {
    let server = Server::new_shared();
    let index = server.unique_index();

    let (task, _code) = index.update_settings(json!({"sortableAttributes": ["id"]})).await;
    server.wait_task(task.uid()).await.succeeded();

    let expected_response = json!({
        "message": "`_geo` is a reserved keyword and thus can't be used as a sort expression. Use the _geoPoint(latitude, longitude) built-in rule to sort on _geo field coordinates.",
        "code": "invalid_search_sort",
        "type": "invalid_request",
        "link": "https://docs.meilisearch.com/errors#invalid_search_sort"
    });
    index
        .search(
            json!({
                "sort": ["_geo:asc"]
            }),
            |response, code| {
                assert_eq!(response, expected_response);
                assert_eq!(code, 400);
            },
        )
        .await;
}

#[actix_rt::test]
async fn sort_reserved_attribute() {
    let server = Server::new_shared();
    let index = server.unique_index();

    let (task, _code) = index.update_settings(json!({"sortableAttributes": ["id"]})).await;
    server.wait_task(task.uid()).await.succeeded();

    let expected_response = json!({
        "message": "`_geoDistance` is a reserved keyword and thus can't be used as a sort expression.",
        "code": "invalid_search_sort",
        "type": "invalid_request",
        "link": "https://docs.meilisearch.com/errors#invalid_search_sort"
    });
    index
        .search(
            json!({
                "sort": ["_geoDistance:asc"]
            }),
            |response, code| {
                assert_eq!(response, expected_response);
                assert_eq!(code, 400);
            },
        )
        .await;
}

#[actix_rt::test]
async fn sort_unsortable_attribute() {
    let server = Server::new_shared();
    let index = server.unique_index();
    let (response, _code) = index.update_settings(json!({"sortableAttributes": ["id"]})).await;
    server.wait_task(response.uid()).await.succeeded();

    let expected_response = json!({
        "message": format!("Index `{}`: Attribute `title` is not sortable. Available sortable attributes are: `id`.", index.uid),
        "code": "invalid_search_sort",
        "type": "invalid_request",
        "link": "https://docs.meilisearch.com/errors#invalid_search_sort"
    });
    index
        .search(
            json!({
                "sort": ["title:asc"]
            }),
            |response, code| {
                assert_eq!(response, expected_response);
                assert_eq!(code, 400);
            },
        )
        .await;
}

#[actix_rt::test]
async fn sort_invalid_syntax() {
    let server = Server::new_shared();
    let index = server.unique_index();

    let (response, _code) = index.update_settings(json!({"sortableAttributes": ["id"]})).await;
    server.wait_task(response.uid()).await.succeeded();

    let expected_response = json!({
        "message": "Invalid syntax for the sort parameter: expected expression ending by `:asc` or `:desc`, found `title`.",
        "code": "invalid_search_sort",
        "type": "invalid_request",
        "link": "https://docs.meilisearch.com/errors#invalid_search_sort"
    });
    index
        .search(
            json!({
                "sort": ["title"]
            }),
            |response, code| {
                assert_eq!(response, expected_response);
                assert_eq!(code, 400);
            },
        )
        .await;
}

#[actix_rt::test]
async fn sort_unset_ranking_rule() {
    let server = Server::new_shared();
    let index = server.unique_index();

    let (response, _code) = index
        .update_settings(
            json!({"sortableAttributes": ["title"], "rankingRules": ["proximity", "exactness"]}),
        )
        .await;
    server.wait_task(response.uid()).await.succeeded();

    let expected_response = json!({
        "message": format!("Index `{}`: You must specify where `sort` is listed in the rankingRules setting to use the sort parameter at search time.", index.uid),
        "code": "invalid_search_sort",
        "type": "invalid_request",
        "link": "https://docs.meilisearch.com/errors#invalid_search_sort"
    });
    index
        .search(
            json!({
                "sort": ["title:asc"]
            }),
            |response, code| {
                assert_eq!(response, expected_response);
                assert_eq!(code, 400);
            },
        )
        .await;
}

#[actix_rt::test]
async fn search_on_unknown_field() {
    test_settings_documents_indexing_swapping_and_search(
        &DOCUMENTS,
        &json!({"searchableAttributes": ["id", "title"]}),
        &json!({"q": "Captain Marvel", "attributesToSearchOn": ["unknown"]}),
        |response, code| {
            snapshot!(code, @"400 Bad Request");
            snapshot!(response, @r###"
            {
              "message": "Index `[uuid]`: Attribute `unknown` is not searchable. Available searchable attributes are: `id, title`.",
              "code": "invalid_search_attributes_to_search_on",
              "type": "invalid_request",
              "link": "https://docs.meilisearch.com/errors#invalid_search_attributes_to_search_on"
            }
            "###);
        },
    )
    .await;
}

#[actix_rt::test]
async fn search_on_unknown_field_plus_joker() {
    test_settings_documents_indexing_swapping_and_search(
        &DOCUMENTS,
        &json!({"searchableAttributes": ["id", "title"]}),
        &json!({"q": "Captain Marvel", "attributesToSearchOn": ["*", "unknown"]}),
        |response, code| {
            snapshot!(code, @"400 Bad Request");
            snapshot!(response, @r###"
            {
              "message": "Index `[uuid]`: Attribute `unknown` is not searchable. Available searchable attributes are: `id, title`.",
              "code": "invalid_search_attributes_to_search_on",
              "type": "invalid_request",
              "link": "https://docs.meilisearch.com/errors#invalid_search_attributes_to_search_on"
            }
            "###);
        },
    )
    .await;

    test_settings_documents_indexing_swapping_and_search(
        &DOCUMENTS,
        &json!({"searchableAttributes": ["id", "title"]}),
        &json!({"q": "Captain Marvel", "attributesToSearchOn": ["unknown", "*"]}),
        |response, code| {
            snapshot!(code, @"400 Bad Request");
            snapshot!(response, @r###"
            {
              "message": "Index `[uuid]`: Attribute `unknown` is not searchable. Available searchable attributes are: `id, title`.",
              "code": "invalid_search_attributes_to_search_on",
              "type": "invalid_request",
              "link": "https://docs.meilisearch.com/errors#invalid_search_attributes_to_search_on"
            }
            "###);
        },
    )
    .await;
}

#[actix_rt::test]
async fn distinct_at_search_time() {
    let server = Server::new_shared();
    let index = server.unique_index();
    let (response, _code) =
        index.add_documents(json!([{"id": 1, "color": "Doggo", "machin": "Action"}]), None).await;
    server.wait_task(response.uid()).await.succeeded();

    let (response, code) =
        index.search_post(json!({"page": 0, "hitsPerPage": 2, "distinct": "doggo.truc"})).await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(response, @r###"
    {
      "message": "Index `[uuid]`: Attribute `doggo.truc` is not filterable and thus, cannot be used as distinct attribute. This index does not have configured filterable attributes.",
      "code": "invalid_search_distinct",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_distinct"
    }
    "###);

    let (task, _) = index.update_settings_filterable_attributes(json!(["color", "machin"])).await;
    server.wait_task(task.uid()).await.succeeded();

    let (response, code) =
        index.search_post(json!({"page": 0, "hitsPerPage": 2, "distinct": "doggo.truc"})).await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(response, @r###"
    {
      "message": "Index `[uuid]`: Attribute `doggo.truc` is not filterable and thus, cannot be used as distinct attribute. Available filterable attributes patterns are: `color, machin`.",
      "code": "invalid_search_distinct",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_distinct"
    }
    "###);

    let (task, _) = index.update_settings_displayed_attributes(json!(["color"])).await;
    server.wait_task(task.uid()).await.succeeded();

    let (response, code) =
        index.search_post(json!({"page": 0, "hitsPerPage": 2, "distinct": "doggo.truc"})).await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(response, @r###"
    {
      "message": "Index `[uuid]`: Attribute `doggo.truc` is not filterable and thus, cannot be used as distinct attribute. Available filterable attributes patterns are: `color, <..hidden-attributes>`.",
      "code": "invalid_search_distinct",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_distinct"
    }
    "###);

    let (response, code) =
        index.search_post(json!({"page": 0, "hitsPerPage": 2, "distinct": true})).await;
    snapshot!(code, @"400 Bad Request");
    snapshot!(response, @r###"
    {
      "message": "Invalid value type at `.distinct`: expected a string, but found a boolean: `true`",
      "code": "invalid_search_distinct",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_distinct"
    }
    "###);
}

#[actix_rt::test]
async fn search_with_contains_without_enabling_the_feature() {
    // Since a filter is deserialized as a json Value it will never fail to deserialize.
    // Thus the error message is not generated by deserr but written by us.
    let server = Server::new_shared();
    let index = server.unique_index();
    // Also, to trigger the error message we need to effectively create the index or else it'll throw an
    // index does not exist error.
    let (task, _code) = index.create(None).await;
    server.wait_task(task.uid()).await.succeeded();

    index
        .search(json!({ "filter": "doggo CONTAINS kefir" }), |response, code| {
            snapshot!(code, @"400 Bad Request");
            snapshot!(json_string!(response), @r#"
            {
              "message": "Using `CONTAINS` in a filter requires enabling the `contains filter` experimental feature. See https://github.com/orgs/meilisearch/discussions/763\n7:15 doggo CONTAINS kefir",
              "code": "feature_not_enabled",
              "type": "invalid_request",
              "link": "https://docs.meilisearch.com/errors#feature_not_enabled"
            }
            "#);
        })
        .await;
    index
        .search(json!({ "filter": "doggo != echo AND doggo CONTAINS kefir" }), |response, code| {
            snapshot!(code, @"400 Bad Request");
            snapshot!(json_string!(response), @r#"
            {
              "message": "Using `CONTAINS` in a filter requires enabling the `contains filter` experimental feature. See https://github.com/orgs/meilisearch/discussions/763\n25:33 doggo != echo AND doggo CONTAINS kefir",
              "code": "feature_not_enabled",
              "type": "invalid_request",
              "link": "https://docs.meilisearch.com/errors#feature_not_enabled"
            }
            "#);
        })
        .await;

    // For the post search we can also use the arrays syntaxes
    let (response, code) =
        index.search_post(json!({ "filter": ["doggo != echo", "doggo CONTAINS kefir"] })).await;

    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r#"
    {
      "message": "Using `CONTAINS` in a filter requires enabling the `contains filter` experimental feature. See https://github.com/orgs/meilisearch/discussions/763\n7:15 doggo CONTAINS kefir",
      "code": "feature_not_enabled",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#feature_not_enabled"
    }
    "#);
    let (response, code) =
        index.search_post(json!({ "filter": ["doggo != echo", ["doggo CONTAINS kefir"]] })).await;

    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r#"
    {
      "message": "Using `CONTAINS` in a filter requires enabling the `contains filter` experimental feature. See https://github.com/orgs/meilisearch/discussions/763\n7:15 doggo CONTAINS kefir",
      "code": "feature_not_enabled",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#feature_not_enabled"
    }
    "#);
}

#[actix_rt::test]
#[ignore]
async fn search_with_personalization_invalid_api_key() {
    // Create a server with a fake personalization API key
    let dir = TempDir::new().unwrap();
    let options = Opt {
        experimental_personalization_api_key: Some("fake-api-key-12345".to_string()),
        ..default_settings(dir.path())
    };
    let server = Server::new_with_options(options).await.unwrap();
    let index = server.unique_index();

    // Create the index and add some documents
    let (task, _code) = index.create(None).await;
    server.wait_task(task.uid()).await.succeeded();

    let (task, _code) = index
        .add_documents(
            json!([
                {"id": 1, "title": "The Dark Knight", "genre": "Action"},
                {"id": 2, "title": "Inception", "genre": "Sci-Fi"},
                {"id": 3, "title": "The Matrix", "genre": "Sci-Fi"}
            ]),
            None,
        )
        .await;
    server.wait_task(task.uid()).await.succeeded();

    // Try to search with personalization - should return remote_invalid_api_key error
    let (response, code) = index
        .search_post(json!({
            "q": "the",
            "personalize": {
                "userContext": "I love science fiction movies"
            }
        }))
        .await;

    snapshot!(code, @"403 Forbidden");
    snapshot!(json_string!(response), @r#"
    {
      "message": "Personalization service: Unauthorized: invalid API key",
      "code": "remote_invalid_api_key",
      "type": "auth",
      "link": "https://docs.meilisearch.com/errors#remote_invalid_api_key"
    }
    "#);
}

#[actix_rt::test]
async fn search_with_personalization_no_user_context() {
    // Create a server with a fake personalization API key
    let dir = TempDir::new().unwrap();
    let options = Opt {
        experimental_personalization_api_key: Some("fake-api-key-12345".to_string()),
        ..default_settings(dir.path())
    };
    let server = Server::new_with_options(options).await.unwrap();
    let index = server.unique_index();

    // Create the index and add some documents
    let (task, _code) = index.create(None).await;
    server.wait_task(task.uid()).await.succeeded();

    let (task, _code) = index
        .add_documents(
            json!([
                {"id": 1, "title": "The Dark Knight", "genre": "Action"},
                {"id": 2, "title": "Inception", "genre": "Sci-Fi"},
                {"id": 3, "title": "The Matrix", "genre": "Sci-Fi"}
            ]),
            None,
        )
        .await;
    server.wait_task(task.uid()).await.succeeded();

    // Try to search with personalization - should return remote_invalid_api_key error
    let (response, code) = index
        .search_post(json!({
            "q": "the",
            "personalize": {}
        }))
        .await;

    snapshot!(code, @"400 Bad Request");
    snapshot!(json_string!(response), @r###"
    {
      "message": "Missing field `userContext` inside `.personalize`",
      "code": "invalid_search_personalize",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_search_personalize"
    }
    "###);
}
