"""Tests for DualGateFilter and haversine distance."""

from __future__ import annotations

from tescmd.openclaw.config import FieldFilter
from tescmd.openclaw.filters import DualGateFilter, haversine


class TestHaversine:
    def test_same_point_is_zero(self) -> None:
        assert haversine(40.0, -74.0, 40.0, -74.0) == 0.0

    def test_known_distance(self) -> None:
        # New York (40.7128, -74.0060) to Los Angeles (34.0522, -118.2437)
        # ~3944 km
        dist = haversine(40.7128, -74.0060, 34.0522, -118.2437)
        assert 3_900_000 < dist < 4_000_000  # meters

    def test_short_distance(self) -> None:
        # Two points ~111m apart (0.001 degrees latitude at equator)
        dist = haversine(0.0, 0.0, 0.001, 0.0)
        assert 100 < dist < 120  # ~111m

    def test_antipodal_points(self) -> None:
        # Opposite sides of Earth: ~20,000 km
        dist = haversine(0.0, 0.0, 0.0, 180.0)
        assert 20_000_000 < dist < 20_100_000


class TestDualGateFilter:
    def _make_filter(self, **fields: tuple[float, float]) -> DualGateFilter:
        """Helper: create filter from field_name → (granularity, throttle_seconds)."""
        filters = {}
        for name, (gran, throttle) in fields.items():
            filters[name] = FieldFilter(granularity=gran, throttle_seconds=throttle)
        return DualGateFilter(filters)

    def test_first_value_always_emits(self) -> None:
        filt = self._make_filter(Soc=(5.0, 10.0))
        assert filt.should_emit("Soc", 72, 0.0) is True

    def test_unknown_field_uses_default_filter(self) -> None:
        """Unconfigured fields pass through with the module-level default fallback."""
        filt = self._make_filter(Soc=(5.0, 10.0))
        # First value for an unconfigured field — always emits
        assert filt.should_emit("Unknown", 42, 0.0) is True

    def test_default_filter_throttles(self) -> None:
        """Default fallback enforces its 5-second throttle on unconfigured fields."""
        filt = self._make_filter()
        assert filt.should_emit("NewField", 1, 0.0) is True
        filt.record_emit("NewField", 1, 0.0)
        # Changed value but within 5s throttle window
        assert filt.should_emit("NewField", 2, 3.0) is False
        # After 5s throttle
        assert filt.should_emit("NewField", 2, 6.0) is True

    def test_disabled_field_still_rejected(self) -> None:
        """Explicitly disabled fields are blocked even if unconfigured ones pass."""
        filters = {"Blocked": FieldFilter(enabled=False, granularity=0.0, throttle_seconds=0.0)}
        filt = DualGateFilter(filters)
        assert filt.should_emit("Blocked", 42, 0.0) is False

    def test_disabled_field_rejected(self) -> None:
        filters = {"Soc": FieldFilter(enabled=False, granularity=0.0, throttle_seconds=0.0)}
        filt = DualGateFilter(filters)
        assert filt.should_emit("Soc", 72, 0.0) is False

    def test_delta_below_threshold_rejected(self) -> None:
        filt = self._make_filter(Soc=(5.0, 0.0))
        assert filt.should_emit("Soc", 72, 0.0) is True
        filt.record_emit("Soc", 72, 0.0)
        # Change of 2, below granularity of 5
        assert filt.should_emit("Soc", 74, 1.0) is False

    def test_delta_at_threshold_emits(self) -> None:
        filt = self._make_filter(Soc=(5.0, 0.0))
        assert filt.should_emit("Soc", 72, 0.0) is True
        filt.record_emit("Soc", 72, 0.0)
        # Change of 5, equal to granularity
        assert filt.should_emit("Soc", 77, 1.0) is True

    def test_delta_above_threshold_emits(self) -> None:
        filt = self._make_filter(Soc=(5.0, 0.0))
        assert filt.should_emit("Soc", 72, 0.0) is True
        filt.record_emit("Soc", 72, 0.0)
        assert filt.should_emit("Soc", 80, 1.0) is True

    def test_throttle_blocks_rapid_emissions(self) -> None:
        filt = self._make_filter(Soc=(0.0, 10.0))
        assert filt.should_emit("Soc", 72, 0.0) is True
        filt.record_emit("Soc", 72, 0.0)
        # Value changed, but throttle (10s) not elapsed
        assert filt.should_emit("Soc", 80, 5.0) is False

    def test_throttle_allows_after_interval(self) -> None:
        filt = self._make_filter(Soc=(0.0, 10.0))
        assert filt.should_emit("Soc", 72, 0.0) is True
        filt.record_emit("Soc", 72, 0.0)
        # Throttle elapsed, value changed
        assert filt.should_emit("Soc", 80, 11.0) is True

    def test_zero_granularity_emits_on_any_change(self) -> None:
        filt = self._make_filter(ChargeState=(0.0, 0.0))
        assert filt.should_emit("ChargeState", "Charging", 0.0) is True
        filt.record_emit("ChargeState", "Charging", 0.0)
        # Same value — should NOT emit
        assert filt.should_emit("ChargeState", "Charging", 1.0) is False
        # Different value — should emit
        assert filt.should_emit("ChargeState", "Complete", 1.0) is True

    def test_location_uses_haversine(self) -> None:
        filt = self._make_filter(Location=(50.0, 0.0))  # 50m granularity
        loc1 = {"latitude": 40.7128, "longitude": -74.0060}
        loc2 = {"latitude": 40.7128, "longitude": -74.0060}  # same
        loc3 = {"latitude": 40.7138, "longitude": -74.0060}  # ~111m away

        assert filt.should_emit("Location", loc1, 0.0) is True
        filt.record_emit("Location", loc1, 0.0)

        # Same location — below 50m threshold
        assert filt.should_emit("Location", loc2, 1.0) is False

        # ~111m away — above 50m threshold
        assert filt.should_emit("Location", loc3, 1.0) is True

    def test_both_gates_must_pass(self) -> None:
        """Delta passes but throttle blocks."""
        filt = self._make_filter(Soc=(5.0, 10.0))
        assert filt.should_emit("Soc", 72, 0.0) is True
        filt.record_emit("Soc", 72, 0.0)
        # Big delta (10) but throttle not elapsed (5s < 10s)
        assert filt.should_emit("Soc", 82, 5.0) is False
        # Throttle elapsed (11s > 10s)
        assert filt.should_emit("Soc", 82, 11.0) is True

    def test_reset_clears_state(self) -> None:
        filt = self._make_filter(Soc=(5.0, 10.0))
        filt.record_emit("Soc", 72, 0.0)
        filt.reset()
        # After reset, first value always emits
        assert filt.should_emit("Soc", 72, 0.0) is True

    def test_non_numeric_value_treated_as_changed(self) -> None:
        filt = self._make_filter(Gear=(1.0, 0.0))
        assert filt.should_emit("Gear", "D", 0.0) is True
        filt.record_emit("Gear", "D", 0.0)
        # Non-numeric delta → infinity → always passes
        assert filt.should_emit("Gear", "R", 1.0) is True


class TestStalenessGate:
    """Tests for max_seconds (staleness override)."""

    def test_max_seconds_forces_emit_when_stale(self) -> None:
        """Value barely changes but max_seconds elapsed → force emit."""
        filters = {"Soc": FieldFilter(granularity=5.0, throttle_seconds=1.0, max_seconds=60.0)}
        filt = DualGateFilter(filters)
        assert filt.should_emit("Soc", 72, 0.0) is True
        filt.record_emit("Soc", 72, 0.0)
        # Delta too small (1), throttle passed, but max_seconds not reached
        assert filt.should_emit("Soc", 73, 30.0) is False
        # max_seconds reached (61s > 60s) → force through despite small delta
        assert filt.should_emit("Soc", 73, 61.0) is True

    def test_max_seconds_zero_disables_staleness(self) -> None:
        """max_seconds=0 means staleness gate is inactive."""
        filters = {"Soc": FieldFilter(granularity=5.0, throttle_seconds=1.0, max_seconds=0.0)}
        filt = DualGateFilter(filters)
        assert filt.should_emit("Soc", 72, 0.0) is True
        filt.record_emit("Soc", 72, 0.0)
        # Even after a long time, small delta still blocked
        assert filt.should_emit("Soc", 73, 999.0) is False

    def test_max_seconds_respects_throttle(self) -> None:
        """Staleness gate doesn't bypass the throttle gate."""
        filters = {"Soc": FieldFilter(granularity=5.0, throttle_seconds=10.0, max_seconds=5.0)}
        filt = DualGateFilter(filters)
        assert filt.should_emit("Soc", 72, 0.0) is True
        filt.record_emit("Soc", 72, 0.0)
        # max_seconds elapsed (6s > 5s) but throttle blocks (6s < 10s)
        assert filt.should_emit("Soc", 73, 6.0) is False
        # Both staleness and throttle satisfied (11s > 10s > 5s)
        assert filt.should_emit("Soc", 73, 11.0) is True

    def test_max_seconds_on_first_value(self) -> None:
        """First value always emits regardless of max_seconds config."""
        filters = {"Soc": FieldFilter(granularity=5.0, throttle_seconds=1.0, max_seconds=60.0)}
        filt = DualGateFilter(filters)
        assert filt.should_emit("Soc", 72, 0.0) is True

    def test_max_seconds_resets_after_forced_emit(self) -> None:
        """After staleness forces an emit, timer resets normally."""
        filters = {"Soc": FieldFilter(granularity=5.0, throttle_seconds=1.0, max_seconds=10.0)}
        filt = DualGateFilter(filters)
        assert filt.should_emit("Soc", 72, 0.0) is True
        filt.record_emit("Soc", 72, 0.0)
        # Force emit at 11s via staleness
        assert filt.should_emit("Soc", 73, 11.0) is True
        filt.record_emit("Soc", 73, 11.0)
        # Small delta again, staleness timer reset — not stale yet
        assert filt.should_emit("Soc", 74, 15.0) is False
        # Stale again at 22s (11s since last emit at 11s)
        assert filt.should_emit("Soc", 74, 22.0) is True

    def test_max_seconds_with_location(self) -> None:
        """Staleness gate works for location fields too."""
        filters = {
            "Location": FieldFilter(granularity=50.0, throttle_seconds=1.0, max_seconds=30.0),
        }
        filt = DualGateFilter(filters)
        loc = {"latitude": 40.7128, "longitude": -74.0060}
        assert filt.should_emit("Location", loc, 0.0) is True
        filt.record_emit("Location", loc, 0.0)
        # Same location, not stale yet
        assert filt.should_emit("Location", loc, 15.0) is False
        # Same location, but stale (31s > 30s)
        assert filt.should_emit("Location", loc, 31.0) is True
