"""Tests for OfflineBuffer."""

from datetime import UTC, datetime

import pytest

from ingress_edge.offline import OfflineBuffer
from ingress_edge.types import IngressDataPoint


def make_point(value: int = 1) -> IngressDataPoint:
    """Create a test data point."""
    return IngressDataPoint(
        channel_id="ch_test",
        timestamp=datetime.now(UTC),
        data={"value": value},
    )


class TestOfflineBufferInit:
    """Tests for OfflineBuffer initialization."""

    def test_init_default_max_size(self):
        """Test default max_size is 10000."""
        buffer = OfflineBuffer()

        stats = buffer.get_stats()
        assert stats.max_size == 10000

    def test_init_custom_max_size(self):
        """Test custom max_size."""
        buffer = OfflineBuffer(max_size=500)

        stats = buffer.get_stats()
        assert stats.max_size == 500

    def test_init_empty_buffer(self):
        """Test that buffer starts empty."""
        buffer = OfflineBuffer()

        assert buffer.is_empty()
        stats = buffer.get_stats()
        assert stats.size == 0


class TestOfflineBufferAdd:
    """Tests for add() method."""

    def test_add_single_point(self):
        """Test adding a single point."""
        buffer = OfflineBuffer()

        buffer.add(make_point())

        stats = buffer.get_stats()
        assert stats.size == 1

    def test_add_multiple_points(self):
        """Test adding multiple points."""
        buffer = OfflineBuffer()

        for i in range(5):
            buffer.add(make_point(i))

        stats = buffer.get_stats()
        assert stats.size == 5

    def test_add_preserves_order(self):
        """Test that points are stored in FIFO order."""
        buffer = OfflineBuffer()

        for i in range(3):
            buffer.add(make_point(i))

        points = buffer.drain()
        values = [p.data["value"] for p in points]
        assert values == [0, 1, 2]

    def test_add_circular_behavior_drops_oldest(self):
        """Test that oldest points are dropped when buffer is full."""
        buffer = OfflineBuffer(max_size=3)

        # Add 5 points to a buffer of size 3
        for i in range(5):
            buffer.add(make_point(i))

        # Buffer should only contain last 3 points
        stats = buffer.get_stats()
        assert stats.size == 3

        points = buffer.drain()
        values = [p.data["value"] for p in points]
        assert values == [2, 3, 4]  # Oldest (0, 1) were dropped

    def test_add_at_capacity(self):
        """Test adding at capacity marks is_at_capacity."""
        buffer = OfflineBuffer(max_size=3)

        for i in range(3):
            buffer.add(make_point(i))

        stats = buffer.get_stats()
        assert stats.is_at_capacity is True

    def test_add_over_capacity_maintains_max_size(self):
        """Test that buffer never exceeds max_size."""
        buffer = OfflineBuffer(max_size=5)

        for i in range(100):
            buffer.add(make_point(i))

        stats = buffer.get_stats()
        assert stats.size == 5


class TestOfflineBufferDrain:
    """Tests for drain() method."""

    def test_drain_returns_all_points(self):
        """Test that drain returns all buffered points."""
        buffer = OfflineBuffer()

        for i in range(5):
            buffer.add(make_point(i))

        points = buffer.drain()

        assert len(points) == 5

    def test_drain_clears_buffer(self):
        """Test that drain clears the buffer."""
        buffer = OfflineBuffer()

        for i in range(5):
            buffer.add(make_point(i))

        buffer.drain()

        assert buffer.is_empty()
        stats = buffer.get_stats()
        assert stats.size == 0

    def test_drain_empty_buffer_returns_empty_list(self):
        """Test that draining empty buffer returns empty list."""
        buffer = OfflineBuffer()

        points = buffer.drain()

        assert points == []

    def test_drain_returns_copy(self):
        """Test that drain returns a copy, not the internal list."""
        buffer = OfflineBuffer()
        buffer.add(make_point())

        points = buffer.drain()
        points.append(make_point())

        # Buffer should still be empty
        assert buffer.is_empty()

    def test_drain_resets_capacity_flag(self):
        """Test that drain resets is_at_capacity."""
        buffer = OfflineBuffer(max_size=3)

        for i in range(3):
            buffer.add(make_point(i))

        assert buffer.get_stats().is_at_capacity is True

        buffer.drain()

        assert buffer.get_stats().is_at_capacity is False


class TestOfflineBufferGetStats:
    """Tests for get_stats() method."""

    def test_get_stats_empty_buffer(self):
        """Test stats for empty buffer."""
        buffer = OfflineBuffer(max_size=100)

        stats = buffer.get_stats()

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

    def test_get_stats_partial_buffer(self):
        """Test stats for partially filled buffer."""
        buffer = OfflineBuffer(max_size=100)

        for i in range(50):
            buffer.add(make_point(i))

        stats = buffer.get_stats()

        assert stats.size == 50
        assert stats.max_size == 100
        assert stats.is_at_capacity is False

    def test_get_stats_full_buffer(self):
        """Test stats for full buffer."""
        buffer = OfflineBuffer(max_size=10)

        for i in range(10):
            buffer.add(make_point(i))

        stats = buffer.get_stats()

        assert stats.size == 10
        assert stats.max_size == 10
        assert stats.is_at_capacity is True

    def test_get_stats_over_capacity(self):
        """Test stats when adding beyond capacity."""
        buffer = OfflineBuffer(max_size=5)

        for i in range(20):
            buffer.add(make_point(i))

        stats = buffer.get_stats()

        assert stats.size == 5  # Capped at max_size
        assert stats.is_at_capacity is True


class TestOfflineBufferClear:
    """Tests for clear() method."""

    def test_clear_empties_buffer(self):
        """Test that clear empties the buffer."""
        buffer = OfflineBuffer()

        for i in range(10):
            buffer.add(make_point(i))

        buffer.clear()

        assert buffer.is_empty()
        stats = buffer.get_stats()
        assert stats.size == 0

    def test_clear_empty_buffer_no_op(self):
        """Test that clearing empty buffer is safe."""
        buffer = OfflineBuffer()

        buffer.clear()

        assert buffer.is_empty()

    def test_clear_does_not_return_points(self):
        """Test that clear does not return points (unlike drain)."""
        buffer = OfflineBuffer()
        buffer.add(make_point())

        result = buffer.clear()

        assert result is None


class TestOfflineBufferIsEmpty:
    """Tests for is_empty() method."""

    def test_is_empty_true_initially(self):
        """Test that new buffer is empty."""
        buffer = OfflineBuffer()

        assert buffer.is_empty() is True

    def test_is_empty_false_with_points(self):
        """Test that buffer with points is not empty."""
        buffer = OfflineBuffer()
        buffer.add(make_point())

        assert buffer.is_empty() is False

    def test_is_empty_true_after_drain(self):
        """Test that buffer is empty after drain."""
        buffer = OfflineBuffer()
        buffer.add(make_point())
        buffer.drain()

        assert buffer.is_empty() is True

    def test_is_empty_true_after_clear(self):
        """Test that buffer is empty after clear."""
        buffer = OfflineBuffer()
        buffer.add(make_point())
        buffer.clear()

        assert buffer.is_empty() is True


class TestOfflineBufferIsFull:
    """Tests for is_full() method."""

    def test_is_full_false_initially(self):
        """Test that new buffer is not full."""
        buffer = OfflineBuffer()

        assert buffer.is_full() is False

    def test_is_full_false_partial(self):
        """Test that partially filled buffer is not full."""
        buffer = OfflineBuffer(max_size=10)

        for i in range(5):
            buffer.add(make_point(i))

        assert buffer.is_full() is False

    def test_is_full_true_at_capacity(self):
        """Test that buffer at capacity is full."""
        buffer = OfflineBuffer(max_size=5)

        for i in range(5):
            buffer.add(make_point(i))

        assert buffer.is_full() is True

    def test_is_full_true_over_capacity(self):
        """Test that buffer remains full after overflow."""
        buffer = OfflineBuffer(max_size=5)

        for i in range(10):
            buffer.add(make_point(i))

        assert buffer.is_full() is True

    def test_is_full_false_after_drain(self):
        """Test that buffer is not full after drain."""
        buffer = OfflineBuffer(max_size=5)

        for i in range(5):
            buffer.add(make_point(i))

        buffer.drain()

        assert buffer.is_full() is False
