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

Tests the Docker SDK object-level service management operations
with proper mocking and error handling.
"""

import unittest
from unittest.mock import Mock, patch, MagicMock
import docker
from docker.errors import DockerException, NotFound, APIError

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

from blackant.services.dao import ServiceManagerDAO, ServiceConfig
from blackant.exceptions import BlackAntDockerError


class TestServiceManagerDAO(unittest.TestCase):
    """Test ServiceManagerDAO functionality."""

    def setUp(self):
        """Set up test environment with mocked Docker client."""
        self.mock_docker_client = Mock()
        self.mock_docker_client.ping.return_value = True

        # Mock docker.from_env() to return our mock client
        self.docker_patch = patch('blackant.services.dao.docker.from_env')
        self.mock_docker_from_env = self.docker_patch.start()
        self.mock_docker_from_env.return_value = self.mock_docker_client

    def tearDown(self):
        """Clean up patches."""
        self.docker_patch.stop()

    def test_initialization_success(self):
        """Test successful ServiceManagerDAO initialization."""
        dao = ServiceManagerDAO()

        self.assertIsNotNone(dao)
        # Check that docker.from_env() was called
        self.mock_docker_from_env.assert_called_once()
        self.mock_docker_client.ping.assert_called_once()

    def test_initialization_failure(self):
        """Test ServiceManagerDAO initialization failure."""
        self.mock_docker_from_env.side_effect = DockerException("Connection failed")

        with self.assertRaises(BlackAntDockerError):
            ServiceManagerDAO()

    def test_create_service_success(self):
        """Test successful service creation."""
        # Mock Docker service object
        mock_service = Mock()
        mock_service.id = "service_123"
        mock_service.name = "test-service"

        self.mock_docker_client.services.create.return_value = mock_service

        dao = ServiceManagerDAO()
        config = ServiceConfig(
            name="test-service",
            image="nginx:latest",
            environments={"ENV": "test"},
            replicas=2
        )

        result = dao.create_service(config)

        self.assertEqual(result, mock_service)
        self.mock_docker_client.services.create.assert_called_once()

        # Verify call arguments
        call_args = self.mock_docker_client.services.create.call_args
        self.assertEqual(call_args[1]['name'], "test-service")
        self.assertEqual(call_args[1]['image'], "nginx:latest")

    def test_create_service_with_dict_config(self):
        """Test service creation with dictionary configuration."""
        mock_service = Mock()
        mock_service.id = "service_456"
        mock_service.name = "dict-service"

        self.mock_docker_client.services.create.return_value = mock_service

        dao = ServiceManagerDAO()
        config_dict = {
            "name": "dict-service",
            "image": "alpine:latest"
        }

        result = dao.create_service(config_dict)

        self.assertEqual(result, mock_service)

    def test_create_service_api_error(self):
        """Test service creation with Docker API error."""
        self.mock_docker_client.services.create.side_effect = APIError("Service creation failed")

        dao = ServiceManagerDAO()
        config = ServiceConfig(name="failing-service", image="nginx:latest")

        with self.assertRaises(BlackAntDockerError):
            dao.create_service(config)

    def test_get_service_success(self):
        """Test successful service retrieval."""
        mock_service = Mock()
        mock_service.id = "service_789"
        mock_service.name = "existing-service"

        self.mock_docker_client.services.get.return_value = mock_service

        dao = ServiceManagerDAO()
        result = dao.get_service("service_789")

        self.assertEqual(result, mock_service)
        self.mock_docker_client.services.get.assert_called_once_with("service_789")

    def test_get_service_not_found(self):
        """Test service retrieval when service doesn't exist."""
        self.mock_docker_client.services.get.side_effect = NotFound("Service not found")

        dao = ServiceManagerDAO()
        result = dao.get_service("nonexistent")

        self.assertIsNone(result)

    def test_get_service_api_error(self):
        """Test service retrieval with Docker API error."""
        self.mock_docker_client.services.get.side_effect = APIError("Docker API error")

        dao = ServiceManagerDAO()

        with self.assertRaises(BlackAntDockerError):
            dao.get_service("error_service")

    def test_list_services_success(self):
        """Test successful service listing."""
        mock_services = [Mock(), Mock(), Mock()]
        for i, service in enumerate(mock_services):
            service.id = f"service_{i}"
            service.name = f"service-{i}"

        self.mock_docker_client.services.list.return_value = mock_services

        dao = ServiceManagerDAO()
        result = dao.list_services()

        self.assertEqual(len(result), 3)
        self.assertEqual(result, mock_services)

    def test_list_services_with_filters(self):
        """Test service listing with filters."""
        mock_services = [Mock()]
        self.mock_docker_client.services.list.return_value = mock_services

        dao = ServiceManagerDAO()
        filters = {'label': 'env=prod'}
        result = dao.list_services(filters=filters)

        self.mock_docker_client.services.list.assert_called_once_with(filters=filters)
        self.assertEqual(result, mock_services)

    def test_remove_service_success(self):
        """Test successful service removal."""
        mock_service = Mock()
        mock_service.remove = Mock()

        # Mock get_service to return the service
        dao = ServiceManagerDAO()
        dao.get_service = Mock(return_value=mock_service)

        result = dao.remove_service("service_to_remove")

        self.assertTrue(result)
        mock_service.remove.assert_called_once()

    def test_remove_service_not_found(self):
        """Test service removal when service doesn't exist."""
        dao = ServiceManagerDAO()
        dao.get_service = Mock(return_value=None)

        result = dao.remove_service("nonexistent")

        self.assertFalse(result)

    def test_scale_service_success(self):
        """Test successful service scaling."""
        mock_service = Mock()
        mock_service.update = Mock()

        dao = ServiceManagerDAO()
        dao.get_service = Mock(return_value=mock_service)

        result = dao.scale_service("service_to_scale", 5)

        self.assertEqual(result, mock_service)
        mock_service.update.assert_called_once()

    def test_is_service_running_true(self):
        """Test service running status check - running."""
        mock_service = Mock()
        mock_service.tasks.return_value = [
            {"Status": {"State": "running"}},
            {"Status": {"State": "pending"}}
        ]

        dao = ServiceManagerDAO()
        dao.get_service = Mock(return_value=mock_service)

        result = dao.is_service_running("running_service")

        self.assertTrue(result)

    def test_is_service_running_false(self):
        """Test service running status check - not running."""
        mock_service = Mock()
        mock_service.tasks.return_value = [
            {"Status": {"State": "pending"}},
            {"Status": {"State": "failed"}}
        ]

        dao = ServiceManagerDAO()
        dao.get_service = Mock(return_value=mock_service)

        result = dao.is_service_running("stopped_service")

        self.assertFalse(result)

    def test_is_service_running_service_not_found(self):
        """Test service running status when service doesn't exist."""
        dao = ServiceManagerDAO()
        dao.get_service = Mock(return_value=None)

        result = dao.is_service_running("nonexistent")

        self.assertFalse(result)

    def test_get_service_logs_success(self):
        """Test successful service logs retrieval."""
        mock_service = Mock()
        mock_service.logs.return_value = b"Service log output"

        dao = ServiceManagerDAO()
        dao.get_service = Mock(return_value=mock_service)

        result = dao.get_service_logs("service_with_logs", tail=100)

        self.assertEqual(result, "Service log output")
        mock_service.logs.assert_called_once_with(tail=100)

    def test_get_service_logs_service_not_found(self):
        """Test service logs retrieval when service doesn't exist."""
        dao = ServiceManagerDAO()
        dao.get_service = Mock(return_value=None)

        with self.assertRaises(BlackAntDockerError):
            dao.get_service_logs("nonexistent")

    def test_wait_for_service_ready_success(self):
        """Test waiting for service to become ready - success."""
        dao = ServiceManagerDAO()
        dao.is_service_running = Mock(side_effect=[False, False, True])

        with patch('blackant.services.dao.time.sleep'):
            result = dao.wait_for_service_ready("test_service", timeout=5)

        self.assertTrue(result)

    def test_wait_for_service_ready_timeout(self):
        """Test waiting for service to become ready - timeout."""
        dao = ServiceManagerDAO()
        dao.is_service_running = Mock(return_value=False)

        with patch('blackant.services.dao.time.sleep'):
            result = dao.wait_for_service_ready("test_service", timeout=2)

        self.assertFalse(result)

    def test_get_service_tasks_success(self):
        """Test successful service tasks retrieval."""
        mock_service = Mock()
        mock_tasks = [
            {"ID": "task1", "Status": {"State": "running"}},
            {"ID": "task2", "Status": {"State": "pending"}}
        ]
        mock_service.tasks.return_value = mock_tasks

        dao = ServiceManagerDAO()
        dao.get_service = Mock(return_value=mock_service)

        result = dao.get_service_tasks("service_with_tasks")

        self.assertEqual(result, mock_tasks)

    def test_context_manager(self):
        """Test ServiceManagerDAO as context manager."""
        self.mock_docker_client.close = Mock()

        with ServiceManagerDAO() as dao:
            self.assertIsNotNone(dao)

        # Note: close() should be called if it exists
        # but we can't test it easily with the current mock setup

    @patch('blackant.services.dao.docker.types.Resources')
    @patch('blackant.services.dao.docker.types.ServiceMode')
    def test_create_service_with_resources(self, mock_service_mode, mock_resources):
        """Test service creation with resource limits."""
        mock_service = Mock()
        mock_service.id = "resource_service"
        self.mock_docker_client.services.create.return_value = mock_service

        dao = ServiceManagerDAO()
        config = ServiceConfig(
            name="resource-service",
            image="nginx:latest",
            cpu_limit=2000000000,  # 2 CPU cores in nanocpus
            memory_limit=1073741824,  # 1GB in bytes
            replicas=3
        )

        result = dao.create_service(config)

        self.assertEqual(result, mock_service)
        mock_resources.assert_called_once_with(
            cpu_limit=2000000000,
            mem_limit=1073741824
        )
        mock_service_mode.assert_called_once_with('replicated', 3)


class TestServiceConfig(unittest.TestCase):
    """Test ServiceConfig data class."""

    def test_service_config_defaults(self):
        """Test ServiceConfig default values."""
        config = ServiceConfig()

        self.assertEqual(config.name, "")
        self.assertEqual(config.image, "")
        self.assertEqual(config.replicas, 1)
        self.assertIsNone(config.cpu_limit)
        self.assertIsNone(config.memory_limit)
        self.assertEqual(config.environments, {})
        self.assertEqual(config.networks, [])

    def test_service_config_initialization(self):
        """Test ServiceConfig with custom values."""
        config = ServiceConfig(
            name="test-service",
            image="nginx:alpine",
            replicas=5,
            cpu_limit=1000000000,
            memory_limit=536870912,
            environments={"ENV": "production"},
            networks=["frontend", "backend"]
        )

        self.assertEqual(config.name, "test-service")
        self.assertEqual(config.image, "nginx:alpine")
        self.assertEqual(config.replicas, 5)
        self.assertEqual(config.cpu_limit, 1000000000)
        self.assertEqual(config.memory_limit, 536870912)
        self.assertEqual(config.environments, {"ENV": "production"})
        self.assertEqual(config.networks, ["frontend", "backend"])


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