"""Tests for features.py — calculate_feature_status with Apple Silicon fallback."""

import os
from unittest.mock import patch, AsyncMock

from routers.features import calculate_feature_status


class TestCalculateFeatureStatusDefaults:
    """calculate_feature_status uses .get() defaults for optional feature fields."""

    def test_missing_optional_fields_use_defaults(self):
        """A feature with only id, name, and requirements should not KeyError."""
        minimal_feature = {
            "id": "minimal",
            "name": "Minimal Feature",
            "requirements": {"vram_gb": 0, "services": [], "services_any": []},
        }
        result = calculate_feature_status(minimal_feature, [], None)

        assert result["id"] == "minimal"
        assert result["name"] == "Minimal Feature"
        assert result["description"] == ""
        assert result["icon"] == "Package"
        assert result["category"] == "other"
        assert result["setupTime"] == "Unknown"
        assert result["priority"] == 99


class TestCalculateFeatureStatusAppleFallback:

    def _make_feature(self, vram_gb=0):
        return {
            "id": "test-feat",
            "name": "Test Feature",
            "description": "A test feature",
            "icon": "Zap",
            "category": "inference",
            "setup_time": "5 min",
            "priority": 1,
            "requirements": {
                "vram_gb": vram_gb,
                "services": [],
                "services_any": [],
            },
            "enabled_services_all": ["required-svc"],
            "enabled_services_any": [],
        }

    def test_apple_fallback_uses_host_ram_when_gpu_info_none(self):
        """When GPU_BACKEND=apple and gpu_info is None, HOST_RAM_GB gates VRAM."""
        from routers.features import calculate_feature_status
        feature = self._make_feature(vram_gb=16)
        with patch.dict(os.environ, {"HOST_RAM_GB": "24", "GPU_BACKEND": "apple"}):
            with patch("routers.features.GPU_BACKEND", "apple"):
                result = calculate_feature_status(feature, [], None)
        assert result["requirements"]["vramOk"] is True
        assert result["status"] != "insufficient_vram"

    def test_apple_fallback_insufficient_when_ram_too_low(self):
        """When HOST_RAM_GB < feature vram_gb, feature is insufficient_vram."""
        from routers.features import calculate_feature_status
        feature = self._make_feature(vram_gb=32)
        with patch.dict(os.environ, {"HOST_RAM_GB": "16", "GPU_BACKEND": "apple"}):
            with patch("routers.features.GPU_BACKEND", "apple"):
                result = calculate_feature_status(feature, [], None)
        assert result["requirements"]["vramOk"] is False
        assert result["status"] == "insufficient_vram"

    def test_apple_fallback_not_triggered_on_linux(self):
        """HOST_RAM fallback does NOT apply on non-apple backends."""
        from routers.features import calculate_feature_status
        feature = self._make_feature(vram_gb=8)
        with patch.dict(os.environ, {"HOST_RAM_GB": "64", "GPU_BACKEND": "nvidia"}):
            with patch("routers.features.GPU_BACKEND", "nvidia"):
                result = calculate_feature_status(feature, [], None)
        # gpu_info is None, so gpu_vram_gb=0, which is < 8 → insufficient_vram on nvidia
        assert result["status"] == "insufficient_vram"


class TestApiFeaturesAppleFallback:
    """Tests for the endpoint-level Apple Silicon VRAM fallback in api_features()."""

    def test_api_features_apple_fallback_gpu_summary(self, test_client):
        """api_features() endpoint applies Apple Silicon HOST_RAM_GB fallback for GPU summary."""
        with patch.dict(os.environ, {"HOST_RAM_GB": "16", "GPU_BACKEND": "apple"}):
            with patch("routers.features.GPU_BACKEND", "apple"):
                with patch("routers.features.get_gpu_info", return_value=None):
                    with patch("helpers.get_all_services", new_callable=AsyncMock, return_value=[]):
                        response = test_client.get(
                            "/api/features",
                            headers=test_client.auth_headers,
                        )
        assert response.status_code == 200
        data = response.json()
        assert data["gpu"]["vramGb"] == 16.0


# --- calculate_feature_status general cases ---


class TestCalculateFeatureStatusGeneral:

    def _make_feature(self, vram_gb=0, services=None, services_any=None,
                      enabled_all=None, enabled_any=None):
        return {
            "id": "test-feat",
            "name": "Test Feature",
            "description": "A test feature",
            "icon": "Zap",
            "category": "inference",
            "setup_time": "5 min",
            "priority": 1,
            "requirements": {
                "vram_gb": vram_gb,
                "services": services or [],
                "services_any": services_any or [],
            },
            "enabled_services_all": enabled_all if enabled_all is not None else (services or []),
            "enabled_services_any": enabled_any if enabled_any is not None else (services_any or []),
        }

    def _make_service_status(self, sid, status="healthy"):
        from models import ServiceStatus
        return ServiceStatus(
            id=sid, name=sid, port=8080, external_port=8080, status=status,
        )

    def test_enabled_when_all_services_healthy(self):
        from routers.features import calculate_feature_status
        from models import GPUInfo

        gpu = GPUInfo(
            name="RTX 4090", memory_used_mb=2048, memory_total_mb=24576,
            memory_percent=8.3, utilization_percent=35, temperature_c=62,
            gpu_backend="nvidia",
        )
        feature = self._make_feature(vram_gb=8, services=["llama-server"],
                                     enabled_all=["llama-server"])
        services = [self._make_service_status("llama-server", "healthy")]

        with patch("routers.features.GPU_BACKEND", "nvidia"):
            result = calculate_feature_status(feature, services, gpu)
        assert result["status"] == "enabled"
        assert result["enabled"] is True

    def test_preserves_launch_and_enabled_service_metadata(self):
        from routers.features import calculate_feature_status
        from models import GPUInfo

        gpu = GPUInfo(
            name="RTX 4090", memory_used_mb=2048, memory_total_mb=24576,
            memory_percent=8.3, utilization_percent=35, temperature_c=62,
            gpu_backend="nvidia",
        )
        feature = self._make_feature(
            vram_gb=8,
            services=["llama-server"],
            enabled_all=["llama-server"],
        )
        feature["launch"] = {"type": "service", "service": "open-webui"}
        services = [self._make_service_status("llama-server", "healthy")]

        with patch("routers.features.GPU_BACKEND", "nvidia"):
            result = calculate_feature_status(feature, services, gpu)

        assert result["enabledServicesAll"] == ["llama-server"]
        assert result["enabledServicesAny"] == []
        assert result["launch"] == {"type": "service", "service": "open-webui"}

    def test_insufficient_vram(self):
        from routers.features import calculate_feature_status
        from models import GPUInfo

        gpu = GPUInfo(
            name="GTX 1050", memory_used_mb=1024, memory_total_mb=4096,
            memory_percent=25.0, utilization_percent=10, temperature_c=50,
            gpu_backend="nvidia",
        )
        # enabled_all must reference a service not in the service list so
        # is_enabled is False, allowing the vram check to be reached.
        feature = self._make_feature(vram_gb=16, services=[],
                                     enabled_all=["llama-server"])

        with patch("routers.features.GPU_BACKEND", "nvidia"):
            result = calculate_feature_status(feature, [], gpu)
        assert result["status"] == "insufficient_vram"
        assert result["requirements"]["vramOk"] is False

    def test_services_needed_when_deps_missing(self):
        from routers.features import calculate_feature_status
        from models import GPUInfo

        gpu = GPUInfo(
            name="RTX 4090", memory_used_mb=2048, memory_total_mb=24576,
            memory_percent=8.3, utilization_percent=35, temperature_c=62,
            gpu_backend="nvidia",
        )
        feature = self._make_feature(vram_gb=8, services=["whisper", "tts"],
                                     enabled_all=["whisper", "tts"])
        services = [self._make_service_status("whisper", "healthy")]

        with patch("routers.features.GPU_BACKEND", "nvidia"):
            result = calculate_feature_status(feature, services, gpu)
        assert result["status"] == "services_needed"
        assert "tts" in result["requirements"]["servicesMissing"]

    def test_available_when_vram_ok_but_not_enabled(self):
        from routers.features import calculate_feature_status
        from models import GPUInfo

        gpu = GPUInfo(
            name="RTX 4090", memory_used_mb=2048, memory_total_mb=24576,
            memory_percent=8.3, utilization_percent=35, temperature_c=62,
            gpu_backend="nvidia",
        )
        feature = self._make_feature(vram_gb=8, services=[],
                                     enabled_all=["some-service"])
        services = []

        with patch("routers.features.GPU_BACKEND", "nvidia"):
            result = calculate_feature_status(feature, services, gpu)
        assert result["status"] == "available"


class TestHermesFeatureContracts:
    @staticmethod
    def _service(service_id, status="healthy"):
        from models import ServiceStatus
        return ServiceStatus(
            id=service_id,
            name=service_id,
            port=9120 if service_id == "hermes-proxy" else 0,
            external_port=9120 if service_id == "hermes-proxy" else 0,
            status=status,
        )

    @staticmethod
    def _feature(feature_id):
        from pathlib import Path

        import yaml

        services_dir = Path(__file__).resolve().parents[2]
        manifest_name = "hermes-proxy" if feature_id == "hermes-sso" else "hermes"
        manifest = yaml.safe_load((services_dir / manifest_name / "manifest.yaml").read_text())
        return next(feature for feature in manifest["features"] if feature["id"] == feature_id)

    def test_agent_uses_authenticated_proxy_and_accepts_local_provider(self):
        services = [
            self._service("hermes"),
            self._service("hermes-proxy"),
            self._service("dashboard-api"),
            self._service("llama-server"),
        ]

        result = calculate_feature_status(self._feature("hermes-agent"), services, None)

        assert result["status"] == "enabled"
        assert result["launch"] == {"type": "service", "service": "hermes-proxy"}
        assert result["requirements"]["servicesAny"] == ["llama-server", "litellm"]

    def test_agent_accepts_litellm_without_llama_server(self):
        services = [
            self._service("hermes"),
            self._service("hermes-proxy"),
            self._service("dashboard-api"),
            self._service("litellm"),
        ]

        result = calculate_feature_status(self._feature("hermes-agent"), services, None)

        assert result["status"] == "enabled"
        assert result["requirements"]["servicesOk"] is True

    def test_agent_is_not_ready_without_auth_proxy(self):
        services = [
            self._service("hermes"),
            self._service("dashboard-api"),
            self._service("litellm"),
        ]

        result = calculate_feature_status(self._feature("hermes-agent"), services, None)

        assert result["status"] == "services_needed"
        assert result["enabled"] is False
        assert "hermes-proxy" in result["requirements"]["servicesMissing"]

    def test_sso_opens_access_management_and_requires_complete_chain(self):
        services = [
            self._service("hermes"),
            self._service("hermes-proxy"),
            self._service("dashboard-api"),
        ]

        result = calculate_feature_status(self._feature("hermes-sso"), services, None)

        assert result["status"] == "enabled"
        assert result["launch"] == {"type": "internal", "path": "/invites"}
        assert result["enabledServicesAll"] == ["hermes", "hermes-proxy", "dashboard-api"]


# --- /api/features/{feature_id}/enable ---


class TestFeatureEnableInstructions:

    def test_returns_instructions_for_known_feature(self, test_client, monkeypatch):
        test_features = [
            {"id": "chat", "name": "Chat", "description": "AI Chat",
             "icon": "MessageSquare", "category": "inference",
             "setup_time": "1 min", "priority": 1,
             "requirements": {"vram_gb": 0, "services": [], "services_any": []},
             "enabled_services_all": [], "enabled_services_any": []}
        ]
        monkeypatch.setattr("routers.features.FEATURES", test_features)

        resp = test_client.get(
            "/api/features/chat/enable",
            headers=test_client.auth_headers,
        )
        assert resp.status_code == 200
        data = resp.json()
        assert data["featureId"] == "chat"
        assert "instructions" in data
        assert "steps" in data["instructions"]

    def test_instruction_links_use_request_host(self, test_client, monkeypatch):
        test_features = [
            {"id": "documents", "name": "Documents", "description": "Document Q&A",
             "icon": "FileText", "category": "productivity",
             "setup_time": "2 min", "priority": 3,
             "requirements": {"vram_gb": 0, "services": [], "services_any": []},
             "enabled_services_all": [], "enabled_services_any": []}
        ]
        monkeypatch.setattr("routers.features.FEATURES", test_features)
        monkeypatch.setattr(
            "routers.features.SERVICES",
            {"open-webui": {"external_port": 3000}},
        )

        resp = test_client.get(
            "/api/features/documents/enable",
            headers={**test_client.auth_headers, "host": "dashboard.ods.local:3001"},
        )

        assert resp.status_code == 200
        data = resp.json()
        assert data["instructions"]["links"][0] == {
            "label": "Open Chat",
            "url": "http://dashboard.ods.local:3000",
        }

    def test_lan_web_instructions_are_explicit_about_ods_proxy(self, test_client, monkeypatch):
        test_features = [
            {"id": "lan-web", "name": "LAN web entry", "description": "LAN entry",
             "icon": "Globe", "category": "networking",
             "setup_time": "Ready", "priority": 1,
             "requirements": {"vram_gb": 0, "services": [], "services_any": []},
             "enabled_services_all": ["ods-proxy"], "enabled_services_any": []}
        ]
        monkeypatch.setattr("routers.features.FEATURES", test_features)
        monkeypatch.setattr(
            "routers.features.SERVICES",
            {"ods-proxy": {"external_port": 80}},
        )

        resp = test_client.get(
            "/api/features/lan-web/enable",
            headers={**test_client.auth_headers, "host": "dashboard.ods.local:3001"},
        )

        assert resp.status_code == 200
        data = resp.json()
        steps = " ".join(data["instructions"]["steps"])
        assert "ods-proxy" in steps
        assert "port 80" in steps
        assert data["instructions"]["links"][0] == {
            "label": "Open LAN entry",
            "url": "http://dashboard.ods.local:80",
        }

    def test_hermes_sso_instructions_open_access_management(self, test_client, monkeypatch):
        test_features = [
            {"id": "hermes-sso", "name": "Hermes Single Sign-On",
             "description": "Manage Hermes access", "icon": "Shield",
             "category": "privacy", "setup_time": "Ready", "priority": 5,
             "requirements": {"vram_gb": 0, "services": ["hermes", "hermes-proxy", "dashboard-api"]},
             "enabled_services_all": ["hermes", "hermes-proxy", "dashboard-api"]}
        ]
        monkeypatch.setattr("routers.features.FEATURES", test_features)

        resp = test_client.get(
            "/api/features/hermes-sso/enable",
            headers=test_client.auth_headers,
        )

        assert resp.status_code == 200
        instructions = resp.json()["instructions"]
        assert instructions["links"] == [{"label": "Manage Hermes access", "url": "/invites"}]
        assert "owner cards" in " ".join(instructions["steps"]).lower()

    def test_404_for_unknown_feature(self, test_client, monkeypatch):
        monkeypatch.setattr("routers.features.FEATURES", [])

        resp = test_client.get(
            "/api/features/nonexistent/enable",
            headers=test_client.auth_headers,
        )
        assert resp.status_code == 404
