"""Tests for IngressClient."""

from datetime import UTC, datetime
from unittest.mock import AsyncMock, MagicMock, patch

import httpx
import pytest

from ingress_edge.client import IngressClient
from ingress_edge.types import (
    IngressClientConfig,
    IngressDataPoint,
    IngressResponse,
)


class TestIngressClientInit:
    """Tests for IngressClient initialization."""

    def test_init_with_config(self, sample_config):
        """Test client initialization with config."""
        client = IngressClient(sample_config)

        assert client._base_url == "http://localhost:8000"
        assert client._api_key == "test-api-key"
        assert client._is_online is True

    def test_init_normalizes_base_url(self):
        """Test that trailing slash is removed from base URL."""
        config = IngressClientConfig(
            base_url="http://localhost:8000/",
            api_key="test-key",
        )
        client = IngressClient(config)

        assert client._base_url == "http://localhost:8000"

    def test_init_creates_http_client_with_auth_header(self, sample_config):
        """Test that HTTP client has Authorization header."""
        client = IngressClient(sample_config)

        headers = client._http_client.headers
        assert "Authorization" in headers
        assert headers["Authorization"] == "Bearer test-api-key"

    def test_init_creates_batch_manager(self, sample_config):
        """Test that batch manager is created."""
        client = IngressClient(sample_config)

        assert client._batch_manager is not None
        assert client._batch_manager._batch_size == 100

    def test_init_creates_retry_manager(self, sample_config):
        """Test that retry manager is created."""
        client = IngressClient(sample_config)

        assert client._retry_manager is not None
        assert client._retry_manager._max_retries == 3

    def test_init_creates_offline_buffer(self, sample_config):
        """Test that offline buffer is created."""
        client = IngressClient(sample_config)

        assert client._offline_buffer is not None
        stats = client._offline_buffer.get_stats()
        assert stats.max_size == 1000


class TestIngressClientSend:
    """Tests for send() method."""

    @pytest.mark.asyncio
    async def test_send_adds_to_batch_when_online(self, sample_config, sample_data_point):
        """Test that send adds point to batch when online."""
        client = IngressClient(sample_config)
        client._batch_manager.add = AsyncMock()

        await client.send(sample_data_point)

        client._batch_manager.add.assert_called_once_with(sample_data_point)

    @pytest.mark.asyncio
    async def test_send_adds_to_buffer_when_offline(self, sample_config, sample_data_point):
        """Test that send adds point to offline buffer when offline."""
        client = IngressClient(sample_config)
        client._is_online = False

        await client.send(sample_data_point)

        stats = client._offline_buffer.get_stats()
        assert stats.size == 1

    @pytest.mark.asyncio
    async def test_send_does_not_batch_when_offline(self, sample_config, sample_data_point):
        """Test that send does not use batch manager when offline."""
        client = IngressClient(sample_config)
        client._is_online = False
        client._batch_manager.add = AsyncMock()

        await client.send(sample_data_point)

        client._batch_manager.add.assert_not_called()


class TestIngressClientFlush:
    """Tests for flush() method."""

    @pytest.mark.asyncio
    async def test_flush_calls_batch_manager_flush(self, sample_config):
        """Test that flush delegates to batch manager."""
        client = IngressClient(sample_config)
        client._batch_manager.flush = AsyncMock()

        await client.flush()

        client._batch_manager.flush.assert_called_once()


class TestIngressClientBufferStats:
    """Tests for get_buffer_stats() method."""

    def test_get_buffer_stats_returns_stats(self, sample_config, sample_data_point):
        """Test that get_buffer_stats returns correct stats."""
        client = IngressClient(sample_config)
        client._is_online = False

        # Add a point to the buffer via offline path
        client._offline_buffer.add(sample_data_point)

        stats = client.get_buffer_stats()

        assert stats.size == 1
        assert stats.max_size == 1000
        assert stats.is_at_capacity is False

    def test_get_buffer_stats_empty_buffer(self, sample_config):
        """Test stats for empty buffer."""
        client = IngressClient(sample_config)

        stats = client.get_buffer_stats()

        assert stats.size == 0
        assert stats.is_at_capacity is False


class TestIngressClientOnlineStatus:
    """Tests for online status management."""

    def test_is_client_online_initial_state(self, sample_config):
        """Test that client starts online."""
        client = IngressClient(sample_config)

        assert client.is_client_online() is True

    @pytest.mark.asyncio
    async def test_set_online_false(self, sample_config):
        """Test setting client offline."""
        client = IngressClient(sample_config)

        await client.set_online(False)

        assert client.is_client_online() is False

    @pytest.mark.asyncio
    async def test_set_online_true_drains_buffer(self, sample_config, sample_data_point):
        """Test that coming online drains the offline buffer."""
        client = IngressClient(sample_config)

        # Go offline and buffer some data
        await client.set_online(False)
        client._offline_buffer.add(sample_data_point)
        assert client.get_buffer_stats().size == 1

        # Mock _send_batch to prevent actual HTTP calls
        client._send_batch = AsyncMock(
            return_value=IngressResponse(accepted=1, rejected=0)
        )

        # Come back online
        await client.set_online(True)

        # Buffer should be drained
        assert client.get_buffer_stats().size == 0
        assert client._send_batch.called

    @pytest.mark.asyncio
    async def test_set_online_true_when_already_online_no_drain(self, sample_config):
        """Test that setting online when already online doesn't drain."""
        client = IngressClient(sample_config)
        client.drain_offline_buffer = AsyncMock()

        await client.set_online(True)

        client.drain_offline_buffer.assert_not_called()


class TestIngressClientDrainBuffer:
    """Tests for drain_offline_buffer() method."""

    @pytest.mark.asyncio
    async def test_drain_empty_buffer_no_op(self, sample_config):
        """Test that draining empty buffer does nothing."""
        client = IngressClient(sample_config)
        client._send_batch = AsyncMock()

        await client.drain_offline_buffer()

        client._send_batch.assert_not_called()

    @pytest.mark.asyncio
    async def test_drain_sends_buffered_points(self, sample_config, sample_data_point):
        """Test that drain sends all buffered points."""
        client = IngressClient(sample_config)
        client._offline_buffer.add(sample_data_point)
        client._offline_buffer.add(sample_data_point)
        client._send_batch = AsyncMock(
            return_value=IngressResponse(accepted=2, rejected=0)
        )

        await client.drain_offline_buffer()

        client._send_batch.assert_called_once()
        call_args = client._send_batch.call_args[0][0]
        assert len(call_args) == 2

    @pytest.mark.asyncio
    async def test_drain_batches_large_buffers(self, sample_config):
        """Test that drain sends in batches of 1000."""
        config = IngressClientConfig(
            base_url="http://localhost:8000",
            api_key="test-key",
            offline_buffer_size=3000,
        )
        client = IngressClient(config)

        # Add 2500 points
        for i in range(2500):
            client._offline_buffer.add(
                IngressDataPoint(
                    channel_id="ch_test",
                    timestamp=datetime.now(UTC),
                    data={"index": i},
                )
            )

        client._send_batch = AsyncMock(
            return_value=IngressResponse(accepted=1000, rejected=0)
        )

        await client.drain_offline_buffer()

        # Should be called 3 times: 1000 + 1000 + 500
        assert client._send_batch.call_count == 3


class TestIngressClientPendingCount:
    """Tests for get_pending_count() method."""

    def test_get_pending_count_initial(self, sample_config):
        """Test pending count is 0 initially."""
        client = IngressClient(sample_config)

        assert client.get_pending_count() == 0


class TestIngressClientContextManager:
    """Tests for async context manager."""

    @pytest.mark.asyncio
    async def test_context_manager_returns_client(self, sample_config):
        """Test that context manager returns client instance."""
        async with IngressClient(sample_config) as client:
            assert isinstance(client, IngressClient)

    @pytest.mark.asyncio
    async def test_context_manager_closes_on_exit(self, sample_config):
        """Test that context manager closes client on exit."""
        client = IngressClient(sample_config)
        client.close = AsyncMock()

        async with client:
            pass

        client.close.assert_called_once()


class TestIngressClientClose:
    """Tests for close() method."""

    @pytest.mark.asyncio
    async def test_close_closes_http_client(self, sample_config):
        """Test that close closes the HTTP client."""
        client = IngressClient(sample_config)

        await client.close()

        # After closing, the client should not be usable
        # In real implementation, this would raise an error
        assert True  # If we got here without error, close worked


class TestIngressClientSendBatch:
    """Tests for _send_batch() internal method."""

    @pytest.mark.asyncio
    async def test_send_batch_posts_to_ingest_endpoint(self, sample_config, sample_data_point):
        """Test that _send_batch POSTs to /ingest endpoint."""
        client = IngressClient(sample_config)

        # Mock the HTTP client
        mock_response = MagicMock()
        mock_response.json.return_value = {"accepted": 1, "rejected": 0, "errors": None}
        mock_response.raise_for_status = MagicMock()

        client._http_client.post = AsyncMock(return_value=mock_response)

        await client._send_batch([sample_data_point])

        client._http_client.post.assert_called_once()
        call_args = client._http_client.post.call_args
        assert call_args[0][0] == "http://localhost:8000/ingest"

    @pytest.mark.asyncio
    async def test_send_batch_includes_points_in_body(self, sample_config, sample_data_point):
        """Test that _send_batch includes points in request body."""
        client = IngressClient(sample_config)

        mock_response = MagicMock()
        mock_response.json.return_value = {"accepted": 1, "rejected": 0, "errors": None}
        mock_response.raise_for_status = MagicMock()

        client._http_client.post = AsyncMock(return_value=mock_response)

        await client._send_batch([sample_data_point])

        call_args = client._http_client.post.call_args
        body = call_args[1]["json"]
        assert "points" in body
        assert len(body["points"]) == 1

    @pytest.mark.asyncio
    async def test_send_batch_returns_response(self, sample_config, sample_data_point):
        """Test that _send_batch returns IngressResponse."""
        client = IngressClient(sample_config)

        mock_response = MagicMock()
        mock_response.json.return_value = {"accepted": 1, "rejected": 0, "errors": None}
        mock_response.raise_for_status = MagicMock()

        client._http_client.post = AsyncMock(return_value=mock_response)

        result = await client._send_batch([sample_data_point])

        assert isinstance(result, IngressResponse)
        assert result.accepted == 1
        assert result.rejected == 0
