use crate::transpiler::nosql::{dynamo::ToDynamo, mongo::ToMongo, qdrant::ToQdrant};

#[test]
fn test_qdrant_search() {
    use crate::ast::*;
    // Qdrant with vector search uses special syntax, use manual construction
    let mut cmd = Qail::get("points");
    cmd.columns.push(Expr::Named("id".to_string()));
    cmd.columns.push(Expr::Named("score".to_string()));
    cmd.cages.push(Cage {
        kind: CageKind::Filter,
        conditions: vec![
            Condition {
                left: Expr::Named("vector".to_string()),
                op: Operator::Fuzzy,
                value: Value::String("cute cat".to_string()),
                is_array_unnest: false,
            },
            Condition {
                left: Expr::Named("city".to_string()),
                op: Operator::Eq,
                value: Value::String("London".to_string()),
                is_array_unnest: false,
            },
        ],
        logical_op: LogicalOp::And,
    });
    cmd.cages.push(Cage {
        kind: CageKind::Limit(10),
        conditions: vec![],
        logical_op: LogicalOp::And,
    });
    let qdrant = cmd.to_qdrant_search();

    assert!(qdrant.contains("{{EMBED:cute cat}}"));
    assert!(qdrant.contains("\"filter\": { \"must\": ["));
    assert!(qdrant.contains("\"key\": \"city\", \"match\": { \"value\": \"London\" }"));
}

#[test]
fn test_qdrant_or_filter_output() {
    use crate::ast::{Operator, Qail};

    let qdrant = Qail::get("points")
        .or_filter("city", Operator::Eq, "London")
        .or_filter("country", Operator::Eq, "UK")
        .to_qdrant_search();

    assert!(
        qdrant.contains("\"should\": ["),
        "Expected should group: {qdrant}"
    );
}

#[test]
fn test_qdrant_and_plus_or_filter_output() {
    use crate::ast::{Operator, Qail};

    let qdrant = Qail::get("points")
        .filter("is_active", Operator::Eq, true)
        .or_filter("city", Operator::Eq, "London")
        .or_filter("country", Operator::Eq, "UK")
        .to_qdrant_search();

    assert!(
        qdrant.contains("\"must\": ["),
        "Expected must group: {qdrant}"
    );
    assert!(
        qdrant.contains("\"should\": ["),
        "Expected should group: {qdrant}"
    );
}

#[test]
fn test_qdrant_multiple_or_cages_remain_separate_groups() {
    use crate::ast::{Cage, CageKind, Condition, Expr, LogicalOp, Operator, Qail, Value};

    let qdrant = Qail {
        table: "points".to_string(),
        cages: vec![
            Cage {
                kind: CageKind::Filter,
                conditions: vec![Condition {
                    left: Expr::Named("tenant_id".to_string()),
                    op: Operator::Eq,
                    value: Value::String("t1".to_string()),
                    is_array_unnest: false,
                }],
                logical_op: LogicalOp::And,
            },
            Cage {
                kind: CageKind::Filter,
                conditions: vec![
                    Condition {
                        left: Expr::Named("city".to_string()),
                        op: Operator::Eq,
                        value: Value::String("London".to_string()),
                        is_array_unnest: false,
                    },
                    Condition {
                        left: Expr::Named("city".to_string()),
                        op: Operator::Eq,
                        value: Value::String("Paris".to_string()),
                        is_array_unnest: false,
                    },
                ],
                logical_op: LogicalOp::Or,
            },
            Cage {
                kind: CageKind::Filter,
                conditions: vec![
                    Condition {
                        left: Expr::Named("country".to_string()),
                        op: Operator::Eq,
                        value: Value::String("UK".to_string()),
                        is_array_unnest: false,
                    },
                    Condition {
                        left: Expr::Named("country".to_string()),
                        op: Operator::Eq,
                        value: Value::String("FR".to_string()),
                        is_array_unnest: false,
                    },
                ],
                logical_op: LogicalOp::Or,
            },
        ],
        ..Default::default()
    }
    .to_qdrant_search();

    let should_count = qdrant.matches("\"should\": [").count();
    assert!(
        should_count >= 2,
        "Expected multiple nested OR groups, got {should_count}: {qdrant}"
    );
}

#[test]
fn test_qdrant_json_strings_are_escaped() {
    use crate::ast::{Cage, CageKind, Condition, Expr, LogicalOp, Operator, Qail, Value};

    let search = Qail {
        table: "points".to_string(),
        columns: vec![Expr::Named("payload\"key".to_string())],
        cages: vec![Cage {
            kind: CageKind::Filter,
            conditions: vec![
                Condition {
                    left: Expr::Named("vector".to_string()),
                    op: Operator::Fuzzy,
                    value: Value::String("cute \"cat\"".to_string()),
                    is_array_unnest: false,
                },
                Condition {
                    left: Expr::Named("city\", \"must\": [".to_string()),
                    op: Operator::Eq,
                    value: Value::String("London\"}, \"must\": []".to_string()),
                    is_array_unnest: false,
                },
            ],
            logical_op: LogicalOp::And,
        }],
        ..Default::default()
    }
    .to_qdrant_search();

    let parsed: serde_json::Value =
        serde_json::from_str(&search).expect("qdrant search JSON must stay valid");
    assert_eq!(parsed["vector"], "{{EMBED:cute \"cat\"}}");
    assert_eq!(parsed["with_payload"]["include"][0], "payload\"key");
    assert_eq!(parsed["filter"]["must"][0]["key"], "city\", \"must\": [");
    assert_eq!(
        parsed["filter"]["must"][0]["match"]["value"],
        "London\"}, \"must\": []"
    );

    let upsert = Qail {
        action: crate::ast::Action::Add,
        table: "points".to_string(),
        cages: vec![Cage {
            kind: CageKind::Payload,
            conditions: vec![Condition {
                left: Expr::Named("name\"bad".to_string()),
                op: Operator::Eq,
                value: Value::String("Ana\"bad".to_string()),
                is_array_unnest: false,
            }],
            logical_op: LogicalOp::And,
        }],
        ..Default::default()
    }
    .to_qdrant_search();

    let parsed: serde_json::Value =
        serde_json::from_str(&upsert).expect("qdrant upsert JSON must stay valid");
    assert_eq!(parsed["points"][0]["payload"]["name\"bad"], "Ana\"bad");
}

#[test]
fn test_qdrant_transpiler_rejects_invalid_json_values() {
    use crate::ast::{Cage, CageKind, Condition, Expr, LogicalOp, Operator, Qail, Value};

    let search = Qail {
        table: "points".to_string(),
        cages: vec![Cage {
            kind: CageKind::Filter,
            conditions: vec![Condition {
                left: Expr::Named("score".to_string()),
                op: Operator::Eq,
                value: Value::Float(f64::NAN),
                is_array_unnest: false,
            }],
            logical_op: LogicalOp::And,
        }],
        ..Default::default()
    }
    .to_qdrant_search();

    let parsed: serde_json::Value =
        serde_json::from_str(&search).expect("qdrant error JSON must be valid");
    assert!(
        parsed["error"]
            .as_str()
            .expect("error should be a string")
            .contains("non-finite")
    );
}

#[test]
fn test_qdrant_transpiler_preserves_payload_arrays() {
    use crate::ast::{Action, Cage, CageKind, Condition, Expr, LogicalOp, Operator, Qail, Value};

    let upsert = Qail {
        action: Action::Add,
        table: "points".to_string(),
        cages: vec![Cage {
            kind: CageKind::Payload,
            conditions: vec![Condition {
                left: Expr::Named("tags".to_string()),
                op: Operator::Eq,
                value: Value::Array(vec![
                    Value::String("blue".to_string()),
                    Value::Bool(true),
                    Value::Int(7),
                ]),
                is_array_unnest: false,
            }],
            logical_op: LogicalOp::And,
        }],
        ..Default::default()
    }
    .to_qdrant_search();

    let parsed: serde_json::Value =
        serde_json::from_str(&upsert).expect("qdrant upsert JSON must stay valid");
    assert_eq!(
        parsed["points"][0]["payload"]["tags"],
        serde_json::json!(["blue", true, 7])
    );
}

#[test]
fn test_qdrant_transpiler_rejects_invalid_vector_values() {
    use crate::ast::{Cage, CageKind, Condition, Expr, LogicalOp, Operator, Qail, Value};

    let search = Qail {
        table: "points".to_string(),
        cages: vec![Cage {
            kind: CageKind::Filter,
            conditions: vec![Condition {
                left: Expr::Named("vector".to_string()),
                op: Operator::Fuzzy,
                value: Value::Array(vec![Value::String("oops".to_string())]),
                is_array_unnest: false,
            }],
            logical_op: LogicalOp::And,
        }],
        ..Default::default()
    }
    .to_qdrant_search();

    let parsed: serde_json::Value =
        serde_json::from_str(&search).expect("qdrant error JSON must be valid");
    assert!(
        parsed["error"]
            .as_str()
            .expect("error should be a string")
            .contains("vector values must be numeric")
    );
}

#[test]
fn test_qdrant_transpiler_rejects_unsupported_filter_operator() {
    use crate::ast::{Operator, Qail};

    let search = Qail::get("points")
        .filter("city", Operator::Like, "%Lon%")
        .to_qdrant_search();

    let parsed: serde_json::Value =
        serde_json::from_str(&search).expect("qdrant error JSON must be valid");
    assert!(
        parsed["error"]
            .as_str()
            .expect("error should be a string")
            .contains("unsupported Qdrant filter operator")
    );
}

#[test]
fn test_qdrant_delete_without_filter_returns_error_json() {
    use crate::ast::{Action, Qail};

    let delete = Qail {
        action: Action::Del,
        table: "points".to_string(),
        ..Default::default()
    }
    .to_qdrant_search();

    let parsed: serde_json::Value =
        serde_json::from_str(&delete).expect("qdrant error JSON must be valid");
    assert!(
        parsed["error"]
            .as_str()
            .expect("error should be a string")
            .contains("requires an id or filter")
    );
}

#[test]
fn test_mongo_shell_fragments_are_escaped() {
    use crate::ast::{
        Action, Cage, CageKind, Condition, Expr, LogicalOp, Operator, Qail, SortOrder, Value,
    };

    let insert = Qail {
        action: Action::Add,
        table: "users\"); db.dropDatabase(); //".to_string(),
        cages: vec![Cage {
            kind: CageKind::Payload,
            conditions: vec![Condition {
                left: Expr::Named("name\"bad".to_string()),
                op: Operator::Eq,
                value: Value::String("Ana\"bad".to_string()),
                is_array_unnest: false,
            }],
            logical_op: LogicalOp::And,
        }],
        ..Default::default()
    }
    .to_mongo();

    assert!(insert.starts_with("db.getCollection("), "{insert}");
    assert!(
        insert.contains("\"users\\\"); db.dropDatabase(); //\""),
        "{insert}"
    );
    assert!(
        insert.contains("\"name\\\"bad\": \"Ana\\\"bad\""),
        "{insert}"
    );

    let find = Qail {
        table: "events; db.evil()".to_string(),
        columns: vec![Expr::Named("payload\"key".to_string())],
        cages: vec![
            Cage {
                kind: CageKind::Filter,
                conditions: vec![Condition {
                    left: Expr::Named("city\", $where: evil".to_string()),
                    op: Operator::Eq,
                    value: Value::String("London\" }".to_string()),
                    is_array_unnest: false,
                }],
                logical_op: LogicalOp::And,
            },
            Cage {
                kind: CageKind::Sort(SortOrder::Desc),
                conditions: vec![Condition {
                    left: Expr::Named("score\"bad".to_string()),
                    op: Operator::Eq,
                    value: Value::Null,
                    is_array_unnest: false,
                }],
                logical_op: LogicalOp::And,
            },
        ],
        ..Default::default()
    }
    .to_mongo();

    assert!(find.starts_with("db.getCollection("), "{find}");
    assert!(find.contains("\"events; db.evil()\""), "{find}");
    assert!(find.contains("\"payload\\\"key\": 1"), "{find}");
    assert!(
        find.contains("\"city\\\", $where: evil\": \"London\\\" }\""),
        "{find}"
    );
    assert!(find.contains(".sort({ \"score\\\"bad\": -1 })"), "{find}");
}

#[test]
fn test_mongo_rejects_non_finite_numbers() {
    use crate::ast::{Action, Cage, CageKind, Condition, Expr, LogicalOp, Operator, Qail, Value};

    let insert = Qail {
        action: Action::Add,
        table: "events".to_string(),
        cages: vec![Cage {
            kind: CageKind::Payload,
            conditions: vec![Condition {
                left: Expr::Named("score".to_string()),
                op: Operator::Eq,
                value: Value::Float(f64::NAN),
                is_array_unnest: false,
            }],
            logical_op: LogicalOp::And,
        }],
        ..Default::default()
    }
    .to_mongo();

    assert!(insert.starts_with("throw new Error("), "{insert}");
    assert!(insert.contains("non-finite"), "{insert}");
}

#[test]
fn test_mongo_rejects_unsupported_filter_operator() {
    use crate::ast::{Operator, Qail};

    let find = Qail::get("events")
        .filter("name", Operator::Like, "%ana%")
        .to_mongo();

    assert!(find.starts_with("throw new Error("), "{find}");
    assert!(
        find.contains("unsupported MongoDB filter operator"),
        "{find}"
    );
}

#[test]
fn test_mongo_delete_without_filter_returns_error() {
    use crate::ast::{Action, Qail};

    let delete = Qail {
        action: Action::Del,
        table: "events".to_string(),
        ..Default::default()
    }
    .to_mongo();

    assert!(delete.starts_with("throw new Error("), "{delete}");
    assert!(
        delete.contains("delete requires at least one filter"),
        "{delete}"
    );
}

#[test]
fn test_mongo_preserves_array_payload_values() {
    use crate::ast::{Action, Cage, CageKind, Condition, Expr, LogicalOp, Operator, Qail, Value};

    let insert = Qail {
        action: Action::Add,
        table: "events".to_string(),
        cages: vec![Cage {
            kind: CageKind::Payload,
            conditions: vec![Condition {
                left: Expr::Named("tags".to_string()),
                op: Operator::Eq,
                value: Value::Array(vec![
                    Value::String("blue".to_string()),
                    Value::Bool(true),
                    Value::Int(7),
                ]),
                is_array_unnest: false,
            }],
            logical_op: LogicalOp::And,
        }],
        ..Default::default()
    }
    .to_mongo();

    assert!(insert.contains("\"tags\": [\"blue\", true, 7]"), "{insert}");
}

#[test]
fn test_mongo_update_does_not_write_filter_fields_into_set_payload() {
    use crate::ast::Qail;

    let update = Qail::set("users")
        .set_value("name", "Ana")
        .eq("id", 1)
        .to_mongo();

    assert_eq!(
        update,
        "db.users.updateMany({ \"id\": 1 }, { $set: { \"name\": \"Ana\" } })"
    );
}

#[test]
fn test_mongo_or_filters_are_rendered_as_or_clauses() {
    use crate::ast::{Operator, Qail};

    let find = Qail::get("events")
        .or_filter("city", Operator::Eq, "London")
        .or_filter("city", Operator::Eq, "Paris")
        .to_mongo();

    assert_eq!(
        find,
        "db.events.find({ \"$or\": [{ \"city\": \"London\" }, { \"city\": \"Paris\" }] }, {})"
    );
}

#[test]
fn test_mongo_repeated_field_and_filters_are_not_flattened() {
    use crate::ast::{Operator, Qail};

    let find = Qail::get("events")
        .filter("score", Operator::Gte, 10)
        .filter("score", Operator::Lt, 20)
        .to_mongo();

    assert_eq!(
        find,
        "db.events.find({ \"$and\": [{ \"score\": { \"$gte\": 10 } }, { \"score\": { \"$lt\": 20 } }] }, {})"
    );
}

#[test]
fn test_dynamo_json_and_expression_names_are_escaped() {
    use crate::ast::{Action, Cage, CageKind, Condition, Expr, LogicalOp, Operator, Qail, Value};

    let get = Qail {
        table: "users\"bad".to_string(),
        columns: vec![Expr::Named("payload\"key".to_string())],
        cages: vec![Cage {
            kind: CageKind::Filter,
            conditions: vec![
                Condition {
                    left: Expr::Named("city\", #x = :evil".to_string()),
                    op: Operator::Eq,
                    value: Value::String("London\"bad".to_string()),
                    is_array_unnest: false,
                },
                Condition {
                    left: Expr::Named("index".to_string()),
                    op: Operator::Eq,
                    value: Value::String("gsi\"bad".to_string()),
                    is_array_unnest: false,
                },
            ],
            logical_op: LogicalOp::And,
        }],
        ..Default::default()
    }
    .to_dynamo();

    let parsed: serde_json::Value =
        serde_json::from_str(&get).expect("dynamo get JSON must stay valid");
    assert_eq!(parsed["TableName"], "users\"bad");
    assert_eq!(parsed["IndexName"], "gsi\"bad");
    assert_eq!(parsed["FilterExpression"], "#f1 = :v1");
    assert_eq!(
        parsed["ExpressionAttributeNames"]["#f1"],
        "city\", #x = :evil"
    );
    assert_eq!(parsed["ExpressionAttributeNames"]["#p1"], "payload\"key");
    assert_eq!(
        parsed["ExpressionAttributeValues"][":v1"]["S"],
        "London\"bad"
    );
    assert_eq!(parsed["ProjectionExpression"], "#p1");

    let update = Qail {
        action: Action::Set,
        table: "users".to_string(),
        cages: vec![
            Cage {
                kind: CageKind::Filter,
                conditions: vec![Condition {
                    left: Expr::Named("pk\"bad".to_string()),
                    op: Operator::Eq,
                    value: Value::String("user\"1".to_string()),
                    is_array_unnest: false,
                }],
                logical_op: LogicalOp::And,
            },
            Cage {
                kind: CageKind::Payload,
                conditions: vec![Condition {
                    left: Expr::Named("set\", danger = :x".to_string()),
                    op: Operator::Eq,
                    value: Value::String("active\"yes".to_string()),
                    is_array_unnest: false,
                }],
                logical_op: LogicalOp::And,
            },
        ],
        ..Default::default()
    }
    .to_dynamo();

    let parsed: serde_json::Value =
        serde_json::from_str(&update).expect("dynamo update JSON must stay valid");
    assert_eq!(parsed["Key"]["pk\"bad"]["S"], "user\"1");
    assert_eq!(parsed["UpdateExpression"], "SET #u101 = :u101");
    assert_eq!(
        parsed["ExpressionAttributeNames"]["#u101"],
        "set\", danger = :x"
    );
    assert_eq!(
        parsed["ExpressionAttributeValues"][":u101"]["S"],
        "active\"yes"
    );
}

#[test]
fn test_dynamo_rejects_non_finite_numbers() {
    use crate::ast::{Action, Cage, CageKind, Condition, Expr, LogicalOp, Operator, Qail, Value};

    let put = Qail {
        action: Action::Add,
        table: "users".to_string(),
        cages: vec![Cage {
            kind: CageKind::Payload,
            conditions: vec![Condition {
                left: Expr::Named("score".to_string()),
                op: Operator::Eq,
                value: Value::Float(f64::INFINITY),
                is_array_unnest: false,
            }],
            logical_op: LogicalOp::And,
        }],
        ..Default::default()
    }
    .to_dynamo();

    let parsed: serde_json::Value =
        serde_json::from_str(&put).expect("dynamo error JSON must stay valid");
    assert!(
        parsed["error"]
            .as_str()
            .expect("error should be a string")
            .contains("non-finite")
    );
}

#[test]
fn test_dynamo_rejects_unsupported_filter_operator() {
    use crate::ast::{Operator, Qail};

    let get = Qail::get("users")
        .filter("name", Operator::Like, "%ana%")
        .to_dynamo();

    let parsed: serde_json::Value =
        serde_json::from_str(&get).expect("dynamo error JSON must stay valid");
    assert!(
        parsed["error"]
            .as_str()
            .expect("error should be a string")
            .contains("unsupported DynamoDB filter operator")
    );
}

#[test]
fn test_dynamo_delete_without_key_filter_returns_error_json() {
    use crate::ast::{Action, Qail};

    let delete = Qail {
        action: Action::Del,
        table: "users".to_string(),
        ..Default::default()
    }
    .to_dynamo();

    let parsed: serde_json::Value =
        serde_json::from_str(&delete).expect("dynamo error JSON must stay valid");
    assert!(
        parsed["error"]
            .as_str()
            .expect("error should be a string")
            .contains("requires an equality key filter")
    );
}

#[test]
fn test_dynamo_preserves_array_payload_values() {
    use crate::ast::{Action, Cage, CageKind, Condition, Expr, LogicalOp, Operator, Qail, Value};

    let put = Qail {
        action: Action::Add,
        table: "users".to_string(),
        cages: vec![Cage {
            kind: CageKind::Payload,
            conditions: vec![Condition {
                left: Expr::Named("tags".to_string()),
                op: Operator::Eq,
                value: Value::Array(vec![
                    Value::String("blue".to_string()),
                    Value::Bool(true),
                    Value::Int(7),
                ]),
                is_array_unnest: false,
            }],
            logical_op: LogicalOp::And,
        }],
        ..Default::default()
    }
    .to_dynamo();

    let parsed: serde_json::Value =
        serde_json::from_str(&put).expect("dynamo put JSON must stay valid");
    assert_eq!(parsed["Item"]["tags"]["L"][0]["S"], "blue");
    assert_eq!(parsed["Item"]["tags"]["L"][1]["BOOL"], true);
    assert_eq!(parsed["Item"]["tags"]["L"][2]["N"], "7");
}
