use super::AdditionalProperties;
use super::JsonSchema;
use super::JsonSchemaPrimitiveType;
use super::JsonSchemaType;
use super::parse_tool_input_schema;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;

// Tests in this section exercise normalization transforms that mutate badly
// formed JSON for consumption by the Responses API.

#[test]
fn parse_tool_input_schema_coerces_boolean_schemas() {
    // Example schema shape:
    // true
    //
    // Expected normalization behavior:
    // - JSON Schema boolean forms are coerced to `{ "type": "string" }`
    //   because the baseline enum model cannot represent boolean-schema
    //   semantics directly.
    let schema = parse_tool_input_schema(&serde_json::json!(true)).expect("parse schema");

    assert_eq!(schema, JsonSchema::string(/*description*/ None));
}

#[test]
fn parse_tool_input_schema_infers_object_shape_and_defaults_properties() {
    // Example schema shape:
    // {
    //   "properties": {
    //     "query": { "description": "search query" }
    //   }
    // }
    //
    // Expected normalization behavior:
    // - `properties` implies an object schema when `type` is omitted.
    // - The child property keeps its description and defaults to a string type.
    let schema = parse_tool_input_schema(&serde_json::json!({
        "properties": {
            "query": {"description": "search query"}
        }
    }))
    .expect("parse schema");

    assert_eq!(
        schema,
        JsonSchema::object(
            BTreeMap::from([(
                "query".to_string(),
                JsonSchema::string(Some("search query".to_string())),
            )]),
            /*required*/ None,
            /*additional_properties*/ None
        )
    );
}

#[test]
fn parse_tool_input_schema_preserves_integer_and_defaults_array_items() {
    // Example schema shape:
    // {
    //   "type": "object",
    //   "properties": {
    //     "page": { "type": "integer" },
    //     "tags": { "type": "array" }
    //   }
    // }
    //
    // Expected normalization behavior:
    // - `"integer"` is preserved distinctly from `"number"`.
    // - Arrays missing `items` receive a permissive string `items` schema.
    let schema = parse_tool_input_schema(&serde_json::json!({
        "type": "object",
        "properties": {
            "page": {"type": "integer"},
            "tags": {"type": "array"}
        }
    }))
    .expect("parse schema");

    assert_eq!(
        schema,
        JsonSchema::object(
            BTreeMap::from([
                (
                    "page".to_string(),
                    JsonSchema::integer(/*description*/ None),
                ),
                (
                    "tags".to_string(),
                    JsonSchema::array(
                        JsonSchema::string(/*description*/ None),
                        /*description*/ None,
                    )
                ),
            ]),
            /*required*/ None,
            /*additional_properties*/ None
        )
    );
}

#[test]
fn parse_tool_input_schema_sanitizes_additional_properties_schema() {
    // Example schema shape:
    // {
    //   "type": "object",
    //   "additionalProperties": {
    //     "required": ["value"],
    //     "properties": {
    //       "value": {
    //         "anyOf": [
    //           { "type": "string" },
    //           { "type": "number" }
    //         ]
    //       }
    //     }
    //   }
    // }
    //
    // Expected normalization behavior:
    // - `additionalProperties` schema objects are recursively sanitized.
    // - The nested schema is normalized into the current object/anyOf form.
    let schema = parse_tool_input_schema(&serde_json::json!({
        "type": "object",
        "additionalProperties": {
            "required": ["value"],
            "properties": {
                "value": {"anyOf": [{"type": "string"}, {"type": "number"}]}
            }
        }
    }))
    .expect("parse schema");

    assert_eq!(
        schema,
        JsonSchema::object(
            BTreeMap::new(),
            /*required*/ None,
            Some(AdditionalProperties::Schema(Box::new(JsonSchema::object(
                BTreeMap::from([(
                    "value".to_string(),
                    JsonSchema::any_of(
                        vec![
                            JsonSchema::string(/*description*/ None),
                            JsonSchema::number(/*description*/ None),
                        ],
                        /*description*/ None,
                    ),
                )]),
                Some(vec!["value".to_string()]),
                /*additional_properties*/ None,
            ))))
        )
    );
}

#[test]
fn parse_tool_input_schema_infers_object_shape_from_boolean_additional_properties_only() {
    // Example schema shape:
    // {
    //   "additionalProperties": false
    // }
    //
    // Expected normalization behavior:
    // - `additionalProperties` implies an object schema when `type` is omitted.
    // - The boolean `additionalProperties` setting is preserved.
    let schema = parse_tool_input_schema(&serde_json::json!({
        "additionalProperties": false
    }))
    .expect("parse schema");

    assert_eq!(
        schema,
        JsonSchema::object(BTreeMap::new(), /*required*/ None, Some(false.into()))
    );
}

#[test]
fn parse_tool_input_schema_infers_number_from_numeric_keywords() {
    // Example schema shape:
    // {
    //   "minimum": 1
    // }
    //
    // Expected normalization behavior:
    // - Numeric constraint keywords imply a number schema when `type` is
    //   omitted.
    let schema = parse_tool_input_schema(&serde_json::json!({
        "minimum": 1
    }))
    .expect("parse schema");

    assert_eq!(schema, JsonSchema::number(/*description*/ None));
}

#[test]
fn parse_tool_input_schema_infers_number_from_multiple_of() {
    // Example schema shape:
    // {
    //   "multipleOf": 5
    // }
    //
    // Expected normalization behavior:
    // - `multipleOf` follows the same numeric-keyword inference path as
    //   `minimum` / `maximum`.
    let schema = parse_tool_input_schema(&serde_json::json!({
        "multipleOf": 5
    }))
    .expect("parse schema");

    assert_eq!(schema, JsonSchema::number(/*description*/ None));
}

#[test]
fn parse_tool_input_schema_infers_string_from_enum_const_and_format_keywords() {
    // Example schema shapes:
    // { "enum": ["fast", "safe"] }
    // { "const": "file" }
    // { "format": "date-time" }
    //
    // Expected normalization behavior:
    // - `enum` and `const` normalize into explicit string-enum schemas.
    // - `format` still falls back to a plain string schema.
    let enum_schema = parse_tool_input_schema(&serde_json::json!({
        "enum": ["fast", "safe"]
    }))
    .expect("parse enum schema");
    let const_schema = parse_tool_input_schema(&serde_json::json!({
        "const": "file"
    }))
    .expect("parse const schema");
    let format_schema = parse_tool_input_schema(&serde_json::json!({
        "format": "date-time"
    }))
    .expect("parse format schema");

    assert_eq!(
        enum_schema,
        JsonSchema::string_enum(
            vec![serde_json::json!("fast"), serde_json::json!("safe")],
            /*description*/ None,
        )
    );
    assert_eq!(
        const_schema,
        JsonSchema::string_enum(vec![serde_json::json!("file")], /*description*/ None)
    );
    assert_eq!(format_schema, JsonSchema::string(/*description*/ None));
}

#[test]
fn parse_tool_input_schema_defaults_empty_schema_to_string() {
    // Example schema shape:
    // {}
    //
    // Expected normalization behavior:
    // - With no structural hints at all, the normalizer falls back to a
    //   permissive string schema.
    let schema = parse_tool_input_schema(&serde_json::json!({})).expect("parse schema");

    assert_eq!(schema, JsonSchema::string(/*description*/ None));
}

#[test]
fn parse_tool_input_schema_infers_array_from_prefix_items() {
    // Example schema shape:
    // {
    //   "prefixItems": [
    //     { "type": "string" }
    //   ]
    // }
    //
    // Expected normalization behavior:
    // - `prefixItems` implies an array schema when `type` is omitted.
    // - The normalized result is stored as a regular array schema with string
    //   items.
    let schema = parse_tool_input_schema(&serde_json::json!({
        "prefixItems": [
            {"type": "string"}
        ]
    }))
    .expect("parse schema");

    assert_eq!(
        schema,
        JsonSchema::array(
            JsonSchema::string(/*description*/ None),
            /*description*/ None,
        )
    );
}

#[test]
fn parse_tool_input_schema_preserves_boolean_additional_properties_on_inferred_object() {
    // Example schema shape:
    // {
    //   "type": "object",
    //   "properties": {
    //     "metadata": {
    //       "additionalProperties": true
    //     }
    //   }
    // }
    //
    // Expected normalization behavior:
    // - The nested `metadata` schema is inferred to be an object because it has
    //   `additionalProperties`.
    // - `additionalProperties: true` is preserved rather than rewritten.
    let schema = parse_tool_input_schema(&serde_json::json!({
        "type": "object",
        "properties": {
            "metadata": {
                "additionalProperties": true
            }
        }
    }))
    .expect("parse schema");

    assert_eq!(
        schema,
        JsonSchema::object(
            BTreeMap::from([(
                "metadata".to_string(),
                JsonSchema::object(BTreeMap::new(), /*required*/ None, Some(true.into())),
            )]),
            /*required*/ None,
            /*additional_properties*/ None
        )
    );
}

#[test]
fn parse_tool_input_schema_infers_object_shape_from_schema_additional_properties_only() {
    // Example schema shape:
    // {
    //   "additionalProperties": {
    //     "type": "string"
    //   }
    // }
    //
    // Expected normalization behavior:
    // - A schema-valued `additionalProperties` also implies an object schema
    //   when `type` is omitted.
    // - The nested schema is preserved as the object's
    //   `additionalProperties` definition.
    let schema = parse_tool_input_schema(&serde_json::json!({
        "additionalProperties": {
            "type": "string"
        }
    }))
    .expect("parse schema");

    assert_eq!(
        schema,
        JsonSchema::object(
            BTreeMap::new(),
            /*required*/ None,
            Some(JsonSchema::string(/*description*/ None).into())
        )
    );
}

#[test]
fn parse_tool_input_schema_rewrites_const_to_single_value_enum() {
    // Example schema shape:
    // {
    //   "const": "tagged"
    // }
    //
    // Expected normalization behavior:
    // - `const` is rewritten through the sanitizer's `map.remove("const")`
    //   path into an equivalent single-value string enum schema.
    let schema = parse_tool_input_schema(&serde_json::json!({
        "const": "tagged"
    }))
    .expect("parse schema");

    assert_eq!(
        schema,
        JsonSchema::string_enum(vec![serde_json::json!("tagged")], /*description*/ None)
    );
}

#[test]
fn parse_tool_input_schema_rejects_singleton_null_type() {
    let err = parse_tool_input_schema(&serde_json::json!({
        "type": "null"
    }))
    .expect_err("singleton null should be rejected");

    assert!(
        err.to_string()
            .contains("tool input schema must not be a singleton null type"),
        "unexpected error: {err}"
    );
}

#[test]
fn parse_tool_input_schema_fills_default_properties_for_nullable_object_union() {
    // Example schema shape:
    // {
    //   "type": ["object", "null"]
    // }
    //
    // Expected normalization behavior:
    // - The full union is preserved.
    // - Object members of the union still receive default `properties`.
    let schema = parse_tool_input_schema(&serde_json::json!({
        "type": ["object", "null"]
    }))
    .expect("parse schema");

    assert_eq!(
        schema,
        JsonSchema {
            schema_type: Some(JsonSchemaType::Multiple(vec![
                JsonSchemaPrimitiveType::Object,
                JsonSchemaPrimitiveType::Null,
            ])),
            properties: Some(BTreeMap::new()),
            ..Default::default()
        }
    );
}

#[test]
fn parse_tool_input_schema_fills_default_items_for_nullable_array_union() {
    // Example schema shape:
    // {
    //   "type": ["array", "null"]
    // }
    //
    // Expected normalization behavior:
    // - The full union is preserved.
    // - Array members of the union still receive default `items`.
    let schema = parse_tool_input_schema(&serde_json::json!({
        "type": ["array", "null"]
    }))
    .expect("parse schema");

    assert_eq!(
        schema,
        JsonSchema {
            schema_type: Some(JsonSchemaType::Multiple(vec![
                JsonSchemaPrimitiveType::Array,
                JsonSchemaPrimitiveType::Null,
            ])),
            items: Some(Box::new(JsonSchema::string(/*description*/ None))),
            ..Default::default()
        }
    );
}

// Schemas that should be preserved for Responses API compatibility rather than
// being rewritten into a different shape.

#[test]
fn parse_tool_input_schema_preserves_nested_nullable_any_of_shape() {
    // Example schema shape:
    // {
    //   "type": "object",
    //   "properties": {
    //     "open": {
    //       "anyOf": [
    //         {
    //           "type": "array",
    //           "items": {
    //             "type": "object",
    //             "properties": {
    //               "ref_id": { "type": "string" },
    //               "lineno": { "anyOf": [{ "type": "integer" }, { "type": "null" }] }
    //             },
    //             "required": ["ref_id"],
    //             "additionalProperties": false
    //           }
    //         },
    //         { "type": "null" }
    //       ]
    //     }
    //   }
    // }
    //
    // Expected normalization behavior:
    // - Nested nullable `anyOf` shapes are preserved all the way down.
    let schema = parse_tool_input_schema(&serde_json::json!({
        "type": "object",
        "properties": {
            "open": {
                "anyOf": [
                    {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "ref_id": {"type": "string"},
                                "lineno": {"anyOf": [{"type": "integer"}, {"type": "null"}]}
                            },
                            "required": ["ref_id"],
                            "additionalProperties": false
                        }
                    },
                    {"type": "null"}
                ]
            }
        }
    }))
    .expect("parse schema");

    assert_eq!(
        schema,
        JsonSchema::object(
            BTreeMap::from([(
                "open".to_string(),
                JsonSchema::any_of(
                    vec![
                        JsonSchema::array(
                            JsonSchema::object(
                                BTreeMap::from([
                                    (
                                        "lineno".to_string(),
                                        JsonSchema::any_of(
                                            vec![
                                                JsonSchema::integer(/*description*/ None),
                                                JsonSchema::null(/*description*/ None),
                                            ],
                                            /*description*/ None,
                                        ),
                                    ),
                                    (
                                        "ref_id".to_string(),
                                        JsonSchema::string(/*description*/ None),
                                    ),
                                ]),
                                Some(vec!["ref_id".to_string()]),
                                Some(false.into()),
                            ),
                            /*description*/ None,
                        ),
                        JsonSchema::null(/*description*/ None),
                    ],
                    /*description*/ None,
                ),
            ),]),
            /*required*/ None,
            /*additional_properties*/ None
        )
    );
}

#[test]
fn parse_tool_input_schema_preserves_nested_nullable_type_union() {
    // Example schema shape:
    // {
    //   "type": "object",
    //   "properties": {
    //     "nickname": {
    //       "type": ["string", "null"],
    //       "description": "Optional nickname"
    //     }
    //   },
    //   "required": ["nickname"],
    //   "additionalProperties": false
    // }
    //
    // Expected normalization behavior:
    // - The nested property keeps the explicit `["string", "null"]` union.
    // - The object-level `required` and `additionalProperties: false` stay intact.
    let schema = parse_tool_input_schema(&serde_json::json!({
        "type": "object",
        "properties": {
            "nickname": {
                "type": ["string", "null"],
                "description": "Optional nickname"
            }
        },
        "required": ["nickname"],
        "additionalProperties": false
    }))
    .expect("parse schema");

    assert_eq!(
        schema,
        JsonSchema::object(
            BTreeMap::from([(
                "nickname".to_string(),
                JsonSchema {
                    schema_type: Some(JsonSchemaType::Multiple(vec![
                        JsonSchemaPrimitiveType::String,
                        JsonSchemaPrimitiveType::Null,
                    ])),
                    description: Some("Optional nickname".to_string()),
                    ..Default::default()
                },
            )]),
            Some(vec!["nickname".to_string()]),
            Some(false.into()),
        )
    );
}

#[test]
fn parse_tool_input_schema_preserves_nested_any_of_property() {
    // Example schema shape:
    // {
    //   "type": "object",
    //   "properties": {
    //     "query": {
    //       "anyOf": [
    //         { "type": "string" },
    //         { "type": "number" }
    //       ]
    //     }
    //   }
    // }
    //
    // Expected normalization behavior:
    // - The nested `anyOf` is preserved rather than flattened into a single
    //   fallback type.
    let schema = parse_tool_input_schema(&serde_json::json!({
        "type": "object",
        "properties": {
            "query": {
                "anyOf": [
                    { "type": "string" },
                    { "type": "number" }
                ]
            }
        }
    }))
    .expect("parse schema");

    assert_eq!(
        schema,
        JsonSchema::object(
            BTreeMap::from([(
                "query".to_string(),
                JsonSchema::any_of(
                    vec![
                        JsonSchema::string(/*description*/ None),
                        JsonSchema::number(/*description*/ None),
                    ],
                    /*description*/ None,
                ),
            )]),
            /*required*/ None,
            /*additional_properties*/ None
        )
    );
}

#[test]
fn parse_tool_input_schema_preserves_type_unions_without_rewriting_to_any_of() {
    // Example schema shape:
    // {
    //   "type": ["string", "null"],
    //   "description": "optional string"
    // }
    //
    // Expected normalization behavior:
    // - Explicit type unions are preserved as unions rather than rewritten to
    //   `anyOf`.
    let schema = parse_tool_input_schema(&serde_json::json!({
        "type": ["string", "null"],
        "description": "optional string"
    }))
    .expect("parse schema");

    assert_eq!(
        schema,
        JsonSchema {
            schema_type: Some(JsonSchemaType::Multiple(vec![
                JsonSchemaPrimitiveType::String,
                JsonSchemaPrimitiveType::Null,
            ])),
            description: Some("optional string".to_string()),
            ..Default::default()
        }
    );
}

#[test]
fn parse_tool_input_schema_preserves_explicit_enum_type_union() {
    // Example schema shape:
    // {
    //   "type": ["string", "null"],
    //   "enum": ["short", "medium", "long"],
    //   "description": "optional response length"
    // }
    //
    // Expected normalization behavior:
    // - The explicit string/null union is preserved alongside the enum values.
    let schema = super::parse_tool_input_schema(&serde_json::json!({
        "type": ["string", "null"],
        "enum": ["short", "medium", "long"],
        "description": "optional response length"
    }))
    .expect("parse schema");

    assert_eq!(
        schema,
        JsonSchema {
            schema_type: Some(JsonSchemaType::Multiple(vec![
                JsonSchemaPrimitiveType::String,
                JsonSchemaPrimitiveType::Null,
            ])),
            description: Some("optional response length".to_string()),
            enum_values: Some(vec![
                serde_json::json!("short"),
                serde_json::json!("medium"),
                serde_json::json!("long"),
            ]),
            ..Default::default()
        }
    );
}

#[test]
fn parse_tool_input_schema_preserves_string_enum_constraints() {
    // Example schema shape:
    // {
    //   "type": "object",
    //   "properties": {
    //     "response_length": { "type": "enum", "enum": ["short", "medium", "long"] },
    //     "kind": { "type": "const", "const": "tagged" },
    //     "scope": { "type": "enum", "enum": ["one", "two"] }
    //   }
    // }
    //
    // Expected normalization behavior:
    // - Legacy `type: "enum"` and `type: "const"` inputs are normalized into
    //   the current string-enum representation.
    let schema = super::parse_tool_input_schema(&serde_json::json!({
        "type": "object",
        "properties": {
            "response_length": {
                "type": "enum",
                "enum": ["short", "medium", "long"]
            },
            "kind": {
                "type": "const",
                "const": "tagged"
            },
            "scope": {
                "type": "enum",
                "enum": ["one", "two"]
            }
        }
    }))
    .expect("parse schema");

    assert_eq!(
        schema,
        JsonSchema::object(
            BTreeMap::from([
                (
                    "kind".to_string(),
                    JsonSchema::string_enum(
                        vec![serde_json::json!("tagged")],
                        /*description*/ None,
                    ),
                ),
                (
                    "response_length".to_string(),
                    JsonSchema::string_enum(
                        vec![
                            serde_json::json!("short"),
                            serde_json::json!("medium"),
                            serde_json::json!("long"),
                        ],
                        /*description*/ None,
                    ),
                ),
                (
                    "scope".to_string(),
                    JsonSchema::string_enum(
                        vec![serde_json::json!("one"), serde_json::json!("two")],
                        /*description*/ None,
                    ),
                ),
            ]),
            /*required*/ None,
            /*additional_properties*/ None
        )
    );
}
