"""Architecture conformance tests for sageLLM.

╔════════════════════════════════════════════════════════════════════════════╗
║  🏗️ ARCHITECTURE TESTS - 架构合规性测试                                     ║
║                                                                            ║
║  这些测试确保多团队开发时遵循正确的代码架构：                                ║
║    1. 依赖层级（sagellm-protocol 是基础层，不能依赖其他 sagellm-*）          ║
║    2. 禁止循环导入                                                          ║
║    3. 禁止硬件 SDK 直接侵入核心逻辑                                         ║
║    4. 组件接口稳定性                                                        ║
╚════════════════════════════════════════════════════════════════════════════╝
"""

from __future__ import annotations

import importlib
import importlib.metadata
import sys

import pytest

# ============================================================================
# Dependency Hierarchy Tests
# ============================================================================

# Official dependency hierarchy (bottom to top):
# Level 0: sagellm-protocol (no sagellm-* dependencies)
# Level 1: sagellm-backend (depends on protocol)
# Level 2: sagellm-core (depends on protocol, backend)
# Level 3: sagellm-control-plane (depends on protocol, core)
# Level 4: sagellm-gateway (depends on protocol, control-plane)
# Level 5: sagellm (depends on all above)

DEPENDENCY_HIERARCHY = {
    "isagellm-protocol": {
        "level": 0,
        "allowed_sagellm_deps": [],
    },
    "isagellm-backend": {
        "level": 1,
        "allowed_sagellm_deps": ["isagellm-protocol"],
    },
    "isagellm-core": {
        "level": 2,
        "allowed_sagellm_deps": ["isagellm-protocol", "isagellm-backend"],
    },
    "isagellm-control-plane": {
        "level": 3,
        "allowed_sagellm_deps": ["isagellm-protocol", "isagellm-core"],
    },
    "isagellm-gateway": {
        "level": 4,
        "allowed_sagellm_deps": ["isagellm-protocol", "isagellm-control-plane"],
    },
    "isagellm": {
        "level": 5,
        "allowed_sagellm_deps": [
            "isagellm-protocol",
            "isagellm-backend",
            "isagellm-core",
            "isagellm-control-plane",
            "isagellm-gateway",
        ],
    },
}


@pytest.mark.architecture
class TestDependencyHierarchy:
    """Tests for verifying dependency hierarchy is correct."""

    def test_protocol_has_no_sagellm_deps(self) -> None:
        """Verify sagellm-protocol has no dependencies on other sagellm packages.

        Protocol is Level 0 - the foundation that all others depend on.
        """
        violations = self._check_package_deps("isagellm-protocol")
        assert not violations, (
            f"isagellm-protocol should have NO sagellm dependencies, found: {violations}"
        )

    def test_backend_deps_are_valid(self) -> None:
        """Verify sagellm-backend only depends on protocol."""
        violations = self._check_package_deps("isagellm-backend")
        assert not violations, f"isagellm-backend has invalid dependencies: {violations}"

    def test_core_deps_are_valid(self) -> None:
        """Verify sagellm-core only depends on protocol and backend."""
        violations = self._check_package_deps("isagellm-core")
        assert not violations, f"isagellm-core has invalid dependencies: {violations}"

    def _check_package_deps(self, package_name: str) -> list[str]:
        """Check if package has any invalid sagellm dependencies.

        Args:
            package_name: The package to check

        Returns:
            List of invalid dependency names (should be empty)
        """
        if package_name not in DEPENDENCY_HIERARCHY:
            return []

        allowed = DEPENDENCY_HIERARCHY[package_name]["allowed_sagellm_deps"]
        violations = []

        try:
            dist = importlib.metadata.distribution(package_name)
            requires = dist.requires or []

            for req in requires:
                # Skip dev/test extras (they have ; extra == "dev" etc)
                if "; extra ==" in req or ";extra==" in req:
                    continue

                # Extract package name from requirement string
                dep_name = (
                    req.split()[0]
                    .split(";")[0]
                    .split("[")[0]
                    .split(">")[0]
                    .split("<")[0]
                    .split("=")[0]
                    .split("!")[0]
                )

                # Check if it's a sagellm package
                if dep_name.startswith("isagellm"):
                    if dep_name not in allowed:
                        violations.append(dep_name)

        except importlib.metadata.PackageNotFoundError:
            pytest.skip(f"Package {package_name} not installed")

        return violations


# ============================================================================
# Import Isolation Tests
# ============================================================================


@pytest.mark.architecture
class TestImportIsolation:
    """Tests for verifying import isolation rules."""

    def test_protocol_importable_standalone(self) -> None:
        """Verify sagellm-protocol can be imported without other sagellm packages.

        This is critical for the Protocol-First development model.
        """
        # Clear any cached imports
        modules_to_remove = [m for m in sys.modules if m.startswith("sagellm_")]
        for m in modules_to_remove:
            del sys.modules[m]

        # Import protocol
        import sagellm_protocol

        # Verify key types are available
        assert hasattr(sagellm_protocol, "Request")
        assert hasattr(sagellm_protocol, "Response")
        assert hasattr(sagellm_protocol, "Metrics")

    def test_no_hardware_sdk_in_protocol(self) -> None:
        """Verify protocol doesn't import any hardware SDKs.

        Protocol should be pure Python with Pydantic only.
        """
        forbidden_imports = [
            "torch",
            "torch_npu",
            "tensorflow",
            "jax",
            "cuda",
            "nccl",
            "hccl",
        ]

        # Check loaded modules
        protocol_modules = [m for m in sys.modules if m.startswith("sagellm_protocol")]

        for mod_name in protocol_modules:
            mod = sys.modules.get(mod_name)
            if mod is None:
                continue

            # Check module's __dict__ for forbidden imports
            for forbidden in forbidden_imports:
                assert forbidden not in dir(mod), (
                    f"sagellm_protocol.{mod_name} should not import {forbidden}"
                )


# ============================================================================
# Component Interface Tests
# ============================================================================


@pytest.mark.architecture
class TestComponentInterfaces:
    """Tests for verifying component interfaces are stable and complete."""

    def test_backend_engine_interface(self) -> None:
        """Verify backend engines implement required interface."""
        from sagellm_core.engines import CPUEngine, HFCudaEngine

        required_methods = [
            "start",
            "stop",
            "execute",
            "stream",
            "health_check",
        ]

        required_properties = [
            "engine_id",
            "is_running",
        ]

        for engine_cls in [CPUEngine, HFCudaEngine]:
            for method in required_methods:
                assert hasattr(engine_cls, method), (
                    f"{engine_cls.__name__} missing method: {method}"
                )
                assert callable(getattr(engine_cls, method, None)) or isinstance(
                    getattr(type, method, None), property
                ), f"{engine_cls.__name__}.{method} should be callable"

            for prop in required_properties:
                assert hasattr(engine_cls, prop), f"{engine_cls.__name__} missing property: {prop}"

    def test_engine_factory_has_entry_points(self) -> None:
        """Verify sagellm.engines entry points are registered."""
        try:
            eps = importlib.metadata.entry_points(group="sagellm.engines")
            ep_names = [ep.name for ep in eps]

            # At minimum, should have cpu and hf-cuda
            assert "cpu" in ep_names, "Missing 'cpu' entry point"
            assert "hf-cuda" in ep_names, "Missing 'hf-cuda' entry point"

        except Exception as e:
            pytest.fail(f"Failed to get entry points: {e}")

    def test_core_create_engine_function(self) -> None:
        """Verify sagellm-core exports create_engine function."""
        from sagellm_core import EngineConfig, create_engine

        assert callable(create_engine)
        assert EngineConfig is not None


# ============================================================================
# Component Trace Verification
# ============================================================================


@pytest.mark.architecture
class TestComponentTraceArchitecture:
    """Tests for component trace architecture."""

    def test_component_trace_field_exists(self) -> None:
        """Verify Metrics has component_trace field."""
        from sagellm_protocol import Metrics

        assert "component_trace" in Metrics.model_fields
        field = Metrics.model_fields["component_trace"]

        # Should be list[str] | None
        assert field.annotation is not None

    def test_direct_backend_has_single_trace(self) -> None:
        """Verify direct backend calls produce single-component trace."""
        from sagellm_core.engines.cpu import CPUEngine, CPUEngineConfig

        config = CPUEngineConfig(engine_id="test-direct", model_path="sshleifer/tiny-gpt2")
        engine = CPUEngine(config)

        # Direct creation should NOT have _created_via_core
        assert not getattr(engine, "_created_via_core", False)

        # Build trace should have only backend
        trace = engine._build_component_trace()
        assert trace == ["sagellm-backend:CPUEngine"]
        assert "sagellm-core" not in trace

    def test_core_created_engine_has_full_trace(self) -> None:
        """Verify engine created via core has full trace."""
        from sagellm_core.engines.cpu import create_cpu_engine

        # Factory expects a dict-like config (Mapping) or a pydantic model.
        # Use a plain dict here to avoid relying on an ad-hoc object protocol.
        config = {
            "engine_id": "test-via-core",
            # create_cpu_engine 的 dict 输入通道期望使用 model/model_path 语义
            "model": "sshleifer/tiny-gpt2",
            "model_path": "sshleifer/tiny-gpt2",
        }

        engine = create_cpu_engine(config)

        assert getattr(engine, "_created_via_core", False)

        trace = engine._build_component_trace()
        assert "sagellm-core" in trace
        assert "sagellm-backend:CPUEngine" in trace


# ============================================================================
# Cross-Team Development Guidelines
# ============================================================================


@pytest.mark.architecture
class TestCrossTeamGuidelines:
    """Tests that enforce cross-team development guidelines.

    These tests help catch common mistakes when multiple teams
    are working on different sagellm repositories simultaneously.
    """

    def test_all_engines_have_component_trace(self) -> None:
        """Verify all engine implementations have _build_component_trace method.

        This ensures observability works across all backends.
        """
        from sagellm_core.engines import CPUEngine, HFCudaEngine

        for engine_cls in [CPUEngine, HFCudaEngine]:
            assert hasattr(engine_cls, "_build_component_trace"), (
                f"{engine_cls.__name__} must implement _build_component_trace()"
            )

    def test_factory_functions_set_core_flag(self) -> None:
        """Verify all factory functions set _created_via_core flag.

        This is critical for proper component tracing.
        """
        from sagellm_core.engines.cpu import create_cpu_engine

        # Test CPU factory (doesn't require GPU)
        config = {
            "engine_id": "test-factory",
            # create_cpu_engine 的 dict 输入通道期望使用 model/model_path 语义
            "model": "sshleifer/tiny-gpt2",
            "model_path": "sshleifer/tiny-gpt2",
        }

        engine = create_cpu_engine(config)
        assert getattr(engine, "_created_via_core", False), (
            "Factory function must set _created_via_core=True"
        )
