"""Tests for hytop.core.history.SlidingHistory."""

from __future__ import annotations

from dataclasses import dataclass

import pytest

from hytop.core.history import SlidingHistory


@dataclass
class _Sample:
    """Minimal MetricSample for testing."""

    ts: float
    value: float = 0.0


class TestSlidingHistoryBasics:
    def test_empty_latest_is_none(self):
        h = SlidingHistory(max_window_s=10)
        assert h.latest() is None

    def test_add_and_latest(self):
        h = SlidingHistory(max_window_s=10)
        s = _Sample(ts=1.0, value=42.0)
        h.add(s)
        assert h.latest() is s

    def test_latest_returns_most_recent(self):
        h = SlidingHistory(max_window_s=10)
        h.add(_Sample(ts=1.0, value=1.0))
        s2 = _Sample(ts=2.0, value=2.0)
        h.add(s2)
        assert h.latest() is s2

    def test_add_prunes_outside_window(self):
        h = SlidingHistory(max_window_s=5)
        h.add(_Sample(ts=0.0))
        h.add(_Sample(ts=6.0))  # now=6.0, cutoff=1.0 → ts=0.0 pruned
        assert len(h.samples) == 1

    def test_add_keeps_samples_within_window(self):
        h = SlidingHistory(max_window_s=10)
        h.add(_Sample(ts=0.0))
        h.add(_Sample(ts=8.0))  # cutoff=-2.0, both kept
        assert len(h.samples) == 2


class TestSlidingHistoryAvg:
    def test_empty_returns_zero(self):
        h = SlidingHistory(max_window_s=10)
        assert h.avg("value", window_s=5, now=10.0) == 0.0

    def test_single_sample_in_window(self):
        h = SlidingHistory(max_window_s=10)
        h.add(_Sample(ts=9.0, value=4.0))
        assert h.avg("value", window_s=5, now=10.0) == pytest.approx(4.0)

    def test_average_of_multiple(self):
        h = SlidingHistory(max_window_s=10)
        h.add(_Sample(ts=8.0, value=2.0))
        h.add(_Sample(ts=9.0, value=4.0))
        assert h.avg("value", window_s=5, now=10.0) == pytest.approx(3.0)

    def test_sample_outside_window_excluded(self):
        h = SlidingHistory(max_window_s=20)
        h.add(_Sample(ts=0.0, value=100.0))  # inside max_window but outside avg window
        h.add(_Sample(ts=9.0, value=2.0))
        # avg window=5, now=10 → cutoff=5.0 → ts=0.0 excluded
        assert h.avg("value", window_s=5, now=10.0) == pytest.approx(2.0)

    def test_no_samples_in_window_returns_zero(self):
        h = SlidingHistory(max_window_s=100)
        h.add(_Sample(ts=0.0, value=5.0))
        # window=2, now=10 → cutoff=8, ts=0 excluded
        assert h.avg("value", window_s=2, now=10.0) == 0.0

    def test_none_value_excluded_from_avg(self):
        """avg should skip samples where the attribute is None."""
        h = SlidingHistory(max_window_s=10)
        h.add(_Sample(ts=9.0, value=6.0))

        # Add a sample without a 'value' attr at all — use missing attribute branch
        # (MetricSample Protocol only requires ts; extra attrs may be None)
        @dataclass
        class _NoneValue:
            ts: float
            value: None = None

        h.add(_NoneValue(ts=9.5))
        # Only the float sample should count
        assert h.avg("value", window_s=5, now=10.0) == pytest.approx(6.0)
