"""Tests for container build functionality."""

from __future__ import annotations

import json
from datetime import datetime
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch, PropertyMock

import pytest

from gaol.build import (
    # Models
    BuildConfig,
    BuildMethod,
    BuildResult,
    DockerfileInstruction,
    GeneratedDockerfile,
    ImageInfo,
    ImageLayer,
    PackageManager,
    PushProgress,
    PushResult,
    # Functions
    build_image,
    generate_dockerfile,
    get_image_info,
    get_image_history,
    push_image,
    push_image_with_progress,
    # Classes
    DockerfileGenerator,
    ImageBuilder,
    RegistryClient,
    # Exceptions
    BaseImageNotFoundError,
    BuildError,
    ContainerNotFoundError,
    ContainerNotRunningError,
    DockerfileGenerationError,
    ImageBuildError,
    ImageNotFoundError,
    ImageTagError,
    PackageDetectionError,
    RegistryAuthenticationError,
    RegistryError,
    RegistryPushError,
    UnsupportedRuntimeError,
    # Constants
    BUILD_REPO_PREFIX,
)


# =============================================================================
# Model Tests
# =============================================================================


class TestBuildMethod:
    """Tests for BuildMethod enum."""

    def test_commit_value(self):
        """Test COMMIT enum value."""
        assert BuildMethod.COMMIT.value == "commit"

    def test_dockerfile_value(self):
        """Test DOCKERFILE enum value."""
        assert BuildMethod.DOCKERFILE.value == "dockerfile"


class TestPackageManager:
    """Tests for PackageManager enum."""

    def test_all_values(self):
        """Test all package manager values."""
        assert PackageManager.APT.value == "apt"
        assert PackageManager.APK.value == "apk"
        assert PackageManager.DNF.value == "dnf"
        assert PackageManager.PACMAN.value == "pacman"
        assert PackageManager.UNKNOWN.value == "unknown"


class TestBuildConfig:
    """Tests for BuildConfig model."""

    def test_defaults(self):
        """Test default values."""
        config = BuildConfig(container_name="test")
        assert config.container_name == "test"
        assert config.base_image is None
        assert config.tag == "latest"
        assert config.repository is None
        assert config.include_packages is True
        assert config.include_environment is True
        assert config.labels == {}
        assert config.method == BuildMethod.COMMIT

    def test_custom_values(self):
        """Test custom values."""
        config = BuildConfig(
            container_name="myapp",
            base_image="debian:bookworm",
            tag="v1.0",
            repository="myrepo",
            include_packages=False,
            labels={"version": "1.0"},
            method=BuildMethod.DOCKERFILE,
        )
        assert config.container_name == "myapp"
        assert config.base_image == "debian:bookworm"
        assert config.tag == "v1.0"
        assert config.repository == "myrepo"
        assert config.include_packages is False
        assert config.labels == {"version": "1.0"}
        assert config.method == BuildMethod.DOCKERFILE


class TestDockerfileInstruction:
    """Tests for DockerfileInstruction model."""

    def test_basic_instruction(self):
        """Test basic instruction."""
        inst = DockerfileInstruction(
            instruction="FROM",
            arguments="debian:trixie",
        )
        assert inst.instruction == "FROM"
        assert inst.arguments == "debian:trixie"
        assert inst.comment is None

    def test_render_without_comment(self):
        """Test rendering without comment."""
        inst = DockerfileInstruction(
            instruction="RUN",
            arguments="apt-get update",
        )
        assert inst.render() == "RUN apt-get update"

    def test_render_with_comment(self):
        """Test rendering with comment."""
        inst = DockerfileInstruction(
            instruction="RUN",
            arguments="apt-get install -y vim",
            comment="Install editor",
        )
        rendered = inst.render()
        assert "# Install editor" in rendered
        assert "RUN apt-get install -y vim" in rendered


class TestGeneratedDockerfile:
    """Tests for GeneratedDockerfile model."""

    def test_defaults(self):
        """Test default values."""
        dockerfile = GeneratedDockerfile(
            container_name="test",
            base_image="debian:trixie",
        )
        assert dockerfile.container_name == "test"
        assert dockerfile.base_image == "debian:trixie"
        assert dockerfile.instructions == []
        assert dockerfile.packages == []
        assert dockerfile.package_manager == PackageManager.UNKNOWN
        assert dockerfile.environment == {}
        assert dockerfile.exposed_ports == []
        assert dockerfile.working_dir is None
        assert dockerfile.entrypoint is None
        assert dockerfile.cmd is None

    def test_render(self):
        """Test rendering complete dockerfile."""
        dockerfile = GeneratedDockerfile(
            container_name="myapp",
            base_image="debian:trixie",
            instructions=[
                DockerfileInstruction(
                    instruction="FROM", arguments="debian:trixie"
                ),
                DockerfileInstruction(
                    instruction="RUN",
                    arguments="apt-get update",
                    comment="Update packages",
                ),
            ],
        )
        content = dockerfile.render()
        assert "# Auto-generated by gaol" in content
        assert "myapp" in content
        assert "FROM debian:trixie" in content
        assert "# Update packages" in content
        assert "RUN apt-get update" in content


class TestImageLayer:
    """Tests for ImageLayer model."""

    def test_basic_layer(self):
        """Test basic layer."""
        layer = ImageLayer(
            id="abc123",
            created_by="RUN apt-get update",
            size_bytes=1024,
        )
        assert layer.id == "abc123"
        assert layer.created_by == "RUN apt-get update"
        assert layer.size_bytes == 1024
        assert layer.created_at is None
        assert layer.comment is None


class TestImageInfo:
    """Tests for ImageInfo model."""

    def test_defaults(self):
        """Test default values."""
        info = ImageInfo(id="sha256:abc123")
        assert info.id == "sha256:abc123"
        assert info.tags == []
        assert info.size_bytes == 0
        assert info.created_at is None
        assert info.layers == []
        assert info.architecture is None
        assert info.os == "linux"
        assert info.labels == {}

    def test_layer_count(self):
        """Test layer_count property."""
        info = ImageInfo(
            id="sha256:abc",
            layers=[
                ImageLayer(id="l1", size_bytes=100),
                ImageLayer(id="l2", size_bytes=200),
            ],
        )
        assert info.layer_count == 2

    def test_primary_tag(self):
        """Test primary_tag property."""
        info = ImageInfo(id="sha256:abc", tags=["myapp:latest", "myapp:v1"])
        assert info.primary_tag == "myapp:latest"

        info_no_tags = ImageInfo(id="sha256:abc")
        assert info_no_tags.primary_tag is None


class TestBuildResult:
    """Tests for BuildResult model."""

    def test_successful_result(self):
        """Test successful build result."""
        now = datetime.now()
        result = BuildResult(
            success=True,
            image_id="sha256:abc123",
            tags=["myapp:latest"],
            started_at=now,
        )
        assert result.success is True
        assert result.image_id == "sha256:abc123"
        assert result.error is None

    def test_failed_result(self):
        """Test failed build result."""
        result = BuildResult(
            success=False,
            error="Container not found",
        )
        assert result.success is False
        assert result.error == "Container not found"

    def test_duration(self):
        """Test duration calculation."""
        started = datetime(2024, 1, 1, 12, 0, 0)
        finished = datetime(2024, 1, 1, 12, 0, 30)
        result = BuildResult(
            success=True,
            started_at=started,
            finished_at=finished,
        )
        assert result.duration_seconds == 30.0

    def test_duration_not_finished(self):
        """Test duration when not finished."""
        result = BuildResult(success=True)
        assert result.duration_seconds == 0.0


class TestPushResult:
    """Tests for PushResult model."""

    def test_successful_push(self):
        """Test successful push result."""
        result = PushResult(
            success=True,
            image_tag="myapp:latest",
            registry="docker.io",
            digest="sha256:abc123",
            size_bytes=1024 * 1024,
        )
        assert result.success is True
        assert result.digest == "sha256:abc123"

    def test_failed_push(self):
        """Test failed push result."""
        result = PushResult(
            success=False,
            image_tag="myapp:latest",
            registry="ghcr.io",
            error="Authentication failed",
        )
        assert result.success is False
        assert result.error == "Authentication failed"


class TestPushProgress:
    """Tests for PushProgress model."""

    def test_progress_percent(self):
        """Test percent calculation."""
        progress = PushProgress(
            status="Pushing",
            current=500,
            total=1000,
        )
        assert progress.percent == 50.0

    def test_progress_percent_zero_total(self):
        """Test percent with zero total."""
        progress = PushProgress(
            status="Waiting",
            current=0,
            total=0,
        )
        assert progress.percent == 0.0


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


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

    def test_build_error_base(self):
        """Test BuildError base exception."""
        err = BuildError("Something went wrong")
        assert str(err) == "Something went wrong"

    def test_container_not_found(self):
        """Test ContainerNotFoundError."""
        err = ContainerNotFoundError("mycontainer")
        assert "mycontainer" in str(err)
        assert err.container_name == "mycontainer"

    def test_container_not_running(self):
        """Test ContainerNotRunningError."""
        err = ContainerNotRunningError("mycontainer")
        assert "not running" in str(err)
        assert err.container_name == "mycontainer"

    def test_base_image_not_found(self):
        """Test BaseImageNotFoundError."""
        err = BaseImageNotFoundError("mycontainer", "No labels found")
        assert "mycontainer" in str(err)
        assert "No labels found" in str(err)
        assert err.reason == "No labels found"

    def test_dockerfile_generation_error(self):
        """Test DockerfileGenerationError."""
        err = DockerfileGenerationError("mycontainer", "Package detection failed")
        assert "mycontainer" in str(err)
        assert err.reason == "Package detection failed"

    def test_image_build_error(self):
        """Test ImageBuildError."""
        err = ImageBuildError(
            "mycontainer",
            "Build failed",
            build_log=["Step 1: OK", "Step 2: FAILED"],
        )
        assert "mycontainer" in str(err)
        assert err.build_log == ["Step 1: OK", "Step 2: FAILED"]

    def test_image_not_found(self):
        """Test ImageNotFoundError."""
        err = ImageNotFoundError("myimage:latest")
        assert "myimage:latest" in str(err)
        assert err.image_ref == "myimage:latest"

    def test_image_tag_error(self):
        """Test ImageTagError."""
        err = ImageTagError("myimage:v1", "myimage:v2", "Permission denied")
        assert err.image_ref == "myimage:v1"
        assert err.tag == "myimage:v2"
        assert err.reason == "Permission denied"

    def test_registry_error(self):
        """Test RegistryError."""
        err = RegistryError("ghcr.io", "Connection refused")
        assert "ghcr.io" in str(err)
        assert err.registry == "ghcr.io"

    def test_registry_auth_error(self):
        """Test RegistryAuthenticationError."""
        err = RegistryAuthenticationError("ghcr.io", "Invalid token")
        assert "Authentication failed" in str(err)
        assert err.reason == "Invalid token"

    def test_registry_push_error(self):
        """Test RegistryPushError."""
        err = RegistryPushError("ghcr.io", "myapp:latest", "Quota exceeded")
        assert err.image_tag == "myapp:latest"
        assert err.reason == "Quota exceeded"

    def test_package_detection_error(self):
        """Test PackageDetectionError."""
        err = PackageDetectionError("mycontainer", "dpkg database corrupt")
        assert "mycontainer" in str(err)
        assert err.reason == "dpkg database corrupt"

    def test_unsupported_runtime_error(self):
        """Test UnsupportedRuntimeError."""
        err = UnsupportedRuntimeError("podman", "dockerfile generation")
        assert "podman" in str(err)
        assert "dockerfile generation" in str(err)
        assert err.runtime == "podman"
        assert err.operation == "dockerfile generation"


# =============================================================================
# DockerfileGenerator Tests
# =============================================================================


class TestDockerfileGenerator:
    """Tests for DockerfileGenerator class."""

    def test_init_default_runtime(self):
        """Test default runtime."""
        gen = DockerfileGenerator()
        assert gen.runtime == "docker"

    def test_init_custom_runtime(self):
        """Test custom runtime."""
        gen = DockerfileGenerator(runtime="nspawn")
        assert gen.runtime == "nspawn"

    @patch("gaol.build.dockerfile.DockerfileGenerator.docker_client", new_callable=PropertyMock)
    def test_generate_basic(self, mock_client):
        """Test basic Dockerfile generation."""
        # Setup mock container
        mock_container = MagicMock()
        mock_container.attrs = {
            "Config": {
                "Labels": {"dev.gaol.distro": "trixie"},
                "Image": "debian:trixie",
                "Env": [],
                "ExposedPorts": {},
                "WorkingDir": "",
            }
        }
        mock_client.return_value.containers.get.return_value = mock_container

        gen = DockerfileGenerator(runtime="docker")
        dockerfile = gen.generate(
            "testcontainer",
            base_image="debian:trixie",
            include_packages=False,
            include_environment=False,
        )

        assert dockerfile.container_name == "testcontainer"
        assert dockerfile.base_image == "debian:trixie"
        assert len(dockerfile.instructions) >= 1
        assert dockerfile.instructions[0].instruction == "FROM"

    def test_detect_distro_debian(self):
        """Test distro detection for Debian."""
        gen = DockerfileGenerator()
        # This uses the _detect_distro internal method
        info = {"Config": {"Labels": {"dev.gaol.distro": "trixie"}}}
        result = gen._detect_distro(info, "debian:trixie")
        assert result == "trixie"

    def test_detect_distro_from_image(self):
        """Test distro detection from image name."""
        gen = DockerfileGenerator()
        info = {"Config": {"Labels": {}}}
        result = gen._detect_distro(info, "debian:bookworm")
        assert result == "bookworm"

    def test_detect_distro_alpine(self):
        """Test distro detection for Alpine."""
        gen = DockerfileGenerator()
        info = {"Config": {"Labels": {}}}
        result = gen._detect_distro(info, "alpine:3.19")
        assert result == "alpine"


# =============================================================================
# ImageBuilder Tests
# =============================================================================


class TestImageBuilder:
    """Tests for ImageBuilder class."""

    @patch("gaol.build.image.docker.from_env")
    def test_client_lazy_init(self, mock_from_env):
        """Test Docker client lazy initialization."""
        mock_from_env.return_value = MagicMock()
        builder = ImageBuilder()

        # Client not initialized yet
        assert builder._client is None

        # Access client property
        _ = builder.client

        # Now initialized
        mock_from_env.assert_called_once()

    @patch("gaol.build.image.ImageBuilder.client", new_callable=PropertyMock)
    def test_build_commit_method(self, mock_client):
        """Test building with commit method."""
        # Setup mock
        mock_container = MagicMock()
        mock_image = MagicMock()
        mock_image.id = "sha256:abc123"
        mock_container.commit.return_value = mock_image
        mock_client.return_value.containers.get.return_value = mock_container

        builder = ImageBuilder()
        result = builder.build(
            "mycontainer",
            tag="v1.0",
            method=BuildMethod.COMMIT,
        )

        assert result.success is True
        assert result.image_id == "sha256:abc123"
        assert "gaol-build/mycontainer:v1.0" in result.tags

    @patch("gaol.build.image.ImageBuilder.client", new_callable=PropertyMock)
    def test_build_container_not_found(self, mock_client):
        """Test build when container not found."""
        from docker.errors import NotFound
        mock_client.return_value.containers.get.side_effect = NotFound("Not found")

        builder = ImageBuilder()
        result = builder.build("nonexistent", method=BuildMethod.COMMIT)

        assert result.success is False
        assert "not found" in result.error.lower()

    @patch("gaol.build.image.ImageBuilder.client", new_callable=PropertyMock)
    def test_get_image_info(self, mock_client):
        """Test getting image info."""
        mock_image = MagicMock()
        mock_image.id = "sha256:abc123"
        mock_image.tags = ["myapp:latest"]
        mock_image.attrs = {
            "Size": 100 * 1024 * 1024,
            "Created": "2024-01-01T12:00:00",
            "Architecture": "amd64",
            "Os": "linux",
            "Config": {"Labels": {"version": "1.0"}},
        }
        mock_image.history.return_value = [
            {"Id": "layer1", "Created": 1704110400, "CreatedBy": "RUN test", "Size": 1000},
        ]
        mock_client.return_value.images.get.return_value = mock_image

        builder = ImageBuilder()
        info = builder.get_image_info("myapp:latest")

        assert info.id == "sha256:abc123"
        assert "myapp:latest" in info.tags
        assert info.architecture == "amd64"

    @patch("gaol.build.image.ImageBuilder.client", new_callable=PropertyMock)
    def test_tag_image(self, mock_client):
        """Test tagging an image."""
        mock_image = MagicMock()
        mock_image.tag.return_value = True
        mock_client.return_value.images.get.return_value = mock_image

        builder = ImageBuilder()
        result = builder.tag_image("myapp:v1", "myrepo", "v2")

        assert result is True
        mock_image.tag.assert_called_once_with("myrepo", tag="v2")

    @patch("gaol.build.image.ImageBuilder.client", new_callable=PropertyMock)
    def test_delete_image(self, mock_client):
        """Test deleting an image."""
        builder = ImageBuilder()
        result = builder.delete_image("myapp:old")

        assert result is True
        mock_client.return_value.images.remove.assert_called_once_with("myapp:old", force=False)


# =============================================================================
# RegistryClient Tests
# =============================================================================


class TestRegistryClient:
    """Tests for RegistryClient class."""

    @patch("gaol.build.registry.docker.from_env")
    def test_client_lazy_init(self, mock_from_env):
        """Test Docker client lazy initialization."""
        mock_from_env.return_value = MagicMock()
        client = RegistryClient()

        assert client._client is None
        _ = client.client
        mock_from_env.assert_called_once()

    @patch("gaol.build.registry.RegistryClient.client", new_callable=PropertyMock)
    def test_login_success(self, mock_client):
        """Test successful login."""
        mock_client.return_value.login.return_value = {"Status": "Login Succeeded"}

        client = RegistryClient()
        result = client.login("ghcr.io", "user", "token")

        assert result is True

    @patch("gaol.build.registry.RegistryClient.client", new_callable=PropertyMock)
    def test_login_failure(self, mock_client):
        """Test login failure."""
        from docker.errors import APIError
        mock_client.return_value.login.side_effect = APIError("Invalid credentials")

        client = RegistryClient()
        with pytest.raises(RegistryAuthenticationError):
            client.login("ghcr.io", "user", "wrong_token")

    @patch("gaol.build.registry.RegistryClient.client", new_callable=PropertyMock)
    def test_push_success(self, mock_client):
        """Test successful push."""
        mock_image = MagicMock()
        mock_client.return_value.images.get.return_value = mock_image
        mock_client.return_value.images.push.return_value = [
            {"status": "Pushing", "id": "layer1"},
            {"aux": {"Digest": "sha256:abc123"}},
        ]

        client = RegistryClient()
        result = client.push("myapp:latest")

        assert result.success is True
        assert result.digest == "sha256:abc123"

    @patch("gaol.build.registry.RegistryClient.client", new_callable=PropertyMock)
    def test_push_image_not_found(self, mock_client):
        """Test push when image not found."""
        from docker.errors import ImageNotFound
        mock_client.return_value.images.get.side_effect = ImageNotFound("Not found")

        client = RegistryClient()
        result = client.push("nonexistent:latest")

        assert result.success is False
        assert "not found" in result.error.lower()

    @patch("gaol.build.registry.RegistryClient.client", new_callable=PropertyMock)
    def test_push_with_registry(self, mock_client):
        """Test push with registry override."""
        mock_image = MagicMock()
        mock_client.return_value.images.get.return_value = mock_image
        mock_client.return_value.images.push.return_value = [
            {"status": "Pushed"},
            {"aux": {"Digest": "sha256:def456"}},
        ]

        client = RegistryClient()
        result = client.push("myapp:v1", registry="ghcr.io/user")

        assert result.success is True
        assert result.registry == "ghcr.io/user"
        mock_image.tag.assert_called()

    def test_extract_registry(self):
        """Test registry extraction."""
        client = RegistryClient()

        assert client._extract_registry("myapp:latest") == "docker.io"
        assert client._extract_registry("ghcr.io/user/app:v1") == "ghcr.io"
        assert client._extract_registry("localhost:5000/app") == "localhost:5000"


# =============================================================================
# High-Level Function Tests
# =============================================================================


class TestHighLevelFunctions:
    """Tests for high-level convenience functions."""

    @patch("gaol.build.image.ImageBuilder")
    def test_build_image_function(self, mock_builder_class):
        """Test build_image function."""
        mock_instance = MagicMock()
        mock_instance.build.return_value = BuildResult(
            success=True,
            image_id="sha256:abc",
            tags=["test:latest"],
        )
        mock_builder_class.return_value = mock_instance

        result = build_image("mycontainer", tag="v1.0")

        assert result.success is True
        mock_instance.build.assert_called_once()

    @patch("gaol.build.dockerfile.DockerfileGenerator")
    def test_generate_dockerfile_function(self, mock_gen_class):
        """Test generate_dockerfile function."""
        mock_instance = MagicMock()
        mock_instance.generate.return_value = GeneratedDockerfile(
            container_name="test",
            base_image="debian:trixie",
        )
        mock_gen_class.return_value = mock_instance

        result = generate_dockerfile("mycontainer")

        assert result.container_name == "test"
        mock_instance.generate.assert_called_once()

    @patch("gaol.build.image.ImageBuilder")
    def test_get_image_info_function(self, mock_builder_class):
        """Test get_image_info function."""
        mock_instance = MagicMock()
        mock_instance.get_image_info.return_value = ImageInfo(
            id="sha256:abc",
            tags=["test:latest"],
        )
        mock_builder_class.return_value = mock_instance

        result = get_image_info("test:latest")

        assert result.id == "sha256:abc"

    @patch("gaol.build.registry.RegistryClient")
    def test_push_image_function(self, mock_client_class):
        """Test push_image function."""
        mock_instance = MagicMock()
        mock_instance.push.return_value = PushResult(
            success=True,
            image_tag="test:latest",
            registry="docker.io",
        )
        mock_client_class.return_value = mock_instance

        result = push_image("test:latest")

        assert result.success is True


# =============================================================================
# Constants Tests
# =============================================================================


class TestConstants:
    """Tests for module constants."""

    def test_build_repo_prefix(self):
        """Test BUILD_REPO_PREFIX constant."""
        assert BUILD_REPO_PREFIX == "gaol-build"
