"""Tests for template gallery functionality."""

from __future__ import annotations

import json
import tempfile
from datetime import datetime
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock, patch

import pytest
import yaml

from gaol.specs import ContainerSpec
from gaol.templates import (
    BUILTIN_TEMPLATES,
    CategoryInfo,
    GalleryError,
    GalleryFetchError,
    GalleryIndex,
    GalleryManager,
    GalleryOfflineError,
    Template,
    TemplateEntry,
    TemplateExistsError,
    TemplateLoader,
    TemplateFetcher,
    TemplateMetadata,
    TemplateNotFoundError,
    TemplateParseError,
    TemplateStore,
    get_builtin_categories,
    get_builtin_template,
    get_template_loader,
    get_template_store,
    list_builtin_templates,
)


# ============================================================================
# TemplateMetadata Tests
# ============================================================================


class TestTemplateMetadata:
    """Tests for TemplateMetadata model."""

    def test_basic_metadata(self) -> None:
        """Test creating basic metadata."""
        meta = TemplateMetadata(
            id="web-dev/test",
            display_name="Test Template",
            description="A test template",
            category="web-dev",
            author="tester",
        )
        assert meta.id == "web-dev/test"
        assert meta.display_name == "Test Template"
        assert meta.version == "1.0.0"  # default
        assert meta.license == "MIT"  # default

    def test_metadata_with_tags(self) -> None:
        """Test metadata with tags."""
        meta = TemplateMetadata(
            id="data-science/test",
            display_name="Test",
            description="Test",
            category="data-science",
            author="tester",
            tags=["python", "ml", "jupyter"],
        )
        assert meta.tags == ["python", "ml", "jupyter"]

    def test_metadata_with_icon(self) -> None:
        """Test metadata with icon."""
        meta = TemplateMetadata(
            id="test/test",
            display_name="Test",
            description="Test",
            category="test",
            author="tester",
            icon="🐍",
        )
        assert meta.icon == "🐍"

    def test_id_parsing(self) -> None:
        """Test template ID parsing."""
        meta = TemplateMetadata(
            id="web-dev/my-template",
            display_name="Test",
            description="Test",
            category="web-dev",
            author="tester",
        )
        assert "/" in meta.id
        category, name = meta.id.split("/", 1)
        assert category == "web-dev"
        assert name == "my-template"


# ============================================================================
# Template Tests
# ============================================================================


class TestTemplate:
    """Tests for Template model."""

    def test_basic_template(self) -> None:
        """Test creating a basic template."""
        meta = TemplateMetadata(
            id="test/basic",
            display_name="Basic",
            description="Basic template",
            category="test",
            author="tester",
        )
        spec = ContainerSpec(name="test-container", runtime="docker")
        template = Template(metadata=meta, spec=spec)

        assert template.metadata.id == "test/basic"
        assert template.spec.name == "test-container"
        assert template.post_create_commands == []
        assert template.example_env == {}

    def test_template_with_post_create(self) -> None:
        """Test template with post-create commands."""
        meta = TemplateMetadata(
            id="test/post",
            display_name="Post",
            description="Template with post-create",
            category="test",
            author="tester",
        )
        spec = ContainerSpec(name="test", runtime="docker")
        template = Template(
            metadata=meta,
            spec=spec,
            post_create_commands=["pip install flask", "npm install"],
        )

        assert len(template.post_create_commands) == 2
        assert "pip install flask" in template.post_create_commands

    def test_template_from_dict(self) -> None:
        """Test creating template from dict."""
        data = {
            "metadata": {
                "id": "test/from-dict",
                "display_name": "From Dict",
                "description": "Created from dict",
                "category": "test",
                "author": "tester",
            },
            "spec": {
                "name": "test-container",
                "runtime": "docker",
                "distro": "trixie",
            },
        }
        template = Template.from_dict(data)
        assert template.metadata.id == "test/from-dict"
        assert template.spec.runtime == "docker"

    def test_template_to_dict(self) -> None:
        """Test converting template to dict."""
        meta = TemplateMetadata(
            id="test/to-dict",
            display_name="To Dict",
            description="Convert to dict",
            category="test",
            author="tester",
        )
        spec = ContainerSpec(name="test", runtime="docker")
        template = Template(metadata=meta, spec=spec)

        data = template.model_dump(mode="json")
        assert data["metadata"]["id"] == "test/to-dict"
        assert data["spec"]["runtime"] == "docker"


# ============================================================================
# TemplateEntry Tests
# ============================================================================


class TestTemplateEntry:
    """Tests for TemplateEntry model."""

    def test_basic_entry(self) -> None:
        """Test creating a basic entry."""
        entry = TemplateEntry(
            id="test/entry",
            display_name="Entry",
            description="Test entry",
            category="test",
            tags=["test"],
            author="tester",
            version="1.0.0",
            template_url="https://example.com/template.yaml",
        )
        assert entry.id == "test/entry"
        assert entry.template_url == "https://example.com/template.yaml"

    def test_entry_from_template(self) -> None:
        """Test creating entry from template."""
        meta = TemplateMetadata(
            id="test/from-template",
            display_name="From Template",
            description="Entry from template",
            category="test",
            author="tester",
            tags=["tag1", "tag2"],
        )
        spec = ContainerSpec(name="test", runtime="docker")
        template = Template(metadata=meta, spec=spec)

        entry = TemplateEntry.from_template(template, "https://example.com/template.yaml")
        assert entry.id == "test/from-template"
        assert entry.template_url == "https://example.com/template.yaml"
        assert entry.tags == ["tag1", "tag2"]


# ============================================================================
# GalleryIndex Tests
# ============================================================================


class TestGalleryIndex:
    """Tests for GalleryIndex model."""

    def test_empty_index(self) -> None:
        """Test creating empty index."""
        index = GalleryIndex(
            version="1",
            updated_at=datetime.now(),
            templates={},
            categories={},
        )
        assert len(index.templates) == 0
        assert len(index.categories) == 0

    def test_index_with_templates(self) -> None:
        """Test index with templates."""
        entry = TemplateEntry(
            id="test/template",
            display_name="Test",
            description="Test",
            category="test",
            tags=["test"],
            author="tester",
            version="1.0.0",
            template_url="https://example.com/t.yaml",
        )
        index = GalleryIndex(
            version="1",
            updated_at=datetime.now(),
            templates={"test/template": entry},
            categories={},
        )
        assert "test/template" in index.templates

    def test_index_search(self) -> None:
        """Test searching the index."""
        entries = {
            "test/python": TemplateEntry(
                id="test/python",
                display_name="Python Dev",
                description="Python development environment",
                category="test",
                tags=["python", "dev"],
                author="tester",
                version="1.0.0",
                template_url="https://example.com/python.yaml",
            ),
            "test/node": TemplateEntry(
                id="test/node",
                display_name="Node Dev",
                description="Node.js development environment",
                category="test",
                tags=["node", "javascript"],
                author="tester",
                version="1.0.0",
                template_url="https://example.com/node.yaml",
            ),
        }
        index = GalleryIndex(
            version="1",
            updated_at=datetime.now(),
            templates=entries,
            categories={},
        )

        # Search by name
        results = index.search("python")
        assert len(results) == 1
        assert results[0].id == "test/python"

        # Search by description
        results = index.search("development")
        assert len(results) == 2

    def test_index_get_by_category(self) -> None:
        """Test getting templates by category."""
        entries = {
            "web-dev/django": TemplateEntry(
                id="web-dev/django",
                display_name="Django",
                description="Django",
                category="web-dev",
                tags=[],
                author="tester",
                version="1.0.0",
                template_url="https://example.com/django.yaml",
            ),
            "data-science/jupyter": TemplateEntry(
                id="data-science/jupyter",
                display_name="Jupyter",
                description="Jupyter",
                category="data-science",
                tags=[],
                author="tester",
                version="1.0.0",
                template_url="https://example.com/jupyter.yaml",
            ),
        }
        index = GalleryIndex(
            version="1",
            updated_at=datetime.now(),
            templates=entries,
            categories={},
        )

        web_dev = index.get_by_category("web-dev")
        assert len(web_dev) == 1
        assert web_dev[0].id == "web-dev/django"


# ============================================================================
# Built-in Templates Tests
# ============================================================================


class TestBuiltinTemplates:
    """Tests for built-in templates."""

    def test_builtin_templates_exist(self) -> None:
        """Test that built-in templates are defined."""
        assert len(BUILTIN_TEMPLATES) > 0
        # Check some expected templates
        assert "web-dev/django" in BUILTIN_TEMPLATES
        assert "data-science/jupyter" in BUILTIN_TEMPLATES

    def test_all_builtin_templates_valid(self) -> None:
        """Test that all built-in templates can be loaded."""
        for template_id in BUILTIN_TEMPLATES:
            template_data = get_builtin_template(template_id)
            assert template_data is not None
            # Convert to Template object
            template = Template.from_dict(template_data)
            assert template.metadata.id == template_id
            assert template.spec is not None

    def test_list_builtin_templates(self) -> None:
        """Test listing built-in templates."""
        templates = list_builtin_templates()
        assert len(templates) > 0
        assert "web-dev/django" in templates

    def test_get_builtin_categories(self) -> None:
        """Test getting built-in categories."""
        categories = get_builtin_categories()
        assert len(categories) > 0
        assert "web-dev" in categories
        assert "data-science" in categories

    def test_django_template(self) -> None:
        """Test Django template configuration."""
        template_data = get_builtin_template("web-dev/django")
        assert template_data is not None
        template = Template.from_dict(template_data)
        assert template.metadata.display_name == "Django Development"
        assert "django" in template.metadata.tags
        assert "python" in template.metadata.tags

    def test_jupyter_template(self) -> None:
        """Test Jupyter template configuration."""
        template_data = get_builtin_template("data-science/jupyter")
        assert template_data is not None
        template = Template.from_dict(template_data)
        assert "jupyter" in template.metadata.tags or "jupyter" in template.metadata.id
        assert template.spec is not None


# ============================================================================
# TemplateStore Tests
# ============================================================================


class TestTemplateStore:
    """Tests for TemplateStore class."""

    def test_save_and_get_template(self, tmp_path: Path) -> None:
        """Test saving and retrieving a template."""
        store = TemplateStore(store_dir=tmp_path)

        meta = TemplateMetadata(
            id="custom/my-template",
            display_name="My Template",
            description="My custom template",
            category="custom",
            author="me",
        )
        spec = ContainerSpec(name="test", runtime="docker")
        template = Template(metadata=meta, spec=spec)

        # Save
        path = store.save(template)
        assert path.exists()
        assert path.suffix == ".yaml"

        # Get
        loaded = store.get("custom/my-template")
        assert loaded is not None
        assert loaded.metadata.id == "custom/my-template"
        assert loaded.metadata.display_name == "My Template"

    def test_save_existing_template_fails(self, tmp_path: Path) -> None:
        """Test that saving existing template fails by default."""
        store = TemplateStore(store_dir=tmp_path)

        meta = TemplateMetadata(
            id="custom/existing",
            display_name="Existing",
            description="Existing template",
            category="custom",
            author="me",
        )
        spec = ContainerSpec(name="test", runtime="docker")
        template = Template(metadata=meta, spec=spec)

        store.save(template)

        with pytest.raises(TemplateExistsError):
            store.save(template)

    def test_save_overwrite(self, tmp_path: Path) -> None:
        """Test overwriting existing template."""
        store = TemplateStore(store_dir=tmp_path)

        meta = TemplateMetadata(
            id="custom/overwrite",
            display_name="Original",
            description="Original template",
            category="custom",
            author="me",
        )
        spec = ContainerSpec(name="test", runtime="docker")
        template = Template(metadata=meta, spec=spec)

        store.save(template)

        # Modify and overwrite
        meta2 = meta.model_copy()
        meta2.display_name = "Updated"
        template2 = Template(metadata=meta2, spec=spec)
        store.save(template2, overwrite=True)

        loaded = store.get("custom/overwrite")
        assert loaded.metadata.display_name == "Updated"

    def test_delete_template(self, tmp_path: Path) -> None:
        """Test deleting a template."""
        store = TemplateStore(store_dir=tmp_path)

        meta = TemplateMetadata(
            id="custom/delete-me",
            display_name="Delete Me",
            description="Template to delete",
            category="custom",
            author="me",
        )
        spec = ContainerSpec(name="test", runtime="docker")
        template = Template(metadata=meta, spec=spec)

        store.save(template)
        assert store.exists("custom/delete-me")

        result = store.delete("custom/delete-me")
        assert result is True
        assert not store.exists("custom/delete-me")

    def test_list_templates(self, tmp_path: Path) -> None:
        """Test listing templates."""
        store = TemplateStore(store_dir=tmp_path)

        # Save some templates
        for i in range(3):
            meta = TemplateMetadata(
                id=f"custom/template-{i}",
                display_name=f"Template {i}",
                description=f"Template number {i}",
                category="custom",
                author="me",
            )
            spec = ContainerSpec(name="test", runtime="docker")
            template = Template(metadata=meta, spec=spec)
            store.save(template)

        templates = store.list()
        assert len(templates) == 3

    def test_list_by_category(self, tmp_path: Path) -> None:
        """Test listing templates by category."""
        store = TemplateStore(store_dir=tmp_path)

        # Save templates in different categories with unique IDs
        templates_to_create = [
            ("web-dev", "template-1"),
            ("data-science", "template-2"),
            ("web-dev", "template-3"),
        ]
        for cat, name in templates_to_create:
            meta = TemplateMetadata(
                id=f"{cat}/{name}",
                display_name=f"Template {name}",
                description=f"Template in {cat}",
                category=cat,
                author="me",
            )
            spec = ContainerSpec(name="test", runtime="docker")
            template = Template(metadata=meta, spec=spec)
            store.save(template)

        web_dev = store.list("web-dev")
        assert len(web_dev) == 2

        data_science = store.list("data-science")
        assert len(data_science) == 1

    def test_get_nonexistent_template(self, tmp_path: Path) -> None:
        """Test getting a template that doesn't exist."""
        store = TemplateStore(store_dir=tmp_path)
        result = store.get("nonexistent/template")
        assert result is None


# ============================================================================
# TemplateLoader Tests
# ============================================================================


class TestTemplateLoader:
    """Tests for TemplateLoader class."""

    def test_get_builtin_template(self, tmp_path: Path) -> None:
        """Test getting a built-in template."""
        store = TemplateStore(store_dir=tmp_path)
        loader = TemplateLoader(store=store, enable_remote=False)

        template = loader.get("web-dev/django")
        assert template.metadata.id == "web-dev/django"

    def test_get_user_template(self, tmp_path: Path) -> None:
        """Test getting a user template."""
        store = TemplateStore(store_dir=tmp_path)

        # Save a user template
        meta = TemplateMetadata(
            id="custom/user-template",
            display_name="User Template",
            description="A user template",
            category="custom",
            author="user",
        )
        spec = ContainerSpec(name="test", runtime="docker")
        template = Template(metadata=meta, spec=spec)
        store.save(template)

        loader = TemplateLoader(store=store, enable_remote=False)
        loaded = loader.get("custom/user-template")
        assert loaded.metadata.id == "custom/user-template"

    def test_get_nonexistent_template(self, tmp_path: Path) -> None:
        """Test getting a template that doesn't exist."""
        store = TemplateStore(store_dir=tmp_path)
        loader = TemplateLoader(store=store, enable_remote=False)

        with pytest.raises(TemplateNotFoundError) as exc_info:
            loader.get("nonexistent/template")

        assert "nonexistent/template" in str(exc_info.value)

    def test_list_local_templates(self, tmp_path: Path) -> None:
        """Test listing local templates."""
        store = TemplateStore(store_dir=tmp_path)
        loader = TemplateLoader(store=store, enable_remote=False)

        templates = loader.list_local()
        # Should include built-in templates
        ids = [t.metadata.id for t in templates]
        assert "web-dev/django" in ids

    def test_search_templates(self, tmp_path: Path) -> None:
        """Test searching templates."""
        store = TemplateStore(store_dir=tmp_path)
        loader = TemplateLoader(store=store, enable_remote=False)

        results = loader.search("django", include_remote=False)
        assert len(results) > 0
        assert any(r.id == "web-dev/django" for r in results)

    def test_is_builtin(self, tmp_path: Path) -> None:
        """Test checking if template is built-in."""
        store = TemplateStore(store_dir=tmp_path)
        loader = TemplateLoader(store=store, enable_remote=False)

        assert loader.is_builtin("web-dev/django") is True
        assert loader.is_builtin("custom/nonexistent") is False

    def test_template_caching(self, tmp_path: Path) -> None:
        """Test that templates are cached."""
        store = TemplateStore(store_dir=tmp_path)
        loader = TemplateLoader(store=store, enable_remote=False)

        t1 = loader.get("web-dev/django")
        t2 = loader.get("web-dev/django")
        assert t1 is t2  # Same object due to caching

    def test_clear_cache(self, tmp_path: Path) -> None:
        """Test clearing the cache."""
        store = TemplateStore(store_dir=tmp_path)
        loader = TemplateLoader(store=store, enable_remote=False)

        t1 = loader.get("web-dev/django")
        loader.clear_cache()
        t2 = loader.get("web-dev/django")
        assert t1 is not t2  # Different objects after cache clear

    def test_get_categories(self, tmp_path: Path) -> None:
        """Test getting categories."""
        store = TemplateStore(store_dir=tmp_path)
        loader = TemplateLoader(store=store, enable_remote=False)

        categories = loader.get_categories(include_remote=False)
        names = [c.name for c in categories]
        assert "web-dev" in names
        assert "data-science" in names


# ============================================================================
# TemplateFetcher Tests
# ============================================================================


class TestTemplateFetcher:
    """Tests for TemplateFetcher class."""

    def test_fetch_json(self, tmp_path: Path) -> None:
        """Test fetching and parsing JSON."""
        fetcher = TemplateFetcher(cache_dir=tmp_path)

        # Mock the fetch method
        with patch.object(fetcher, "fetch") as mock_fetch:
            mock_fetch.return_value = '{"key": "value", "number": 42}'

            result = fetcher.fetch_json("https://example.com/test.json")
            assert result == {"key": "value", "number": 42}

    def test_fetch_yaml(self, tmp_path: Path) -> None:
        """Test fetching and parsing YAML."""
        fetcher = TemplateFetcher(cache_dir=tmp_path)

        # Mock the fetch method
        with patch.object(fetcher, "fetch") as mock_fetch:
            mock_fetch.return_value = "key: value\nnumber: 42"

            result = fetcher.fetch_yaml("https://example.com/test.yaml")
            assert result == {"key": "value", "number": 42}

    def test_fetch_cached(self, tmp_path: Path) -> None:
        """Test fetching with caching."""
        fetcher = TemplateFetcher(cache_dir=tmp_path)

        # Mock the fetch method
        call_count = 0

        def mock_fetch(url: str) -> str:
            nonlocal call_count
            call_count += 1
            return "cached content"

        with patch.object(fetcher, "fetch", side_effect=mock_fetch):
            # First call should fetch
            result1 = fetcher.fetch_cached("https://example.com/test.txt")
            assert result1 == "cached content"
            assert call_count == 1

            # Second call should use cache
            result2 = fetcher.fetch_cached("https://example.com/test.txt")
            assert result2 == "cached content"
            assert call_count == 1  # No additional fetch

    def test_cache_key_generation(self, tmp_path: Path) -> None:
        """Test that cache keys are deterministic."""
        fetcher = TemplateFetcher(cache_dir=tmp_path)

        key1 = fetcher._get_cache_key("https://example.com/test")
        key2 = fetcher._get_cache_key("https://example.com/test")
        key3 = fetcher._get_cache_key("https://example.com/other")

        assert key1 == key2
        assert key1 != key3

    def test_clear_cache(self, tmp_path: Path) -> None:
        """Test clearing the cache."""
        fetcher = TemplateFetcher(cache_dir=tmp_path)

        # Create some cache files
        (tmp_path / "file1.txt").write_text("content1")
        (tmp_path / "file2.txt").write_text("content2")

        count = fetcher.clear_cache()
        assert count == 2
        assert list(tmp_path.glob("*")) == []

    def test_get_cache_size(self, tmp_path: Path) -> None:
        """Test getting cache size."""
        fetcher = TemplateFetcher(cache_dir=tmp_path)

        # Create some cache files
        (tmp_path / "file1.txt").write_text("12345")
        (tmp_path / "file2.txt").write_text("67890")

        size = fetcher.get_cache_size()
        assert size == 10


# ============================================================================
# GalleryManager Tests
# ============================================================================


class TestGalleryManager:
    """Tests for GalleryManager class."""

    def test_get_index_from_cache(self, tmp_path: Path) -> None:
        """Test getting index from cache."""
        cache_dir = tmp_path / "gallery"
        cache_dir.mkdir()

        # Create a cached index
        index_data = {
            "version": "1",
            "updated_at": datetime.now().isoformat(),
            "templates": {},
            "categories": {},
        }
        (cache_dir / "index.json").write_text(json.dumps(index_data))
        (cache_dir / "index.meta.json").write_text(
            json.dumps({"fetched_at": 9999999999, "url": "test"})  # Future time
        )

        manager = GalleryManager(cache_dir=cache_dir)
        index = manager.get_index()
        assert index.version == "1"

    def test_list_categories(self, tmp_path: Path) -> None:
        """Test listing categories from gallery."""
        cache_dir = tmp_path / "gallery"
        cache_dir.mkdir()

        # Create a cached index with categories and templates
        # Note: list_categories only returns categories that have templates
        index_data = {
            "version": "1",
            "updated_at": datetime.now().isoformat(),
            "templates": {
                "web-dev/example": {
                    "id": "web-dev/example",
                    "display_name": "Example",
                    "description": "Example template",
                    "category": "web-dev",
                    "tags": [],
                    "author": "tester",
                    "version": "1.0.0",
                    "template_url": "https://example.com/t.yaml",
                },
            },
            "categories": {
                "web-dev": {
                    "name": "web-dev",
                    "display_name": "Web Development",
                    "description": "Web development templates",
                },
            },
        }
        (cache_dir / "index.json").write_text(json.dumps(index_data))
        (cache_dir / "index.meta.json").write_text(
            json.dumps({"fetched_at": 9999999999, "url": "test"})
        )

        manager = GalleryManager(cache_dir=cache_dir)
        categories = manager.list_categories()
        assert len(categories) == 1
        assert categories[0].name == "web-dev"

    def test_search_templates(self, tmp_path: Path) -> None:
        """Test searching templates in gallery."""
        cache_dir = tmp_path / "gallery"
        cache_dir.mkdir()

        # Create a cached index with templates
        index_data = {
            "version": "1",
            "updated_at": datetime.now().isoformat(),
            "templates": {
                "web-dev/django": {
                    "id": "web-dev/django",
                    "display_name": "Django",
                    "description": "Django web framework",
                    "category": "web-dev",
                    "tags": ["python", "web"],
                    "author": "tester",
                    "version": "1.0.0",
                    "template_url": "https://example.com/django.yaml",
                },
            },
            "categories": {},
        }
        (cache_dir / "index.json").write_text(json.dumps(index_data))
        (cache_dir / "index.meta.json").write_text(
            json.dumps({"fetched_at": 9999999999, "url": "test"})
        )

        manager = GalleryManager(cache_dir=cache_dir)
        results = manager.search("django")
        assert len(results) == 1
        assert results[0].id == "web-dev/django"

    def test_clear_cache(self, tmp_path: Path) -> None:
        """Test clearing gallery cache."""
        cache_dir = tmp_path / "gallery"
        cache_dir.mkdir()

        # Create cache files
        (cache_dir / "index.json").write_text("{}")
        (cache_dir / "index.meta.json").write_text("{}")

        manager = GalleryManager(cache_dir=cache_dir)
        manager.clear_cache()

        assert not (cache_dir / "index.json").exists()
        assert not (cache_dir / "index.meta.json").exists()


# ============================================================================
# CategoryInfo Tests
# ============================================================================


class TestCategoryInfo:
    """Tests for CategoryInfo model."""

    def test_basic_category(self) -> None:
        """Test creating basic category info."""
        cat = CategoryInfo(name="web-dev", display_name="Web Development")
        assert cat.name == "web-dev"
        assert cat.display_name == "Web Development"

    def test_category_with_description(self) -> None:
        """Test category with description."""
        cat = CategoryInfo(
            name="data-science",
            display_name="Data Science",
            description="Templates for data science and ML",
            icon="📊",
        )
        assert cat.description == "Templates for data science and ML"
        assert cat.icon == "📊"


# ============================================================================
# Exception Tests
# ============================================================================


class TestExceptions:
    """Tests for template exceptions."""

    def test_template_not_found_error(self) -> None:
        """Test TemplateNotFoundError."""
        error = TemplateNotFoundError("test/template", ["builtin", "user"])
        assert "test/template" in str(error)
        assert error.template_id == "test/template"
        assert error.searched == ["builtin", "user"]

    def test_template_parse_error(self) -> None:
        """Test TemplateParseError."""
        error = TemplateParseError("test/template", "Invalid YAML")
        assert "test/template" in str(error)
        assert "Invalid YAML" in str(error)

    def test_template_exists_error(self) -> None:
        """Test TemplateExistsError."""
        error = TemplateExistsError("test/template", Path("/path/to/template.yaml"))
        assert "test/template" in str(error)

    def test_gallery_fetch_error(self) -> None:
        """Test GalleryFetchError."""
        error = GalleryFetchError("https://example.com", "Connection refused")
        assert error.url == "https://example.com"
        assert "Connection refused" in str(error)

    def test_gallery_offline_error(self) -> None:
        """Test GalleryOfflineError."""
        error = GalleryOfflineError("https://gallery.example.com", "Network unreachable")
        assert "gallery.example.com" in str(error)


# ============================================================================
# Global Loader Tests
# ============================================================================


class TestGlobalLoader:
    """Tests for global template loader."""

    def test_get_template_loader(self) -> None:
        """Test getting global loader instance."""
        loader1 = get_template_loader()
        loader2 = get_template_loader()
        # Should be the same instance
        assert loader1 is loader2

    def test_get_template_store(self) -> None:
        """Test getting global store instance."""
        store1 = get_template_store()
        store2 = get_template_store()
        # Should be the same instance
        assert store1 is store2


# ============================================================================
# Integration Tests
# ============================================================================


class TestIntegration:
    """Integration tests for template system."""

    def test_full_workflow(self, tmp_path: Path) -> None:
        """Test complete template workflow."""
        store = TemplateStore(store_dir=tmp_path)
        loader = TemplateLoader(store=store, enable_remote=False)

        # 1. Get a built-in template
        django = loader.get("web-dev/django")
        assert django is not None

        # 2. Modify and save as user template
        meta = TemplateMetadata(
            id="custom/my-django",
            display_name="My Django",
            description="My customized Django template",
            category="custom",
            author="me",
            tags=["python", "django", "custom"],
        )
        custom = Template(
            metadata=meta,
            spec=django.spec.model_copy(deep=True),
            post_create_commands=django.post_create_commands,
        )
        store.save(custom)

        # 3. Verify it can be loaded
        loaded = loader.get("custom/my-django")
        assert loaded.metadata.display_name == "My Django"

        # 4. Search should find it
        results = loader.search("my django", include_remote=False)
        assert any(r.id == "custom/my-django" for r in results)

        # 5. Delete it
        assert store.delete("custom/my-django")
        assert not store.exists("custom/my-django")

    def test_template_yaml_roundtrip(self, tmp_path: Path) -> None:
        """Test saving and loading template as YAML."""
        meta = TemplateMetadata(
            id="test/yaml-roundtrip",
            display_name="YAML Roundtrip",
            description="Test YAML serialization",
            category="test",
            author="tester",
            tags=["test", "yaml"],
        )
        spec = ContainerSpec(
            name="test-container",
            runtime="docker",
            distro="trixie",
            profiles=["base", "dev-python"],
        )
        template = Template(
            metadata=meta,
            spec=spec,
            post_create_commands=["pip install flask"],
            example_env={"DEBUG": "true"},
        )

        # Save as YAML
        yaml_path = tmp_path / "template.yaml"
        yaml_data = template.model_dump(mode="json")
        yaml_path.write_text(yaml.safe_dump(yaml_data, sort_keys=False))

        # Load from YAML
        loaded_data = yaml.safe_load(yaml_path.read_text())
        loaded = Template.from_dict(loaded_data)

        assert loaded.metadata.id == template.metadata.id
        assert loaded.spec.profiles == ["base", "dev-python"]
        assert loaded.post_create_commands == ["pip install flask"]
        assert loaded.example_env == {"DEBUG": "true"}
