"""Coverage for misc model methods, factories, and helper functions.

Targets the long-tail of small-percent gaps in roboco.models.* — convenience
properties, factory functions, lookup helpers, and __post_init__ branches.
"""

from __future__ import annotations

from pathlib import Path
from unittest.mock import patch
from uuid import uuid4

from roboco.models import AgentRole, Team
from roboco.models.a2a import (
    A2AConversation,
    A2AConversationStatus,
    A2ATaskState,
    a2a_state_to_task_status,
    task_status_to_a2a_state,
)
from roboco.models.agents import AgentConfig
from roboco.models.base import ModelProvider
from roboco.models.llm_catalog import (
    MODEL_CATALOG,
    _build_anthropic_entries,
    provider_type_for_model,
)
from roboco.models.permissions import (
    AgentContext,
    PermissionLevel,
    _build_role_levels,
)
from roboco.models.runtime import (
    AgentInstance,
    OrchestratorAgentConfig,
    OrchestratorAgentState,
)

# ---------------------------------------------------------------------------
# AgentConfig convenience properties (roboco/models/agents.py 63, 68, 73, 78)
# ---------------------------------------------------------------------------


def test_agent_config_convenience_properties() -> None:
    cfg = AgentConfig(
        name="Test",
        slug="test",
        role=AgentRole.DEVELOPER,
        team=Team.BACKEND,
        system_prompt="hello",
    )
    # provider, model, temperature, max_tokens come from model_config_data
    assert cfg.provider == cfg.model_config_data.provider
    assert cfg.model == cfg.model_config_data.name
    assert cfg.temperature == cfg.model_config_data.temperature
    assert cfg.max_tokens == cfg.model_config_data.max_tokens


# ---------------------------------------------------------------------------
# AgentInstance.__post_init__ — explicit None id branch (runtime.py 76-77)
# ---------------------------------------------------------------------------


def test_agent_instance_post_init_assigns_uuid_when_falsy() -> None:
    # Forcing an empty UUID(int=0) is falsy → the post_init triggers re-assign.
    inst = AgentInstance.__new__(AgentInstance)
    # Fill required dataclass fields explicitly so __post_init__ runs cleanly.
    inst.id = None  # type: ignore[assignment]
    inst.agent_id = "be-dev-1"
    inst.state = OrchestratorAgentState.OFFLINE
    inst.container_id = None
    inst.config = None
    inst.started_at = None
    inst.last_activity = None
    inst.current_task_id = None
    inst.error_count = 0
    inst.waiting_for = None
    inst.waiting_context = {}
    inst.__post_init__()
    assert inst.id is not None


def test_agent_instance_default_factory_assigns_id() -> None:
    inst = AgentInstance(agent_id="be-dev-1")
    assert inst.id is not None


def test_orchestrator_agent_config_defaults() -> None:

    cfg = OrchestratorAgentConfig(
        agent_id="be-dev-1",
        blueprint_path=Path("/tmp/blueprint"),
    )
    assert cfg.model == "sonnet"
    assert cfg.provider_type == "anthropic"


# ---------------------------------------------------------------------------
# llm_catalog: catalog skip when MODEL_MAP missing entry (line 56)
# ---------------------------------------------------------------------------


def test_build_anthropic_entries_skips_missing_models() -> None:
    # Patch MODEL_MAP to be missing one entry → loop hits the `continue` branch.
    with patch(
        "roboco.models.llm_catalog.MODEL_MAP",
        {"opus": "claude-opus-4-6"},  # sonnet+haiku missing
    ):
        entries = _build_anthropic_entries()
    # Only one entry survives — opus.
    assert len(entries) == 1
    assert entries[0].model_name == "opus"


def test_provider_type_for_model_known() -> None:
    # Pick the first catalog entry deterministically.
    sample = MODEL_CATALOG[0]
    assert provider_type_for_model(sample.model_name) == sample.provider_type


def test_provider_type_for_model_unknown_returns_none() -> None:
    assert provider_type_for_model("nonexistent-model-x") is None


# ---------------------------------------------------------------------------
# permissions._build_role_levels — exception branch (lines 35-36)
# ---------------------------------------------------------------------------


def test_build_role_levels_skips_invalid_role() -> None:
    """Bad role string should be silently swallowed in the except branch."""
    with patch(
        "roboco.models.permissions.ROLE_PERMISSION_LEVELS",
        {"not_a_role": "CEO", "ceo": "CEO"},
    ):
        result = _build_role_levels()
    # 'not_a_role' silently dropped; ceo retained.
    assert AgentRole.CEO in result
    assert result[AgentRole.CEO] == PermissionLevel.CEO


def test_build_role_levels_skips_invalid_level() -> None:
    with patch(
        "roboco.models.permissions.ROLE_PERMISSION_LEVELS",
        {"ceo": "NOT_A_LEVEL"},
    ):
        result = _build_role_levels()
    assert result == {}


# ---------------------------------------------------------------------------
# A2A helpers — A2AConversation methods + a2a_state_to_task_status fallback
# ---------------------------------------------------------------------------


def test_a2a_conversation_other_agent() -> None:
    conv = A2AConversation(agent_a="be-dev-1", agent_b="fe-dev-1")
    assert conv.other_agent("be-dev-1") == "fe-dev-1"
    assert conv.other_agent("fe-dev-1") == "be-dev-1"


def test_a2a_conversation_my_unread() -> None:
    _UNREAD_A = 2
    _UNREAD_B = 5
    conv = A2AConversation(
        agent_a="be-dev-1",
        agent_b="fe-dev-1",
        unread_by_a=_UNREAD_A,
        unread_by_b=_UNREAD_B,
    )
    assert conv.my_unread("be-dev-1") == _UNREAD_A
    assert conv.my_unread("fe-dev-1") == _UNREAD_B


def test_a2a_conversation_is_participant() -> None:
    conv = A2AConversation(agent_a="be-dev-1", agent_b="fe-dev-1")
    assert conv.is_participant("be-dev-1") is True
    assert conv.is_participant("fe-dev-1") is True
    assert conv.is_participant("ux-dev-1") is False


def test_a2a_state_to_task_status_unknown_returns_pending() -> None:
    # Default fallback for any unknown state is "pending".
    class _FakeState:
        pass

    fake = _FakeState()
    assert a2a_state_to_task_status(fake) == "pending"  # type: ignore[arg-type]


def test_a2a_state_to_task_status_known() -> None:
    assert a2a_state_to_task_status(A2ATaskState.SUBMITTED) == "pending"
    assert a2a_state_to_task_status(A2ATaskState.WORKING) == "in_progress"
    assert a2a_state_to_task_status(A2ATaskState.COMPLETED) == "completed"
    assert a2a_state_to_task_status(A2ATaskState.FAILED) == "cancelled"
    assert a2a_state_to_task_status(A2ATaskState.INPUT_REQUIRED) == "blocked"


def test_task_status_to_a2a_state_known() -> None:
    assert task_status_to_a2a_state("pending") == A2ATaskState.SUBMITTED
    assert task_status_to_a2a_state("in_progress") == A2ATaskState.WORKING
    assert task_status_to_a2a_state("completed") == A2ATaskState.COMPLETED
    assert task_status_to_a2a_state("cancelled") == A2ATaskState.CANCELLED
    assert task_status_to_a2a_state("blocked") == A2ATaskState.INPUT_REQUIRED


def test_task_status_to_a2a_state_unknown_returns_working() -> None:
    assert task_status_to_a2a_state("garbage_status") == A2ATaskState.WORKING


# ---------------------------------------------------------------------------
# AgentContext.level — line 79
# ---------------------------------------------------------------------------


def test_agent_context_level_returns_role_level() -> None:
    ctx = AgentContext(agent_id=uuid4(), role=AgentRole.CEO)
    assert ctx.level == PermissionLevel.CEO


def test_agent_context_level_falls_back_when_role_missing() -> None:
    """Edge: build an AgentContext for a role removed from ROLE_LEVELS."""
    ctx = AgentContext(agent_id=uuid4(), role=AgentRole.DEVELOPER)
    # Default fallback returns CELL_MEMBER even when not present.
    assert ctx.level in PermissionLevel


# ---------------------------------------------------------------------------
# Conversation status enum + closed/paused round-trip
# ---------------------------------------------------------------------------


def test_a2a_conversation_status_values() -> None:
    assert A2AConversationStatus.ACTIVE == "active"
    assert A2AConversationStatus.PAUSED == "paused"
    assert A2AConversationStatus.CLOSED == "closed"


# ---------------------------------------------------------------------------
# Sanity: ModelProvider import is reachable
# ---------------------------------------------------------------------------


def test_model_provider_enum_has_anthropic() -> None:
    assert ModelProvider.ANTHROPIC == "anthropic"
