import json
from dataclasses import fields

from openai.types.shared import Reasoning
from pydantic import TypeAdapter
from pydantic_core import to_json

from agents.model_settings import MCPToolChoice, ModelSettings
from agents.retry import ModelRetryBackoffSettings, ModelRetrySettings, retry_policies


def verify_serialization(model_settings: ModelSettings) -> None:
    """Verify that ModelSettings can be serialized to a JSON string."""
    json_dict = model_settings.to_json_dict()
    json_string = json.dumps(json_dict)
    assert json_string is not None


def test_basic_serialization() -> None:
    """Tests whether ModelSettings can be serialized to a JSON string."""

    # First, lets create a ModelSettings instance
    model_settings = ModelSettings(
        temperature=0.5,
        top_p=0.9,
        max_tokens=100,
    )

    # Now, lets serialize the ModelSettings instance to a JSON string
    verify_serialization(model_settings)


def test_mcp_tool_choice_serialization() -> None:
    """Tests whether ModelSettings with MCPToolChoice can be serialized to a JSON string."""
    # First, lets create a ModelSettings instance
    model_settings = ModelSettings(
        temperature=0.5,
        tool_choice=MCPToolChoice(server_label="mcp", name="mcp_tool"),
    )
    # Now, lets serialize the ModelSettings instance to a JSON string
    verify_serialization(model_settings)


def test_all_fields_serialization() -> None:
    """Tests whether ModelSettings can be serialized to a JSON string."""

    # First, lets create a ModelSettings instance
    model_settings = ModelSettings(
        temperature=0.5,
        top_p=0.9,
        frequency_penalty=0.0,
        presence_penalty=0.0,
        tool_choice="auto",
        parallel_tool_calls=True,
        truncation="auto",
        max_tokens=100,
        reasoning=Reasoning(),
        metadata={"foo": "bar"},
        store=False,
        prompt_cache_retention="24h",
        include_usage=False,
        response_include=["reasoning.encrypted_content"],
        top_logprobs=1,
        verbosity="low",
        extra_query={"foo": "bar"},
        extra_body={"foo": "bar"},
        extra_headers={"foo": "bar"},
        extra_args={"custom_param": "value", "another_param": 42},
        retry=ModelRetrySettings(
            max_retries=2,
            backoff=ModelRetryBackoffSettings(
                initial_delay=0.1,
                max_delay=1.0,
                multiplier=2.0,
                jitter=False,
            ),
        ),
        context_management=[{"type": "compaction", "compact_threshold": 200000}],
        prompt_cache_options={"mode": "explicit", "ttl": "30m"},
    )

    # Verify that every single field is set to a non-None value
    for field in fields(model_settings):
        assert getattr(model_settings, field.name) is not None, (
            f"You must set the {field.name} field"
        )

    # Now, lets serialize the ModelSettings instance to a JSON string
    verify_serialization(model_settings)


def test_gpt_5_6_reasoning_and_prompt_cache_serialization() -> None:
    model_settings = ModelSettings(
        reasoning=Reasoning(mode="pro", effort="max", context="all_turns"),
        prompt_cache_options={"mode": "explicit", "ttl": "30m"},
    )

    serialized_reasoning = model_settings.to_json_dict()["reasoning"]
    assert serialized_reasoning["context"] == "all_turns"
    assert serialized_reasoning["effort"] == "max"
    assert serialized_reasoning["mode"] == "pro"
    assert model_settings.to_traceable_dict()["prompt_cache_options"] == {
        "mode": "explicit",
        "ttl": "30m",
    }


def test_prompt_cache_options_is_appended_to_public_field_order() -> None:
    field_names = [field.name for field in fields(ModelSettings)]

    assert field_names[-2:] == ["context_management", "prompt_cache_options"]


def test_extra_args_serialization() -> None:
    """Test that extra_args are properly serialized."""
    model_settings = ModelSettings(
        temperature=0.5,
        extra_args={"custom_param": "value", "another_param": 42, "nested": {"key": "value"}},
    )

    json_dict = model_settings.to_json_dict()
    assert json_dict["extra_args"] == {
        "custom_param": "value",
        "another_param": 42,
        "nested": {"key": "value"},
    }

    # Verify serialization works
    verify_serialization(model_settings)


def test_traceable_serialization_omits_request_extras() -> None:
    model_settings = ModelSettings(
        temperature=0.5,
        extra_headers={"Authorization": "Bearer provider-token"},
        extra_query={"api-key": "query-token"},
        extra_body={"secret": "body-token"},
        extra_args={"api_key": "arg-token"},
    )

    json_dict = model_settings.to_json_dict()
    assert json_dict["extra_headers"] == {"Authorization": "Bearer provider-token"}
    assert json_dict["extra_query"] == {"api-key": "query-token"}
    assert json_dict["extra_body"] == {"secret": "body-token"}
    assert json_dict["extra_args"] == {"api_key": "arg-token"}

    traceable = model_settings.to_traceable_dict()
    assert traceable["temperature"] == 0.5
    assert "extra_headers" not in traceable
    assert "extra_query" not in traceable
    assert "extra_body" not in traceable
    assert "extra_args" not in traceable


def test_extra_args_resolve() -> None:
    """Test that extra_args are properly merged in the resolve method."""
    base_settings = ModelSettings(
        temperature=0.5, extra_args={"param1": "base_value", "param2": "base_only"}
    )

    override_settings = ModelSettings(
        top_p=0.9, extra_args={"param1": "override_value", "param3": "override_only"}
    )

    resolved = base_settings.resolve(override_settings)

    # Check that regular fields are properly resolved
    assert resolved.temperature == 0.5  # from base
    assert resolved.top_p == 0.9  # from override

    # Check that extra_args are properly merged
    expected_extra_args = {
        "param1": "override_value",  # override wins
        "param2": "base_only",  # from base
        "param3": "override_only",  # from override
    }
    assert resolved.extra_args == expected_extra_args


def test_extra_args_resolve_with_none() -> None:
    """Test that resolve works properly when one side has None extra_args."""
    # Base with extra_args, override with None
    base_settings = ModelSettings(extra_args={"param1": "value1"})
    override_settings = ModelSettings(temperature=0.8)

    resolved = base_settings.resolve(override_settings)
    assert resolved.extra_args == {"param1": "value1"}
    assert resolved.temperature == 0.8

    # Base with None, override with extra_args
    base_settings = ModelSettings(temperature=0.5)
    override_settings = ModelSettings(extra_args={"param2": "value2"})

    resolved = base_settings.resolve(override_settings)
    assert resolved.extra_args == {"param2": "value2"}
    assert resolved.temperature == 0.5


def test_extra_args_resolve_both_none() -> None:
    """Test that resolve works when both sides have None extra_args."""
    base_settings = ModelSettings(temperature=0.5)
    override_settings = ModelSettings(top_p=0.9)

    resolved = base_settings.resolve(override_settings)
    assert resolved.extra_args is None
    assert resolved.temperature == 0.5
    assert resolved.top_p == 0.9


def test_pydantic_serialization() -> None:
    """Tests whether ModelSettings can be serialized with Pydantic."""

    # First, lets create a ModelSettings instance
    model_settings = ModelSettings(
        temperature=0.5,
        top_p=0.9,
        frequency_penalty=0.0,
        presence_penalty=0.0,
        tool_choice="auto",
        parallel_tool_calls=True,
        truncation="auto",
        max_tokens=100,
        reasoning=Reasoning(),
        metadata={"foo": "bar"},
        store=False,
        include_usage=False,
        top_logprobs=1,
        extra_query={"foo": "bar"},
        extra_body={"foo": "bar"},
        extra_headers={"foo": "bar"},
        extra_args={"custom_param": "value", "another_param": 42},
    )

    json = to_json(model_settings)
    deserialized = TypeAdapter(ModelSettings).validate_json(json)

    assert model_settings == deserialized


def test_retry_policy_is_excluded_from_json_dict() -> None:
    """Tests whether runtime-only retry policies are omitted from JSON serialization."""

    model_settings = ModelSettings(
        retry=ModelRetrySettings(
            max_retries=1,
            backoff=ModelRetryBackoffSettings(initial_delay=0.1),
            policy=retry_policies.http_status([429]),
        )
    )

    json_dict = model_settings.to_json_dict()
    assert json_dict["retry"] == {
        "max_retries": 1,
        "backoff": {
            "initial_delay": 0.1,
            "max_delay": None,
            "multiplier": None,
            "jitter": None,
        },
    }

    verify_serialization(model_settings)


def test_retry_resolve_deep_merges_backoff() -> None:
    """Tests whether retry settings are deep-merged in resolve()."""

    base_settings = ModelSettings(
        retry=ModelRetrySettings(
            max_retries=1,
            backoff=ModelRetryBackoffSettings(initial_delay=0.1, max_delay=1.0),
        )
    )
    override_settings = ModelSettings(
        retry=ModelRetrySettings(
            backoff=ModelRetryBackoffSettings(multiplier=3.0, jitter=False),
            policy=retry_policies.never(),
        )
    )

    resolved = base_settings.resolve(override_settings)

    assert resolved.retry is not None
    assert resolved.retry.max_retries == 1
    assert resolved.retry.policy is not None
    assert resolved.retry.backoff == ModelRetryBackoffSettings(
        initial_delay=0.1,
        max_delay=1.0,
        multiplier=3.0,
        jitter=False,
    )


def test_retry_policy_is_omitted_from_pydantic_round_trip() -> None:
    """Tests whether runtime-only retry policies are omitted from Pydantic serialization."""

    model_settings = ModelSettings(
        retry=ModelRetrySettings(
            max_retries=2,
            backoff=ModelRetryBackoffSettings(initial_delay=0.5),
            policy=retry_policies.http_status([429]),
        )
    )

    serialized = to_json(model_settings)
    deserialized = TypeAdapter(ModelSettings).validate_json(serialized)

    assert deserialized.retry is not None
    assert deserialized.retry.max_retries == 2
    assert deserialized.retry.backoff == ModelRetryBackoffSettings(initial_delay=0.5)
    assert deserialized.retry.policy is None


def test_retry_backoff_validate_python_accepts_nested_dict_input() -> None:
    """Tests whether nested retry/backoff dict input is coerced to dataclasses."""

    deserialized = TypeAdapter(ModelSettings).validate_python(
        {
            "retry": {
                "max_retries": 3,
                "backoff": {
                    "initial_delay": 0.25,
                    "max_delay": 2.0,
                    "multiplier": 3.0,
                    "jitter": False,
                },
            }
        }
    )

    assert deserialized.retry is not None
    assert deserialized.retry.max_retries == 3
    assert deserialized.retry.backoff == ModelRetryBackoffSettings(
        initial_delay=0.25,
        max_delay=2.0,
        multiplier=3.0,
        jitter=False,
    )


def test_retry_backoff_validate_python_preserves_falsey_values() -> None:
    """Tests whether falsey-only retry backoff input survives validation and serialization."""

    deserialized = TypeAdapter(ModelRetrySettings).validate_python(
        {
            "max_retries": 1,
            "backoff": {
                "jitter": False,
            },
        }
    )

    assert deserialized.backoff == ModelRetryBackoffSettings(jitter=False)
    assert deserialized.to_json_dict()["backoff"] == {
        "initial_delay": None,
        "max_delay": None,
        "multiplier": None,
        "jitter": False,
    }
