"""Tests for VS Code devcontainer support."""

import json
import tempfile
from pathlib import Path

import pytest

from gaol.devcontainer import (
    DevcontainerConverter,
    DevcontainerLoader,
    DevcontainerMount,
    DevcontainerNotFoundError,
    DevcontainerParseError,
    DevcontainerSpec,
    DevcontainerBuild,
    DevcontainerCustomizations,
    DevcontainerVSCodeCustomizations,
    devcontainer_to_spec,
    find_devcontainer,
    load_devcontainer,
    save_devcontainer,
    spec_to_devcontainer,
    validate_devcontainer,
)
from gaol.specs.models import ContainerSpec, SpecNetwork


class TestDevcontainerMount:
    """Tests for DevcontainerMount model."""

    def test_basic_mount(self) -> None:
        """Test creating a basic mount."""
        mount = DevcontainerMount(
            type="bind",
            source="/home/user/projects",
            target="/workspace",
        )
        assert mount.type == "bind"
        assert mount.source == "/home/user/projects"
        assert mount.target == "/workspace"
        assert mount.readonly is False

    def test_readonly_mount(self) -> None:
        """Test creating a readonly mount."""
        mount = DevcontainerMount(
            type="bind",
            source="/etc/config",
            target="/config",
            readonly=True,
        )
        assert mount.readonly is True

    def test_to_string(self) -> None:
        """Test converting mount to string format."""
        mount = DevcontainerMount(
            type="bind",
            source="/home/user",
            target="/workspace",
        )
        assert mount.to_string() == "type=bind,source=/home/user,target=/workspace"

    def test_to_string_readonly(self) -> None:
        """Test converting readonly mount to string."""
        mount = DevcontainerMount(
            type="bind",
            source="/home/user",
            target="/workspace",
            readonly=True,
        )
        result = mount.to_string()
        assert "readonly" in result

    def test_from_string(self) -> None:
        """Test parsing mount from string."""
        mount = DevcontainerMount.from_string(
            "type=bind,source=/home/user,target=/workspace"
        )
        assert mount.type == "bind"
        assert mount.source == "/home/user"
        assert mount.target == "/workspace"

    def test_from_string_readonly(self) -> None:
        """Test parsing readonly mount from string."""
        mount = DevcontainerMount.from_string(
            "type=bind,source=/home/user,target=/workspace,readonly"
        )
        assert mount.readonly is True


class TestDevcontainerBuild:
    """Tests for DevcontainerBuild model."""

    def test_basic_build(self) -> None:
        """Test creating a basic build config."""
        build = DevcontainerBuild(
            dockerfile="Dockerfile",
            context=".",
        )
        assert build.dockerfile == "Dockerfile"
        assert build.context == "."

    def test_build_with_args(self) -> None:
        """Test build config with args."""
        build = DevcontainerBuild(
            dockerfile="Dockerfile.dev",
            args={"NODE_VERSION": "18", "DEBUG": "true"},
        )
        assert build.args["NODE_VERSION"] == "18"

    def test_to_json(self) -> None:
        """Test converting build to JSON."""
        build = DevcontainerBuild(
            dockerfile="Dockerfile",
            context=".",
            args={"VERSION": "1.0"},
        )
        data = build.to_json()
        assert data["dockerfile"] == "Dockerfile"
        assert data["args"]["VERSION"] == "1.0"


class TestDevcontainerSpec:
    """Tests for DevcontainerSpec model."""

    def test_minimal_spec(self) -> None:
        """Test creating a minimal spec with just image."""
        spec = DevcontainerSpec(image="python:3.11")
        assert spec.image == "python:3.11"
        assert spec.build is None
        assert spec.forward_ports == []

    def test_spec_with_name(self) -> None:
        """Test spec with name."""
        spec = DevcontainerSpec(
            name="My Dev Container",
            image="python:3.11",
        )
        assert spec.name == "My Dev Container"

    def test_spec_with_ports(self) -> None:
        """Test spec with forwarded ports."""
        spec = DevcontainerSpec(
            image="node:18",
            forward_ports=[3000, 8080, 5432],
        )
        assert spec.forward_ports == [3000, 8080, 5432]

    def test_spec_with_environment(self) -> None:
        """Test spec with environment variables."""
        spec = DevcontainerSpec(
            image="python:3.11",
            container_env={"DEBUG": "true"},
            remote_env={"EDITOR": "vim"},
        )
        assert spec.container_env["DEBUG"] == "true"
        assert spec.remote_env["EDITOR"] == "vim"

    def test_get_all_environment(self) -> None:
        """Test merging all environment variables."""
        spec = DevcontainerSpec(
            image="python:3.11",
            container_env={"A": "1"},
            remote_env={"B": "2"},
        )
        env = spec.get_all_environment()
        assert env["A"] == "1"
        assert env["B"] == "2"

    def test_spec_with_features(self) -> None:
        """Test spec with devcontainer features."""
        spec = DevcontainerSpec(
            image="python:3.11",
            features={
                "ghcr.io/devcontainers/features/git:1": {},
                "ghcr.io/devcontainers/features/github-cli:1": {"version": "latest"},
            },
        )
        assert len(spec.features) == 2

    def test_spec_with_lifecycle_commands(self) -> None:
        """Test spec with lifecycle commands."""
        spec = DevcontainerSpec(
            image="python:3.11",
            post_create_command="pip install -r requirements.txt",
            post_start_command=["echo", "Container started"],
        )
        assert spec.post_create_command == "pip install -r requirements.txt"
        assert spec.post_start_command == ["echo", "Container started"]

    def test_spec_with_customizations(self) -> None:
        """Test spec with VS Code customizations."""
        spec = DevcontainerSpec(
            image="python:3.11",
            customizations=DevcontainerCustomizations(
                vscode=DevcontainerVSCodeCustomizations(
                    extensions=["ms-python.python"],
                    settings={"python.linting.enabled": True},
                )
            ),
        )
        assert "ms-python.python" in spec.get_extensions()
        assert spec.get_settings()["python.linting.enabled"] is True

    def test_cannot_have_both_image_and_build(self) -> None:
        """Test that spec cannot have both image and build."""
        with pytest.raises(ValueError, match="Cannot specify both"):
            DevcontainerSpec(
                image="python:3.11",
                build=DevcontainerBuild(dockerfile="Dockerfile"),
            )

    def test_to_json(self) -> None:
        """Test converting spec to JSON."""
        spec = DevcontainerSpec(
            name="Test",
            image="python:3.11",
            forward_ports=[8000],
            container_env={"DEBUG": "true"},
            workspace_folder="/workspace",
        )
        data = spec.to_json()
        assert data["name"] == "Test"
        assert data["image"] == "python:3.11"
        assert data["forwardPorts"] == [8000]
        assert data["containerEnv"]["DEBUG"] == "true"
        assert data["workspaceFolder"] == "/workspace"

    def test_to_json_only_includes_set_values(self) -> None:
        """Test that to_json only includes explicitly set values."""
        spec = DevcontainerSpec(image="python:3.11")
        data = spec.to_json()
        assert "name" not in data
        assert "forwardPorts" not in data
        assert "containerEnv" not in data

    def test_from_json(self) -> None:
        """Test creating spec from JSON data."""
        data = {
            "name": "Test Dev",
            "image": "python:3.11",
            "forwardPorts": [8000, 5432],
            "containerEnv": {"DEBUG": "true"},
            "postCreateCommand": "pip install -r requirements.txt",
            "customizations": {
                "vscode": {
                    "extensions": ["ms-python.python"],
                }
            },
        }
        spec = DevcontainerSpec.from_json(data)
        assert spec.name == "Test Dev"
        assert spec.image == "python:3.11"
        assert spec.forward_ports == [8000, 5432]
        assert spec.container_env["DEBUG"] == "true"
        assert spec.post_create_command == "pip install -r requirements.txt"
        assert "ms-python.python" in spec.get_extensions()


class TestDevcontainerLoader:
    """Tests for DevcontainerLoader."""

    @pytest.fixture
    def temp_dir(self) -> Path:
        """Create a temporary directory for testing."""
        with tempfile.TemporaryDirectory() as tmpdir:
            yield Path(tmpdir)

    def test_find_devcontainer_standard_location(self, temp_dir: Path) -> None:
        """Test finding devcontainer.json in standard location."""
        devcontainer_dir = temp_dir / ".devcontainer"
        devcontainer_dir.mkdir()
        devcontainer_file = devcontainer_dir / "devcontainer.json"
        devcontainer_file.write_text('{"image": "python:3.11"}')

        loader = DevcontainerLoader(temp_dir)
        found = loader.find_devcontainer()
        assert found == devcontainer_file

    def test_find_devcontainer_root_location(self, temp_dir: Path) -> None:
        """Test finding .devcontainer.json in root."""
        devcontainer_file = temp_dir / ".devcontainer.json"
        devcontainer_file.write_text('{"image": "python:3.11"}')

        loader = DevcontainerLoader(temp_dir)
        found = loader.find_devcontainer()
        assert found == devcontainer_file

    def test_find_devcontainer_not_found(self, temp_dir: Path) -> None:
        """Test when no devcontainer.json exists."""
        loader = DevcontainerLoader(temp_dir)
        found = loader.find_devcontainer()
        assert found is None

    def test_load_devcontainer(self, temp_dir: Path) -> None:
        """Test loading devcontainer.json."""
        devcontainer_dir = temp_dir / ".devcontainer"
        devcontainer_dir.mkdir()
        devcontainer_file = devcontainer_dir / "devcontainer.json"
        devcontainer_file.write_text(json.dumps({
            "name": "Test",
            "image": "python:3.11",
            "forwardPorts": [8000],
        }))

        loader = DevcontainerLoader(temp_dir)
        spec = loader.load()
        assert spec.name == "Test"
        assert spec.image == "python:3.11"
        assert 8000 in spec.forward_ports

    def test_load_not_found(self, temp_dir: Path) -> None:
        """Test loading when file not found."""
        loader = DevcontainerLoader(temp_dir)
        with pytest.raises(DevcontainerNotFoundError):
            loader.load()

    def test_load_invalid_json(self, temp_dir: Path) -> None:
        """Test loading invalid JSON."""
        devcontainer_dir = temp_dir / ".devcontainer"
        devcontainer_dir.mkdir()
        devcontainer_file = devcontainer_dir / "devcontainer.json"
        devcontainer_file.write_text("not valid json")

        loader = DevcontainerLoader(temp_dir)
        with pytest.raises(DevcontainerParseError):
            loader.load()

    def test_load_jsonc_with_comments(self, temp_dir: Path) -> None:
        """Test loading JSONC with comments."""
        devcontainer_dir = temp_dir / ".devcontainer"
        devcontainer_dir.mkdir()
        devcontainer_file = devcontainer_dir / "devcontainer.json"
        devcontainer_file.write_text('''
        {
            // This is a comment
            "name": "Test",
            /* Multi-line
               comment */
            "image": "python:3.11"
        }
        ''')

        loader = DevcontainerLoader(temp_dir)
        spec = loader.load()
        assert spec.name == "Test"
        assert spec.image == "python:3.11"

    def test_save_devcontainer(self, temp_dir: Path) -> None:
        """Test saving devcontainer.json."""
        spec = DevcontainerSpec(
            name="Test",
            image="python:3.11",
            forward_ports=[8000],
        )

        loader = DevcontainerLoader(temp_dir)
        saved_path = loader.save(spec)

        assert saved_path.exists()
        assert saved_path.parent.name == ".devcontainer"

        # Verify content
        data = json.loads(saved_path.read_text())
        assert data["name"] == "Test"
        assert data["image"] == "python:3.11"
        assert data["forwardPorts"] == [8000]

    def test_validate_valid_devcontainer(self, temp_dir: Path) -> None:
        """Test validating a valid devcontainer.json."""
        devcontainer_dir = temp_dir / ".devcontainer"
        devcontainer_dir.mkdir()
        devcontainer_file = devcontainer_dir / "devcontainer.json"
        devcontainer_file.write_text(json.dumps({"image": "python:3.11"}))

        loader = DevcontainerLoader(temp_dir)
        errors = loader.validate()
        assert errors == []

    def test_validate_missing_image_and_build(self, temp_dir: Path) -> None:
        """Test validating devcontainer without image or build."""
        devcontainer_dir = temp_dir / ".devcontainer"
        devcontainer_dir.mkdir()
        devcontainer_file = devcontainer_dir / "devcontainer.json"
        devcontainer_file.write_text('{"name": "Test"}')

        loader = DevcontainerLoader(temp_dir)
        errors = loader.validate()
        assert len(errors) > 0


class TestDevcontainerConverter:
    """Tests for DevcontainerConverter."""

    def test_to_container_spec_basic(self) -> None:
        """Test converting devcontainer to container spec."""
        devcontainer = DevcontainerSpec(
            name="Test Dev",
            image="python:3.11",
            forward_ports=[8000, 5432],
            container_env={"DEBUG": "true"},
            workspace_folder="/workspace",
        )

        converter = DevcontainerConverter()
        spec = converter.to_container_spec(devcontainer)

        assert spec.name == "test-dev"  # Sanitized
        assert spec.image == "python:3.11"
        assert "8000:8000" in spec.network.ports
        assert "5432:5432" in spec.network.ports
        assert spec.environment["DEBUG"] == "true"
        assert spec.working_dir == "/workspace"

    def test_to_container_spec_with_name_override(self) -> None:
        """Test converting with name override."""
        devcontainer = DevcontainerSpec(
            name="Original Name",
            image="python:3.11",
        )

        converter = DevcontainerConverter()
        spec = converter.to_container_spec(devcontainer, name="my-container")

        assert spec.name == "my-container"

    def test_to_container_spec_with_mounts(self) -> None:
        """Test converting with mounts."""
        devcontainer = DevcontainerSpec(
            image="python:3.11",
            mounts=[
                DevcontainerMount(
                    type="bind",
                    source="/home/user/projects",
                    target="/workspace",
                ),
            ],
        )

        converter = DevcontainerConverter()
        spec = converter.to_container_spec(devcontainer)

        assert len(spec.volumes) == 1
        assert "/home/user/projects:/workspace" in spec.volumes

    def test_from_container_spec_basic(self) -> None:
        """Test converting container spec to devcontainer."""
        spec = ContainerSpec(
            name="my-dev",
            image="python:3.11",
            network=SpecNetwork(ports=["8000:8000"]),
            environment={"DEBUG": "true"},
            working_dir="/workspace",
        )

        converter = DevcontainerConverter()
        devcontainer = converter.from_container_spec(spec)

        assert devcontainer.name == "my-dev"
        assert devcontainer.image == "python:3.11"
        assert 8000 in devcontainer.forward_ports
        assert devcontainer.container_env["DEBUG"] == "true"
        assert devcontainer.workspace_folder == "/workspace"

    def test_from_container_spec_with_extensions(self) -> None:
        """Test converting with custom extensions."""
        spec = ContainerSpec(
            name="my-dev",
            image="python:3.11",
        )

        converter = DevcontainerConverter()
        devcontainer = converter.from_container_spec(
            spec,
            vscode_extensions=["ms-python.python", "charliermarsh.ruff"],
        )

        extensions = devcontainer.get_extensions()
        assert "ms-python.python" in extensions
        assert "charliermarsh.ruff" in extensions

    def test_from_container_spec_with_profile_extensions(self) -> None:
        """Test that profile extensions are added."""
        spec = ContainerSpec(
            name="my-dev",
            image="python:3.11",
            profiles=["dev-python"],
        )

        converter = DevcontainerConverter()
        devcontainer = converter.from_container_spec(spec)

        extensions = devcontainer.get_extensions()
        assert "ms-python.python" in extensions


class TestConvenienceFunctions:
    """Tests for convenience functions."""

    @pytest.fixture
    def temp_dir(self) -> Path:
        """Create a temporary directory."""
        with tempfile.TemporaryDirectory() as tmpdir:
            yield Path(tmpdir)

    def test_find_devcontainer_function(self, temp_dir: Path) -> None:
        """Test find_devcontainer convenience function."""
        devcontainer_dir = temp_dir / ".devcontainer"
        devcontainer_dir.mkdir()
        devcontainer_file = devcontainer_dir / "devcontainer.json"
        devcontainer_file.write_text('{"image": "python:3.11"}')

        found = find_devcontainer(temp_dir)
        assert found == devcontainer_file

    def test_validate_devcontainer_function(self, temp_dir: Path) -> None:
        """Test validate_devcontainer convenience function."""
        devcontainer_dir = temp_dir / ".devcontainer"
        devcontainer_dir.mkdir()
        devcontainer_file = devcontainer_dir / "devcontainer.json"
        devcontainer_file.write_text('{"image": "python:3.11"}')

        # Change to temp dir for the test
        import os
        original_cwd = os.getcwd()
        os.chdir(temp_dir)
        try:
            errors = validate_devcontainer()
            assert errors == []
        finally:
            os.chdir(original_cwd)

    def test_devcontainer_to_spec_function(self) -> None:
        """Test devcontainer_to_spec convenience function."""
        devcontainer = DevcontainerSpec(
            name="Test",
            image="python:3.11",
        )

        spec = devcontainer_to_spec(devcontainer)
        assert spec.name == "test"
        assert spec.image == "python:3.11"

    def test_spec_to_devcontainer_function(self) -> None:
        """Test spec_to_devcontainer convenience function."""
        spec = ContainerSpec(
            name="test",
            image="python:3.11",
        )

        devcontainer = spec_to_devcontainer(spec)
        assert devcontainer.name == "test"
        assert devcontainer.image == "python:3.11"


class TestRoundTrip:
    """Test round-trip conversions preserve data."""

    def test_devcontainer_to_spec_and_back(self) -> None:
        """Test converting devcontainer -> spec -> devcontainer."""
        original = DevcontainerSpec(
            name="My Dev",
            image="python:3.11",
            forward_ports=[8000, 5432],
            container_env={"DEBUG": "true", "LOG_LEVEL": "debug"},
            workspace_folder="/workspace",
        )

        converter = DevcontainerConverter()

        # Convert to spec
        spec = converter.to_container_spec(original)

        # Convert back
        result = converter.from_container_spec(spec)

        # Key fields should be preserved
        assert result.image == original.image
        assert set(result.forward_ports) == set(original.forward_ports)
        assert result.container_env == original.container_env
        assert result.workspace_folder == original.workspace_folder

    def test_spec_to_devcontainer_and_back(self) -> None:
        """Test converting spec -> devcontainer -> spec."""
        original = ContainerSpec(
            name="my-dev",
            image="python:3.11",
            network=SpecNetwork(ports=["8000:8000", "5432:5432"]),
            environment={"DEBUG": "true"},
            working_dir="/workspace",
        )

        converter = DevcontainerConverter()

        # Convert to devcontainer
        devcontainer = converter.from_container_spec(original)

        # Convert back
        result = converter.to_container_spec(devcontainer)

        # Key fields should be preserved
        assert result.name == original.name
        assert result.image == original.image
        assert result.environment == original.environment
        assert result.working_dir == original.working_dir
