use crate::blocks as blk;
use crate::queries::{
    all, and, asc, blocks, content, content_filter, count, delete, desc, eq, exists, field, fields,
    filter, find, grep, gt, gte, in_, included_by, includes, key_eq, key_in, lt, lte, ne, nin, nor,
    or, referenced_by, references, size, type_of, update, update_op,
};
use indoc::indoc;
use liwe::query::{
    parse_operation, FieldOp, FieldPath, Filter, FindOp, InclusionAnchor, Limit, Operation,
    OperationKind, ReferenceAnchor, Update, UpdateOperator, YamlType,
};
use serde_yaml::Value;

fn assert_parse(yaml: &str, kind: OperationKind, expected: Operation) {
    let actual = parse_operation(yaml, kind).expect("parse");
    assert_eq!(actual, expected);
}

fn assert_parse_error(yaml: &str, kind: OperationKind, needle: &str) {
    let err = parse_operation(yaml, kind).expect_err("parse must fail");
    let s = format!("{:?}", err);
    assert!(s.contains(needle), "{} not in {}", needle, s);
}

#[test]
fn find_round_trips_filter_project_sort_limit() {
    assert_parse(
        indoc! {"
            filter:
              status: draft
            project:
              title: 1
            sort:
              modified: -1
            limit: 5
        "},
        OperationKind::Find,
        find(
            FindOp::new()
                .filter(eq("status", "draft"))
                .project(fields(&["title"]))
                .sort(desc("modified"))
                .limit(5),
        ),
    );
}

#[test]
fn count_round_trips_filter_and_limit() {
    assert_parse(
        indoc! {"
            filter:
              status: draft
            limit: 0
        "},
        OperationKind::Count,
        count(filter(eq("status", "draft")).limit(0)),
    );
}

#[test]
fn update_round_trips_set_and_unset() {
    assert_parse(
        indoc! {r#"
            filter:
              status: draft
            update:
              $set:
                reviewed: true
              $unset:
                stale: ""
        "#},
        OperationKind::Update,
        update(update_op(
            eq("status", "draft"),
            Update::new(vec![
                UpdateOperator::set("reviewed", true),
                UpdateOperator::unset("stale"),
            ]),
        )),
    );
}

#[test]
fn delete_round_trips_filter() {
    assert_parse(
        indoc! {"
            filter:
              status: archived
        "},
        OperationKind::Delete,
        delete(filter(eq("status", "archived"))),
    );
}

#[test]
fn empty_yaml_parses_to_default_find() {
    assert_parse("", OperationKind::Find, find(FindOp::new()));
}

#[test]
fn sort_positive_one_means_ascending() {
    assert_parse(
        indoc! {"
            sort:
              modified: 1
        "},
        OperationKind::Find,
        find(FindOp::new().sort(asc("modified"))),
    );
}

#[test]
fn filter_eq_bare() {
    assert_parse(
        indoc! {"
            filter:
              x: 5
        "},
        OperationKind::Find,
        find(filter(eq("x", 5i64))),
    );
}

#[test]
fn filter_eq_explicit_operator() {
    assert_parse(
        indoc! {"
            filter:
              x: { $eq: 5 }
        "},
        OperationKind::Find,
        find(filter(eq("x", 5i64))),
    );
}

#[test]
fn filter_ne_round_trip() {
    assert_parse(
        indoc! {"
            filter:
              status: { $ne: draft }
        "},
        OperationKind::Find,
        find(filter(ne("status", "draft"))),
    );
}

#[test]
fn filter_comparison_operators_round_trip() {
    assert_parse(
        indoc! {"
            filter:
              a: { $gt: 1 }
              b: { $gte: 2 }
              c: { $lt: 3 }
              d: { $lte: 4 }
        "},
        OperationKind::Find,
        find(filter(and(vec![
            gt("a", 1i64),
            gte("b", 2i64),
            lt("c", 3i64),
            lte("d", 4i64),
        ]))),
    );
}

#[test]
fn filter_in_round_trip() {
    assert_parse(
        indoc! {"
            filter:
              tags: { $in: [rust, go] }
        "},
        OperationKind::Find,
        find(filter(in_("tags", ["rust", "go"]))),
    );
}

#[test]
fn filter_nin_round_trip() {
    assert_parse(
        indoc! {"
            filter:
              tags: { $nin: [rust] }
        "},
        OperationKind::Find,
        find(filter(nin("tags", ["rust"]))),
    );
}

#[test]
fn filter_exists_true() {
    assert_parse(
        indoc! {"
            filter:
              x: { $exists: true }
        "},
        OperationKind::Find,
        find(filter(exists("x", true))),
    );
}

#[test]
fn filter_exists_false() {
    assert_parse(
        indoc! {"
            filter:
              x: { $exists: false }
        "},
        OperationKind::Find,
        find(filter(exists("x", false))),
    );
}

#[test]
fn filter_type_single() {
    assert_parse(
        indoc! {"
            filter:
              x: { $type: string }
        "},
        OperationKind::Find,
        find(filter(type_of("x", [YamlType::String]))),
    );
}

#[test]
fn filter_type_multiple() {
    assert_parse(
        indoc! {"
            filter:
              x: { $type: [string, number] }
        "},
        OperationKind::Find,
        find(filter(type_of("x", [YamlType::String, YamlType::Number]))),
    );
}

#[test]
fn filter_all_round_trip() {
    assert_parse(
        indoc! {"
            filter:
              tags: { $all: [rust, async] }
        "},
        OperationKind::Find,
        find(filter(all("tags", ["rust", "async"]))),
    );
}

#[test]
fn filter_size_round_trip() {
    assert_parse(
        indoc! {"
            filter:
              tags: { $size: 3 }
        "},
        OperationKind::Find,
        find(filter(size("tags", 3))),
    );
}

#[test]
fn filter_field_level_not() {
    assert_parse(
        indoc! {"
            filter:
              x: { $not: { $eq: 1 } }
        "},
        OperationKind::Find,
        find(filter(Filter::Field {
            path: FieldPath::from_dotted("x"),
            op: FieldOp::Not(Box::new(FieldOp::Eq(Value::Number(1.into())))),
        })),
    );
}

#[test]
fn filter_and() {
    assert_parse(
        indoc! {"
            filter:
              $and:
                - { x: 1 }
                - { y: 2 }
        "},
        OperationKind::Find,
        find(filter(and(vec![eq("x", 1i64), eq("y", 2i64)]))),
    );
}

#[test]
fn filter_or() {
    assert_parse(
        indoc! {"
            filter:
              $or:
                - { x: 1 }
                - { y: 2 }
        "},
        OperationKind::Find,
        find(filter(or(vec![eq("x", 1i64), eq("y", 2i64)]))),
    );
}

#[test]
fn filter_content_membership_round_trips() {
    assert_parse(
        indoc! {"
            filter:
              $content:
                $text: TODO
        "},
        OperationKind::Find,
        find(filter(content_filter(blk::text("TODO")))),
    );
}

#[test]
fn filter_content_composes_under_or() {
    assert_parse(
        indoc! {"
            filter:
              $or:
                - { status: draft }
                - $content: { $header: Status }
        "},
        OperationKind::Find,
        find(filter(or(vec![
            eq("status", "draft"),
            content_filter(blk::header("Status")),
        ]))),
    );
}

#[test]
fn filter_top_level_not_rejected() {
    assert_parse_error(
        indoc! {"
            filter:
              $not:
                x: 1
        "},
        OperationKind::Find,
        "TopLevelNotNotSupported",
    );
}

#[test]
fn filter_nor() {
    assert_parse(
        indoc! {"
            filter:
              $nor:
                - { status: archived }
                - { status: deleted }
        "},
        OperationKind::Find,
        find(filter(nor(vec![
            eq("status", "archived"),
            eq("status", "deleted"),
        ]))),
    );
}

#[test]
fn filter_nor_empty_rejected() {
    assert_parse_error(
        indoc! {"
            filter:
              $nor: []
        "},
        OperationKind::Find,
        "EmptyOperatorList",
    );
}

#[test]
fn filter_per_field_nested_not_parses() {
    assert_parse(
        indoc! {"
            filter:
              priority: { $not: { $not: { $gt: 5 } } }
        "},
        OperationKind::Find,
        find(filter(Filter::Field {
            path: FieldPath::from_dotted("priority"),
            op: FieldOp::Not(Box::new(FieldOp::Not(Box::new(FieldOp::Gt(Value::from(
                5i64,
            )))))),
        })),
    );
}

#[test]
fn filter_per_field_not_wraps_multi_operator_range() {
    assert_parse(
        indoc! {"
            filter:
              priority: { $not: { $gt: 5, $lt: 10 } }
        "},
        OperationKind::Find,
        find(filter(Filter::Field {
            path: FieldPath::from_dotted("priority"),
            op: FieldOp::Not(Box::new(FieldOp::And(vec![
                FieldOp::Gt(Value::from(5i64)),
                FieldOp::Lt(Value::from(10i64)),
            ]))),
        })),
    );
}

#[test]
fn filter_dotted_and_nested_paths_produce_same_path() {
    let dotted = parse_operation("filter:\n  a.b: 1\n", OperationKind::Find).unwrap();
    let nested = parse_operation("filter:\n  a:\n    b: 1\n", OperationKind::Find).unwrap();
    assert_eq!(dotted, nested);
    assert_eq!(dotted, find(filter(eq("a.b", 1i64))),);
}

#[test]
fn filter_array_literal_decodes_as_eq_with_sequence() {
    assert_parse(
        indoc! {"
            filter:
              tags: [rust, async]
        "},
        OperationKind::Find,
        find(filter(eq(
            "tags",
            Value::Sequence(vec![
                Value::String("rust".into()),
                Value::String("async".into()),
            ]),
        ))),
    );
}

#[test]
fn update_set_dotted_path_round_trip() {
    assert_parse(
        indoc! {r#"
            filter: {}
            update:
              $set:
                "a.b.c": 1
        "#},
        OperationKind::Update,
        update(update_op(
            Filter::all(),
            Update::new(vec![UpdateOperator::set("a.b.c", 1i64)]),
        )),
    );
}

#[test]
fn find_rejects_update_field() {
    assert_parse_error(
        indoc! {"
            update:
              $set:
                x: 1
        "},
        OperationKind::Find,
        "OperationFieldNotAllowed",
    );
}

#[test]
fn count_rejects_project_field() {
    assert_parse_error(
        indoc! {"
            project:
              title: 1
        "},
        OperationKind::Count,
        "OperationFieldNotAllowed",
    );
}

#[test]
fn count_rejects_update_field() {
    assert_parse_error(
        indoc! {"
            update:
              $set:
                x: 1
        "},
        OperationKind::Count,
        "OperationFieldNotAllowed",
    );
}

#[test]
fn delete_rejects_update_field() {
    assert_parse_error(
        indoc! {"
            filter: {}
            update:
              $set:
                x: 1
        "},
        OperationKind::Delete,
        "OperationFieldNotAllowed",
    );
}

#[test]
fn delete_rejects_project_field() {
    assert_parse_error(
        indoc! {"
            filter: {}
            project:
              title: 1
        "},
        OperationKind::Delete,
        "OperationFieldNotAllowed",
    );
}

#[test]
fn count_rejects_add_fields() {
    assert_parse_error(
        indoc! {"
            addFields:
              note: $key
        "},
        OperationKind::Count,
        "OperationFieldNotAllowed",
    );
}

#[test]
fn delete_rejects_add_fields() {
    assert_parse_error(
        indoc! {"
            filter: {}
            addFields:
              note: $key
        "},
        OperationKind::Delete,
        "OperationFieldNotAllowed",
    );
}

#[test]
fn update_rejects_project() {
    assert_parse_error(
        indoc! {"
            filter: {}
            project:
              title: 1
            update:
              $set:
                x: 1
        "},
        OperationKind::Update,
        "OperationFieldNotAllowed",
    );
}

#[test]
fn update_rejects_add_fields() {
    assert_parse_error(
        indoc! {"
            filter: {}
            addFields:
              note: $key
            update:
              $set:
                x: 1
        "},
        OperationKind::Update,
        "OperationFieldNotAllowed",
    );
}

#[test]
fn find_rejects_project_and_add_fields_together() {
    assert_parse_error(
        indoc! {"
            project:
              title: 1
            addFields:
              note: $key
        "},
        OperationKind::Find,
        "ProjectAddFieldsConflict",
    );
}

#[test]
fn update_set_unset_conflict_rejected() {
    assert_parse_error(
        indoc! {r#"
            filter: {}
            update:
              $set:
                x: 1
              $unset:
                x: ""
        "#},
        OperationKind::Update,
        "SetUnsetConflict",
    );
}

#[test]
fn update_empty_body_rejected() {
    assert_parse_error(
        indoc! {"
            filter: {}
            update: {}
        "},
        OperationKind::Update,
        "EmptyUpdate",
    );
}

#[test]
fn limit_string_rejected() {
    let err = parse_operation("limit: \"20\"\n", OperationKind::Find)
        .expect_err("string limit must be rejected");
    let s = format!("{:?}", err);
    assert!(
        s.contains("Wire") || s.to_lowercase().contains("invalid"),
        "{}",
        s
    );
}

#[test]
fn update_filter_required_at_parse() {
    assert_parse_error(
        indoc! {"
            update:
              $set:
                x: 1
        "},
        OperationKind::Update,
        "MissingRequiredField",
    );
}

#[test]
fn delete_filter_required_at_parse() {
    assert_parse_error("limit: 10\n", OperationKind::Delete, "MissingRequiredField");
}

#[test]
fn update_rejects_reserved_prefix_target() {
    assert_parse_error(
        indoc! {"
            filter: {}
            update:
              $set:
                _internal: 1
        "},
        OperationKind::Update,
        "ReservedPrefix",
    );
}

#[test]
fn delete_zero_limit_unbounded() {
    assert_parse(
        indoc! {"
            filter:
              status: archived
            limit: 0
        "},
        OperationKind::Delete,
        delete(filter(eq("status", "archived")).limit(0)),
    );
    assert!(Limit(0).is_unbounded());
}

#[test]
fn key_scalar_parses_to_eq() {
    assert_parse(
        indoc! {"
            filter:
              $key: notes/foo
        "},
        OperationKind::Find,
        find(filter(key_eq("notes/foo"))),
    );
}

#[test]
fn key_explicit_eq() {
    assert_parse(
        indoc! {"
            filter:
              $key: { $eq: notes/foo }
        "},
        OperationKind::Find,
        find(filter(key_eq("notes/foo"))),
    );
}

#[test]
fn key_in_list() {
    assert_parse(
        indoc! {"
            filter:
              $key: { $in: [a, b, c] }
        "},
        OperationKind::Find,
        find(filter(key_in(&["a", "b", "c"]))),
    );
}

#[test]
fn key_gt_rejected() {
    assert_parse_error(
        indoc! {"
            filter:
              $key: { $gt: foo }
        "},
        OperationKind::Find,
        "UnknownOperator",
    );
}

#[test]
fn key_regex_reports_unknown_operator() {
    assert_parse_error(
        indoc! {"
            filter:
              $key: { $regex: \"delete\" }
        "},
        OperationKind::Find,
        "UnknownOperator",
    );
}

#[test]
fn key_in_with_non_string_rejected() {
    assert_parse_error(
        indoc! {"
            filter:
              $key: { $in: [1, 2] }
        "},
        OperationKind::Find,
        "OperatorExpectedString",
    );
}

#[test]
fn includes_scalar_shorthand() {
    assert_parse(
        indoc! {"
            filter:
              $includes: roadmap/q2
        "},
        OperationKind::Find,
        find(filter(includes(InclusionAnchor::new("roadmap/q2", 1, 1)))),
    );
}

#[test]
fn includes_single_anchor() {
    assert_parse(
        indoc! {"
            filter:
              $includes: { match: { $key: roadmap/q2 }, maxDepth: 2 }
        "},
        OperationKind::Find,
        find(filter(includes(InclusionAnchor::with_max("roadmap/q2", 2)))),
    );
}

#[test]
fn included_by_range() {
    assert_parse(
        indoc! {"
            filter:
              $includedBy:
                match: { $key: projects/alpha }
                minDepth: 2
                maxDepth: 5
        "},
        OperationKind::Find,
        find(filter(included_by(InclusionAnchor::new(
            "projects/alpha",
            2,
            5,
        )))),
    );
}

#[test]
fn includes_array_form_rejected() {
    assert_parse_error(
        indoc! {"
            filter:
              $includedBy:
                - { match: { $key: projects/alpha }, maxDepth: 5 }
                - { match: { $key: research/q2 }, maxDepth: 2 }
        "},
        OperationKind::Find,
        "ArrayFormRemoved",
    );
}

#[test]
fn anchor_full_form_omitted_bounds_unbounded() {
    assert_parse(
        indoc! {"
            filter:
              $includes: { match: { $key: K } }
        "},
        OperationKind::Find,
        find(filter(includes(InclusionAnchor::new("K", 1, u32::MAX)))),
    );
}

#[test]
fn anchor_wrong_bound_family_rejected() {
    assert_parse_error(
        indoc! {"
            filter:
              $includes: { match: { $key: K }, maxDistance: 1 }
        "},
        OperationKind::Find,
        "WrongBoundFamily",
    );
}

#[test]
fn walk_key_in_expression_anchors_set() {
    assert_parse(
        indoc! {"
            filter:
              $includes: { match: { $key: { $in: [a, b] } }, maxDepth: 1 }
        "},
        OperationKind::Find,
        find(filter(includes(InclusionAnchor::with_match(
            key_in(&["a", "b"]),
            1,
            1,
        )))),
    );
}

#[test]
fn empty_anchor_list_rejected() {
    assert_parse_error(
        indoc! {"
            filter:
              $includes: []
        "},
        OperationKind::Find,
        "ArrayFormRemoved",
    );
}

#[test]
fn empty_anchor_mapping_rejected() {
    assert_parse_error(
        indoc! {"
            filter:
              $includes: {}
        "},
        OperationKind::Find,
        "EmptyAnchorMapping",
    );
}

#[test]
fn match_predicate_form_anchors_by_frontmatter() {
    assert_parse(
        indoc! {"
            filter:
              $includes: { match: { status: draft }, maxDepth: 2 }
        "},
        OperationKind::Find,
        find(filter(includes(InclusionAnchor::with_match(
            eq("status", "draft"),
            1,
            2,
        )))),
    );
}

#[test]
fn match_compound_predicate_anchors_with_full_filter() {
    assert_parse(
        indoc! {"
            filter:
              $includedBy:
                match:
                  type: project
                  status: active
                maxDepth: 5
        "},
        OperationKind::Find,
        find(filter(included_by(InclusionAnchor::with_match(
            and(vec![eq("type", "project"), eq("status", "active")]),
            1,
            5,
        )))),
    );
}

#[test]
fn match_or_expression_anchors_with_filter() {
    assert_parse(
        indoc! {"
            filter:
              $includes:
                match:
                  $or:
                    - status: draft
                    - tag: important
                maxDepth: 2
        "},
        OperationKind::Find,
        find(filter(includes(InclusionAnchor::with_match(
            or(vec![eq("status", "draft"), eq("tag", "important")]),
            1,
            2,
        )))),
    );
}

#[test]
fn match_nested_relational_anchor() {
    assert_parse(
        indoc! {"
            filter:
              $includedBy:
                match:
                  $includedBy: { match: { $key: projects/alpha }, maxDepth: 5 }
                maxDepth: 5
        "},
        OperationKind::Find,
        find(filter(included_by(InclusionAnchor::with_match(
            included_by(InclusionAnchor::with_max("projects/alpha", 5)),
            1,
            5,
        )))),
    );
}

#[test]
fn references_scalar_shorthand() {
    assert_parse(
        indoc! {"
            filter:
              $references: people/alice
        "},
        OperationKind::Find,
        find(filter(references(ReferenceAnchor::new(
            "people/alice",
            1,
            1,
        )))),
    );
}

#[test]
fn references_with_distance() {
    assert_parse(
        indoc! {"
            filter:
              $references: { match: { $key: people/dmytro }, maxDistance: 1 }
        "},
        OperationKind::Find,
        find(filter(references(ReferenceAnchor::with_max(
            "people/dmytro",
            1,
        )))),
    );
}

#[test]
fn referenced_by_range() {
    assert_parse(
        indoc! {"
            filter:
              $referencedBy:
                match: { $key: archive/index }
                minDistance: 1
                maxDistance: 3
        "},
        OperationKind::Find,
        find(filter(referenced_by(ReferenceAnchor::new(
            "archive/index",
            1,
            3,
        )))),
    );
}

#[test]
fn references_with_depth_modifier_rejected() {
    assert_parse_error(
        indoc! {"
            filter:
              $references: { match: { $key: K }, maxDepth: 1 }
        "},
        OperationKind::Find,
        "WrongBoundFamily",
    );
}

#[test]
fn references_zero_distance_rejected() {
    assert_parse_error(
        indoc! {"
            filter:
              $references: { match: { $key: K }, maxDistance: 0 }
        "},
        OperationKind::Find,
        "InvalidDepthValue",
    );
}

#[test]
fn project_bare_blocks_selector_round_trips() {
    assert_parse(
        indoc! {"
            project:
              hits: $blocks
        "},
        OperationKind::Find,
        find(FindOp::new().project(vec![field("hits", blocks(blk::any()))])),
    );
}

#[test]
fn project_empty_content_lowers_to_whole_body() {
    assert_parse(
        indoc! {"
            project:
              body: { $content: {} }
        "},
        OperationKind::Find,
        find(FindOp::new().project(vec![field("body", content(blk::any()))])),
    );
}

#[test]
fn project_within_scalar_lowers_to_exact_section_text() {
    assert_parse(
        indoc! {"
            project:
              hits: { $blocks: { $within: header1 } }
        "},
        OperationKind::Find,
        find(FindOp::new().project(vec![field(
            "hits",
            blocks(blk::within(blk::section(blk::text_eq("header1")))),
        )])),
    );
}

#[test]
fn project_type_scalar_lowers_to_exact_text() {
    assert_parse(
        indoc! {"
            project:
              hits: { $blocks: { $header: header1 } }
        "},
        OperationKind::Find,
        find(FindOp::new().project(vec![field(
            "hits",
            blocks(blk::header(blk::text_eq("header1"))),
        )])),
    );
}

#[test]
fn project_block_predicate_operators_round_trip() {
    assert_parse(
        indoc! {"
            project:
              hits:
                $blocks:
                  $or:
                    - { $paragraph: { $references: '2' } }
                    - { $quote: {} }
                    - { $item: { $matches: 'mark[0-9]' } }
        "},
        OperationKind::Find,
        find(FindOp::new().project(vec![field(
            "hits",
            blocks(blk::or(vec![
                blk::paragraph(blk::references("2")),
                blk::quote(blk::any()),
                blk::item(blk::matches("mark[0-9]")),
            ])),
        )])),
    );
}

#[test]
fn add_fields_scoped_matches_round_trips() {
    assert_parse(
        indoc! {"
            addFields:
              found:
                $matches:
                  pattern: 'mark[0-9]'
                  $within: header1
        "},
        OperationKind::Find,
        find(FindOp::new().add_fields(vec![field(
            "found",
            grep("mark[0-9]", blk::within_section("header1")),
        )])),
    );
}
