"""Tests for advanced features of the OpenAPI utilities."""

from typing import Any

import pytest

from fastmcp.utilities.openapi import parse_openapi_to_http_routes


@pytest.fixture
def complex_schema() -> dict[str, Any]:
    """Fixture that returns a complex OpenAPI schema with nested references."""
    return {
        "openapi": "3.1.0",
        "info": {"title": "Complex API", "version": "1.0.0"},
        "paths": {
            "/users": {
                "get": {
                    "summary": "List all users",
                    "operationId": "listUsers",
                    "parameters": [
                        {"$ref": "#/components/parameters/PageLimit"},
                        {"$ref": "#/components/parameters/PageOffset"},
                    ],
                    "responses": {"200": {"description": "A list of users"}},
                }
            },
            "/users/{userId}": {
                "get": {
                    "summary": "Get user by ID",
                    "operationId": "getUser",
                    "parameters": [
                        {"$ref": "#/components/parameters/UserId"},
                        {"$ref": "#/components/parameters/IncludeInactive"},
                    ],
                    "responses": {"200": {"description": "User details"}},
                }
            },
            "/users/{userId}/orders": {
                "post": {
                    "summary": "Create order for user",
                    "operationId": "createOrder",
                    "parameters": [{"$ref": "#/components/parameters/UserId"}],
                    "requestBody": {"$ref": "#/components/requestBodies/OrderRequest"},
                    "responses": {"201": {"description": "Order created"}},
                }
            },
        },
        "components": {
            "parameters": {
                "UserId": {
                    "name": "userId",
                    "in": "path",
                    "required": True,
                    "schema": {"type": "string", "format": "uuid"},
                },
                "PageLimit": {
                    "name": "limit",
                    "in": "query",
                    "schema": {"type": "integer", "default": 20, "maximum": 100},
                },
                "PageOffset": {
                    "name": "offset",
                    "in": "query",
                    "schema": {"type": "integer", "default": 0},
                },
                "IncludeInactive": {
                    "name": "include_inactive",
                    "in": "query",
                    "schema": {"type": "boolean", "default": False},
                },
            },
            "schemas": {
                "User": {
                    "type": "object",
                    "properties": {
                        "id": {"type": "string", "format": "uuid"},
                        "name": {"type": "string"},
                        "email": {"type": "string", "format": "email"},
                        "role": {"$ref": "#/components/schemas/Role"},
                        "address": {"$ref": "#/components/schemas/Address"},
                    },
                },
                "Role": {
                    "type": "string",
                    "enum": ["admin", "user", "guest"],
                },
                "Address": {
                    "type": "object",
                    "properties": {
                        "street": {"type": "string"},
                        "city": {"type": "string"},
                        "zip": {"type": "string"},
                        "country": {"type": "string"},
                    },
                },
                "Order": {
                    "type": "object",
                    "properties": {
                        "id": {"type": "string", "format": "uuid"},
                        "items": {
                            "type": "array",
                            "items": {"$ref": "#/components/schemas/OrderItem"},
                        },
                        "total": {"type": "number"},
                        "status": {"$ref": "#/components/schemas/OrderStatus"},
                    },
                },
                "OrderItem": {
                    "type": "object",
                    "properties": {
                        "product_id": {"type": "string", "format": "uuid"},
                        "quantity": {"type": "integer"},
                        "price": {"type": "number"},
                    },
                },
                "OrderStatus": {
                    "type": "string",
                    "enum": [
                        "pending",
                        "processing",
                        "shipped",
                        "delivered",
                        "cancelled",
                    ],
                },
            },
            "requestBodies": {
                "OrderRequest": {
                    "description": "Order to create",
                    "required": True,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "required": ["items"],
                                "properties": {
                                    "items": {
                                        "type": "array",
                                        "items": {
                                            "$ref": "#/components/schemas/OrderItem"
                                        },
                                    },
                                    "notes": {"type": "string"},
                                },
                            }
                        }
                    },
                }
            },
        },
    }


@pytest.fixture
def parsed_complex_routes(complex_schema):
    """Return parsed routes from the complex schema."""
    return parse_openapi_to_http_routes(complex_schema)


@pytest.fixture
def complex_route_map(parsed_complex_routes):
    """Return a dictionary of routes by operation ID."""
    return {
        r.operation_id: r for r in parsed_complex_routes if r.operation_id is not None
    }


@pytest.fixture
def schema_with_invalid_reference() -> dict[str, Any]:
    """Fixture that returns a schema with an invalid reference."""
    return {
        "openapi": "3.1.0",
        "info": {"title": "Invalid Reference API", "version": "1.0.0"},
        "paths": {
            "/broken-ref": {
                "get": {
                    "summary": "Endpoint with broken reference",
                    "operationId": "brokenRef",
                    "parameters": [
                        {"$ref": "#/components/parameters/NonExistentParam"}
                    ],
                    "responses": {"200": {"description": "Something"}},
                }
            }
        },
        "components": {
            "parameters": {}  # Empty parameters object to ensure the reference is broken
        },
    }


@pytest.fixture
def schema_with_content_params() -> dict[str, Any]:
    """Fixture that returns a schema with content-based parameters (complex parameters)."""
    return {
        "openapi": "3.1.0",
        "info": {"title": "Content Params API", "version": "1.0.0"},
        "paths": {
            "/complex-params": {
                "post": {
                    "summary": "Endpoint with complex parameter",
                    "operationId": "complexParams",
                    "parameters": [
                        {
                            "name": "filter",
                            "in": "query",
                            "content": {
                                "application/json": {
                                    "schema": {
                                        "type": "object",
                                        "properties": {
                                            "field": {"type": "string"},
                                            "operator": {
                                                "type": "string",
                                                "enum": ["eq", "gt", "lt"],
                                            },
                                            "value": {"type": "string"},
                                        },
                                    }
                                }
                            },
                        }
                    ],
                    "responses": {"200": {"description": "Results"}},
                }
            },
        },
    }


@pytest.fixture
def parsed_content_param_routes(schema_with_content_params):
    """Return parsed routes from the schema with content parameters."""
    return parse_openapi_to_http_routes(schema_with_content_params)


@pytest.fixture
def schema_all_http_methods() -> dict[str, Any]:
    """Fixture that returns a schema with all HTTP methods."""
    return {
        "openapi": "3.1.0",
        "info": {"title": "All Methods API", "version": "1.0.0"},
        "paths": {
            "/resource": {
                "get": {
                    "operationId": "getResource",
                    "responses": {"200": {"description": "Success"}},
                },
                "post": {
                    "operationId": "createResource",
                    "responses": {"201": {"description": "Created"}},
                },
                "put": {
                    "operationId": "updateResource",
                    "responses": {"200": {"description": "Updated"}},
                },
                "delete": {
                    "operationId": "deleteResource",
                    "responses": {"204": {"description": "Deleted"}},
                },
                "patch": {
                    "operationId": "patchResource",
                    "responses": {"200": {"description": "Patched"}},
                },
                "head": {
                    "operationId": "headResource",
                    "responses": {"200": {"description": "Headers only"}},
                },
                "options": {
                    "operationId": "optionsResource",
                    "responses": {"200": {"description": "Options"}},
                },
                "trace": {
                    "operationId": "traceResource",
                    "responses": {"200": {"description": "Trace"}},
                },
            },
        },
    }


@pytest.fixture
def parsed_http_methods_routes(schema_all_http_methods):
    """Return parsed routes from the schema with all HTTP methods."""
    return parse_openapi_to_http_routes(schema_all_http_methods)


# --- Tests for complex schemas with references --- #


def test_complex_schema_route_count(parsed_complex_routes):
    """Test that parsing a schema with references successfully extracts all routes."""
    assert len(parsed_complex_routes) == 3


def test_complex_schema_ref_rewriting(parsed_complex_routes):
    """Test that all #/components references have been rewritten."""

    def no_components(value):
        if isinstance(value, dict):
            for k, v in value.items():
                if k == "$ref":
                    assert not v.startswith("#/components/"), (
                        f"reference '{v}' was not rewritten"
                    )
                else:
                    no_components(v)
        elif isinstance(value, list):
            for v in value:
                no_components(v)

    for route in parsed_complex_routes:
        no_components(route.schema_definitions)
        for param in route.parameters:
            no_components(param.schema_)


def test_complex_schema_list_users_query_param_limit(complex_route_map):
    """Test that a reference to a limit query parameter is correctly resolved."""
    list_users = complex_route_map["listUsers"]

    limit_param = next((p for p in list_users.parameters if p.name == "limit"), None)
    assert limit_param is not None
    assert limit_param.location == "query"
    assert limit_param.schema_.get("default") == 20


def test_complex_schema_list_users_query_param_limit_maximum(complex_route_map):
    """Test that a limit parameter's maximum value is correctly resolved."""
    list_users = complex_route_map["listUsers"]

    limit_param = next((p for p in list_users.parameters if p.name == "limit"), None)
    assert limit_param is not None
    assert limit_param.schema_.get("maximum") == 100


def test_complex_schema_get_user_path_param_existence(complex_route_map):
    """Test that a reference to a path parameter exists."""
    get_user = complex_route_map["getUser"]

    user_id_param = next((p for p in get_user.parameters if p.name == "userId"), None)
    assert user_id_param is not None
    assert user_id_param.location == "path"


def test_complex_schema_get_user_path_param_required(complex_route_map):
    """Test that a path parameter is correctly marked as required."""
    get_user = complex_route_map["getUser"]

    user_id_param = next((p for p in get_user.parameters if p.name == "userId"), None)
    assert user_id_param is not None
    assert user_id_param.required is True


def test_complex_schema_get_user_path_param_format(complex_route_map):
    """Test that a path parameter format is correctly resolved."""
    get_user = complex_route_map["getUser"]

    user_id_param = next((p for p in get_user.parameters if p.name == "userId"), None)
    assert user_id_param is not None
    assert user_id_param.schema_.get("format") == "uuid"


def test_complex_schema_create_order_request_body_presence(complex_route_map):
    """Test that a reference to a request body is resolved correctly."""
    create_order = complex_route_map["createOrder"]

    assert create_order.request_body is not None
    assert create_order.request_body.required is True


def test_complex_schema_create_order_request_body_content_type(complex_route_map):
    """Test that request body content type is correctly resolved."""
    create_order = complex_route_map["createOrder"]

    assert create_order.request_body is not None
    assert "application/json" in create_order.request_body.content_schema


def test_complex_schema_create_order_request_body_properties(complex_route_map):
    """Test that request body properties are correctly resolved."""
    create_order = complex_route_map["createOrder"]

    assert create_order.request_body is not None
    json_schema = create_order.request_body.content_schema["application/json"]
    assert "items" in json_schema.get("properties", {})


def test_complex_schema_create_order_request_body_required_fields(complex_route_map):
    """Test that request body required fields are correctly resolved."""
    create_order = complex_route_map["createOrder"]

    assert create_order.request_body is not None
    json_schema = create_order.request_body.content_schema["application/json"]
    assert json_schema.get("required") == ["items"]


# --- Tests for schema reference resolution errors --- #


def test_parser_handles_broken_references(schema_with_invalid_reference):
    """Test that parser handles broken references gracefully."""
    # We're just checking that the function doesn't throw an exception
    routes = parse_openapi_to_http_routes(schema_with_invalid_reference)

    # Should still return routes list (may be empty)
    assert isinstance(routes, list)

    # Verify that the route with broken parameter reference is still included
    # though it may not have the parameter properly
    broken_route = next(
        (r for r in routes if r.path == "/broken-ref" and r.method == "GET"), None
    )

    # The route should still be present
    assert broken_route is not None
    assert broken_route.operation_id == "brokenRef"


# --- Tests for content-based parameters --- #


def test_content_param_parameter_name(parsed_content_param_routes):
    """Test that parser correctly extracts name for content-based parameters."""
    complex_params = parsed_content_param_routes[0]

    assert len(complex_params.parameters) == 1
    param = complex_params.parameters[0]
    assert param.name == "filter"


def test_content_param_parameter_location(parsed_content_param_routes):
    """Test that parser correctly extracts location for content-based parameters."""
    complex_params = parsed_content_param_routes[0]

    assert len(complex_params.parameters) == 1
    param = complex_params.parameters[0]
    assert param.location == "query"


def test_content_param_schema_properties_presence(parsed_content_param_routes):
    """Test that parser extracts schema properties from content-based parameter."""
    complex_params = parsed_content_param_routes[0]

    param = complex_params.parameters[0]
    properties = param.schema_.get("properties", {})

    assert "field" in properties
    assert "operator" in properties
    assert "value" in properties


def test_content_param_schema_enum_presence(parsed_content_param_routes):
    """Test that parser extracts enum values from content-based parameter."""
    complex_params = parsed_content_param_routes[0]

    param = complex_params.parameters[0]
    properties = param.schema_.get("properties", {})

    assert "enum" in properties.get("operator", {})


# --- Tests for HTTP methods --- #


def test_http_get_method_presence(parsed_http_methods_routes):
    """Test that GET method is correctly extracted."""
    get_route = next((r for r in parsed_http_methods_routes if r.method == "GET"), None)

    assert get_route is not None
    assert get_route.operation_id == "getResource"


def test_http_get_method_path(parsed_http_methods_routes):
    """Test that GET method path is correctly extracted."""
    get_route = next((r for r in parsed_http_methods_routes if r.method == "GET"), None)

    assert get_route is not None
    assert get_route.path == "/resource"


def test_http_post_method_presence(parsed_http_methods_routes):
    """Test that POST method is correctly extracted."""
    post_route = next(
        (r for r in parsed_http_methods_routes if r.method == "POST"), None
    )

    assert post_route is not None
    assert post_route.operation_id == "createResource"


def test_http_post_method_path(parsed_http_methods_routes):
    """Test that POST method path is correctly extracted."""
    post_route = next(
        (r for r in parsed_http_methods_routes if r.method == "POST"), None
    )

    assert post_route is not None
    assert post_route.path == "/resource"


def test_http_put_method_presence(parsed_http_methods_routes):
    """Test that PUT method is correctly extracted."""
    put_route = next((r for r in parsed_http_methods_routes if r.method == "PUT"), None)

    assert put_route is not None
    assert put_route.operation_id == "updateResource"


def test_http_put_method_path(parsed_http_methods_routes):
    """Test that PUT method path is correctly extracted."""
    put_route = next((r for r in parsed_http_methods_routes if r.method == "PUT"), None)

    assert put_route is not None
    assert put_route.path == "/resource"


def test_http_delete_method_presence(parsed_http_methods_routes):
    """Test that DELETE method is correctly extracted."""
    delete_route = next(
        (r for r in parsed_http_methods_routes if r.method == "DELETE"), None
    )

    assert delete_route is not None
    assert delete_route.operation_id == "deleteResource"


def test_http_delete_method_path(parsed_http_methods_routes):
    """Test that DELETE method path is correctly extracted."""
    delete_route = next(
        (r for r in parsed_http_methods_routes if r.method == "DELETE"), None
    )

    assert delete_route is not None
    assert delete_route.path == "/resource"


def test_http_patch_method_presence(parsed_http_methods_routes):
    """Test that PATCH method is correctly extracted."""
    patch_route = next(
        (r for r in parsed_http_methods_routes if r.method == "PATCH"), None
    )

    assert patch_route is not None
    assert patch_route.operation_id == "patchResource"


def test_http_patch_method_path(parsed_http_methods_routes):
    """Test that PATCH method path is correctly extracted."""
    patch_route = next(
        (r for r in parsed_http_methods_routes if r.method == "PATCH"), None
    )

    assert patch_route is not None
    assert patch_route.path == "/resource"


def test_http_head_method_presence(parsed_http_methods_routes):
    """Test that HEAD method is correctly extracted."""
    head_route = next(
        (r for r in parsed_http_methods_routes if r.method == "HEAD"), None
    )

    assert head_route is not None
    assert head_route.operation_id == "headResource"


def test_http_head_method_path(parsed_http_methods_routes):
    """Test that HEAD method path is correctly extracted."""
    head_route = next(
        (r for r in parsed_http_methods_routes if r.method == "HEAD"), None
    )

    assert head_route is not None
    assert head_route.path == "/resource"


def test_http_options_method_presence(parsed_http_methods_routes):
    """Test that OPTIONS method is correctly extracted."""
    options_route = next(
        (r for r in parsed_http_methods_routes if r.method == "OPTIONS"), None
    )

    assert options_route is not None
    assert options_route.operation_id == "optionsResource"


def test_http_options_method_path(parsed_http_methods_routes):
    """Test that OPTIONS method path is correctly extracted."""
    options_route = next(
        (r for r in parsed_http_methods_routes if r.method == "OPTIONS"), None
    )

    assert options_route is not None
    assert options_route.path == "/resource"


def test_http_trace_method_presence(parsed_http_methods_routes):
    """Test that TRACE method is correctly extracted."""
    trace_route = next(
        (r for r in parsed_http_methods_routes if r.method == "TRACE"), None
    )

    assert trace_route is not None
    assert trace_route.operation_id == "traceResource"


def test_http_trace_method_path(parsed_http_methods_routes):
    """Test that TRACE method path is correctly extracted."""
    trace_route = next(
        (r for r in parsed_http_methods_routes if r.method == "TRACE"), None
    )

    assert trace_route is not None
    assert trace_route.path == "/resource"


@pytest.fixture
def schema_with_external_reference() -> dict[str, Any]:
    """Fixture that returns a schema with external schema references like in issue #926."""
    return {
        "openapi": "3.0.0",
        "info": {"title": "External Reference API", "version": "1.0.0"},
        "paths": {
            "/products": {
                "post": {
                    "summary": "Create a product",
                    "operationId": "createProduct",
                    "requestBody": {
                        "required": True,
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "obj": {
                                            "$ref": "http://cyaninc.com/json-schemas/market-v1/product-constraints"
                                        }
                                    },
                                }
                            }
                        },
                    },
                    "responses": {"201": {"description": "Product created"}},
                }
            }
        },
    }


# --- Tests for external schema reference handling --- #


def test_external_reference_raises_clear_error(schema_with_external_reference):
    """Test that external schema references raise a clear, helpful error message."""
    with pytest.raises(ValueError) as exc_info:
        parse_openapi_to_http_routes(schema_with_external_reference)

    error_message = str(exc_info.value)
    assert "External or non-local reference not supported" in error_message
    assert (
        "http://cyaninc.com/json-schemas/market-v1/product-constraints" in error_message
    )
    assert "FastMCP only supports local schema references" in error_message
