"""Tests for config.py — manifest loading and service discovery."""

import logging

import pytest

import config
from config import _detect_container_default_gateway, load_extension_manifests, _read_manifest_file


VALID_MANIFEST = """\
schema_version: ods.services.v1
service:
  id: test-service
  name: Test Service
  port: 8080
  health: /health
  gpu_backends: [amd, nvidia]
  external_port_default: 8080
features:
  - id: test-feature
    name: Test Feature
    icon: Zap
    category: inference
    gpu_backends: [amd, nvidia]
"""


class TestReadManifestFile:

    def test_reads_yaml(self, tmp_path):
        f = tmp_path / "manifest.yaml"
        f.write_text(VALID_MANIFEST)
        data = _read_manifest_file(f)
        assert data["schema_version"] == "ods.services.v1"
        assert data["service"]["id"] == "test-service"

    def test_reads_json(self, tmp_path):
        import json
        f = tmp_path / "manifest.json"
        f.write_text(json.dumps({
            "schema_version": "ods.services.v1",
            "service": {"id": "json-svc", "name": "JSON", "port": 9090},
        }))
        data = _read_manifest_file(f)
        assert data["service"]["id"] == "json-svc"

    def test_rejects_non_dict_root(self, tmp_path):
        f = tmp_path / "manifest.yaml"
        f.write_text("- just\n- a\n- list\n")
        with pytest.raises(ValueError, match="object"):
            _read_manifest_file(f)


class TestHostAgentResolution:

    def test_detect_container_default_gateway_reads_little_endian_route(self, tmp_path):
        route = tmp_path / "route"
        route.write_text(
            "Iface\tDestination\tGateway\tFlags\tRefCnt\tUse\tMetric\tMask\tMTU\tWindow\tIRTT\n"
            "eth0\t00000000\t010012AC\t0003\t0\t0\t0\t00000000\t0\t0\t0\n",
            encoding="utf-8",
        )

        assert _detect_container_default_gateway(str(route)) == "172.18.0.1"

    def test_detect_container_default_gateway_requires_gateway_flag(self, tmp_path):
        route = tmp_path / "route"
        route.write_text(
            "Iface\tDestination\tGateway\tFlags\tRefCnt\tUse\tMetric\tMask\tMTU\tWindow\tIRTT\n"
            "eth0\t00000000\t010012AC\t0001\t0\t0\t0\t00000000\t0\t0\t0\n",
            encoding="utf-8",
        )

        assert _detect_container_default_gateway(str(route)) == ""

    def test_resolve_agent_host_honors_explicit_env(self, monkeypatch):
        monkeypatch.setenv("ODS_AGENT_HOST", "  10.9.8.7  ")
        monkeypatch.setattr(config, "_running_inside_container", lambda: True)
        monkeypatch.setattr(config, "_detect_container_default_gateway", lambda: "172.18.0.1")

        assert config._resolve_agent_host() == "10.9.8.7"

    def test_resolve_agent_host_uses_gateway_inside_container(self, monkeypatch):
        monkeypatch.delenv("ODS_AGENT_HOST", raising=False)
        monkeypatch.setattr(config, "_running_inside_container", lambda: True)
        monkeypatch.setattr(config, "_detect_container_default_gateway", lambda: "172.18.0.1")

        assert config._resolve_agent_host() == "172.18.0.1"

    def test_resolve_agent_host_falls_back_outside_container(self, monkeypatch):
        monkeypatch.delenv("ODS_AGENT_HOST", raising=False)
        monkeypatch.setattr(config, "_running_inside_container", lambda: False)
        monkeypatch.setattr(config, "_detect_container_default_gateway", lambda: "192.168.1.1")

        assert config._resolve_agent_host() == "host.docker.internal"


class TestLoadExtensionManifests:

    def test_loads_valid_manifest(self, tmp_path):
        svc_dir = tmp_path / "test-service"
        svc_dir.mkdir()
        (svc_dir / "manifest.yaml").write_text(VALID_MANIFEST)

        services, features, _ = load_extension_manifests(tmp_path, "nvidia")
        assert "test-service" in services
        assert services["test-service"]["port"] == 8080
        assert services["test-service"]["name"] == "Test Service"
        assert services["test-service"]["health"] == "/health"
        assert len(features) == 1
        assert features[0]["id"] == "test-feature"

    def test_external_port_default_zero_disables_external_port_fallback(self, tmp_path):
        svc_dir = tmp_path / "internal-service"
        svc_dir.mkdir()
        (svc_dir / "manifest.yaml").write_text(
            "schema_version: ods.services.v1\n"
            "service:\n"
            "  id: internal-service\n"
            "  name: Internal Service\n"
            "  port: 9119\n"
            "  external_port_default: 0\n"
            "  health: /api/status\n"
        )

        services, _, _ = load_extension_manifests(tmp_path, "nvidia")

        assert services["internal-service"]["port"] == 9119
        assert services["internal-service"]["external_port"] == 0

    def test_preserves_host_network_flag(self, tmp_path):
        svc_dir = tmp_path / "host-network-service"
        svc_dir.mkdir()
        (svc_dir / "manifest.yaml").write_text(
            "schema_version: ods.services.v1\n"
            "service:\n"
            "  id: host-network-service\n"
            "  name: Host Network Service\n"
            "  host_network: true\n"
            "  port: 0\n"
        )

        services, _, _ = load_extension_manifests(tmp_path, "nvidia")

        assert services["host-network-service"]["host_network"] is True

    def test_skips_wrong_schema_version(self, tmp_path):
        svc_dir = tmp_path / "old-service"
        svc_dir.mkdir()
        (svc_dir / "manifest.yaml").write_text(
            "schema_version: ods.services.v0\nservice:\n  id: old\n  port: 80\n"
        )

        services, features, errors = load_extension_manifests(tmp_path, "nvidia")
        assert len(services) == 0
        assert len(errors) == 1
        assert "Unsupported schema_version" in errors[0]["error"]

    def test_filters_by_gpu_backend(self, tmp_path):
        svc_dir = tmp_path / "nvidia-only"
        svc_dir.mkdir()
        (svc_dir / "manifest.yaml").write_text(
            "schema_version: ods.services.v1\n"
            "service:\n  id: nvidia-only\n  name: NVIDIA Only\n  port: 80\n"
            "  gpu_backends: [nvidia]\n"
        )

        services, _, _ = load_extension_manifests(tmp_path, "amd")
        assert len(services) == 0

        services, _, _ = load_extension_manifests(tmp_path, "nvidia")
        assert "nvidia-only" in services

    def test_empty_directory(self, tmp_path):
        services, features, _ = load_extension_manifests(tmp_path, "nvidia")
        assert services == {}
        assert features == []

    def test_nonexistent_directory(self, tmp_path):
        missing = tmp_path / "does-not-exist"
        services, features, _ = load_extension_manifests(missing, "nvidia")
        assert services == {}
        assert features == []

    def test_features_filtered_by_gpu(self, tmp_path):
        svc_dir = tmp_path / "mixed"
        svc_dir.mkdir()
        (svc_dir / "manifest.yaml").write_text(
            "schema_version: ods.services.v1\n"
            "service:\n  id: mixed\n  name: Mixed\n  port: 80\n"
            "  gpu_backends: [amd, nvidia]\n"
            "features:\n"
            "  - id: amd-feat\n    name: AMD Feature\n    gpu_backends: [amd]\n"
            "  - id: both-feat\n    name: Both Feature\n    gpu_backends: [amd, nvidia]\n"
        )

        _, features, _ = load_extension_manifests(tmp_path, "nvidia")
        feature_ids = [f["id"] for f in features]
        assert "both-feat" in feature_ids
        assert "amd-feat" not in feature_ids

    def test_apple_backend_discovers_services_without_explicit_list(self, tmp_path):
        """Services with no gpu_backends key default to [amd, nvidia, apple]."""
        svc_dir = tmp_path / "generic-svc"
        svc_dir.mkdir()
        (svc_dir / "manifest.yaml").write_text(
            "schema_version: ods.services.v1\n"
            "service:\n  id: generic-svc\n  name: Generic\n  port: 80\n"
        )

        services, _, _ = load_extension_manifests(tmp_path, "apple")
        assert "generic-svc" in services

    def test_apple_backend_filtered_by_explicit_nvidia_amd_list(self, tmp_path):
        """Docker service explicitly listing [amd, nvidia] is still loaded for apple backend."""
        svc_dir = tmp_path / "gpu-only-svc"
        svc_dir.mkdir()
        (svc_dir / "manifest.yaml").write_text(
            "schema_version: ods.services.v1\n"
            "service:\n  id: gpu-only-svc\n  name: GPU Only\n  port: 80\n"
            "  gpu_backends: [amd, nvidia]\n"
        )

        services, _, _ = load_extension_manifests(tmp_path, "apple")
        assert "gpu-only-svc" in services

    def test_apple_backend_discovers_service_explicitly_listing_apple(self, tmp_path):
        """Service that lists apple in gpu_backends is discovered for apple backend."""
        svc_dir = tmp_path / "apple-svc"
        svc_dir.mkdir()
        (svc_dir / "manifest.yaml").write_text(
            "schema_version: ods.services.v1\n"
            "service:\n  id: apple-svc\n  name: Apple Svc\n  port: 80\n"
            "  gpu_backends: [amd, nvidia, apple]\n"
        )

        services, _, _ = load_extension_manifests(tmp_path, "apple")
        assert "apple-svc" in services

    def test_apple_backend_feature_default_discovered(self, tmp_path):
        """Features with no gpu_backends key default to include apple."""
        svc_dir = tmp_path / "svc-with-feature"
        svc_dir.mkdir()
        (svc_dir / "manifest.yaml").write_text(
            "schema_version: ods.services.v1\n"
            "service:\n  id: svc\n  name: Svc\n  port: 80\n"
            "features:\n"
            "  - id: default-feat\n    name: Default Feature\n"
        )

        _, features, _ = load_extension_manifests(tmp_path, "apple")
        assert any(f["id"] == "default-feat" for f in features)

    def test_apple_backend_excludes_host_systemd(self, tmp_path):
        """Services with type: host-systemd are excluded on apple backend."""
        svc_dir = tmp_path / "systemd-svc"
        svc_dir.mkdir()
        (svc_dir / "manifest.yaml").write_text(
            "schema_version: ods.services.v1\n"
            "service:\n  id: systemd-svc\n  name: Systemd Svc\n  port: 80\n"
            "  type: host-systemd\n"
            "  gpu_backends: [amd, nvidia]\n"
        )

        services, _, _ = load_extension_manifests(tmp_path, "apple")
        assert "systemd-svc" not in services

    def test_apple_backend_loads_all_features(self, tmp_path):
        """Features with gpu_backends: [amd, nvidia] are loaded for apple backend."""
        svc_dir = tmp_path / "svc-with-gpu-feature"
        svc_dir.mkdir()
        (svc_dir / "manifest.yaml").write_text(
            "schema_version: ods.services.v1\n"
            "service:\n  id: svc\n  name: Svc\n  port: 80\n"
            "features:\n"
            "  - id: gpu-feat\n    name: GPU Feature\n    gpu_backends: [amd, nvidia]\n"
        )

        _, features, _ = load_extension_manifests(tmp_path, "apple")
        assert any(f["id"] == "gpu-feat" for f in features)

    def test_warns_on_missing_optional_feature_fields(self, tmp_path, caplog):
        """A feature missing optional fields is loaded but a warning is logged."""
        svc_dir = tmp_path / "sparse-svc"
        svc_dir.mkdir()
        (svc_dir / "manifest.yaml").write_text(
            "schema_version: ods.services.v1\n"
            "service:\n  id: sparse-svc\n  name: Sparse\n  port: 80\n"
            "features:\n"
            "  - id: sparse-feat\n    name: Sparse Feature\n"
        )

        with caplog.at_level(logging.WARNING, logger="config"):
            _, features, _ = load_extension_manifests(tmp_path, "nvidia")

        assert any(f["id"] == "sparse-feat" for f in features)
        warning_msgs = [r.message for r in caplog.records if "missing optional fields" in r.message]
        assert len(warning_msgs) == 1
        assert "sparse-feat" in warning_msgs[0]
        for field in ("description", "icon", "category", "setup_time", "priority"):
            assert field in warning_msgs[0]

    def test_collects_parse_errors(self, tmp_path):
        svc_dir = tmp_path / "broken-svc"
        svc_dir.mkdir()
        (svc_dir / "manifest.yaml").write_text("invalid: yaml: [unterminated")

        services, features, errors = load_extension_manifests(tmp_path, "nvidia")
        assert len(errors) == 1
        assert "file" in errors[0]
        assert "error" in errors[0]
        assert "broken-svc" in errors[0]["file"]
        assert services == {}
        assert features == []

    def test_collects_error_for_missing_service_id(self, tmp_path):
        """A manifest with a valid schema but no service.id collects an error."""
        svc_dir = tmp_path / "no-id-svc"
        svc_dir.mkdir()
        (svc_dir / "manifest.yaml").write_text(
            "schema_version: ods.services.v1\n"
            "service:\n  name: No ID\n  port: 80\n"
        )

        services, features, errors = load_extension_manifests(tmp_path, "nvidia")
        assert len(errors) == 1
        assert "service.id is required" in errors[0]["error"]
        assert "no-id-svc" in errors[0]["file"]
        assert services == {}
