use elastic_lens::prelude::*;
use elastic_lens::request::search::SearchTrait;
use serde_json::json;

fn search_to_json(search: Search) -> serde_json::Value {
    serde_json::to_value(search.search_body()).unwrap()
}

#[test]
fn a_default_search_is_empty() {
    let search = Search::default();

    assert_eq!(json!({}), search_to_json(search));
}

#[test]
fn a_search_with_a_limit() {
    let mut search = Search::default();
    search.set_limit(42);

    assert_eq!(
        json!({
            "size": 42
        }),
        search_to_json(search)
    );
}

#[test]
fn a_search_with_a_limit_and_offset() {
    let mut search = Search::default();
    search.set_limit(42);
    search.set_offset(13);

    assert_eq!(
        json!({
            "size": 42,
            "from": 13,
        }),
        search_to_json(search)
    );
}

#[test]
fn a_search_with_a_positive_contains() {
    let mut search = Search::default();
    search.with(field("foo").contains("bar"));

    assert_eq!(
        search_to_json(search),
        json!({
            "query": {
                "bool": {
                    "filter": [
                        { "term": { "foo": "bar" } }
                    ]
                }
            }
        })
    );
}

#[test]
fn a_search_with_a_negative_contains() {
    let mut search = Search::default();
    search.with(!field("foo").contains("bar"));

    assert_eq!(
        search_to_json(search),
        json!({
            "query": {
                "bool": {
                    "must_not": [
                        { "term": { "foo": "bar" } }
                    ]
                }
            }
        })
    );
}

#[test]
fn a_search_with_both_negative_and_positive_contains() {
    let mut search = Search::default();
    search.with(!field("foo").contains("bar"));
    search.with(field("foo").contains("biz"));

    assert_eq!(
        search_to_json(search),
        json!({
            "query": {
                "bool": {
                    "filter": [
                        { "term": { "foo": "biz" } }
                    ],
                    "must_not": [
                        { "term": { "foo": "bar" } }
                    ]
                }
            }
        })
    );
}

#[test]
fn a_search_with_less_than_range() {
    let mut search = Search::default();
    search.with(field("cost").less_than(800));

    assert_eq!(
        search_to_json(search),
        json!({
            "query": {
                "bool": {
                    "filter": [
                        { "range": { "cost": { "lt": 800 } } }
                    ]
                }
            }
        })
    );
}

#[test]
fn a_search_with_less_than_or_equal_range() {
    let mut search = Search::default();
    search.with(field("cost").less_than_or_equal(800));

    assert_eq!(
        search_to_json(search),
        json!({
            "query": {
                "bool": {
                    "filter": [
                        { "range": { "cost": { "lte": 800 } } }
                    ]
                }
            }
        })
    );
}

#[test]
fn a_search_with_greater_than_range() {
    let mut search = Search::default();
    search.with(field("cost").greater_than(800));

    assert_eq!(
        search_to_json(search),
        json!({
            "query": {
                "bool": {
                    "filter": [
                        { "range": { "cost": { "gt": 800 } } }
                    ]
                }
            }
        })
    );
}

#[test]
fn a_search_with_greater_than_or_equal_range() {
    let mut search = Search::default();
    search.with(field("cost").greater_than_or_equal(800));

    assert_eq!(
        search_to_json(search),
        json!({
            "query": {
                "bool": {
                    "filter": [
                        { "range": { "cost": { "gte": 800 } } }
                    ]
                }
            }
        })
    );
}

#[test]
fn a_search_with_between_range() {
    let mut search = Search::default();
    search.with(field("cost").between(200..350));

    assert_eq!(
        search_to_json(search),
        json!({
            "query": {
                "bool": {
                    "filter": [
                        { "range": { "cost": { "gte": 200, "lte": 350 } } }
                    ]
                }
            }
        })
    );
}

#[test]
fn a_search_with_any_of() {
    let mut search = Search::default();
    search.with(field("category").has_any_of(["clothing", "office"]));
    search.with(!field("rankings").has_any_of([1, 2, 3]));

    assert_eq!(
        search_to_json(search),
        json!({
            "query": {
                "bool": {
                    "filter": [
                        { "terms": { "category": ["clothing", "office"] } }
                    ],
                    "must_not": [
                        { "terms": { "rankings": [1, 2, 3] } }
                    ]
                }
            }
        })
    );
}

#[test]
fn a_search_with_exists() {
    let mut search = Search::default();
    search.with(field("user.hobbies").exists());
    search.with(!field("user.dislikes").exists());

    assert_eq!(
        search_to_json(search),
        json!({
            "query": {
                "bool": {
                    "filter": [
                        { "exists": { "field": "user.hobbies" } }
                    ],
                    "must_not": [
                        { "exists": { "field": "user.dislikes" } }
                    ]
                }
            }
        })
    );
}

#[test]
fn a_search_with_geo_distance_in_miles() {
    use elastic_lens::request::search::GeoPoint;

    let mut search = Search::default();
    search.with(
        field("user.address.geo-point")
            .within(50)
            .miles()
            .of(GeoPoint::new(1.1, 2.2)),
    );

    assert_eq!(
        search_to_json(search),
        json!({
            "query": {
                "bool": {
                    "filter": [
                        {
                            "geo_distance": {
                                "distance": "50mi",
                                "user.address.geo-point": {
                                    "lat": 1.1,
                                    "lon": 2.2,
                                }
                            }
                        }
                    ]
                }
            }
        })
    );
}

#[test]
fn a_search_with_geo_distance_in_kilometers_in_not() {
    use elastic_lens::request::search::GeoPoint;

    let mut search = Search::default();
    search.with(
        !field("user.address.geo-point")
            .within(50)
            .kilomenters()
            .of(GeoPoint::new(1.1, 2.2)),
    );

    assert_eq!(
        search_to_json(search),
        json!({
            "query": {
                "bool": {
                    "must_not": [
                        {
                            "geo_distance": {
                                "distance": "50km",
                                "user.address.geo-point": {
                                    "lat": 1.1,
                                    "lon": 2.2,
                                }
                            }
                        }
                    ]
                }
            }
        })
    );
}

#[test]
fn a_search_with_simple_if_any_match() {
    let mut search = Search::default();

    search.with(if_any_match(|critera| {
        critera.with(field("user.role").contains("admin"));
        critera.with(field("user.score").greater_than(9000));
    }));

    assert_eq!(
        search_to_json(search),
        json!({
            "query": {
                "bool": {
                    "filter": [
                        {
                            "bool": {
                                "should": [
                                    { "term": { "user.role": "admin" } },
                                    { "range": { "user.score": { "gt": 9000 } } }
                                ]
                            }
                        }
                    ]
                }
            }
        })
    );
}

#[test]
fn a_search_with_not_in_if_any_match() {
    let mut search = Search::default();

    search.with(if_any_match(|critera| {
        critera.with(field("user.role").contains("admin"));
        critera.with(field("user.score").greater_than(9000));
        critera.with(!field("user.hobbies").has_any_of(["swimming", "tennis"]));
    }));

    assert_eq!(
        search_to_json(search),
        json!({
            "query": {
                "bool": {
                    "filter": [
                        {
                            "bool": {
                                "should": [
                                    { "term": { "user.role": "admin" } },
                                    { "range": { "user.score": { "gt": 9000 } } },
                                    {
                                        "bool": {
                                            "must_not": [
                                                { "terms": { "user.hobbies": ["swimming", "tennis" ] } }
                                            ]
                                        }
                                    }
                                ]
                            }
                        }
                    ]
                }
            }
        })
    );
}

#[test]
fn a_search_using_if_all_match_inside_if_any_match() {
    let mut search = Search::default();

    search.with(if_any_match(|q| {
        q.with(field("user.role").contains("admin"));

        q.with(if_all_match(|a| {
            a.with(!field("user.banned").contains(true));
            a.with(field("user.group").has_any_of(["rust", "fps"]));
        }));
    }));

    assert_eq!(
        search_to_json(search),
        json!({
            "query": {
                "bool": {
                    "filter": [
                        {
                            "bool": {
                                "should": [
                                    { "term": { "user.role": "admin" } },
                                    {
                                        "bool": {
                                            "filter": [
                                                { "terms": { "user.group": ["rust", "fps"] } }
                                            ],
                                            "must_not": [
                                                { "term": { "user.banned": true } }
                                            ]
                                        }
                                    }
                                ]
                            }
                        }
                    ]
                }
            }
        })
    );
}

#[test]
fn building_a_search_with_term_aggregations() {
    let mut search = Search::default();

    search
        .create_aggregation("top-categories")
        .for_field("category")
        .count_terms();

    assert_eq!(
        search_to_json(search),
        json!({
            "aggs": {
                "top-categories": {
                    "terms": { "field": "category" }
                }
            }
        })
    );
}

#[test]
fn building_a_search_with_term_aggregations_and_sub_aggregations() {
    let mut search = Search::default();

    search
        .create_aggregation("top-categories")
        .for_field("category")
        .count_terms()
        .with_sub_aggregations(|aggs| {
            aggs.create_aggregation("sub-categories")
                .for_field("sub_category")
                .count_terms()
                .for_top(20);
        });

    assert_eq!(
        search_to_json(search),
        json!({
            "aggs": {
                "top-categories": {
                    "terms": { "field": "category" },
                    "aggs": {
                        "sub-categories": {
                            "terms": { "field": "sub_category", "size": 20 }
                        }
                    }
                }
            }
        })
    );
}

#[test]
fn building_a_search_with_stats_aggregations() {
    let mut search = Search::default();

    search
        .create_aggregation("price-stats")
        .for_field("item.price")
        .collect_stats();

    assert_eq!(
        search_to_json(search),
        json!({
            "aggs": {
                "price-stats": {
                    "stats": { "field": "item.price" }
                }
            }
        })
    );
}

#[test]
fn building_a_search_with_stats_a_missing_value() {
    let mut search = Search::default();

    search
        .create_aggregation("test-score-stats")
        .for_field("test.score")
        .collect_stats()
        .use_for_missing_value(0);

    assert_eq!(
        search_to_json(search),
        json!({
            "aggs": {
                "test-score-stats": {
                    "stats": { "field": "test.score", "missing": 0 }
                }
            }
        })
    );
}

#[test]
fn building_a_search_with_a_geo_sort() {
    let mut search = Search::default();
    use elastic_lens::request::search::GeoPoint;

    search.sort(
        by_field("user.location")
            .by_distance_from(GeoPoint::default())
            .using_the_plane_formula()
            .in_ascending_order()
            .ignore_unmapped_documents(),
    );

    assert_eq!(
        search_to_json(search),
        json!({
            "sort": [
                {
                    "_geo_distance": {
                        "user.location": { "lat": 0.0, "lon": 0.0 },
                        "order": "asc",
                        "distance_type": "plane",
                        "ignore_unmapped": true
                    }
                }
            ]
        })
    );
}

#[test]
fn building_a_search_with_a_normal_field_sort() {
    let mut search = Search::default();

    search.sort(by_field("user.age").descending());

    assert_eq!(
        search_to_json(search),
        json!({
            "sort": [
                { "user.age": "desc" }
            ]
        })
    );
}

#[test]
fn building_a_search_with_a_normal_field_sort_and_missing_value() {
    let mut search = Search::default();

    search.sort(by_field("user.age").ascending().where_missing_use(42));

    assert_eq!(
        search_to_json(search),
        json!({
            "sort": [
                { "user.age": { "order": "asc", "missing": 42 } }
            ]
        })
    );
}

#[test]
fn building_a_script_sort() {
    let mut search = Search::default();
    search.with(field("user.role").contains("admin"));
    search.sort(by_script("doc['my-int'].value / 10"));
    assert_eq!(
        search_to_json(search),
        json!({
            "query": {
                "bool": {
                    "filter": [
                        { "term": { "user.role": "admin" } }
                    ]
                }
            },
            "sort": [
                { "_script": {
                    "script": {
                        "source": "doc['my-int'].value / 10"
                    },
                    "type": "number"
                } }
            ]
        })
    );
}

#[test]
fn building_a_script_sort_with_bells_and_whistles() {
    let mut search = Search::default();
    search.sort(
        by_script("roflcopter")
            .returns_a_string()
            .descending()
            .with_params([("spice", 42)]),
    );
    assert_eq!(
        search_to_json(search),
        json!({
            "sort": [
                { "_script": {
                    "script": {
                        "source": "roflcopter",
                        "params": {
                            "spice": 42
                        }
                    },
                    "type": "string",
                    "order": "desc"
                } }
            ]
        })
    );
}

#[test]
fn building_a_search_with_a_script_score() {
    let mut search = Search::default();

    search.with(field("user.role").contains("admin"));
    search.sort(by_script_score("doc['my-int'].value / 10"));

    assert_eq!(
        search_to_json(search),
        json!({
            "query": {
                "script_score": {
                    "query": {
                        "bool": {
                            "filter": [
                                { "term": { "user.role": "admin" } }
                            ]
                        }
                    },
                    "script": {
                        "source": "doc['my-int'].value / 10"
                    }
                }
            }
        })
    );
}

#[test]
fn building_a_search_with_a_script_score_and_params() {
    let mut search = Search::default();

    search.with(field("user.role").contains("admin"));
    search.sort(
        by_script_score("doc['my-int'].value / 10 + params.spice").with_params([("spice", 42)]),
    );

    assert_eq!(
        search_to_json(search),
        json!({
            "query": {
                "script_score": {
                    "query": {
                        "bool": {
                            "filter": [
                                { "term": { "user.role": "admin" } }
                            ]
                        }
                    },
                    "script": {
                        "source": "doc['my-int'].value / 10 + params.spice",
                        "params": {
                            "spice": 42
                        }
                    }
                }
            }
        })
    );
}

#[test]
fn building_a_search_with_an_empty_all_match() {
    use elastic_lens::request::search::AllMatch;

    let mut search = Search::default();
    let all = AllMatch::default();
    search.with(all);

    assert_eq!(search_to_json(search), json!({}));
}

#[test]
fn building_a_search_with_an_empty_any_match() {
    use elastic_lens::request::search::AnyMatch;

    let mut search = Search::default();
    let any = AnyMatch::default();
    search.with(any);

    assert_eq!(search_to_json(search), json!({}));
}

#[test]
fn building_a_search_with_an_empty_not_all_match() {
    use elastic_lens::request::search::NotAll;

    let mut search = Search::default();
    let not_all = NotAll::default();
    search.with(not_all);

    assert_eq!(search_to_json(search), json!({}));
}

#[test]
fn building_an_empty_filter_aggregate() {
    let mut search = Search::default();
    // do nothing to the filter
    search.create_aggregation("test").filtered_by(|_| ());
    assert_eq!(
        search_to_json(search),
        json!({
            "aggs": {
                "test": {
                    "filter": {
                        "bool": {}
                    }
                }
            }
        })
    );
}

#[test]
fn building_a_nested_filter() {
    let mut search = Search::default();
    search.with(nested("product.variants", |nested| {
        nested.with(field("product.variants.size").contains("M"));
        nested.with(field("product.variants.color").has_any_of(["blue", "red"]));
    }));
    assert_eq!(
        search_to_json(search),
        json!({
            "query": {
                "bool": {
                    "filter": [
                        {
                            "nested": {
                                "path": "product.variants",
                                "query": {
                                    "bool": {
                                        "filter": [
                                            { "term": { "product.variants.size": "M" } },
                                            { "terms": { "product.variants.color": ["blue", "red"] } },
                                        ]
                                    }
                                }
                            }
                        }
                    ]
                }
            }
        })
    );
}

#[test]
fn collapse_by_field() {
    let mut search = Search::default();
    search.collapse_by_field("category");
    assert_eq!(
        search_to_json(search),
        json!({
            "collapse": {
                "field": "category"
            }
        })
    );
}

#[test]
fn collapse_by_field_and_max_concurrent() {
    let mut search = Search::default();
    search
        .collapse_by_field("category")
        .max_concurrent_searches(6);
    assert_eq!(
        search_to_json(search),
        json!({
            "collapse": {
                "field": "category",
                "max_concurrent_group_searches": 6,
            }
        })
    );
}
