"""Additional tests for the ACB config module."""
import asyncio
import tempfile
from pathlib import Path
from unittest.mock import Mock, AsyncMock, patch, MagicMock

import pytest
from pydantic import SecretStr

from acb.config import (
    Config,
    AppSettings,
    DebugSettings,
    deep_update,
    get_version,
    gen_password,
    Platform,
    AdapterBase,
    get_singleton_instance,
    ConfigHotReload,
    enable_config_hot_reload,
    disable_config_hot_reload,
    _LibraryAppSettings,
    _LibraryDebugSettings,
)


class TestDeepUpdate:
    """Test the deep_update function."""

    def test_deep_update_empty_dicts(self) -> None:
        """Test deep_update with empty dictionaries."""
        result = deep_update({}, {})
        assert result == {}

    def test_deep_update_simple_dicts(self) -> None:
        """Test deep_update with simple dictionaries."""
        dict1 = {"a": 1, "b": 2}
        dict2 = {"b": 3, "c": 4}
        result = deep_update(dict1, dict2)
        assert result == {"a": 1, "b": 3, "c": 4}

    def test_deep_update_nested_dicts(self) -> None:
        """Test deep_update with nested dictionaries."""
        dict1 = {"a": 1, "nested": {"x": 1, "y": 2}}
        dict2 = {"b": 2, "nested": {"y": 3, "z": 4}}
        result = deep_update(dict1, dict2)
        assert result == {"a": 1, "b": 2, "nested": {"x": 1, "y": 3, "z": 4}}

    def test_deep_update_multiple_dicts(self) -> None:
        """Test deep_update with multiple dictionaries."""
        dict1 = {"a": 1}
        dict2 = {"b": 2}
        dict3 = {"c": 3}
        result = deep_update(dict1, dict2, dict3)
        assert result == {"a": 1, "b": 2, "c": 3}

    def test_deep_update_overwrites_non_dict_values(self) -> None:
        """Test that deep_update overwrites non-dict values."""
        dict1 = {"a": {"b": 1}}
        dict2 = {"a": 2}
        result = deep_update(dict1, dict2)
        assert result == {"a": 2}


class TestUtilityFunctions:
    """Test utility functions in the config module."""

    @pytest.mark.asyncio
    async def test_get_version(self) -> None:
        """Test get_version function."""
        # Test with mocked pyproject.toml
        with patch("acb.config.root_path", Mock(parent=Mock())):
            mock_pyproject = AsyncMock()
            mock_pyproject.exists = AsyncMock(return_value=True)

            with patch("acb.config.load.toml", AsyncMock(return_value={"project": {"version": "1.2.3"}})):
                version = await get_version()
                assert version == "1.2.3"

    def test_gen_password(self) -> None:
        """Test gen_password function."""
        password = gen_password()
        assert isinstance(password, str)
        assert len(password) >= 10  # token_urlsafe generates at least this length

    def test_gen_password_custom_size(self) -> None:
        """Test gen_password with custom size."""
        password = gen_password(20)
        assert isinstance(password, str)
        assert len(password) >= 20


class TestPlatformEnum:
    """Test Platform enum."""

    def test_platform_values(self) -> None:
        """Test that Platform enum has correct values."""
        assert Platform.aws.value == "aws"
        assert Platform.gcp.value == "gcp"
        assert Platform.azure.value == "azure"
        assert Platform.cloudflare.value == "cloudflare"


class TestAppSettings:
    """Test AppSettings class."""

    def test_app_settings_defaults(self) -> None:
        """Test AppSettings default values."""
        with patch("acb.config.root_path", Mock(stem="test_app")):
            settings = AppSettings()
            assert settings.name == "test_app"
            assert isinstance(settings.secret_key, SecretStr)
            assert isinstance(settings.secure_salt, SecretStr)
            assert settings.title == "Test App"
            assert settings.timezone == "US/Pacific"

    def test_app_settings_custom_values(self) -> None:
        """Test AppSettings with custom values."""
        settings = AppSettings(
            name="my-custom-app",
            title="My Custom App",
            timezone="Europe/London"
        )
        assert settings.name == "my-custom-app"
        assert settings.title == "My Custom App"
        assert settings.timezone == "Europe/London"

    def test_app_name_validation_valid(self) -> None:
        """Test app name validation with valid names."""
        # Test various valid app names
        valid_names = [
            "my-app",
            "my_app",
            "my.app",
            "My App",
            "my-app-with-numbers-123"
        ]

        for name in valid_names:
            validated_name = AppSettings.cloud_compliant_app_name(name)
            assert "-" in validated_name or validated_name.isalnum()
            assert len(validated_name) >= 3
            assert len(validated_name) <= 63

    def test_app_name_validation_invalid_too_short(self) -> None:
        """Test app name validation with too short names."""
        with pytest.raises(SystemExit):
            AppSettings.cloud_compliant_app_name("ab")

    def test_app_name_validation_invalid_too_long(self) -> None:
        """Test app name validation with too long names."""
        long_name = "a" * 64
        with pytest.raises(SystemExit):
            AppSettings.cloud_compliant_app_name(long_name)


class TestConfigInitialization:
    """Test Config initialization and properties."""

    def test_config_defaults(self) -> None:
        """Test Config default values."""
        config = Config()
        assert config._debug is None
        assert config._app is None
        assert config._initialized is False

    def test_config_debug_property_not_initialized(self) -> None:
        """Test Config debug property when not initialized."""
        config = Config()

        with patch.object(config, 'ensure_initialized') as mock_ensure:
            debug = config.debug
            mock_ensure.assert_called_once()

    def test_config_app_property_not_initialized(self) -> None:
        """Test Config app property when not initialized."""
        config = Config()

        with patch.object(config, 'ensure_initialized') as mock_ensure:
            app = config.app
            mock_ensure.assert_called_once()

    def test_config_app_setter(self) -> None:
        """Test Config app setter."""
        config = Config()
        mock_app = Mock(spec=AppSettings)
        config.app = mock_app
        assert config._app == mock_app

    def test_config_getattr_with_dot_notation(self) -> None:
        """Test Config __getattr__ with dot notation."""
        config = Config()
        config.test_attr = "test_value"

        # This should work for simple attributes
        assert config.test_attr == "test_value"

    def test_config_getattr_with_adapter_access(self) -> None:
        """Test Config __getattr__ with adapter access."""
        config = Config()

        # Mock the adapter system
        with patch("acb.config.get_adapter") as mock_get_adapter:
            mock_adapter = Mock()
            mock_adapter.settings = {"test_setting": "test_value"}
            mock_get_adapter.return_value = mock_adapter

            # This should attempt to access the adapter
            # Since we're not testing the full adapter system, we'll just ensure
            # it doesn't crash
            try:
                result = config.some_adapter
                # If it gets here without crashing, that's good
            except AttributeError:
                # This is expected since we're not setting up the full mock
                pass


class TestAdapterBase:
    """Test AdapterBase class."""

    @pytest.mark.asyncio
    async def test_adapter_base_logger_property(self) -> None:
        """Test AdapterBase logger property."""
        adapter = AdapterBase()

        # Test with successful logger import
        with patch("acb.config.import_adapter") as mock_import:
            mock_logger_class = Mock()
            mock_logger_instance = Mock()
            mock_import.return_value = mock_logger_class
            with patch("acb.config.depends.get", return_value=mock_logger_instance):
                logger = adapter.logger
                assert logger == mock_logger_instance

    @pytest.mark.asyncio
    async def test_adapter_base_logger_property_fallback(self) -> None:
        """Test AdapterBase logger property fallback to standard logging."""
        adapter = AdapterBase()

        # Test with logger import failure
        with patch("acb.config.import_adapter", side_effect=Exception("Import failed")):
            with patch("logging.getLogger") as mock_get_logger:
                mock_logger = Mock()
                mock_get_logger.return_value = mock_logger
                logger = adapter.logger
                assert logger == mock_logger
                mock_get_logger.assert_called_with("AdapterBase")

    @pytest.mark.asyncio
    async def test_adapter_base_logger_setter(self) -> None:
        """Test AdapterBase logger setter."""
        adapter = AdapterBase()
        mock_logger = Mock()
        adapter.logger = mock_logger
        assert adapter._logger == mock_logger

    @pytest.mark.asyncio
    async def test_adapter_base_logger_deleter(self) -> None:
        """Test AdapterBase logger deleter."""
        adapter = AdapterBase()
        mock_logger = Mock()
        adapter._logger = mock_logger
        assert hasattr(adapter, "_logger")
        del adapter.logger
        assert not hasattr(adapter, "_logger")

    @pytest.mark.asyncio
    async def test_adapter_base_ensure_client(self) -> None:
        """Test AdapterBase _ensure_client method."""
        adapter = AdapterBase()

        # Mock the _create_client method
        mock_client = Mock()
        adapter._create_client = AsyncMock(return_value=mock_client)

        client1 = await adapter._ensure_client()
        client2 = await adapter._ensure_client()

        # Should return the same client instance
        assert client1 == mock_client
        assert client2 == mock_client
        adapter._create_client.assert_called_once()

    @pytest.mark.asyncio
    async def test_adapter_base_ensure_resource(self) -> None:
        """Test AdapterBase _ensure_resource method."""
        adapter = AdapterBase()

        async def factory_func():
            return "test_resource"

        # First call should create the resource
        resource1 = await adapter._ensure_resource("test", factory_func)
        assert resource1 == "test_resource"

        # Second call should return the cached resource
        resource2 = await adapter._ensure_resource("test", factory_func)
        assert resource2 == "test_resource"
        # Factory function should only be called once

    @pytest.mark.asyncio
    async def test_adapter_base_cleanup_resources(self) -> None:
        """Test AdapterBase _cleanup_resources method."""
        adapter = AdapterBase()

        # Mock resources with close methods
        mock_resource1 = Mock()
        mock_resource1.close = Mock()
        mock_resource2 = Mock()
        mock_resource2.aclose = AsyncMock()

        adapter._resource_cache = {
            "res1": mock_resource1,
            "res2": mock_resource2
        }
        adapter._client = Mock()
        adapter._client.close = Mock()

        with patch.object(adapter, "logger"):
            await adapter._cleanup_resources()

            # Check that resources were cleaned up
            mock_resource1.close.assert_called_once()
            mock_resource2.aclose.assert_called_once()
            adapter._client.close.assert_called_once()

            # Check that cache is cleared
            assert adapter._resource_cache == {}
            assert adapter._client is None

    @pytest.mark.asyncio
    async def test_adapter_base_cleanup_single_resource(self) -> None:
        """Test AdapterBase _cleanup_single_resource method."""
        adapter = AdapterBase()

        # Test resource with close method
        mock_resource = Mock()
        mock_resource.close = Mock()

        with patch.object(adapter, "logger"):
            await adapter._cleanup_single_resource(mock_resource)
            mock_resource.close.assert_called_once()

    @pytest.mark.asyncio
    async def test_adapter_base_cleanup_single_resource_async(self) -> None:
        """Test AdapterBase _cleanup_single_resource method with async cleanup."""
        adapter = AdapterBase()

        # Test resource with async close method
        mock_resource = Mock()
        mock_resource.aclose = AsyncMock()

        with patch.object(adapter, "logger"):
            await adapter._cleanup_single_resource(mock_resource)
            mock_resource.aclose.assert_called_once()

    @pytest.mark.asyncio
    async def test_adapter_base_cleanup_single_resource_no_method(self) -> None:
        """Test AdapterBase _cleanup_single_resource method with no cleanup method."""
        adapter = AdapterBase()

        # Test resource with no cleanup method
        mock_resource = Mock()
        del mock_resource.close  # Remove close if it exists

        with patch.object(adapter, "logger"):
            # Should not raise an exception
            await adapter._cleanup_single_resource(mock_resource)

    @pytest.mark.asyncio
    async def test_adapter_base_cleanup(self) -> None:
        """Test AdapterBase cleanup method."""
        adapter = AdapterBase()

        with patch.object(adapter, "_cleanup_resources") as mock_cleanup:
            await adapter.cleanup()
            mock_cleanup.assert_called_once()

            # Second call should not call cleanup again (idempotent)
            await adapter.cleanup()
            mock_cleanup.assert_called_once()  # Still only called once

    @pytest.mark.asyncio
    async def test_adapter_base_async_context_manager(self) -> None:
        """Test AdapterBase as async context manager."""
        adapter = AdapterBase()

        with patch.object(adapter, "cleanup") as mock_cleanup:
            async with adapter as a:
                assert a == adapter

            mock_cleanup.assert_called_once()

    @pytest.mark.asyncio
    async def test_adapter_base_init_method(self) -> None:
        """Test AdapterBase init method."""
        adapter = AdapterBase()
        # Should not raise an exception
        await adapter.init()


class TestSingletonAndLibraryModels:
    """Test singleton instance and library models."""

    def test_get_singleton_instance(self) -> None:
        """Test get_singleton_instance function."""
        class TestClass:
            def __init__(self, value: str = "default") -> None:
                self.value = value

        # First call should create a new instance
        instance1 = get_singleton_instance(TestClass, value="first")
        assert instance1.value == "first"

        # Second call should return the same instance
        instance2 = get_singleton_instance(TestClass, value="second")
        assert instance2.value == "first"  # Still the first value
        assert instance1 is instance2

    def test_library_debug_settings(self) -> None:
        """Test _LibraryDebugSettings class."""
        settings = _LibraryDebugSettings()
        assert settings.production is False
        assert settings.secrets is False
        assert settings.logger is False

    def test_library_app_settings(self) -> None:
        """Test _LibraryAppSettings class."""
        settings = _LibraryAppSettings()
        assert settings.name == "library_app"
        assert settings.title == "Library App"
        assert settings.timezone == "US/Pacific"
        assert isinstance(settings.secret_key, SecretStr)
        assert isinstance(settings.secure_salt, SecretStr)

    def test_library_app_settings_custom_name(self) -> None:
        """Test _LibraryAppSettings class with custom name."""
        settings = _LibraryAppSettings(name="custom_app")
        assert settings.name == "custom_app"
        assert settings.title == "Custom App"


class TestConfigHotReload:
    """Test ConfigHotReload functionality."""

    @pytest.mark.asyncio
    async def test_config_hot_reload_start_stop(self) -> None:
        """Test ConfigHotReload start and stop methods."""
        mock_config = Mock(spec=Config)
        hot_reload = ConfigHotReload(mock_config)

        # Should be able to start
        await hot_reload.start()
        assert hot_reload._running is True

        # Should be able to stop
        await hot_reload.stop()
        assert hot_reload._running is False

    @pytest.mark.asyncio
    async def test_enable_disable_config_hot_reload(self) -> None:
        """Test enable_config_hot_reload and disable_config_hot_reload functions."""
        mock_config = Mock(spec=Config)

        # Enable hot reload
        hot_reload = await enable_config_hot_reload(mock_config)
        assert hot_reload is not None
        assert hot_reload.config == mock_config

        # Disable hot reload
        await disable_config_hot_reload()
        # Should not raise an exception

    @pytest.mark.asyncio
    async def test_config_hot_reload_monitor_loop_cancel(self) -> None:
        """Test ConfigHotReload monitor loop cancellation."""
        mock_config = Mock(spec=Config)
        hot_reload = ConfigHotReload(mock_config)
        hot_reload._running = True

        # Create a task that will be cancelled
        task = asyncio.create_task(hot_reload._monitor_loop())

        # Cancel the task
        task.cancel()

        # Wait for cancellation (should not raise an exception)
        try:
            await task
        except asyncio.CancelledError:
            pass  # This is expected
