"""Tests for model string passthrough in create_agent() and LLM timeout.

Spec source: vectl step ``model-simplify.simplify-create-agent``.
Spec source: talk-ux-fixes.llm-call-timeout
"""

import asyncio

import pytest
from pydantic import ValidationError
from returns.result import Success

from anima.core.schemas import AnimaGlobalConfig, PersonaSpec, GlobalDefaultsConfig
from anima.shell.agent import create_agent, AgentRunner
from anima.shell.model_config import (
    DEFAULT_MODEL_CONTEXT_LIMIT_TOKENS,
    evaluate_runtime_model_context_limit,
    resolve_model_context_limit,
)


def test_create_agent_passes_model_string_directly() -> None:
    """create_agent forwards effective model string to Agent(model=...)."""
    persona = PersonaSpec(
        id="test",
        description="You are a test agent.",
        prompt="You are a test agent.",
        model="openai:gpt-4",
    )

    result = asyncio.run(create_agent(persona))

    assert isinstance(result, Success)
    agent = result.unwrap()
    assert isinstance(agent.model, str)
    assert agent.model == "openai:gpt-4"


def test_create_agent_model_override_takes_precedence() -> None:
    """CLI model override still drives effective model resolution."""
    persona = PersonaSpec(
        id="test",
        description="You are a test agent.",
        prompt="You are a test agent.",
        model="openai:gpt-4",
    )

    result = asyncio.run(create_agent(persona, model_override="openai:gpt-5"))

    assert isinstance(result, Success)
    agent = result.unwrap()
    assert isinstance(agent.model, str)
    assert agent.model == "openai:gpt-5"


# Spec source: talk-ux-fixes.llm-call-timeout
# Tests for LLM per-turn timeout configuration


class TestGlobalDefaultsConfigLlmTimeout:
    """Tests for GlobalDefaultsConfig.llm_timeout field."""

    def test_llm_timeout_default_is_none(self) -> None:
        """llm_timeout defaults to None when not specified."""
        config = GlobalDefaultsConfig()
        assert config.llm_timeout is None

    def test_llm_timeout_accepts_positive_integer(self) -> None:
        """llm_timeout accepts positive integers."""
        config = GlobalDefaultsConfig(llm_timeout=120)
        assert config.llm_timeout == 120

    def test_llm_timeout_rejects_zero(self) -> None:
        """llm_timeout rejects zero (must be >= 1)."""
        with pytest.raises(ValidationError):
            GlobalDefaultsConfig(llm_timeout=0)

    def test_llm_timeout_rejects_negative(self) -> None:
        """llm_timeout rejects negative values."""
        with pytest.raises(ValidationError):
            GlobalDefaultsConfig(llm_timeout=-1)

    def test_llm_timeout_rejects_non_integer(self) -> None:
        """llm_timeout rejects non-integer values (coerces string)."""
        # Pydantic coerces numeric strings to int, so "120" becomes 120
        # This is expected behavior for Pydantic int fields
        config = GlobalDefaultsConfig(llm_timeout="120")  # type: ignore
        assert config.llm_timeout == 120

    def test_llm_timeout_with_model(self) -> None:
        """llm_timeout can coexist with model default."""
        config = GlobalDefaultsConfig(model="gpt-5.2", llm_timeout=90)
        assert config.model == "gpt-5.2"
        assert config.llm_timeout == 90


class TestAgentRunnerLlmTimeout:
    """Tests for AgentRunner LLM timeout integration."""

    def test_get_model_settings_with_timeout_returns_none_when_not_configured(
        self,
    ) -> None:
        """_get_model_settings_with_timeout returns None when no timeout set."""
        runner = AgentRunner(
            instance_id="test",
            task="test task",
        )
        # _llm_timeout is None by default
        assert runner._llm_timeout is None

        result = runner._get_model_settings_with_timeout()
        assert result is None

    def test_get_model_settings_with_timeout_returns_model_settings(self) -> None:
        """_get_model_settings_with_timeout returns ModelSettings with timeout."""
        runner = AgentRunner(
            instance_id="test",
            task="test task",
        )
        runner._llm_timeout = 120

        result = runner._get_model_settings_with_timeout()
        assert isinstance(result, dict)  # ModelSettings is a TypedDict
        assert "timeout" in result
        # Timeout is coerced to float when a bare float is passed
        assert result["timeout"] == 120.0

    def test_llm_timeout_is_loaded_from_config_at_run_time(self) -> None:
        """AgentRunner loads llm_timeout from global config during run."""
        # This is verified by integration tests
        # The runner._llm_timeout is set in run() from load_global_config()
        runner = AgentRunner(
            instance_id="test",
            task="test task",
        )
        # Initially None before run() is called
        assert runner._llm_timeout is None


class _FakeRuntimeProfile:
    def __init__(self, context_window_tokens: int | None = None) -> None:
        self.context_window_tokens = context_window_tokens


class _FakeRuntimeModel:
    def __init__(
        self,
        *,
        context_window_tokens: int | None = None,
        profile_context_window_tokens: int | None = None,
    ) -> None:
        self.context_window_tokens = context_window_tokens
        self.profile = _FakeRuntimeProfile(profile_context_window_tokens)


def test_evaluate_runtime_model_context_limit_prefers_model_metadata() -> None:
    """Runtime model metadata is read directly when available."""
    model = _FakeRuntimeModel(context_window_tokens=128000)

    result = evaluate_runtime_model_context_limit(model)

    assert result == 128000


def test_evaluate_runtime_model_context_limit_falls_back_to_profile() -> None:
    """Runtime profile metadata is used when model-level value is unset."""
    model = _FakeRuntimeModel(
        context_window_tokens=None,
        profile_context_window_tokens=96000,
    )

    result = evaluate_runtime_model_context_limit(model)

    assert result == 96000


def test_resolve_model_context_limit_prefers_runtime_then_config_then_default() -> None:
    """Context limit precedence follows runtime > config > hard default."""
    config = AnimaGlobalConfig(
        defaults=GlobalDefaultsConfig(context_window_tokens=64000)
    )

    assert (
        resolve_model_context_limit(
            config=config,
            runtime_model_context_limit=128000,
        )
        == 128000
    )
    assert (
        resolve_model_context_limit(
            config=config,
            runtime_model_context_limit=None,
        )
        == 64000
    )
    assert (
        resolve_model_context_limit(
            config=None,
            runtime_model_context_limit=None,
        )
        == DEFAULT_MODEL_CONTEXT_LIMIT_TOKENS
    )
