"""Tests for the Plugin System (A09).

Covers:
- BackendPlugin ABC (#72)
- EnginePlugin ABC (#73)
- SchedulerPlugin ABC (#74)
- AcceleratorPlugin ABC (#76)
- PluginRegistry discovery + in-process registration
- PluginChain build + auto_build
- SGLang Adapter registration (#64)
- Built-in plugin metadata / availability
"""

from __future__ import annotations

import pytest

# ============================================================================
# Fixtures
# ============================================================================


@pytest.fixture()
def registry():
    """Return a PluginRegistry populated with all built-in plugins."""
    from sagellm_core.plugin import PluginRegistry
    from sagellm_core.plugin.builtin import (
        CPUBackendPlugin,
        FCFSSchedulerPlugin,
        LMDeployEnginePlugin,
        NativeEnginePlugin,
        NoopAcceleratorPlugin,
        PrioritySchedulerPlugin,
        SGLangEnginePlugin,
        vLLMEnginePlugin,
    )

    reg = PluginRegistry()
    reg.register_backend(CPUBackendPlugin)
    reg.register_engine(NativeEnginePlugin)
    reg.register_engine(vLLMEnginePlugin)
    reg.register_engine(SGLangEnginePlugin)
    reg.register_engine(LMDeployEnginePlugin)
    reg.register_scheduler(FCFSSchedulerPlugin)
    reg.register_scheduler(PrioritySchedulerPlugin)
    reg.register_accelerator(NoopAcceleratorPlugin)
    return reg


# ============================================================================
# BackendPlugin (#72)
# ============================================================================


class TestBackendPlugin:
    def test_cpu_plugin_meta(self) -> None:
        from sagellm_core.plugin.builtin import CPUBackendPlugin

        meta = CPUBackendPlugin.meta()
        assert meta.kind == "cpu"
        assert meta.priority == 10
        assert "CPU" in meta.display_name

    def test_cpu_plugin_is_available(self) -> None:
        from sagellm_core.plugin.builtin import CPUBackendPlugin

        assert CPUBackendPlugin.is_available() is True

    def test_cpu_plugin_capabilities_stub(self) -> None:
        from sagellm_core.plugin.builtin import CPUBackendPlugin

        caps = CPUBackendPlugin.capabilities_stub()
        assert caps.device_type == "cpu"
        assert "fp32" in caps.supported_dtypes

    def test_custom_backend_plugin(self) -> None:
        """Sub-classable and self-describing."""
        from sagellm_backend import CapabilityDescriptor

        from sagellm_core.plugin.backend_plugin import BackendPlugin, BackendPluginMeta

        class MyBackend(BackendPlugin):
            _META = BackendPluginMeta(kind="test_hw", display_name="Test HW", priority=999)

            @classmethod
            def meta(cls):
                return cls._META

            @classmethod
            def is_available(cls):
                return True

            @classmethod
            def capabilities_stub(cls):
                return CapabilityDescriptor(
                    device_name="TestHW",
                    device_type="test",
                    compute_capability="1.0",
                    total_memory_bytes=1,
                    supported_dtypes=["fp32"],
                    supported_kernels=[],
                )

            def create_provider(self, **kwargs):
                return object()  # dummy

        p = MyBackend()
        assert p.kind() == "test_hw"
        assert p.priority() == 999
        assert "test_hw" in repr(p)


# ============================================================================
# EnginePlugin (#73 + #64 SGLang)
# ============================================================================


class TestEnginePlugin:
    def test_native_engine_meta(self) -> None:
        from sagellm_core.plugin.builtin import NativeEnginePlugin

        meta = NativeEnginePlugin.meta()
        assert meta.backend_type == "native"
        assert meta.priority == 200
        assert meta.supports_streaming is True

    def test_native_engine_is_available(self) -> None:
        from sagellm_core.plugin.builtin import NativeEnginePlugin

        assert NativeEnginePlugin.is_available() is True

    def test_native_engine_engine_class(self) -> None:
        from sagellm_core.adapters.native_adapter import NativeLLMEngineAdapter
        from sagellm_core.plugin.builtin import NativeEnginePlugin

        assert NativeEnginePlugin.engine_class() is NativeLLMEngineAdapter

    def test_sglang_engine_meta(self) -> None:
        """#64 SGLang Adapter plugin registered and metadata correct."""
        from sagellm_core.plugin.builtin import SGLangEnginePlugin

        meta = SGLangEnginePlugin.meta()
        assert meta.backend_type == "sglang"
        assert meta.priority == 65
        assert meta.supports_streaming is True

    def test_sglang_engine_class_importable(self) -> None:
        """SGLangAdapter must be importable (even if SGLang not installed)."""
        from sagellm_core.plugin.builtin import SGLangEnginePlugin

        # engine_class() may raise ImportError if sglang is not installed,
        # but the *class itself* must be importable.
        try:
            cls = SGLangEnginePlugin.engine_class()
            from sagellm_core.engine import BaseEngine

            assert issubclass(cls, BaseEngine)
        except ImportError:
            pass  # sglang not installed — expected in CI

    def test_vllm_engine_meta(self) -> None:
        from sagellm_core.plugin.builtin import vLLMEnginePlugin

        meta = vLLMEnginePlugin.meta()
        assert meta.backend_type == "vllm"
        assert meta.priority == 70

    def test_priority_ordering(self) -> None:
        """native (200) > vllm (70) > sglang (65) > lmdeploy (60)."""
        from sagellm_core.plugin.builtin import (
            LMDeployEnginePlugin,
            NativeEnginePlugin,
            SGLangEnginePlugin,
            vLLMEnginePlugin,
        )

        priorities = [
            NativeEnginePlugin.priority(),
            vLLMEnginePlugin.priority(),
            SGLangEnginePlugin.priority(),
            LMDeployEnginePlugin.priority(),
        ]
        assert priorities == sorted(priorities, reverse=True)

    def test_create_convenience(self) -> None:
        from sagellm_core.plugin.builtin import NativeEnginePlugin

        engine = NativeEnginePlugin.create(
            {"engine_id": "test-ep", "model_path": "demo-model", "device": "cpu"}
        )
        assert engine is not None
        assert engine.engine_id == "test-ep"


# ============================================================================
# SchedulerPlugin (#74)
# ============================================================================


class TestSchedulerPlugin:
    def test_fcfs_meta(self) -> None:
        from sagellm_core.plugin.builtin import FCFSSchedulerPlugin

        meta = FCFSSchedulerPlugin.meta()
        assert meta.name == "fcfs"
        assert "FCFS" in meta.display_name or "First" in meta.display_name

    def test_fcfs_policies(self) -> None:
        from sagellm_core.plugin.builtin import FCFSSchedulerPlugin

        policies = FCFSSchedulerPlugin.policy_classes()
        assert "fcfs" in policies

    def test_priority_policies(self) -> None:
        from sagellm_core.plugin.builtin import PrioritySchedulerPlugin

        policies = PrioritySchedulerPlugin.policy_classes()
        assert "priority" in policies

    def test_register_adds_to_global_registry(self) -> None:
        from sagellm_core.engine_core.scheduler.base import get_policy, list_policies
        from sagellm_core.plugin.builtin import FCFSSchedulerPlugin

        FCFSSchedulerPlugin.register()
        assert "fcfs" in list_policies()
        policy = get_policy("fcfs")
        assert policy is not None

    def test_auto_register_subclass(self) -> None:
        """auto_register=True on subclass auto-registers policies."""
        from sagellm_core.engine_core.scheduler.base import list_policies
        from sagellm_core.plugin.scheduler_plugin import SchedulerPlugin, SchedulerPluginMeta

        class TestAutoPolicy:
            def select_next(self, waiting, running, kv_bridge, config):
                return []

        class AutoPlugin(SchedulerPlugin, auto_register=True):
            _META = SchedulerPluginMeta(name="auto_test_policy", display_name="Auto Test")

            @classmethod
            def meta(cls):
                return cls._META

            @classmethod
            def policy_classes(cls):
                return {"auto_test_policy": TestAutoPolicy}

        # auto_register=True causes immediate registration
        assert "auto_test_policy" in list_policies()


# ============================================================================
# AcceleratorPlugin (#76)
# ============================================================================


class TestAcceleratorPlugin:
    def test_noop_meta(self) -> None:
        from sagellm_core.plugin.builtin import NoopAcceleratorPlugin

        meta = NoopAcceleratorPlugin.meta()
        assert meta.name == "noop"
        assert meta.cpu_compatible is True

    def test_noop_is_available(self) -> None:
        from sagellm_core.plugin.builtin import NoopAcceleratorPlugin

        assert NoopAcceleratorPlugin.is_available() is True

    def test_noop_apply_passthrough(self) -> None:
        from sagellm_core.plugin.builtin import NoopAcceleratorPlugin

        engine_stub = object()
        result = NoopAcceleratorPlugin().apply(engine_stub)
        assert result is engine_stub  # pass-through

    def test_backend_compatibility_empty_means_all(self) -> None:
        from sagellm_core.plugin.builtin import NoopAcceleratorPlugin

        # Empty compatible_backends → compatible with everything
        assert NoopAcceleratorPlugin.is_compatible_with_backend("cpu") is True
        assert NoopAcceleratorPlugin.is_compatible_with_backend("cuda") is True
        assert NoopAcceleratorPlugin.is_compatible_with_backend("ascend") is True

    def test_backend_compatibility_restriction(self) -> None:
        from sagellm_core.plugin.accelerator_plugin import (
            AcceleratorKind,
            AcceleratorPlugin,
            AcceleratorPluginMeta,
        )

        class CUDAOnlyAccel(AcceleratorPlugin):
            _META = AcceleratorPluginMeta(
                name="cuda_only",
                display_name="CUDA Only",
                kind=AcceleratorKind.KERNEL_FUSION,
                compatible_backends=frozenset({"cuda"}),
            )

            @classmethod
            def meta(cls):
                return cls._META

            @classmethod
            def is_available(cls):
                return True

            def apply(self, engine, **kwargs):
                return engine

        assert CUDAOnlyAccel.is_compatible_with_backend("cuda") is True
        assert CUDAOnlyAccel.is_compatible_with_backend("cpu") is False


# ============================================================================
# PluginRegistry
# ============================================================================


class TestPluginRegistry:
    def test_list_backends(self, registry) -> None:
        assert "cpu" in registry.list_backends()

    def test_available_backends(self, registry) -> None:
        assert "cpu" in registry.available_backends()

    def test_list_engines(self, registry) -> None:
        engines = registry.list_engines()
        assert "native" in engines
        assert "vllm" in engines
        assert "sglang" in engines

    def test_available_engines(self, registry) -> None:
        avail = registry.available_engines()
        assert "native" in avail
        # vllm/sglang may not be available in CI (no GPU/package)

    def test_list_schedulers(self, registry) -> None:
        assert "fcfs" in registry.list_schedulers()
        assert "priority" in registry.list_schedulers()

    def test_register_schedulers(self, registry) -> None:
        from sagellm_core.engine_core.scheduler.base import list_policies

        registry.register_schedulers()
        assert "fcfs" in list_policies()

    def test_register_engines_with_factory(self, registry) -> None:
        from sagellm_core.engine_factory import EngineFactory

        registry.register_engines()
        # native should always be registered
        backends = EngineFactory.all_backends()
        assert "native" in backends

    def test_available_accelerators(self, registry) -> None:
        accels = registry.available_accelerators()
        assert "noop" in accels

    def test_available_accelerators_backend_filter(self, registry) -> None:
        # noop is compatible with all backends
        accels_cpu = registry.available_accelerators(backend_kind="cpu")
        assert "noop" in accels_cpu

    def test_get_accelerator_plugins_sorted(self, registry) -> None:
        plugins = registry.get_accelerator_plugins(available_only=True)
        priorities = [p.priority() for p in plugins]
        assert priorities == sorted(priorities)

    def test_snapshot_repr(self, registry) -> None:
        snap = registry.snapshot()
        text = str(snap)
        assert "Backends" in text
        assert "Engines" in text
        assert "Schedulers" in text
        assert "Accelerators" in text

    def test_create_backend_unknown_raises(self, registry) -> None:
        with pytest.raises(KeyError):
            registry.create_backend("nonexistent_backend_xyz")

    def test_repr(self, registry) -> None:
        r = repr(registry)
        assert "PluginRegistry" in r


# ============================================================================
# PluginChain
# ============================================================================


class TestPluginChain:
    def test_build_native_cpu(self, registry) -> None:
        from sagellm_core.plugin import PluginChain, PluginChainConfig

        config = PluginChainConfig(
            backend_kind=None,
            engine_backend_type="native",
            scheduler_kind="fcfs",
            accelerator_names=[],
            model_path="demo-model-path",
            engine_id="test-chain",
            max_batch_size=4,
            discover_plugins=False,
        )
        chain = PluginChain.build(config, registry=registry)
        assert chain is not None
        assert chain.engine is not None

    def test_chain_engine_id(self, registry) -> None:
        from sagellm_core.plugin import PluginChain, PluginChainConfig

        config = PluginChainConfig(
            backend_kind=None,
            engine_backend_type="native",
            model_path="test-model",
            engine_id="my-engine-42",
            discover_plugins=False,
        )
        chain = PluginChain.build(config, registry=registry)
        assert chain.engine.engine_id == "my-engine-42"

    def test_chain_repr(self, registry) -> None:
        from sagellm_core.plugin import PluginChain, PluginChainConfig

        config = PluginChainConfig(
            backend_kind=None,
            engine_backend_type="native",
            model_path="test-model",
            discover_plugins=False,
        )
        chain = PluginChain.build(config, registry=registry)
        assert "PluginChain" in repr(chain)
        assert "NativeLLMEngineAdapter" in repr(chain)

    def test_chain_with_noop_accelerator(self, registry) -> None:
        """Noop accelerator should pass engine through unchanged."""
        from sagellm_core.plugin import PluginChain, PluginChainConfig

        config = PluginChainConfig(
            backend_kind=None,
            engine_backend_type="native",
            model_path="test-model",
            engine_id="noop-test",
            accelerator_names=["noop"],
            discover_plugins=False,
        )
        chain = PluginChain.build(config, registry=registry)
        # After noop, engine should still be functional
        from sagellm_core.engine import BaseEngine

        assert isinstance(chain.engine, BaseEngine)

    def test_chain_unknown_engine_raises(self, registry) -> None:
        from sagellm_core.plugin import PluginChain, PluginChainConfig

        config = PluginChainConfig(
            backend_kind=None,
            engine_backend_type="nonexistent_xyz",
            model_path="test-model",
            discover_plugins=False,
        )
        with pytest.raises((ValueError, RuntimeError)):
            PluginChain.build(config, registry=registry)


# ============================================================================
# SGLang adapter registered with EngineFactory (#64)
# ============================================================================


class TestSGLangAdapterRegistration:
    def test_sglang_registered_in_factory(self) -> None:
        """SGLangAdapter is auto-registered with EngineFactory at import time."""
        from sagellm_core.engine_factory import EngineFactory

        # Force import of sglang_adapter which registers it
        import sagellm_core.adapters.sglang_adapter  # noqa: F401

        backends = EngineFactory.all_backends()
        assert "sglang" in backends

    def test_sglang_adapter_has_correct_backend_type(self) -> None:
        from sagellm_core.adapters.sglang_adapter import SGLangAdapter

        assert SGLangAdapter.backend_type() == "sglang"

    def test_sglang_adapter_priority_relative_to_others(self) -> None:
        """Verify SGLang priority (65) is between vLLM and LMDeploy.

        Actual registered priorities:
            vLLM:     60
            SGLang:   65
            LMDeploy: 70
        All are in the 50-99 range for NVIDIA CUDA engines.
        """
        from sagellm_core.adapters.lmdeploy_adapter import LMDeployAdapter
        from sagellm_core.adapters.sglang_adapter import SGLangAdapter
        from sagellm_core.adapters.vllm_adapter import vLLMAdapter

        # SGLang priority (65) is between vLLM (60) and LMDeploy (70)
        vllm_prio = vLLMAdapter.priority()
        sglang_prio = SGLangAdapter.priority()
        lmdeploy_prio = LMDeployAdapter.priority()

        # All in CUDA range 50-99
        assert 50 <= vllm_prio <= 99
        assert 50 <= sglang_prio <= 99
        assert 50 <= lmdeploy_prio <= 99
        # All distinct
        assert len({vllm_prio, sglang_prio, lmdeploy_prio}) == 3

    def test_sglang_unavailable_without_cuda(self) -> None:
        """In CPU-only CI, SGLang should report unavailable."""
        from sagellm_core.adapters.sglang_adapter import SGLangAdapter

        # Either False (no GPU/sglang) or True (GPU + sglang installed)
        assert isinstance(SGLangAdapter.is_available(), bool)
