"""Tests for user extension manifest scanner."""

from pathlib import Path

import yaml

from user_extensions import (
    _reset_cache,
    get_user_services_cached,
    scan_user_extension_services,
)


def _write_manifest(ext_dir: Path, manifest: dict) -> None:
    """Write a manifest.yaml into the given extension directory."""
    ext_dir.mkdir(parents=True, exist_ok=True)
    (ext_dir / "manifest.yaml").write_text(yaml.dump(manifest))


def _make_manifest(service_id: str, port: int = 8080, health: str = "/health",
                   name: str | None = None, default_host: str = "badhost") -> dict:
    """Build a minimal manifest dict."""
    svc: dict = {"id": service_id, "port": port, "health": health,
                 "default_host": default_host}
    if name is not None:
        svc["name"] = name
    return {"schema_version": "dream.services.v1", "service": svc}


# --- scan_user_extension_services ---


class TestScanUserExtensions:

    def test_scan_empty_dir(self, tmp_path):
        """Empty directory returns empty dict."""
        d = tmp_path / "user"
        d.mkdir()
        assert scan_user_extension_services(d) == {}

    def test_scan_nonexistent_dir(self, tmp_path):
        """Non-existent directory returns empty dict."""
        assert scan_user_extension_services(tmp_path / "nope") == {}

    def test_scan_enabled_extension(self, tmp_path):
        """Extension with compose.yaml + manifest returns correct config."""
        user_dir = tmp_path / "user"
        ext_dir = user_dir / "my-ext"
        _write_manifest(ext_dir, _make_manifest("my-ext", port=9090,
                                                 health="/api/health",
                                                 name="My Extension"))
        (ext_dir / "compose.yaml").write_text("services:\n  my-ext:\n    image: test\n")

        result = scan_user_extension_services(user_dir)
        assert "my-ext" in result
        cfg = result["my-ext"]
        assert cfg["host"] == "my-ext"
        assert cfg["port"] == 9090
        assert cfg["health"] == "/api/health"
        assert cfg["name"] == "My Extension"

    def test_scan_disabled_extension_skipped(self, tmp_path):
        """Extension with compose.yaml.disabled only is skipped."""
        user_dir = tmp_path / "user"
        ext_dir = user_dir / "my-ext"
        _write_manifest(ext_dir, _make_manifest("my-ext"))
        (ext_dir / "compose.yaml.disabled").write_text("services: {}\n")

        result = scan_user_extension_services(user_dir)
        assert result == {}

    def test_scan_missing_manifest_skipped(self, tmp_path):
        """Extension without manifest.yaml is skipped."""
        user_dir = tmp_path / "user"
        ext_dir = user_dir / "my-ext"
        ext_dir.mkdir(parents=True)
        (ext_dir / "compose.yaml").write_text("services: {}\n")

        result = scan_user_extension_services(user_dir)
        assert result == {}

    def test_scan_no_health_endpoint_included_with_empty_health(self, tmp_path):
        """Extension without health field is included with empty health."""
        user_dir = tmp_path / "user"
        ext_dir = user_dir / "my-ext"
        ext_dir.mkdir(parents=True)
        manifest = {"schema_version": "dream.services.v1",
                     "service": {"id": "my-ext", "port": 8080}}
        (ext_dir / "manifest.yaml").write_text(yaml.dump(manifest))
        (ext_dir / "compose.yaml").write_text("services: {}\n")

        result = scan_user_extension_services(user_dir)
        assert "my-ext" in result
        assert result["my-ext"]["health"] == ""
        assert result["my-ext"]["port"] == 8080

    def test_scan_health_path_validation(self, tmp_path):
        """Reject paths with .., @, ?, #, and scheme prefixes."""
        user_dir = tmp_path / "user"
        bad_paths = [
            "/health/../etc/passwd",
            "/health@evil.com",
            "/health?cmd=exec",
            "/health#fragment",
            "http://evil.com/health",
            "https://evil.com/health",
        ]
        for i, bad_path in enumerate(bad_paths):
            ext_id = f"ext-{i}"
            ext_dir = user_dir / ext_id
            _write_manifest(ext_dir, _make_manifest(ext_id, health=bad_path))
            (ext_dir / "compose.yaml").write_text("services: {}\n")

        result = scan_user_extension_services(user_dir)
        assert result == {}, f"Expected all bad paths rejected, got: {list(result.keys())}"

    def test_scan_host_is_service_id(self, tmp_path):
        """Returned host must be the directory name, not manifest default_host."""
        user_dir = tmp_path / "user"
        ext_dir = user_dir / "my-ext"
        _write_manifest(ext_dir, _make_manifest("my-ext",
                                                 default_host="evil.attacker.com"))
        (ext_dir / "compose.yaml").write_text("services: {}\n")

        result = scan_user_extension_services(user_dir)
        assert result["my-ext"]["host"] == "my-ext"

    def test_scan_name_fallback(self, tmp_path):
        """Extension without name in manifest falls back to service_id."""
        user_dir = tmp_path / "user"
        ext_dir = user_dir / "my-ext"
        # Manifest with no name field
        manifest = {"schema_version": "dream.services.v1",
                     "service": {"id": "my-ext", "port": 8080, "health": "/health"}}
        (ext_dir).mkdir(parents=True)
        (ext_dir / "manifest.yaml").write_text(yaml.dump(manifest))
        (ext_dir / "compose.yaml").write_text("services: {}\n")

        result = scan_user_extension_services(user_dir)
        assert result["my-ext"]["name"] == "my-ext"

    def test_scan_symlink_skipped(self, tmp_path):
        """Symlinked directories in user-extensions are skipped."""
        user_dir = tmp_path / "user"
        ext_dir = user_dir / "real-ext"
        _write_manifest(ext_dir, _make_manifest("real-ext"))
        (ext_dir / "compose.yaml").write_text("services: {}\n")

        (user_dir / "link-ext").symlink_to(ext_dir)

        result = scan_user_extension_services(user_dir)
        assert "real-ext" in result
        assert "link-ext" not in result

    def test_scan_invalid_service_id_skipped(self, tmp_path):
        """Directories with invalid service_id format are skipped."""
        user_dir = tmp_path / "user"
        for bad_name in ["UPPER", "-bad", "has spaces"]:
            ext_dir = user_dir / bad_name
            _write_manifest(ext_dir, _make_manifest("x", health="/health"))
            (ext_dir / "compose.yaml").write_text("services: {}\n")

        result = scan_user_extension_services(user_dir)
        assert result == {}


# --- Caching ---


class TestCaching:

    def setup_method(self):
        _reset_cache()

    def teardown_method(self):
        _reset_cache()

    def test_cache_returns_same_result(self, tmp_path):
        """Second call within TTL returns cached result without rescanning."""
        user_dir = tmp_path / "user"
        ext_dir = user_dir / "my-ext"
        _write_manifest(ext_dir, _make_manifest("my-ext"))
        (ext_dir / "compose.yaml").write_text("services: {}\n")

        r1 = get_user_services_cached(user_dir, ttl=60.0)
        assert "my-ext" in r1

        # Remove the extension directory — cache should still return old result
        import shutil
        shutil.rmtree(ext_dir)

        r2 = get_user_services_cached(user_dir, ttl=60.0)
        assert r2 == r1
        assert "my-ext" in r2

    def test_cache_ttl_expires(self, tmp_path, monkeypatch):
        """After TTL, cache rescans the directory."""
        user_dir = tmp_path / "user"
        ext_dir = user_dir / "my-ext"
        _write_manifest(ext_dir, _make_manifest("my-ext"))
        (ext_dir / "compose.yaml").write_text("services: {}\n")

        r1 = get_user_services_cached(user_dir, ttl=0.0)
        assert "my-ext" in r1

        # Remove and call with ttl=0 (always expired)
        import shutil
        shutil.rmtree(ext_dir)

        r2 = get_user_services_cached(user_dir, ttl=0.0)
        assert r2 == {}

    def test_reset_cache(self, tmp_path):
        """_reset_cache() clears cached data."""
        user_dir = tmp_path / "user"
        ext_dir = user_dir / "my-ext"
        _write_manifest(ext_dir, _make_manifest("my-ext"))
        (ext_dir / "compose.yaml").write_text("services: {}\n")

        r1 = get_user_services_cached(user_dir, ttl=300.0)
        assert "my-ext" in r1

        _reset_cache()

        # Remove the extension
        import shutil
        shutil.rmtree(ext_dir)

        r2 = get_user_services_cached(user_dir, ttl=300.0)
        assert r2 == {}
