"""
Copyright (C) 2025 Luciano Guerche

This file is part of rabbitmq-mcp-server.

rabbitmq-mcp-server is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

rabbitmq-mcp-server is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with rabbitmq-mcp-server. If not, see <https://www.gnu.org/licenses/>.
"""

"""Unit tests for ReconnectionManager."""

import asyncio
from datetime import UTC, datetime, timezone
from unittest.mock import AsyncMock, MagicMock, Mock, patch

import pytest

from rabbitmq_mcp_connection.reconnect import ReconnectionManager
from rabbitmq_mcp_connection.schemas.connection import ConnectionState


@pytest.fixture
def mock_amqp_manager() -> Mock:
    """Create mock AMQP connection manager."""
    manager = Mock()
    manager.connect = AsyncMock()
    return manager


@pytest.fixture
def mock_http_manager() -> Mock:
    """Create mock HTTP client manager."""
    manager = Mock()
    manager.request = AsyncMock()
    return manager


@pytest.fixture
def reconnection_manager(
    mock_amqp_manager: Mock,
    mock_http_manager: Mock,
) -> ReconnectionManager:
    """Create ReconnectionManager instance."""
    return ReconnectionManager(
        amqp_manager=mock_amqp_manager,
        http_manager=mock_http_manager,
        max_delay=60,
        reset_after=300,
    )


class TestReconnectionManager:
    """Test suite for ReconnectionManager."""

    def test_initialization(
        self,
        reconnection_manager: ReconnectionManager,
        mock_amqp_manager: Mock,
        mock_http_manager: Mock,
    ) -> None:
        """Test ReconnectionManager initialization."""
        assert reconnection_manager._amqp_manager is mock_amqp_manager
        assert reconnection_manager._http_manager is mock_http_manager
        assert reconnection_manager._max_delay == 60
        assert reconnection_manager._reset_after == 300
        assert reconnection_manager._state == ConnectionState.DISCONNECTED
        assert reconnection_manager._attempt == 0
        assert not reconnection_manager._reconnecting

    def test_calculate_backoff_delay_schedule(
        self,
        reconnection_manager: ReconnectionManager,
    ) -> None:
        """Test exponential backoff schedule calculation (AC #1)."""
        # Test standard backoff schedule
        assert reconnection_manager._calculate_backoff_delay(1) == 1
        assert reconnection_manager._calculate_backoff_delay(2) == 2
        assert reconnection_manager._calculate_backoff_delay(3) == 4
        assert reconnection_manager._calculate_backoff_delay(4) == 8
        assert reconnection_manager._calculate_backoff_delay(5) == 16
        assert reconnection_manager._calculate_backoff_delay(6) == 32

        # Test max delay cap
        assert reconnection_manager._calculate_backoff_delay(7) == 60
        assert reconnection_manager._calculate_backoff_delay(8) == 60
        assert reconnection_manager._calculate_backoff_delay(100) == 60

    def test_calculate_backoff_delay_edge_cases(
        self,
        reconnection_manager: ReconnectionManager,
    ) -> None:
        """Test backoff delay calculation edge cases."""
        assert reconnection_manager._calculate_backoff_delay(0) == 1
        assert reconnection_manager._calculate_backoff_delay(-1) == 1

    def test_get_state_initial(self, reconnection_manager: ReconnectionManager) -> None:
        """Test getting initial reconnection state (AC #8)."""
        state = reconnection_manager.get_state()

        assert state.state == "disconnected"
        assert state.attempt == 0
        assert state.next_attempt_in_seconds is None
        assert state.total_downtime_seconds == 0.0
        assert state.last_reconnect_timestamp is None

    def test_get_state_during_reconnection(
        self,
        reconnection_manager: ReconnectionManager,
    ) -> None:
        """Test getting state during reconnection (AC #8)."""
        reconnection_manager._state = ConnectionState.RECONNECTING
        reconnection_manager._attempt = 5
        reconnection_manager._next_attempt_in = 32
        reconnection_manager._total_downtime = 125.5
        reconnection_manager._reconnecting = True

        state = reconnection_manager.get_state()

        assert state.state == "reconnecting"
        assert state.attempt == 5
        assert state.next_attempt_in_seconds == 32
        assert state.total_downtime_seconds == 125.5

    @pytest.mark.asyncio
    async def test_successful_reconnection(
        self,
        reconnection_manager: ReconnectionManager,
        mock_amqp_manager: Mock,
        mock_http_manager: Mock,
    ) -> None:
        """Test successful reconnection on first attempt (AC #4, #5, #7)."""
        # Mock successful connections
        mock_amqp_manager.connect.return_value = None
        mock_response = Mock()
        mock_response.status_code = 200
        mock_http_manager.request.return_value = mock_response

        with patch("asyncio.sleep", new_callable=AsyncMock) as mock_sleep:
            await reconnection_manager.handle_connection_loss()

        # Verify state after successful reconnection
        assert reconnection_manager._state == ConnectionState.CONNECTED
        assert not reconnection_manager._reconnecting
        assert reconnection_manager._last_reconnect_timestamp is not None

        # Verify connection attempts
        mock_amqp_manager.connect.assert_called_once()
        mock_http_manager.request.assert_called_once_with("GET", "/overview")

        # Verify backoff delay
        mock_sleep.assert_called_once_with(1)

    @pytest.mark.asyncio
    async def test_reconnection_retries_with_exponential_backoff(
        self,
        reconnection_manager: ReconnectionManager,
        mock_amqp_manager: Mock,
        mock_http_manager: Mock,
    ) -> None:
        """Test reconnection retries with exponential backoff (AC #1, #2, #3)."""
        # Mock 3 failures then success
        mock_amqp_manager.connect.side_effect = [
            Exception("Connection refused"),
            Exception("Connection refused"),
            Exception("Connection refused"),
            None,  # Success on 4th attempt
        ]
        mock_response = Mock()
        mock_response.status_code = 200
        mock_http_manager.request.return_value = mock_response

        with patch("asyncio.sleep", new_callable=AsyncMock) as mock_sleep:
            await reconnection_manager.handle_connection_loss()

        # Verify backoff delays: 1s, 2s, 4s, 8s
        assert mock_sleep.call_count == 4
        mock_sleep.assert_any_call(1)
        mock_sleep.assert_any_call(2)
        mock_sleep.assert_any_call(4)
        mock_sleep.assert_any_call(8)

        # Verify final state
        assert reconnection_manager._state == ConnectionState.CONNECTED
        assert reconnection_manager._attempt == 4

    @pytest.mark.asyncio
    async def test_partial_reconnection_amqp_success_http_fail(
        self,
        reconnection_manager: ReconnectionManager,
        mock_amqp_manager: Mock,
        mock_http_manager: Mock,
    ) -> None:
        """Test partial reconnection: AMQP succeeds, HTTP fails (AC #5)."""
        # AMQP succeeds, HTTP fails then succeeds
        mock_amqp_manager.connect.return_value = None
        mock_http_manager.request.side_effect = [
            Exception("HTTP connection failed"),
            Mock(status_code=200),
        ]

        with patch("asyncio.sleep", new_callable=AsyncMock):
            await reconnection_manager.handle_connection_loss()

        # Should retry and eventually succeed
        assert reconnection_manager._state == ConnectionState.CONNECTED
        assert mock_http_manager.request.call_count == 2

    @pytest.mark.asyncio
    async def test_partial_reconnection_http_success_amqp_fail(
        self,
        reconnection_manager: ReconnectionManager,
        mock_amqp_manager: Mock,
        mock_http_manager: Mock,
    ) -> None:
        """Test partial reconnection: HTTP succeeds, AMQP fails (AC #5)."""
        # AMQP fails then succeeds, HTTP succeeds
        mock_amqp_manager.connect.side_effect = [
            Exception("AMQP connection failed"),
            None,
        ]
        mock_response = Mock()
        mock_response.status_code = 200
        mock_http_manager.request.return_value = mock_response

        with patch("asyncio.sleep", new_callable=AsyncMock):
            await reconnection_manager.handle_connection_loss()

        # Should retry and eventually succeed
        assert reconnection_manager._state == ConnectionState.CONNECTED
        assert mock_amqp_manager.connect.call_count == 2

    @pytest.mark.asyncio
    async def test_reconnection_cancelled_during_shutdown(
        self,
        reconnection_manager: ReconnectionManager,
        mock_amqp_manager: Mock,
        mock_http_manager: Mock,
    ) -> None:
        """Test graceful cancellation during shutdown."""
        # Mock indefinite failures
        mock_amqp_manager.connect.side_effect = Exception("Connection refused")
        mock_http_manager.request.side_effect = Exception("Connection refused")

        # Start reconnection task
        reconnection_task = asyncio.create_task(
            reconnection_manager.handle_connection_loss(),
        )
        reconnection_manager._reconnect_task = reconnection_task

        # Wait a moment for reconnection to start
        await asyncio.sleep(0.05)

        # Cancel the task
        reconnection_task.cancel()

        # Verify CancelledError is raised
        with pytest.raises(asyncio.CancelledError):
            await reconnection_task

        # Verify reconnecting flag cleared
        assert not reconnection_manager._reconnecting

    @pytest.mark.asyncio
    async def test_duplicate_reconnection_trigger_ignored(
        self,
        reconnection_manager: ReconnectionManager,
        mock_amqp_manager: Mock,
        mock_http_manager: Mock,
    ) -> None:
        """Test that duplicate reconnection triggers are ignored (race condition)."""
        # Mock successful reconnection on first attempt
        mock_amqp_manager.connect = AsyncMock()
        mock_response = Mock()
        mock_response.status_code = 200
        mock_http_manager.request = AsyncMock(return_value=mock_response)

        # Start first reconnection
        task1 = asyncio.create_task(reconnection_manager.handle_connection_loss())

        # Wait very briefly to let first task acquire lock
        await asyncio.sleep(0.001)

        # Try to start second reconnection (should be ignored)
        task2 = asyncio.create_task(reconnection_manager.handle_connection_loss())

        await task1
        await task2

        # Both tasks complete, but only one reconnection should have occurred
        assert reconnection_manager._state == ConnectionState.CONNECTED
        # Should have exactly one successful connection attempt (first task)
        assert mock_amqp_manager.connect.call_count == 1

    @pytest.mark.asyncio
    async def test_shutdown_cancels_tasks(
        self,
        reconnection_manager: ReconnectionManager,
    ) -> None:
        """Test shutdown cancels all reconnection tasks."""
        # Create mock tasks
        reconnection_manager._reconnect_task = asyncio.create_task(asyncio.sleep(10))
        reconnection_manager._reset_task = asyncio.create_task(asyncio.sleep(10))

        await reconnection_manager.shutdown()

        # Verify tasks cancelled
        assert reconnection_manager._reconnect_task.cancelled()
        assert reconnection_manager._reset_task.cancelled()
        assert not reconnection_manager._reconnecting

    def test_get_state_after_reconnection(
        self,
        reconnection_manager: ReconnectionManager,
    ) -> None:
        """Test get_state returns correct data after successful reconnection."""
        now = datetime.now(UTC)
        reconnection_manager._state = ConnectionState.CONNECTED
        reconnection_manager._attempt = 0
        reconnection_manager._total_downtime = 150.0
        reconnection_manager._last_reconnect_timestamp = now
        reconnection_manager._reconnecting = False

        state = reconnection_manager.get_state()

        assert state.state == "connected"
        assert state.attempt == 0
        assert state.next_attempt_in_seconds is None
        assert state.total_downtime_seconds == 150.0
        assert state.last_reconnect_timestamp == now

    @pytest.mark.asyncio
    async def test_backoff_reset_after_stable_connection(
        self,
        reconnection_manager: ReconnectionManager,
        mock_amqp_manager: Mock,
        mock_http_manager: Mock,
    ) -> None:
        """Test backoff resets after 5 minutes of stable connection (AC #9)."""
        # Simulate previous reconnection with high attempt count
        reconnection_manager._state = ConnectionState.CONNECTED
        reconnection_manager._attempt = 5
        reconnection_manager._connection_start = datetime.now(UTC)

        # Trigger backoff reset
        reconnection_manager._reset_task = asyncio.create_task(
            reconnection_manager._schedule_backoff_reset()
        )

        # Wait for reset to complete (using very short duration for test)
        reconnection_manager._reset_after = 0.1
        await asyncio.sleep(0.15)

        # Verify backoff reset
        assert reconnection_manager._attempt == 0

    @pytest.mark.asyncio
    async def test_backoff_not_reset_on_premature_disconnection(
        self,
        reconnection_manager: ReconnectionManager,
    ) -> None:
        """Test backoff NOT reset if disconnected before 5 minutes (AC #9)."""
        # Simulate reconnection with high attempt count
        reconnection_manager._state = ConnectionState.CONNECTED
        reconnection_manager._attempt = 5
        reconnection_manager._connection_start = datetime.now(UTC)

        # Start reset task
        reconnection_manager._reset_task = asyncio.create_task(
            reconnection_manager._schedule_backoff_reset()
        )

        # Disconnect before reset completes
        await asyncio.sleep(0.01)
        reconnection_manager._state = ConnectionState.DISCONNECTED

        # Wait a bit more
        await asyncio.sleep(0.05)

        # Backoff should still be at attempt 5 (not reset)
        assert reconnection_manager._attempt == 5
