"""
Unit tests for OracleMultiPool - Multi-server Oracle connection pool manager.

Tests cover:
- Server registration and lazy pool creation
- Multiple simultaneous connections on different servers
- Graceful shutdown of all pools
- Backward compatibility with legacy single-pool mode
"""
import asyncio
import pytest
from unittest.mock import MagicMock, patch, AsyncMock
import sys
import os

# Add project paths
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../shared'))
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../'))


class MockConnection:
    """Mock Oracle connection for testing."""

    def __init__(self, server_id: str = "mock"):
        self.server_id = server_id
        self._closed = False

    def cursor(self):
        return MockCursor(self.server_id)

    def close(self):
        self._closed = True

    def commit(self):
        pass

    def rollback(self):
        pass


class MockCursor:
    """Mock Oracle cursor for testing."""

    def __init__(self, server_id: str = "mock"):
        self.server_id = server_id
        self._result = [(1,)]

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        pass

    def execute(self, query, params=None):
        pass

    def fetchall(self):
        return self._result

    def fetchone(self):
        return self._result[0] if self._result else None


class MockPool:
    """Mock Oracle connection pool for testing."""

    def __init__(self, server_id: str = "mock"):
        self.server_id = server_id
        self.opened = 2
        self.busy = 0
        self.min = 2
        self.max = 10
        self._closed = False

    def acquire(self):
        self.busy += 1
        return MockConnection(self.server_id)

    def close(self):
        self._closed = True


@pytest.fixture
def fresh_pool_manager():
    """Create a fresh OracleMultiPool instance for each test."""
    # Import here to ensure fresh state
    from database.oracle_pool import OracleMultiPool

    # Reset singleton
    OracleMultiPool._instance = None

    pool_manager = OracleMultiPool()
    yield pool_manager

    # Cleanup
    asyncio.get_event_loop().run_until_complete(pool_manager.close_pool())
    OracleMultiPool._instance = None


class TestOracleMultiPoolRegistration:
    """Tests for server registration functionality."""

    def test_register_server_stores_config(self, fresh_pool_manager):
        """Test that register_server correctly stores server configuration."""
        fresh_pool_manager.register_server(
            server_id="test_server",
            host="localhost",
            port=1521,
            user="test_user",
            password="test_pass",
            sid="TESTDB"
        )

        assert fresh_pool_manager.is_server_registered("test_server")
        assert "test_server" in fresh_pool_manager.get_registered_servers()

    def test_register_multiple_servers(self, fresh_pool_manager):
        """Test registering multiple servers."""
        fresh_pool_manager.register_server(
            server_id="server_a",
            host="host_a",
            port=1521,
            user="user_a",
            password="pass_a",
            sid="DBA"
        )
        fresh_pool_manager.register_server(
            server_id="server_b",
            host="host_b",
            port=1522,
            user="user_b",
            password="pass_b",
            service_name="SERVICE_B"
        )

        assert len(fresh_pool_manager.get_registered_servers()) == 2
        assert fresh_pool_manager.is_server_registered("server_a")
        assert fresh_pool_manager.is_server_registered("server_b")

    def test_pool_not_active_until_connection(self, fresh_pool_manager):
        """Test that pool is not created until first connection (lazy loading)."""
        fresh_pool_manager.register_server(
            server_id="lazy_server",
            host="localhost",
            port=1521,
            user="user",
            password="pass",
            sid="DB"
        )

        # Server is registered but pool is not active yet
        assert fresh_pool_manager.is_server_registered("lazy_server")
        assert not fresh_pool_manager.is_pool_active("lazy_server")
        assert "lazy_server" not in fresh_pool_manager.get_active_pools()


class TestOracleMultiPoolConnections:
    """Tests for connection management."""

    @pytest.mark.asyncio
    @patch('database.oracle_pool.oracledb.create_pool')
    async def test_lazy_pool_creation(self, mock_create_pool, fresh_pool_manager):
        """Test that pool is created lazily on first connection."""
        mock_pool = MockPool("lazy_test")
        mock_create_pool.return_value = mock_pool

        fresh_pool_manager.register_server(
            server_id="lazy_test",
            host="localhost",
            port=1521,
            user="user",
            password="pass",
            sid="DB"
        )

        # Before connection - pool not created
        assert not fresh_pool_manager.is_pool_active("lazy_test")

        # First connection - pool should be created
        async with fresh_pool_manager.get_connection("lazy_test") as conn:
            assert conn is not None

        # After connection - pool should be active
        assert fresh_pool_manager.is_pool_active("lazy_test")
        mock_create_pool.assert_called_once()

    @pytest.mark.asyncio
    @patch('database.oracle_pool.oracledb.create_pool')
    async def test_multiple_servers_simultaneous_connections(self, mock_create_pool, fresh_pool_manager):
        """Test simultaneous connections to different servers."""
        # Create mock pools for different servers
        mock_pools = {
            "server_1": MockPool("server_1"),
            "server_2": MockPool("server_2"),
        }
        mock_create_pool.side_effect = lambda **kwargs: mock_pools.get(
            kwargs.get('sid', 'unknown'),
            MockPool("unknown")
        )

        # Register servers
        fresh_pool_manager.register_server(
            server_id="server_1",
            host="host1",
            port=1521,
            user="user1",
            password="pass1",
            sid="server_1"
        )
        fresh_pool_manager.register_server(
            server_id="server_2",
            host="host2",
            port=1522,
            user="user2",
            password="pass2",
            sid="server_2"
        )

        # Simultaneous connections
        async with fresh_pool_manager.get_connection("server_1") as conn1:
            async with fresh_pool_manager.get_connection("server_2") as conn2:
                assert conn1 is not None
                assert conn2 is not None
                # Verify both pools are active
                assert fresh_pool_manager.is_pool_active("server_1")
                assert fresh_pool_manager.is_pool_active("server_2")

        # Both pools should still be active after connection closed
        assert len(fresh_pool_manager.get_active_pools()) == 2

    @pytest.mark.asyncio
    async def test_unregistered_server_raises_error(self, fresh_pool_manager):
        """Test that requesting connection to unregistered server raises error."""
        with pytest.raises(ValueError) as exc_info:
            async with fresh_pool_manager.get_connection("nonexistent_server"):
                pass

        assert "not registered" in str(exc_info.value)


class TestOracleMultiPoolShutdown:
    """Tests for graceful shutdown."""

    @pytest.mark.asyncio
    @patch('database.oracle_pool.oracledb.create_pool')
    async def test_close_specific_pool(self, mock_create_pool, fresh_pool_manager):
        """Test closing a specific pool."""
        mock_pool = MockPool("to_close")
        mock_create_pool.return_value = mock_pool

        fresh_pool_manager.register_server(
            server_id="to_close",
            host="localhost",
            port=1521,
            user="user",
            password="pass",
            sid="DB"
        )

        # Create pool by connecting
        async with fresh_pool_manager.get_connection("to_close"):
            pass

        assert fresh_pool_manager.is_pool_active("to_close")

        # Close specific pool
        await fresh_pool_manager.close_pool("to_close")

        assert not fresh_pool_manager.is_pool_active("to_close")
        assert mock_pool._closed

    @pytest.mark.asyncio
    @patch('database.oracle_pool.oracledb.create_pool')
    async def test_close_all_pools(self, mock_create_pool, fresh_pool_manager):
        """Test closing all pools at once."""
        mock_pools = []

        def create_mock_pool(**kwargs):
            pool = MockPool(kwargs.get('sid', 'unknown'))
            mock_pools.append(pool)
            return pool

        mock_create_pool.side_effect = create_mock_pool

        # Register and connect to multiple servers
        for i in range(3):
            fresh_pool_manager.register_server(
                server_id=f"server_{i}",
                host=f"host{i}",
                port=1521 + i,
                user=f"user{i}",
                password=f"pass{i}",
                sid=f"DB{i}"
            )
            async with fresh_pool_manager.get_connection(f"server_{i}"):
                pass

        assert len(fresh_pool_manager.get_active_pools()) == 3

        # Close all pools
        await fresh_pool_manager.close_pool()

        assert len(fresh_pool_manager.get_active_pools()) == 0
        for pool in mock_pools:
            assert pool._closed


class TestOracleMultiPoolBackwardCompatibility:
    """Tests for backward compatibility with legacy single-pool mode."""

    @pytest.mark.asyncio
    @patch('database.oracle_pool.oracledb.create_pool')
    async def test_legacy_pool_with_env_vars(self, mock_create_pool, fresh_pool_manager):
        """Test that initialize() with env vars creates legacy pool."""
        mock_pool = MockPool("legacy")
        mock_create_pool.return_value = mock_pool

        with patch.dict(os.environ, {
            'ORACLE_USER': 'test_user',
            'ORACLE_PASSWORD': 'test_pass',
            'ORACLE_HOST': 'localhost',
            'ORACLE_PORT': '1526',
            'ORACLE_SID': 'TESTDB'
        }):
            await fresh_pool_manager.initialize()

            # Should be able to get connection without server_id
            async with fresh_pool_manager.get_connection() as conn:
                assert conn is not None

    @pytest.mark.asyncio
    @patch('database.oracle_pool.oracledb.create_pool')
    async def test_default_server_fallback(self, mock_create_pool, fresh_pool_manager):
        """Test that get_connection() without server_id uses 'default' server if no legacy pool."""
        mock_pool = MockPool("default")
        mock_create_pool.return_value = mock_pool

        # Register a 'default' server
        fresh_pool_manager.register_server(
            server_id="default",
            host="localhost",
            port=1521,
            user="user",
            password="pass",
            sid="DB"
        )

        await fresh_pool_manager.initialize()

        # Should use 'default' server when no server_id provided
        async with fresh_pool_manager.get_connection() as conn:
            assert conn is not None

        assert fresh_pool_manager.is_pool_active("default")


class TestOracleMultiPoolStats:
    """Tests for pool statistics."""

    @pytest.mark.asyncio
    @patch('database.oracle_pool.oracledb.create_pool')
    async def test_get_pool_stats(self, mock_create_pool, fresh_pool_manager):
        """Test getting pool statistics."""
        mock_pool = MockPool("stats_test")
        mock_pool.opened = 5
        mock_pool.busy = 2
        mock_pool.min = 2
        mock_pool.max = 10
        mock_create_pool.return_value = mock_pool

        fresh_pool_manager.register_server(
            server_id="stats_test",
            host="localhost",
            port=1521,
            user="user",
            password="pass",
            sid="DB"
        )

        async with fresh_pool_manager.get_connection("stats_test"):
            pass

        stats = fresh_pool_manager.get_pool_stats("stats_test")

        assert "stats_test" in stats
        assert stats["stats_test"]["opened"] == 5
        assert stats["stats_test"]["min"] == 2
        assert stats["stats_test"]["max"] == 10

    @pytest.mark.asyncio
    @patch('database.oracle_pool.oracledb.create_pool')
    async def test_get_all_stats(self, mock_create_pool, fresh_pool_manager):
        """Test getting statistics for all pools."""
        mock_create_pool.return_value = MockPool("any")

        for i in range(2):
            fresh_pool_manager.register_server(
                server_id=f"srv{i}",
                host=f"host{i}",
                port=1521,
                user=f"user{i}",
                password=f"pass{i}",
                sid=f"DB{i}"
            )
            async with fresh_pool_manager.get_connection(f"srv{i}"):
                pass

        all_stats = fresh_pool_manager.get_pool_stats()

        assert len(all_stats) == 2
        assert "srv0" in all_stats
        assert "srv1" in all_stats


class TestOracleMultiPoolThreadSafety:
    """Tests for thread safety (race condition prevention)."""

    @pytest.mark.asyncio
    @patch('database.oracle_pool.oracledb.create_pool')
    async def test_concurrent_first_connections(self, mock_create_pool, fresh_pool_manager):
        """Test that concurrent first connections don't create duplicate pools."""
        call_count = 0

        def counting_create_pool(**kwargs):
            nonlocal call_count
            call_count += 1
            return MockPool("concurrent")

        mock_create_pool.side_effect = counting_create_pool

        fresh_pool_manager.register_server(
            server_id="concurrent",
            host="localhost",
            port=1521,
            user="user",
            password="pass",
            sid="DB"
        )

        # Simulate concurrent first connections
        async def connect():
            async with fresh_pool_manager.get_connection("concurrent"):
                await asyncio.sleep(0.01)

        await asyncio.gather(*[connect() for _ in range(5)])

        # Pool should only be created once despite concurrent requests
        assert call_count == 1
        assert len(fresh_pool_manager.get_active_pools()) == 1


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
