import pytest

from free_claude_code.application.reasoning import (
    client_reasoning_policy,
    resolve_reasoning_policy,
)
from free_claude_code.config.reasoning import ReasoningPreference
from free_claude_code.core.anthropic.models import MessagesRequest
from free_claude_code.core.reasoning import (
    ReasoningControl,
    ReasoningEffort,
    ReasoningPolicy,
)


def _request(**overrides) -> MessagesRequest:
    payload = {
        "model": "provider/model",
        "messages": [{"role": "user", "content": "hello"}],
    }
    payload.update(overrides)
    return MessagesRequest.model_validate(payload)


def test_client_without_reasoning_control_uses_provider_default() -> None:
    assert client_reasoning_policy(_request()) == ReasoningPolicy.provider_default()


def test_client_reasoning_preserves_effort_and_exact_budget() -> None:
    policy = client_reasoning_policy(
        _request(
            thinking={"type": "enabled", "budget_tokens": 4096},
            output_config={"effort": "xhigh"},
        )
    )

    assert policy == ReasoningPolicy.on(
        effort=ReasoningEffort.XHIGH,
        budget_tokens=4096,
    )


def test_named_effort_preserves_intent_without_exact_client_budget() -> None:
    policy = client_reasoning_policy(_request(output_config={"effort": "high"}))

    assert policy == ReasoningPolicy(
        control=ReasoningControl.DEFAULT,
        effort=ReasoningEffort.HIGH,
    )
    assert policy.budget_tokens is None
    assert policy.requests_reasoning is True


def test_invalid_budget_does_not_implicitly_enable_reasoning() -> None:
    policy = client_reasoning_policy(_request(thinking={"budget_tokens": 0}))

    assert policy == ReasoningPolicy.provider_default()


@pytest.mark.parametrize(
    "messages_request",
    [
        _request(thinking={"type": "disabled"}),
        _request(output_config={"effort": "none"}),
    ],
)
def test_client_disable_is_explicit(messages_request: MessagesRequest) -> None:
    policy = client_reasoning_policy(messages_request)

    assert policy.control is ReasoningControl.OFF
    assert policy.output_enabled is False
    assert policy.requests_reasoning is False


def test_disabled_thinking_preserves_independent_effort_intent() -> None:
    policy = client_reasoning_policy(
        _request(
            thinking={"type": "disabled"},
            output_config={"effort": "medium"},
        )
    )

    assert policy == ReasoningPolicy(
        control=ReasoningControl.OFF,
        effort=ReasoningEffort.MEDIUM,
    )
    assert policy.requests_reasoning is False


def test_fixed_route_effort_overrides_client_disable() -> None:
    policy = resolve_reasoning_policy(
        _request(thinking={"type": "disabled"}),
        ReasoningPreference.MAX,
    )

    assert policy == ReasoningPolicy.on(effort=ReasoningEffort.MAX)


def test_fixed_off_overrides_client_enable() -> None:
    policy = resolve_reasoning_policy(
        _request(thinking={"type": "enabled", "budget_tokens": 1024}),
        ReasoningPreference.OFF,
    )

    assert policy == ReasoningPolicy.off()


def test_client_preference_preserves_client_policy() -> None:
    request = _request(output_config={"effort": "low"})

    assert resolve_reasoning_policy(
        request, ReasoningPreference.CLIENT
    ) == client_reasoning_policy(request)


def test_unresolved_inherit_is_rejected() -> None:
    with pytest.raises(ValueError, match="must be resolved"):
        resolve_reasoning_policy(_request(), ReasoningPreference.INHERIT)


@pytest.mark.parametrize("budget", [0, -1, True])
def test_reasoning_budget_requires_a_positive_integer(budget: int) -> None:
    with pytest.raises(ValueError, match="positive integer"):
        ReasoningPolicy.on(budget_tokens=budget)


def test_reasoning_budget_requires_explicit_on_control() -> None:
    with pytest.raises(ValueError, match="control to be on"):
        ReasoningPolicy(budget_tokens=100)


@pytest.mark.parametrize(
    ("effort", "expected"),
    (
        (ReasoningEffort.MINIMAL, 512),
        (ReasoningEffort.LOW, 512),
        (ReasoningEffort.MEDIUM, 1_024),
        (ReasoningEffort.HIGH, 2_048),
        (ReasoningEffort.XHIGH, 4_096),
        (ReasoningEffort.MAX, 8_192),
    ),
)
def test_reasoning_effort_has_one_fcc_numeric_budget(
    effort: ReasoningEffort, expected: int
) -> None:
    assert ReasoningPolicy.on(effort=effort).numeric_budget_tokens == expected


def test_exact_reasoning_budget_takes_precedence_over_effort_mapping() -> None:
    policy = ReasoningPolicy.on(
        effort=ReasoningEffort.XHIGH,
        budget_tokens=777,
    )

    assert policy.numeric_budget_tokens == 777


@pytest.mark.parametrize(
    "policy",
    (
        ReasoningPolicy.provider_default(),
        ReasoningPolicy.off(),
        ReasoningPolicy.on(),
    ),
)
def test_reasoning_without_numeric_intensity_has_no_budget(
    policy: ReasoningPolicy,
) -> None:
    assert policy.numeric_budget_tokens is None
