use serde_json_bytes::Value as JSON;
use shape::Shape;

use crate::connectors::json_selection::ApplyToError;
use crate::connectors::json_selection::ApplyToInternal;
use crate::connectors::json_selection::MethodArgs;
use crate::connectors::json_selection::ShapeContext;
use crate::connectors::json_selection::VarsWithPathsMap;
use crate::connectors::json_selection::immutable::InputPath;
use crate::connectors::json_selection::location::Ranged;
use crate::connectors::json_selection::location::WithRange;
use crate::connectors::json_selection::methods::common::is_comparable_shape_combination;
use crate::connectors::json_selection::methods::common::number_value_as_float;
use crate::connectors::spec::ConnectSpec;
use crate::impl_arrow_method;

impl_arrow_method!(GteMethod, gte_method, gte_shape);
/// Returns true if the applied to value is greater than or equal to the argument value.
/// Simple examples:
///
/// $(3)->gte(3)       results in true
/// $(4)->gte(3)       results in true
/// $(2)->gte(3)       results in false
/// $("a")->gte("b")   results in false
/// $("c")->gte("b")   results in true
fn gte_method(
    method_name: &WithRange<String>,
    method_args: Option<&MethodArgs>,
    data: &JSON,
    vars: &VarsWithPathsMap,
    input_path: &InputPath<JSON>,
    spec: ConnectSpec,
) -> (Option<JSON>, Vec<ApplyToError>) {
    let Some(first_arg) = method_args.and_then(|args| args.args.first()) else {
        return (
            None,
            vec![ApplyToError::new(
                format!(
                    "Method ->{} requires exactly one argument",
                    method_name.as_ref()
                ),
                input_path.to_vec(),
                method_name.range(),
                spec,
            )],
        );
    };

    let (value_opt, arg_errors) = first_arg.apply_to_path(data, vars, input_path, spec);
    let mut apply_to_errors = arg_errors;
    // We have to do this because Value doesn't implement PartialOrd
    let matches = value_opt.and_then(|value| {
        match (data, &value) {
            // Number comparisons
            (JSON::Number(left), JSON::Number(right)) => {
                let left = match number_value_as_float(left, method_name, input_path, spec) {
                    Ok(f) => f,
                    Err(err) => {
                        apply_to_errors.push(err);
                        return None;
                    }
                };
                let right = match number_value_as_float(right, method_name, input_path, spec) {
                    Ok(f) => f,
                    Err(err) => {
                        apply_to_errors.push(err);
                        return None;
                    }
                };

                Some(JSON::Bool(left >= right))
            }
            // String comparisons
            (JSON::String(left), JSON::String(right)) => Some(JSON::Bool(left >= right)),
            // Mixed types or incomparable types (including arrays and objects) return false
            _ => {
                apply_to_errors.push(ApplyToError::new(
                    format!(
                        "Method ->{} can only compare numbers and strings. Found: {data} >= {value}",
                        method_name.as_ref(),
                    ),
                    input_path.to_vec(),
                    method_name.range(),
                    spec,
                ));

                None
            }
        }
    });

    (matches, apply_to_errors)
}

#[allow(dead_code)] // method type-checking disabled until we add name resolution
fn gte_shape(
    context: &ShapeContext,
    method_name: &WithRange<String>,
    method_args: Option<&MethodArgs>,
    input_shape: Shape,
    dollar_shape: Shape,
) -> Shape {
    let arg_count = method_args.map(|args| args.args.len()).unwrap_or_default();
    if arg_count > 1 {
        return Shape::error(
            format!(
                "Method ->{} requires only one argument, but {arg_count} were provided",
                method_name.as_ref(),
            ),
            vec![],
        );
    }

    let Some(first_arg) = method_args.and_then(|args| args.args.first()) else {
        return Shape::error(
            format!("Method ->{} requires one argument", method_name.as_ref()),
            method_name.shape_location(context.source_id()),
        );
    };

    let arg_shape = first_arg.compute_output_shape(context, input_shape.clone(), dollar_shape);

    if is_comparable_shape_combination(&arg_shape, &input_shape) {
        Shape::bool(method_name.shape_location(context.source_id()))
    } else {
        Shape::error_with_partial(
            format!(
                "Method ->{} can only compare two numbers or two strings. Found {input_shape} >= {arg_shape}",
                method_name.as_ref()
            ),
            Shape::bool_value(false, method_name.shape_location(context.source_id())),
            method_name.shape_location(context.source_id()),
        )
    }
}

#[cfg(test)]
mod method_tests {
    use serde_json_bytes::json;

    use crate::connectors::ConnectSpec;
    use crate::connectors::json_selection::ApplyToError;
    use crate::selection;

    #[test]
    fn gte_should_return_true_when_applied_to_number_is_greater_than_argument() {
        assert_eq!(
            selection!(
                r#"
                    result: value->gte(3)
                "#
            )
            .apply_to(&json!({ "value": 4 })),
            (
                Some(json!({
                    "result": true,
                })),
                vec![],
            ),
        );
    }

    #[test]
    fn gte_should_return_true_when_applied_to_number_equals_argument() {
        assert_eq!(
            selection!(
                r#"
                    result: value->gte(3)
                "#
            )
            .apply_to(&json!({ "value": 3 })),
            (
                Some(json!({
                    "result": true,
                })),
                vec![],
            ),
        );
    }

    #[test]
    fn gte_should_return_false_when_applied_to_number_is_less_than_argument() {
        assert_eq!(
            selection!(
                r#"
                    result: value->gte(3)
                "#
            )
            .apply_to(&json!({ "value": 2 })),
            (
                Some(json!({
                    "result": false,
                })),
                vec![],
            ),
        );
    }

    #[test]
    fn gte_should_return_true_when_applied_to_string_is_greater_than_argument() {
        assert_eq!(
            selection!(
                r#"
                    result: value->gte("b")
                "#
            )
            .apply_to(&json!({ "value": "c" })),
            (
                Some(json!({
                    "result": true,
                })),
                vec![],
            ),
        );
    }

    #[test]
    fn gte_should_return_true_when_applied_to_string_equals_argument() {
        assert_eq!(
            selection!(
                r#"
                    result: value->gte("a")
                "#
            )
            .apply_to(&json!({ "value": "a" })),
            (
                Some(json!({
                    "result": true,
                })),
                vec![],
            ),
        );
    }

    #[test]
    fn gte_should_return_false_when_applied_to_string_is_less_than_argument() {
        assert_eq!(
            selection!(
                r#"
                    result: value->gte("b")
                "#
            )
            .apply_to(&json!({ "value": "a" })),
            (
                Some(json!({
                    "result": false,
                })),
                vec![],
            ),
        );
    }

    #[test]
    fn gte_should_error_for_null_values() {
        let result = selection!(
            r#"
                result: value->gte(null)
            "#
        )
        .apply_to(&json!({ "value": null }));

        assert_eq!(result.0, Some(json!({})),);
        assert!(!result.1.is_empty());
        assert!(
            result.1[0]
                .message()
                .contains("Method ->gte can only compare numbers and strings. Found: null >= null")
        );
    }

    #[test]
    fn gte_should_error_for_boolean_values() {
        let result = selection!(
            r#"
                result: value->gte(false)
            "#
        )
        .apply_to(&json!({ "value": true }));

        assert_eq!(result.0, Some(json!({})),);
        assert!(!result.1.is_empty());
        assert!(
            result.1[0].message().contains(
                "Method ->gte can only compare numbers and strings. Found: true >= false"
            )
        );
    }

    #[test]
    fn gte_should_error_for_arrays() {
        let result = selection!(
            r#"
                    result: value->gte([1,2])
                "#
        )
        .apply_to(&json!({ "value": [1,2,3] }));

        assert_eq!(result.0, Some(json!({})),);
        assert!(!result.1.is_empty());
        assert!(result.1[0].message().contains(
            "Method ->gte can only compare numbers and strings. Found: [1,2,3] >= [1,2]"
        ));
    }

    #[test]
    fn gte_should_error_for_objects() {
        let result = selection!(
            r#"
                    result: value->gte({"a": 1})
                "#
        )
        .apply_to(&json!({ "value": {"a": 1, "b": 2} }));

        assert_eq!(result.0, Some(json!({})),);
        assert!(!result.1.is_empty());
        assert!(result.1[0].message().contains(
            "Method ->gte can only compare numbers and strings. Found: {\"a\":1,\"b\":2} >= {\"a\":1}"
        ));
    }

    #[test]
    fn gte_should_error_for_mixed_types() {
        let result = selection!(
            r#"
                    result: value->gte("string")
                "#
        )
        .apply_to(&json!({ "value": 42 }));

        assert_eq!(result.0, Some(json!({})),);
        assert!(!result.1.is_empty());
        assert!(result.1[0].message().contains(
            "Method ->gte can only compare numbers and strings. Found: 42 >= \"string\""
        ));
    }

    #[test]
    fn gte_should_return_error_when_no_arguments_provided() {
        let result = selection!(
            r#"
                    result: value->gte()
                "#
        )
        .apply_to(&json!({ "value": 42 }));

        assert_eq!(result.0, Some(json!({})),);
        assert!(!result.1.is_empty());
        assert!(
            result.1[0]
                .message()
                .contains("Method ->gte requires exactly one argument")
        );
    }

    #[rstest::rstest]
    #[case::v0_2(ConnectSpec::V0_2)]
    #[case::v0_3(ConnectSpec::V0_3)]
    #[case::v0_4(ConnectSpec::V0_4)]
    fn gte_should_return_none_when_argument_evaluates_to_none(#[case] spec: ConnectSpec) {
        assert_eq!(
            selection!("$.a->gte($.missing)", spec).apply_to(&json!({
                "a": 5,
            })),
            (
                None,
                vec![ApplyToError::from_json(&json!({
                    "message": "Property .missing not found in object",
                    "path": ["missing"],
                    "range": [11, 18],
                    "spec": spec.to_string(),
                }))]
            ),
        );
    }
}

#[cfg(test)]
mod shape_tests {
    use serde_json::Number;
    use shape::location::Location;
    use shape::location::SourceId;

    use super::*;
    use crate::connectors::json_selection::lit_expr::LitExpr;

    fn get_location() -> Location {
        Location {
            source_id: SourceId::new("test".to_string()),
            span: 0..7,
        }
    }

    fn get_shape(args: Vec<WithRange<LitExpr>>, input: Shape) -> Shape {
        let location = get_location();
        gte_shape(
            &ShapeContext::new(location.source_id),
            &WithRange::new("gte".to_string(), Some(location.span)),
            Some(&MethodArgs { args, range: None }),
            input,
            Shape::none(),
        )
    }

    #[test]
    fn gte_shape_should_return_bool_on_valid_strings() {
        assert_eq!(
            get_shape(
                vec![WithRange::new(LitExpr::String("a".to_string()), None)],
                Shape::string([])
            ),
            Shape::bool([get_location()])
        );
    }

    #[test]
    fn gte_shape_should_return_bool_on_valid_numbers() {
        assert_eq!(
            get_shape(
                vec![WithRange::new(LitExpr::Number(Number::from(42)), None)],
                Shape::int([])
            ),
            Shape::bool([get_location()])
        );
    }

    #[test]
    fn gte_shape_should_error_on_mixed_types() {
        assert_eq!(
            get_shape(
                vec![WithRange::new(LitExpr::String("a".to_string()), None)],
                Shape::int([])
            ),
            Shape::error_with_partial(
                "Method ->gte can only compare two numbers or two strings. Found Int >= \"a\""
                    .to_string(),
                Shape::bool_value(false, [get_location()]),
                [get_location()]
            )
        );
    }

    #[test]
    fn gte_shape_should_error_on_no_args() {
        assert_eq!(
            get_shape(vec![], Shape::string([])),
            Shape::error(
                "Method ->gte requires one argument".to_string(),
                [get_location()]
            )
        );
    }

    #[test]
    fn gte_shape_should_error_on_too_many_args() {
        assert_eq!(
            get_shape(
                vec![
                    WithRange::new(LitExpr::Number(Number::from(42)), None),
                    WithRange::new(LitExpr::Number(Number::from(42)), None)
                ],
                Shape::int([])
            ),
            Shape::error(
                "Method ->gte requires only one argument, but 2 were provided".to_string(),
                []
            )
        );
    }

    #[test]
    fn gte_shape_should_error_on_none_args() {
        let location = get_location();
        assert_eq!(
            gte_shape(
                &ShapeContext::new(location.source_id),
                &WithRange::new("gte".to_string(), Some(location.span)),
                None,
                Shape::string([]),
                Shape::none(),
            ),
            Shape::error(
                "Method ->gte requires one argument".to_string(),
                [get_location()]
            )
        );
    }
}
