"""Tests for third-party adapter discovery via Python entry points."""
import importlib.metadata
import warnings
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock

import pytest

from agentmux.adapters.base import AdapterRegistry, ConvertedFile, ToolAdapter
from agentmux.core.component import (
    Agent,
    Command,
    Component,
    ComponentType,
    Instructions,
    MCPConfig,
    Rule,
    Skill,
)

# ── Helpers ────────────────────────────────────────────────────────────────────


class _FakeAdapter(ToolAdapter):
    """Minimal valid ToolAdapter for use as a fake plugin."""

    @property
    def name(self) -> str:
        return "fake-tool"

    def detect(self) -> bool:
        return False

    def get_global_base_dir(self) -> Path:
        return Path("/tmp/fake")

    def get_project_subdir(self) -> Path:
        return Path(".fake")

    def convert_agent(self, agent: Agent) -> list[ConvertedFile]:
        return []

    def convert_command(self, command: Command) -> list[ConvertedFile]:
        return []

    def convert_skill(self, skill: Skill) -> list[ConvertedFile]:
        return []

    def convert_rule(self, rule: Rule) -> list[ConvertedFile]:
        return []

    def convert_instructions(self, instructions: Instructions) -> list[ConvertedFile]:
        return []

    def convert_mcp(self, mcp: MCPConfig) -> list[ConvertedFile]:
        return []

    def validate(self, component: Component) -> list[str]:
        return []


def _make_entry_point(name: str, load_result: Any) -> MagicMock:
    ep = MagicMock(spec=importlib.metadata.EntryPoint)
    ep.name = name
    ep.load.return_value = load_result
    return ep


def _make_failing_entry_point(name: str, exc: Exception) -> MagicMock:
    ep = MagicMock(spec=importlib.metadata.EntryPoint)
    ep.name = name
    ep.load.side_effect = exc
    return ep


# ── Built-in adapter discovery ─────────────────────────────────────────────────


def test_builtin_adapters_are_discovered() -> None:
    """All four built-in adapters are present after entry-point discovery."""
    adapters = AdapterRegistry.get_adapters()
    names = {a.name for a in adapters}
    assert {"cursor", "gemini", "opencode", "claude-code"} <= names


def test_get_adapters_returns_cached_instance() -> None:
    first = AdapterRegistry.get_adapters()
    second = AdapterRegistry.get_adapters()
    assert first is second


# ── Third-party plugin injection ───────────────────────────────────────────────


def test_third_party_adapter_is_discovered(monkeypatch: pytest.MonkeyPatch) -> None:
    """A fake entry point exposing a valid ToolAdapter subclass is included."""
    fake_ep = _make_entry_point("fake-tool", _FakeAdapter)

    def mock_entry_points(*, group: str) -> list[MagicMock]:
        if group == "agentmux.adapters":
            return [fake_ep]
        return []

    monkeypatch.setattr(importlib.metadata, "entry_points", mock_entry_points)

    adapters = AdapterRegistry.get_adapters()
    names = {a.name for a in adapters}
    assert "fake-tool" in names


# ── Error handling ──────────────────────────────────────────────────────────────


def test_import_error_in_entry_point_emits_warning(monkeypatch: pytest.MonkeyPatch) -> None:
    """An entry point that raises ImportError is skipped with a warning."""
    bad_ep = _make_failing_entry_point("broken-tool", ImportError("no module named broken"))

    monkeypatch.setattr(
        importlib.metadata,
        "entry_points",
        lambda *, group: [bad_ep] if group == "agentmux.adapters" else [],
    )

    with warnings.catch_warnings(record=True) as caught:
        warnings.simplefilter("always")
        adapters = AdapterRegistry.get_adapters()

    names = {a.name for a in adapters}
    assert "broken-tool" not in names
    assert any("broken-tool" in str(w.message) for w in caught)


def test_non_adapter_class_entry_point_emits_warning(monkeypatch: pytest.MonkeyPatch) -> None:
    """An entry point that resolves to a non-ToolAdapter class is skipped with a warning."""

    class _NotAnAdapter:
        pass

    bad_ep = _make_entry_point("not-adapter", _NotAnAdapter)

    monkeypatch.setattr(
        importlib.metadata,
        "entry_points",
        lambda *, group: [bad_ep] if group == "agentmux.adapters" else [],
    )

    with warnings.catch_warnings(record=True) as caught:
        warnings.simplefilter("always")
        adapters = AdapterRegistry.get_adapters()

    names = {a.name for a in adapters}
    assert "not-adapter" not in names
    assert any("not-adapter" in str(w.message) for w in caught)


def test_non_class_entry_point_emits_warning(monkeypatch: pytest.MonkeyPatch) -> None:
    """An entry point that resolves to a non-class object is skipped with a warning."""
    bad_ep = _make_entry_point("not-a-class", "just a string")

    monkeypatch.setattr(
        importlib.metadata,
        "entry_points",
        lambda *, group: [bad_ep] if group == "agentmux.adapters" else [],
    )

    with warnings.catch_warnings(record=True) as caught:
        warnings.simplefilter("always")
        AdapterRegistry.get_adapters()

    assert any("not-a-class" in str(w.message) for w in caught)


def test_instantiation_error_emits_warning(monkeypatch: pytest.MonkeyPatch) -> None:
    """An entry point whose class raises on instantiation is skipped with a warning."""

    class _BadInit(ToolAdapter):
        def __init__(self) -> None:
            raise RuntimeError("init failed")

        @property
        def name(self) -> str:
            return "bad-init"

        def detect(self) -> bool:
            return False

        def get_global_base_dir(self) -> Path:
            return Path("/tmp")

        def get_project_subdir(self) -> Path:
            return Path(".bad")

        def convert_agent(self, agent: Agent) -> list[ConvertedFile]:
            return []

        def convert_command(self, command: Command) -> list[ConvertedFile]:
            return []

        def convert_skill(self, skill: Skill) -> list[ConvertedFile]:
            return []

        def convert_rule(self, rule: Rule) -> list[ConvertedFile]:
            return []

        def convert_instructions(self, instructions: Instructions) -> list[ConvertedFile]:
            return []

        def convert_mcp(self, mcp: MCPConfig) -> list[ConvertedFile]:
            return []

        def validate(self, component: Component) -> list[str]:
            return []

    bad_ep = _make_entry_point("bad-init", _BadInit)

    monkeypatch.setattr(
        importlib.metadata,
        "entry_points",
        lambda *, group: [bad_ep] if group == "agentmux.adapters" else [],
    )

    with warnings.catch_warnings(record=True) as caught:
        warnings.simplefilter("always")
        adapters = AdapterRegistry.get_adapters()

    names = {a.name for a in adapters}
    assert "bad-init" not in names
    assert any("bad-init" in str(w.message) for w in caught)


# ── Deduplication ───────────────────────────────────────────────────────────────


def test_duplicate_name_is_skipped(monkeypatch: pytest.MonkeyPatch) -> None:
    """A third-party adapter with the same name as an already-loaded adapter is ignored."""

    class _DuplicateCursor(_FakeAdapter):
        @property
        def name(self) -> str:
            return "cursor"

    from agentmux.adapters.cursor import CursorAdapter

    builtin_ep = _make_entry_point("cursor", CursorAdapter)
    duplicate_ep = _make_entry_point("cursor", _DuplicateCursor)

    monkeypatch.setattr(
        importlib.metadata,
        "entry_points",
        lambda *, group: [builtin_ep, duplicate_ep] if group == "agentmux.adapters" else [],
    )

    adapters = AdapterRegistry.get_adapters()
    cursor_adapters = [a for a in adapters if a.name == "cursor"]
    assert len(cursor_adapters) == 1
    assert isinstance(cursor_adapters[0], CursorAdapter)
