import pytest
from pydantic import JsonValue, TypeAdapter, ValidationError
from pydantic_ai.output import ToolOutput

import dify_agent.layers.output as output_exports
from dify_agent.layers.output import DIFY_OUTPUT_LAYER_TYPE_ID, DifyOutputLayerConfig
from dify_agent.layers.output.output_layer import DifyOutputLayer


def _json_schema() -> dict[str, JsonValue]:
    return {
        "type": "object",
        "properties": {
            "title": {"type": "string"},
            "severity": {"type": "string", "enum": ["low", "medium", "high"]},
            "actions": {"type": "array", "items": {"type": "string"}},
        },
        "required": ["title", "severity", "actions"],
        "additionalProperties": False,
    }


def _recursive_json_schema() -> dict[str, JsonValue]:
    return {
        "type": "object",
        "properties": {"node": {"$ref": "#/$defs/node"}},
        "$defs": {
            "node": {
                "type": "object",
                "properties": {"child": {"$ref": "#/$defs/node"}},
                "additionalProperties": False,
            }
        },
        "additionalProperties": False,
    }


def _remote_ref_schema() -> dict[str, JsonValue]:
    return {
        "type": "object",
        "properties": {
            "title": {"$ref": "https://example.com/schema.json"},
        },
    }


def _literal_dollar_ref_value_schema() -> dict[str, JsonValue]:
    return {
        "type": "object",
        "properties": {
            "payload": {
                "const": {
                    "$ref": "https://example.com/literal",
                    "kind": "literal",
                },
            },
            "metadata": {
                "type": "object",
                "examples": [
                    {
                        "$ref": "https://example.com/example",
                        "note": "example value",
                    }
                ],
            },
        },
        "required": ["payload", "metadata"],
        "additionalProperties": False,
    }


def _object_local_definitions_ref_schema() -> dict[str, JsonValue]:
    return {
        "type": "object",
        "properties": {
            "items": {"$ref": "#/definitions/itemArray"},
        },
        "required": ["items"],
        "definitions": {
            "itemArray": {
                "type": "array",
                "items": {"type": "string"},
            },
        },
    }


def _object_local_defs_ref_schema() -> dict[str, JsonValue]:
    return {
        "type": "object",
        "properties": {
            "items": {"$ref": "#/$defs/itemArray"},
        },
        "required": ["items"],
        "$defs": {
            "itemArray": {
                "type": "array",
                "items": {"type": "string"},
            },
        },
    }


def _invalid_json_schema() -> dict[str, JsonValue]:
    return {
        "type": "object",
        "properties": {
            "title": {"type": "wat"},
        },
    }


def _validated_output_type(output_spec: object) -> object:
    assert isinstance(output_spec, ToolOutput)
    return output_spec.output


def test_output_package_exports_client_safe_config_symbols_only() -> None:
    assert output_exports.__all__ == ["DIFY_OUTPUT_LAYER_TYPE_ID", "DifyOutputLayerConfig"]
    assert output_exports.DIFY_OUTPUT_LAYER_TYPE_ID == "dify.output"
    assert not hasattr(output_exports, "DifyOutputLayer")


def test_output_layer_config_accepts_valid_object_schema_without_public_tool_name() -> None:
    config = DifyOutputLayerConfig(json_schema=_json_schema())

    assert DIFY_OUTPUT_LAYER_TYPE_ID == "dify.output"
    assert hasattr(config, "name") is False
    assert config.model_dump(mode="json") == {
        "json_schema": _json_schema(),
        "description": None,
        "strict": None,
    }
    assert config.description is None
    assert config.strict is None


def test_output_layer_config_rejects_non_object_top_level_json_schema() -> None:
    with pytest.raises(ValidationError, match="Schema must declare an object output"):
        _ = DifyOutputLayerConfig(json_schema={"type": "array", "items": {"type": "string"}})


@pytest.mark.parametrize(
    ("payload", "message"),
    [
        ({"json_schema": _json_schema(), "name": "bad name"}, "Extra inputs are not permitted"),
        ({"json_schema": _json_schema(), "unknown": True}, "Extra inputs are not permitted"),
    ],
)
def test_output_layer_config_rejects_invalid_input(payload: dict[str, object], message: str) -> None:
    with pytest.raises(ValidationError, match=message):
        _ = DifyOutputLayerConfig.model_validate(payload)


def test_output_layer_builds_validated_output_contract_for_object_schema() -> None:
    config = DifyOutputLayerConfig(
        json_schema=_json_schema(),
        description="Structured incident summary.",
        strict=True,
    )

    layer = DifyOutputLayer.from_config(config)
    output_contract = layer.build_output_contract()
    output_type = output_contract.output_type
    output_schema = TypeAdapter(output_type.output).json_schema() if isinstance(output_type, ToolOutput) else {}
    valid_output = {"title": "Database outage", "severity": "high", "actions": ["page on-call"]}
    output_adapter = TypeAdapter(_validated_output_type(output_contract.output_type))

    assert isinstance(output_type, ToolOutput)
    assert output_type.name == "final_output"
    assert output_type.description is None
    assert output_type.strict is True
    assert output_schema["type"] == "object"
    assert output_schema["title"] == "final_output"
    assert output_schema["description"] == "Structured incident summary."
    assert output_adapter.validate_python(valid_output) == valid_output


@pytest.mark.parametrize(
    ("invalid_output", "message"),
    [
        (
            {"title": "Database outage", "severity": "high", "actions": "page on-call"},
            "Output does not match JSON Schema",
        ),
        ({"title": "Database outage", "actions": []}, "Output does not match JSON Schema"),
        ({"title": "Database outage", "severity": "urgent", "actions": []}, "Output does not match JSON Schema"),
        (
            {"title": "Database outage", "severity": "high", "actions": [], "extra": True},
            "Output does not match JSON Schema",
        ),
    ],
)
def test_output_layer_object_contract_retries_invalid_model_output(invalid_output: JsonValue, message: str) -> None:
    output_contract = DifyOutputLayer.from_config(
        DifyOutputLayerConfig(json_schema=_json_schema())
    ).build_output_contract()
    output_adapter = TypeAdapter(_validated_output_type(output_contract.output_type))

    with pytest.raises(ValidationError, match=message):
        _ = output_adapter.validate_python(invalid_output)


def test_output_layer_rejects_non_defs_local_ref_in_direct_object_schema() -> None:
    layer = DifyOutputLayer.from_config(DifyOutputLayerConfig(json_schema=_object_local_definitions_ref_schema()))

    with pytest.raises(ValueError, match=r"Only local refs under '#/\$defs/' are supported"):
        _ = layer.build_output_contract()


def test_output_layer_keeps_local_defs_ref_working_in_direct_object_schema() -> None:
    output_contract = DifyOutputLayer.from_config(
        DifyOutputLayerConfig(json_schema=_object_local_defs_ref_schema())
    ).build_output_contract()
    output_adapter = TypeAdapter(_validated_output_type(output_contract.output_type))
    output_schema = output_adapter.json_schema()

    assert isinstance(output_contract.output_type, ToolOutput)
    assert output_schema == {
        "type": "object",
        "properties": {
            "items": {
                "type": "array",
                "items": {"type": "string"},
            },
        },
        "required": ["items"],
        "title": "final_output",
    }
    assert output_adapter.validate_python({"items": ["a", "b"]}) == {"items": ["a", "b"]}


def test_output_layer_rejects_recursive_json_schema_during_contract_build() -> None:
    layer = DifyOutputLayer.from_config(DifyOutputLayerConfig(json_schema=_recursive_json_schema()))

    with pytest.raises(ValueError):
        _ = layer.build_output_contract()


def test_output_layer_rejects_invalid_json_schema_during_contract_build() -> None:
    layer = DifyOutputLayer.from_config(DifyOutputLayerConfig(json_schema=_invalid_json_schema()))

    with pytest.raises(ValueError):
        _ = layer.build_output_contract()


def test_output_layer_rejects_remote_ref_during_contract_build() -> None:
    layer = DifyOutputLayer.from_config(DifyOutputLayerConfig(json_schema=_remote_ref_schema()))

    with pytest.raises(ValueError, match=r"Remote \$ref values are not supported"):
        _ = layer.build_output_contract()


def test_output_layer_allows_literal_dollar_ref_values_under_const_and_examples() -> None:
    layer = DifyOutputLayer.from_config(DifyOutputLayerConfig(json_schema=_literal_dollar_ref_value_schema()))

    output_contract = layer.build_output_contract()
    output_adapter = TypeAdapter(_validated_output_type(output_contract.output_type))

    assert output_adapter.validate_python(
        {
            "payload": {
                "$ref": "https://example.com/literal",
                "kind": "literal",
            },
            "metadata": {"note": "runtime value"},
        }
    ) == {
        "payload": {
            "$ref": "https://example.com/literal",
            "kind": "literal",
        },
        "metadata": {"note": "runtime value"},
    }
