"""Unit tests for built-in actor v3 YAML generation and virtual resolution.

After issue #10923, built-in actors are **virtual** — resolved on-demand from
the provider registry in-memory, never persisted to the database.

Tests cover:
- ``_generate_builtin_actor_yaml()``: YAML text generation helpers (unchanged)
- ``_resolve_virtual_builtin_actors()``: virtual actor in-memory generation
- ``list()``: merging virtual actors with custom DB actors
- ``get()`` / ``get_actor()``: DB-first then virtual fallback resolution
"""

from __future__ import annotations

import pytest
import yaml

from cleveragents.actor.registry import ActorRegistry
from cleveragents.actor.schema import ActorConfigSchema, ActorType, is_v3_yaml
from cleveragents.config.settings import ProviderDefaults
from cleveragents.core.exceptions import NotFoundError
from cleveragents.domain.models.core.actor import Actor
from cleveragents.providers.registry import (
    ProviderCapabilities,
    ProviderInfo,
    ProviderType,
)


class _StubActorService:
    """Stub actor service for testing virtual built-in resolution."""

    def __init__(self) -> None:
        self.actors: dict[str, Actor] = {}
        self.default_actor_name: str | None = None

    def upsert_actor(
        self,
        *,
        name: str,
        provider: str,
        model: str,
        config_blob: dict,
        graph_descriptor: dict | None,
        unsafe: bool,
        set_default: bool,
        is_built_in: bool,
        yaml_text: str | None = None,
        schema_version: str | None = None,
        compiled_metadata: dict | None = None,
    ) -> Actor:
        """Store actor data for verification."""
        actor = Actor(
            name=name,
            provider=provider,
            model=model,
            config_blob=config_blob,
            config_hash=Actor.compute_hash(config_blob),
            is_built_in=is_built_in,
        )
        self.actors[name] = actor
        if set_default:
            self.default_actor_name = name
        return actor

    def get_default_actor(self) -> Actor | None:
        """Return default actor."""
        if self.default_actor_name and self.default_actor_name in self.actors:
            return self.actors[self.default_actor_name]
        return None

    def get_default_actor_name(self) -> str | None:
        return self.default_actor_name

    def set_default_actor_name(self, name: str) -> None:
        self.default_actor_name = name

    def set_default_actor(self, name: str) -> Actor:
        """Set default actor."""
        actor = self.actors.get(name)
        if actor is None:
            raise NotFoundError(resource_type="actor", resource_id=name)
        self.default_actor_name = name
        return actor

    def get_actor(self, name: str) -> Actor:
        """Return actor or raise NotFoundError."""
        actor = self.actors.get(name)
        if actor is None:
            raise NotFoundError(resource_type="actor", resource_id=name)
        return actor

    def list_actors(self) -> list[Actor]:
        return list(self.actors.values())


class _StubProviderRegistry:
    """Stub provider registry for testing."""

    def __init__(self, providers: list[ProviderInfo]) -> None:
        self._providers = providers

    def get_configured_providers(self) -> list[ProviderInfo]:
        """Return configured providers."""
        return list(self._providers)


class _StubSettings:
    """Stub settings for testing."""

    def __init__(self, defaults: ProviderDefaults) -> None:
        self._defaults = defaults

    def resolve_provider_defaults(self) -> ProviderDefaults:
        """Return provider defaults."""
        return self._defaults


@pytest.fixture
def stub_actor_service() -> _StubActorService:
    """Create a stub actor service."""
    return _StubActorService()


@pytest.fixture
def stub_settings() -> _StubSettings:
    """Create stub settings."""
    return _StubSettings(
        ProviderDefaults(
            provider=None, provider_source="test", model=None, model_source="test"
        )
    )


class TestGenerateBuiltinActorYaml:
    """Tests for _generate_builtin_actor_yaml method."""

    def test_generates_yaml_with_type_field(
        self, stub_actor_service: _StubActorService, stub_settings: _StubSettings
    ) -> None:
        """YAML should include type: llm field."""
        provider_registry = _StubProviderRegistry([])
        registry = ActorRegistry(
            actor_service=stub_actor_service,
            provider_registry=provider_registry,
            settings=stub_settings,
        )

        yaml_text = registry._generate_builtin_actor_yaml(
            provider="openai",
            model="gpt-4",
        )

        assert yaml_text is not None
        assert "type: llm" in yaml_text

    def test_generates_yaml_with_description_field(
        self, stub_actor_service: _StubActorService, stub_settings: _StubSettings
    ) -> None:
        """YAML should include description field."""
        provider_registry = _StubProviderRegistry([])
        registry = ActorRegistry(
            actor_service=stub_actor_service,
            provider_registry=provider_registry,
            settings=stub_settings,
        )

        yaml_text = registry._generate_builtin_actor_yaml(
            provider="openai",
            model="gpt-4",
        )

        assert "description:" in yaml_text
        assert "Built-in actor from provider registry" in yaml_text

    def test_generates_yaml_with_name_field(
        self, stub_actor_service: _StubActorService, stub_settings: _StubSettings
    ) -> None:
        """YAML should include properly formatted name field."""
        provider_registry = _StubProviderRegistry([])
        registry = ActorRegistry(
            actor_service=stub_actor_service,
            provider_registry=provider_registry,
            settings=stub_settings,
        )

        yaml_text = registry._generate_builtin_actor_yaml(
            provider="openai",
            model="gpt-4",
        )

        assert "name: openai/gpt-4" in yaml_text

    def test_generates_yaml_with_provider_field(
        self, stub_actor_service: _StubActorService, stub_settings: _StubSettings
    ) -> None:
        """YAML should include provider field."""
        provider_registry = _StubProviderRegistry([])
        registry = ActorRegistry(
            actor_service=stub_actor_service,
            provider_registry=provider_registry,
            settings=stub_settings,
        )

        yaml_text = registry._generate_builtin_actor_yaml(
            provider="anthropic",
            model="claude-3-opus",
        )

        assert "provider: anthropic" in yaml_text

    def test_generates_yaml_with_model_field(
        self, stub_actor_service: _StubActorService, stub_settings: _StubSettings
    ) -> None:
        """YAML should include model field."""
        provider_registry = _StubProviderRegistry([])
        registry = ActorRegistry(
            actor_service=stub_actor_service,
            provider_registry=provider_registry,
            settings=stub_settings,
        )

        yaml_text = registry._generate_builtin_actor_yaml(
            provider="openai",
            model="gpt-4",
        )

        assert "model: openai/gpt-4" in yaml_text

    def test_generates_yaml_with_source_field(
        self, stub_actor_service: _StubActorService, stub_settings: _StubSettings
    ) -> None:
        """YAML should include source: provider-registry field."""
        provider_registry = _StubProviderRegistry([])
        registry = ActorRegistry(
            actor_service=stub_actor_service,
            provider_registry=provider_registry,
            settings=stub_settings,
        )

        yaml_text = registry._generate_builtin_actor_yaml(
            provider="openai",
            model="gpt-4",
        )

        assert "source: provider-registry" in yaml_text

    def test_generated_yaml_is_valid_v3_format(
        self, stub_actor_service: _StubActorService, stub_settings: _StubSettings
    ) -> None:
        """Generated YAML should be recognized as v3 format."""
        provider_registry = _StubProviderRegistry([])
        registry = ActorRegistry(
            actor_service=stub_actor_service,
            provider_registry=provider_registry,
            settings=stub_settings,
        )

        yaml_text = registry._generate_builtin_actor_yaml(
            provider="openai",
            model="gpt-4",
        )

        config_blob = yaml.safe_load(yaml_text)
        assert is_v3_yaml(config_blob), (
            "Generated YAML should be recognized as v3 format"
        )

    def test_generated_yaml_validates_against_schema(
        self, stub_actor_service: _StubActorService, stub_settings: _StubSettings
    ) -> None:
        """Generated YAML should validate against ActorConfigSchema."""
        provider_registry = _StubProviderRegistry([])
        registry = ActorRegistry(
            actor_service=stub_actor_service,
            provider_registry=provider_registry,
            settings=stub_settings,
        )

        yaml_text = registry._generate_builtin_actor_yaml(
            provider="openai",
            model="gpt-4",
        )

        config_blob = yaml.safe_load(yaml_text)
        schema = ActorConfigSchema.model_validate(config_blob)
        assert schema.type == ActorType.LLM
        assert schema.name == "openai/gpt-4"
        assert schema.provider == "openai"
        assert schema.model == "openai/gpt-4"

    def test_generated_yaml_includes_capabilities(
        self, stub_actor_service: _StubActorService, stub_settings: _StubSettings
    ) -> None:
        """Generated YAML should include capabilities when provided."""
        provider_registry = _StubProviderRegistry([])
        registry = ActorRegistry(
            actor_service=stub_actor_service,
            provider_registry=provider_registry,
            settings=stub_settings,
        )

        capabilities = ProviderCapabilities(
            supports_tool_calls=True, supports_vision=False
        )
        yaml_text = registry._generate_builtin_actor_yaml(
            provider="openai",
            model="gpt-4",
            capabilities=capabilities,
        )

        assert "capabilities:" in yaml_text
        assert "supports_tool_calls: true" in yaml_text

    def test_generated_yaml_sanitizes_slashes_in_name(
        self, stub_actor_service: _StubActorService, stub_settings: _StubSettings
    ) -> None:
        """Generated YAML name should sanitize slashes."""
        provider_registry = _StubProviderRegistry([])
        registry = ActorRegistry(
            actor_service=stub_actor_service,
            provider_registry=provider_registry,
            settings=stub_settings,
        )

        yaml_text = registry._generate_builtin_actor_yaml(
            provider="openrouter",
            model="meta-llama/llama-3-70b-instruct",
        )

        # Name should have slashes replaced with dashes
        assert "name: openrouter/meta-llama-llama-3-70b-instruct" in yaml_text


class TestResolveVirtualBuiltinActors:
    """Tests for _resolve_virtual_builtin_actors — the new virtual resolution method.

    Replaces old ``TestEnsureBuiltInActorsWithYaml`` which tested the removed
    ``ensure_built_in_actors()`` DB-persistence path.
    """

    def test_resolve_virtual_builtin_actors_includes_yaml_text(
        self, stub_actor_service: _StubActorService, stub_settings: _StubSettings
    ) -> None:
        """Virtual built-in actors should have yaml_text populated."""
        provider_registry = _StubProviderRegistry(
            [
                ProviderInfo(
                    provider_type=ProviderType.OPENAI,
                    name="openai",
                    api_key_env_var="ENV",
                    default_model="gpt-4",
                    capabilities=ProviderCapabilities(supports_tool_calls=True),
                    is_configured=True,
                )
            ]
        )
        registry = ActorRegistry(
            actor_service=stub_actor_service,
            provider_registry=provider_registry,
            settings=stub_settings,
        )

        actors = registry._resolve_virtual_builtin_actors()

        assert len(actors) == 1
        actor = actors[0]
        assert actor.name == "openai/gpt-4"
        assert actor.yaml_text is not None
        assert "type: llm" in actor.yaml_text

    def test_resolve_virtual_builtin_actors_yaml_is_v3_format(
        self, stub_actor_service: _StubActorService, stub_settings: _StubSettings
    ) -> None:
        """Virtual built-in actor yaml_text should be v3 format."""
        provider_registry = _StubProviderRegistry(
            [
                ProviderInfo(
                    provider_type=ProviderType.ANTHROPIC,
                    name="anthropic",
                    api_key_env_var="ENV",
                    default_model="claude-3-opus",
                    capabilities=ProviderCapabilities(),
                    is_configured=True,
                )
            ]
        )
        registry = ActorRegistry(
            actor_service=stub_actor_service,
            provider_registry=provider_registry,
            settings=stub_settings,
        )

        actors = registry._resolve_virtual_builtin_actors()

        assert len(actors) == 1
        actor = actors[0]
        assert actor.name == "anthropic/claude-3-opus"
        assert actor.yaml_text is not None
        config_blob = yaml.safe_load(actor.yaml_text)
        assert is_v3_yaml(config_blob)

    def test_resolve_virtual_builtin_actors_multiple_providers(
        self, stub_actor_service: _StubActorService, stub_settings: _StubSettings
    ) -> None:
        """Multiple providers should each produce a virtual actor."""
        provider_registry = _StubProviderRegistry(
            [
                ProviderInfo(
                    provider_type=ProviderType.OPENAI,
                    name="openai",
                    api_key_env_var="ENV",
                    default_model="gpt-4",
                    capabilities=ProviderCapabilities(supports_tool_calls=True),
                    is_configured=True,
                ),
                ProviderInfo(
                    provider_type=ProviderType.ANTHROPIC,
                    name="anthropic",
                    api_key_env_var="ENV",
                    default_model="claude-3-opus",
                    capabilities=ProviderCapabilities(),
                    is_configured=True,
                ),
            ]
        )
        registry = ActorRegistry(
            actor_service=stub_actor_service,
            provider_registry=provider_registry,
            settings=stub_settings,
        )

        actors = registry._resolve_virtual_builtin_actors()

        assert len(actors) == 2
        # Sorted alphabetically
        assert actors[0].name == "anthropic/claude-3-opus"
        assert actors[1].name == "openai/gpt-4"
        for actor in actors:
            assert actor.yaml_text is not None
            assert "type: llm" in actor.yaml_text
            assert actor.is_built_in is True
            assert actor.id is None  # Not persisted

    def test_resolve_virtual_builtin_actors_empty_when_no_providers(
        self, stub_actor_service: _StubActorService, stub_settings: _StubSettings
    ) -> None:
        """No providers should result in no virtual actors."""
        provider_registry = _StubProviderRegistry([])
        registry = ActorRegistry(
            actor_service=stub_actor_service,
            provider_registry=provider_registry,
            settings=stub_settings,
        )

        actors = registry._resolve_virtual_builtin_actors()

        assert actors == []
        # No DB writes occurred
        assert len(stub_actor_service.actors) == 0

    def test_virtual_actors_are_not_persisted(
        self, stub_actor_service: _StubActorService, stub_settings: _StubSettings
    ) -> None:
        """Virtual built-in actors must NOT be persisted to the database."""
        provider_registry = _StubProviderRegistry(
            [
                ProviderInfo(
                    provider_type=ProviderType.OPENAI,
                    name="openai",
                    api_key_env_var="ENV",
                    default_model="gpt-4",
                    capabilities=ProviderCapabilities(),
                    is_configured=True,
                )
            ]
        )
        registry = ActorRegistry(
            actor_service=stub_actor_service,
            provider_registry=provider_registry,
            settings=stub_settings,
        )

        actors = registry._resolve_virtual_builtin_actors()

        assert len(actors) == 1
        # No upsert_actor calls — DB stays empty
        assert len(stub_actor_service.actors) == 0


class TestListActors:
    """Tests for list() merging virtual + DB actors."""

    def test_list_includes_virtual_built_ins(
        self, stub_actor_service: _StubActorService, stub_settings: _StubSettings
    ) -> None:
        """list() should include virtual built-in actors from provider registry."""
        provider_registry = _StubProviderRegistry(
            [
                ProviderInfo(
                    provider_type=ProviderType.OPENAI,
                    name="openai",
                    api_key_env_var="ENV",
                    default_model="gpt-4",
                    capabilities=ProviderCapabilities(),
                    is_configured=True,
                )
            ]
        )
        registry = ActorRegistry(
            actor_service=stub_actor_service,
            provider_registry=provider_registry,
            settings=stub_settings,
        )

        actors = registry.list()

        assert len(actors) == 1
        assert actors[0].name == "openai/gpt-4"
        assert actors[0].is_built_in is True

    def test_list_merges_virtual_and_custom(
        self, stub_actor_service: _StubActorService, stub_settings: _StubSettings
    ) -> None:
        """list() should merge virtual built-ins with custom DB actors."""
        # Add a custom actor to the stub service
        custom = Actor(
            name="local/my-actor",
            provider="openai",
            model="gpt-4",
            config_blob={},
            config_hash=Actor.compute_hash({}),
            is_built_in=False,
        )
        stub_actor_service.actors["local/my-actor"] = custom

        provider_registry = _StubProviderRegistry(
            [
                ProviderInfo(
                    provider_type=ProviderType.OPENAI,
                    name="openai",
                    api_key_env_var="ENV",
                    default_model="gpt-4",
                    capabilities=ProviderCapabilities(),
                    is_configured=True,
                )
            ]
        )
        registry = ActorRegistry(
            actor_service=stub_actor_service,
            provider_registry=provider_registry,
            settings=stub_settings,
        )

        actors = registry.list()

        names = [a.name for a in actors]
        assert "local/my-actor" in names
        assert "openai/gpt-4" in names

    def test_custom_actor_overrides_virtual_builtin(
        self, stub_actor_service: _StubActorService, stub_settings: _StubSettings
    ) -> None:
        """A custom actor with the same name as a virtual built-in takes precedence."""
        custom = Actor(
            name="openai/gpt-4",
            provider="openai",
            model="gpt-4",
            config_blob={"custom": True},
            config_hash=Actor.compute_hash({"custom": True}),
            is_built_in=False,
        )
        stub_actor_service.actors["openai/gpt-4"] = custom

        provider_registry = _StubProviderRegistry(
            [
                ProviderInfo(
                    provider_type=ProviderType.OPENAI,
                    name="openai",
                    api_key_env_var="ENV",
                    default_model="gpt-4",
                    capabilities=ProviderCapabilities(),
                    is_configured=True,
                )
            ]
        )
        registry = ActorRegistry(
            actor_service=stub_actor_service,
            provider_registry=provider_registry,
            settings=stub_settings,
        )

        actors = registry.list()

        # Only one actor with this name
        matching = [a for a in actors if a.name == "openai/gpt-4"]
        assert len(matching) == 1
        # Custom actor (from DB) wins over virtual built-in
        assert matching[0].config_blob == {"custom": True}
        assert matching[0].is_built_in is False


class TestGetActor:
    """Tests for get() / get_actor() resolution order."""

    def test_get_resolves_db_actor_first(
        self, stub_actor_service: _StubActorService, stub_settings: _StubSettings
    ) -> None:
        """get() should return DB actor when it exists."""
        custom = Actor(
            name="local/my-actor",
            provider="test",
            model="model",
            config_blob={},
            config_hash=Actor.compute_hash({}),
            is_built_in=False,
        )
        stub_actor_service.actors["local/my-actor"] = custom

        registry = ActorRegistry(
            actor_service=stub_actor_service,
            provider_registry=_StubProviderRegistry([]),
            settings=stub_settings,
        )

        actor = registry.get("local/my-actor")
        assert actor.name == "local/my-actor"
        assert actor.is_built_in is False

    def test_get_resolves_virtual_builtin_when_not_in_db(
        self, stub_actor_service: _StubActorService, stub_settings: _StubSettings
    ) -> None:
        """get() should resolve virtual built-in when not found in DB."""
        provider_registry = _StubProviderRegistry(
            [
                ProviderInfo(
                    provider_type=ProviderType.OPENAI,
                    name="openai",
                    api_key_env_var="ENV",
                    default_model="gpt-4",
                    capabilities=ProviderCapabilities(),
                    is_configured=True,
                )
            ]
        )
        registry = ActorRegistry(
            actor_service=stub_actor_service,
            provider_registry=provider_registry,
            settings=stub_settings,
        )

        actor = registry.get("openai/gpt-4")
        assert actor.name == "openai/gpt-4"
        assert actor.is_built_in is True
        assert actor.id is None  # Not from DB

    def test_get_raises_not_found_when_neither(
        self, stub_actor_service: _StubActorService, stub_settings: _StubSettings
    ) -> None:
        """get() should raise NotFoundError when actor is in neither DB nor virtual."""
        registry = ActorRegistry(
            actor_service=stub_actor_service,
            provider_registry=_StubProviderRegistry([]),
            settings=stub_settings,
        )

        with pytest.raises(NotFoundError):
            registry.get("nonexistent/actor")


class TestRemoveActor:
    """Tests for remove() rejecting virtual built-ins."""

    def test_remove_virtual_builtin_raises_validation_error(
        self, stub_actor_service: _StubActorService, stub_settings: _StubSettings
    ) -> None:
        """remove() should raise ValidationError for virtual built-in actors."""
        from cleveragents.core.exceptions import ValidationError

        provider_registry = _StubProviderRegistry(
            [
                ProviderInfo(
                    provider_type=ProviderType.OPENAI,
                    name="openai",
                    api_key_env_var="ENV",
                    default_model="gpt-4",
                    capabilities=ProviderCapabilities(),
                    is_configured=True,
                )
            ]
        )
        registry = ActorRegistry(
            actor_service=stub_actor_service,
            provider_registry=provider_registry,
            settings=stub_settings,
        )

        with pytest.raises(ValidationError, match="built-in"):
            registry.remove("openai/gpt-4")


class TestDefaultActor:
    """Tests for set_default_actor / get_default_actor with virtual built-ins."""

    def test_set_default_virtual_builtin(
        self, stub_actor_service: _StubActorService, stub_settings: _StubSettings
    ) -> None:
        """set_default_actor() with a virtual built-in stores the name preference."""
        provider_registry = _StubProviderRegistry(
            [
                ProviderInfo(
                    provider_type=ProviderType.OPENAI,
                    name="openai",
                    api_key_env_var="ENV",
                    default_model="gpt-4",
                    capabilities=ProviderCapabilities(),
                    is_configured=True,
                )
            ]
        )
        registry = ActorRegistry(
            actor_service=stub_actor_service,
            provider_registry=provider_registry,
            settings=stub_settings,
        )

        actor = registry.set_default_actor("openai/gpt-4")

        assert actor.name == "openai/gpt-4"
        assert actor.is_default is True
        assert actor.is_built_in is True
        # Name preference stored but no DB row created
        assert stub_actor_service.default_actor_name == "openai/gpt-4"
        assert len(stub_actor_service.actors) == 0  # No DB writes

    def test_get_default_resolves_virtual_builtin(
        self, stub_actor_service: _StubActorService, stub_settings: _StubSettings
    ) -> None:
        """get_default_actor() resolves virtual built-in from preference name."""
        provider_registry = _StubProviderRegistry(
            [
                ProviderInfo(
                    provider_type=ProviderType.OPENAI,
                    name="openai",
                    api_key_env_var="ENV",
                    default_model="gpt-4",
                    capabilities=ProviderCapabilities(),
                    is_configured=True,
                )
            ]
        )
        registry = ActorRegistry(
            actor_service=stub_actor_service,
            provider_registry=provider_registry,
            settings=stub_settings,
        )
        stub_actor_service.default_actor_name = "openai/gpt-4"

        default = registry.get_default_actor()

        assert default is not None
        assert default.name == "openai/gpt-4"
        assert default.is_built_in is True
        assert default.is_default is True

    def test_get_default_returns_none_when_not_set(
        self, stub_actor_service: _StubActorService, stub_settings: _StubSettings
    ) -> None:
        """get_default_actor() returns None when no default is configured."""
        registry = ActorRegistry(
            actor_service=stub_actor_service,
            provider_registry=_StubProviderRegistry([]),
            settings=stub_settings,
        )

        default = registry.get_default_actor()

        assert default is None
