#!/usr/bin/env python3
"""Unit tests for DockerImageBuilder."""

import unittest
import tempfile
import os
from unittest.mock import Mock, patch, MagicMock
from pathlib import Path

import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'src'))

from blackant.docker.builder import DockerImageBuilder
from blackant.exceptions import BlackAntDockerError


class TestDockerImageBuilder(unittest.TestCase):
    """Test DockerImageBuilder functionality."""

    def setUp(self):
        """Set up test environment with mocked Docker client."""
        # Mock docker client
        self.mock_docker_client = Mock()
        self.mock_docker_client.ping.return_value = True
        self.mock_docker_client.version.return_value = {"Version": "20.10.0", "ApiVersion": "1.41"}

        # Mock config
        self.mock_config = Mock()
        self.mock_config.registry.url = "env.blackant.app"
        self.mock_config.registry.namespace = "systemdevelopers"
        self.mock_config.build.timeout = 600
        self.mock_config.build.rm = True
        self.mock_config.build.forcerm = True
        self.mock_config.build.pull = True

        # Mock daemon config (REQUIRED for remote Docker builder)
        self.mock_config.daemon.base_url = "http://localhost:2375"
        self.mock_config.daemon.use_nginx_proxy = False
        self.mock_config.daemon.timeout = 30
        self.mock_config.daemon.get_tls_config.return_value = None

        # Mock config methods
        self.mock_config.get_full_image_name.return_value = "env.blackant.app/systemdevelopers/test-service:v1.0.0"
        self.mock_config.get_build_args.return_value = {"SERVICE_NAME": "test-service"}
        self.mock_config.get_build_labels.return_value = {"blackant.service.name": "test-service"}
        self.mock_config.get_registry_credentials.return_value = {}

        # Mock SDK Services config (disabled by default for unit tests)
        self.mock_config.sdk_services.use_for_registry = False
        self.mock_config.sdk_services.url = ""

        # Patch dependencies
        self.docker_patch = patch('blackant.docker.builder.docker.from_env')
        self.docker_client_patch = patch('blackant.docker.builder.docker.DockerClient')
        self.config_patch = patch('blackant.docker.builder.get_docker_config')

        self.mock_docker_from_env = self.docker_patch.start()
        self.mock_docker_client_class = self.docker_client_patch.start()
        self.mock_get_config = self.config_patch.start()

        self.mock_docker_from_env.return_value = self.mock_docker_client
        self.mock_docker_client_class.return_value = self.mock_docker_client
        self.mock_get_config.return_value = self.mock_config

        # Create test directories
        self.test_dir = tempfile.mkdtemp()
        self.impl_path = os.path.join(self.test_dir, "impl")
        self.dockerfile_path = os.path.join(self.test_dir, "Dockerfile")

        os.makedirs(self.impl_path)

        # Create test implementation file
        with open(os.path.join(self.impl_path, "test_calc.py"), "w") as f:
            f.write("print('Hello from calculation')")

        # Create test Dockerfile
        with open(self.dockerfile_path, "w") as f:
            f.write("FROM python:3.9-slim\\nCOPY impl/ /app/\\nCMD python /app/test_calc.py")

    def tearDown(self):
        """Clean up patches and test files."""
        self.docker_patch.stop()
        self.docker_client_patch.stop()
        self.config_patch.stop()

        # Clean up test directory
        import shutil
        shutil.rmtree(self.test_dir, ignore_errors=True)

    def test_initialization_success(self):
        """Test successful DockerImageBuilder initialization."""
        builder = DockerImageBuilder()

        self.assertIsNotNone(builder)
        # Builder uses DockerClient (not from_env) for remote connections
        self.mock_docker_client_class.assert_called_once()
        self.mock_docker_client.version.assert_called_once()
        self.assertEqual(builder.registry_url, "env.blackant.app")

    def test_initialization_docker_failure(self):
        """Test DockerImageBuilder initialization with Docker version check failure.

        Builder tolerates version() failures and logs warnings.
        Build operations might still work even if version check fails.
        """
        self.mock_docker_client.version.side_effect = Exception("Docker daemon not running")

        # Builder should NOT raise exception - it tolerates connection check failures
        builder = DockerImageBuilder()

        # Verify builder was created despite version() failure
        self.assertIsNotNone(builder)
        self.assertEqual(builder.registry_url, "env.blackant.app")

    def test_build_service_success(self):
        """Test successful service build."""
        # Mock image build
        mock_image = Mock()
        mock_image.id = "sha256:abcdef123456"
        mock_image.attrs = {"Size": 125000000, "Created": "2025-01-01T00:00:00Z"}
        mock_image.labels = {"test": "label"}

        build_logs = [{"stream": "Step 1/3 : FROM python:3.9-slim\\n"}]

        self.mock_docker_client.images.build.return_value = (mock_image, build_logs)

        # Create builder and test
        builder = DockerImageBuilder()

        with patch.object(builder, '_prepare_build_context', return_value=self.test_dir):
            with patch.object(builder, '_push_to_registry', return_value=True):
                result = builder.build_service(
                    service_name="test-service",
                    impl_path=self.impl_path,
                    dockerfile_path=self.dockerfile_path,
                    tag="v1.0.0",
                    push_to_registry=True
                )

        # Verify result
        self.assertEqual(result["tag"], "v1.0.0")
        self.assertEqual(result["image_id"], "sha256:abcdef123456")
        self.assertEqual(result["size"], 125000000)
        self.assertTrue(result["pushed"])
        self.assertIn("test-service", result["image_name"])

        # Verify Docker build was called
        self.mock_docker_client.images.build.assert_called_once()

    def test_build_service_with_auto_tag(self):
        """Test service build with auto-generated tag."""
        mock_image = Mock()
        mock_image.id = "sha256:abcdef123456"
        mock_image.attrs = {"Size": 100000, "Created": "2025-01-01T00:00:00Z"}
        mock_image.labels = {}

        self.mock_docker_client.images.build.return_value = (mock_image, [])

        builder = DockerImageBuilder()

        with patch.object(builder, '_prepare_build_context', return_value=self.test_dir):
            with patch.object(builder, '_push_to_registry', return_value=False):
                result = builder.build_service(
                    service_name="auto-tag-service",
                    impl_path=self.impl_path,
                    dockerfile_path=self.dockerfile_path,
                    tag=None,  # Auto-generate tag
                    push_to_registry=False
                )

        # Verify auto-generated tag format
        self.assertTrue(result["tag"].startswith("v1.0.0-"))
        self.assertFalse(result["pushed"])

    def test_validate_build_inputs_invalid_service_name(self):
        """Test build input validation with invalid service name."""
        builder = DockerImageBuilder()

        # Empty service name
        with self.assertRaises(BlackAntDockerError):
            builder._validate_build_inputs("", self.impl_path, self.dockerfile_path)

        # Invalid characters in service name
        with self.assertRaises(BlackAntDockerError):
            builder._validate_build_inputs("invalid@service", self.impl_path, self.dockerfile_path)

    def test_validate_build_inputs_missing_paths(self):
        """Test build input validation with missing paths."""
        builder = DockerImageBuilder()

        # Non-existent implementation path
        with self.assertRaises(BlackAntDockerError):
            builder._validate_build_inputs("test-service", "/non/existent/path", self.dockerfile_path)

        # Non-existent Dockerfile
        with self.assertRaises(BlackAntDockerError):
            builder._validate_build_inputs("test-service", self.impl_path, "/non/existent/Dockerfile")

    def test_prepare_build_context(self):
        """Test build context preparation."""
        builder = DockerImageBuilder()

        with patch('tempfile.mkdtemp', return_value=self.test_dir + "_context"):
            os.makedirs(self.test_dir + "_context", exist_ok=True)

            context_path = builder._prepare_build_context(self.impl_path, self.dockerfile_path)

            # Verify context was created
            self.assertTrue(os.path.exists(context_path))

            # Clean up
            import shutil
            shutil.rmtree(context_path, ignore_errors=True)

    def test_build_image_success(self):
        """Test Docker image build execution."""
        mock_image = Mock()
        mock_image.id = "sha256:test123"
        build_logs = [
            {"stream": "Step 1/3 : FROM python:3.9-slim\\n"},
            {"stream": "Successfully built test123\\n"}
        ]

        self.mock_docker_client.images.build.return_value = (mock_image, build_logs)

        builder = DockerImageBuilder()
        image, logs = builder._build_image(
            "env.blackant.app/systemdevelopers/test:v1.0.0",
            self.test_dir,
            "test-service"
        )

        self.assertEqual(image.id, "sha256:test123")
        self.assertEqual(logs, build_logs)
        self.mock_docker_client.images.build.assert_called_once()

    def test_build_image_failure(self):
        """Test Docker image build failure."""
        from docker.errors import BuildError

        self.mock_docker_client.images.build.side_effect = BuildError("Build failed", build_log=[])

        builder = DockerImageBuilder()

        with self.assertRaises(BuildError):
            builder._build_image(
                "env.blackant.app/systemdevelopers/test:v1.0.0",
                self.test_dir,
                "test-service"
            )

    def test_push_to_registry_success(self):
        """Test successful image push to registry."""
        push_response = [
            {"status": "Preparing"},
            {"status": "Pushing", "id": "layer1"},
            {"status": "Pushed", "id": "layer1"}
        ]

        self.mock_docker_client.images.push.return_value = push_response

        builder = DockerImageBuilder()
        result = builder._push_to_registry("env.blackant.app/systemdevelopers/test:v1.0.0")

        self.assertTrue(result)
        self.mock_docker_client.images.push.assert_called_once()

    def test_push_to_registry_with_credentials(self):
        """Test image push with registry credentials."""
        # Setup credentials
        self.mock_config.get_registry_credentials.return_value = {
            "username": "testuser",
            "password": "testpass",
            "registry": "env.blackant.app"
        }

        push_response = [{"status": "Pushed"}]
        self.mock_docker_client.images.push.return_value = push_response

        builder = DockerImageBuilder()
        result = builder._push_to_registry("env.blackant.app/systemdevelopers/test:v1.0.0")

        self.assertTrue(result)
        self.mock_docker_client.login.assert_called_once_with(
            username="testuser",
            password="testpass",
            registry="env.blackant.app"
        )

    def test_push_to_registry_failure(self):
        """Test image push failure."""
        push_response = [
            {"error": "Authentication failed"}
        ]

        self.mock_docker_client.images.push.return_value = push_response

        builder = DockerImageBuilder()
        result = builder._push_to_registry("env.blackant.app/systemdevelopers/test:v1.0.0")

        self.assertFalse(result)

    def test_list_local_images(self):
        """Test listing local Docker images."""
        mock_image1 = Mock()
        mock_image1.id = "sha256:image1"
        mock_image1.tags = ["env.blackant.app/systemdevelopers/service1:v1.0.0"]
        mock_image1.attrs = {"Created": "2025-01-01T00:00:00Z", "Size": 100000}
        mock_image1.labels = {"service": "service1"}

        mock_image2 = Mock()
        mock_image2.id = "sha256:image2"
        mock_image2.tags = ["env.blackant.app/systemdevelopers/service2:v1.0.0"]
        mock_image2.attrs = {"Created": "2025-01-01T00:00:00Z", "Size": 200000}
        mock_image2.labels = {}

        self.mock_docker_client.images.list.return_value = [mock_image1, mock_image2]

        builder = DockerImageBuilder()
        images = builder.list_local_images()

        self.assertEqual(len(images), 2)
        self.assertEqual(images[0]["id"], "sha256:image1")
        self.assertEqual(images[1]["size"], 200000)

    def test_list_local_images_with_filter(self):
        """Test listing local images with service prefix filter."""
        mock_image = Mock()
        mock_image.id = "sha256:image1"
        mock_image.tags = ["env.blackant.app/systemdevelopers/service1:v1.0.0"]
        mock_image.attrs = {"Created": "2025-01-01T00:00:00Z", "Size": 100000}
        mock_image.labels = {}

        self.mock_docker_client.images.list.return_value = [mock_image]

        builder = DockerImageBuilder()
        images = builder.list_local_images(service_prefix="service1")

        self.assertEqual(len(images), 1)
        self.assertIn("service1", images[0]["tag"])

    def test_remove_image_success(self):
        """Test successful image removal."""
        builder = DockerImageBuilder()
        result = builder.remove_image("sha256:test123")

        self.assertTrue(result)
        self.mock_docker_client.images.remove.assert_called_once_with("sha256:test123", force=False)

    def test_remove_image_not_found(self):
        """Test image removal when image not found."""
        from docker.errors import ImageNotFound

        self.mock_docker_client.images.remove.side_effect = ImageNotFound("Image not found")

        builder = DockerImageBuilder()
        result = builder.remove_image("sha256:notfound")

        self.assertFalse(result)

    def test_context_manager(self):
        """Test DockerImageBuilder as context manager."""
        with DockerImageBuilder() as builder:
            self.assertIsNotNone(builder)
            self.assertEqual(builder.registry_url, "env.blackant.app")

        # Context manager should close Docker client
        # (Tested by the fact that no exceptions are raised)

    def test_build_service_docker_build_error(self):
        """Test build_service handling Docker build errors."""
        from docker.errors import BuildError

        self.mock_docker_client.images.build.side_effect = BuildError("Build failed", build_log=[])

        builder = DockerImageBuilder()

        with self.assertRaises(BlackAntDockerError):
            builder.build_service(
                service_name="failing-service",
                impl_path=self.impl_path,
                dockerfile_path=self.dockerfile_path
            )

    def test_build_service_no_push(self):
        """Test build_service without pushing to registry."""
        mock_image = Mock()
        mock_image.id = "sha256:local123"
        mock_image.attrs = {"Size": 50000, "Created": "2025-01-01T00:00:00Z"}
        mock_image.labels = {}

        self.mock_docker_client.images.build.return_value = (mock_image, [])

        builder = DockerImageBuilder()

        with patch.object(builder, '_prepare_build_context', return_value=self.test_dir):
            result = builder.build_service(
                service_name="local-service",
                impl_path=self.impl_path,
                dockerfile_path=self.dockerfile_path,
                push_to_registry=False
            )

        self.assertFalse(result["pushed"])
        self.assertEqual(result["image_id"], "sha256:local123")


if __name__ == '__main__':
    unittest.main(verbosity=2)