"""
test_night_vision.py — Unit tests for LightAnalyser and GPIOHeadlight.

Runs without ROS2 / Jetson hardware (no rclpy, no GPIO).
"""

import sys
import os
import math

import numpy as np
import pytest

sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))

from saltybot_night_vision.light_analyser import LightAnalyser
from saltybot_night_vision.gpio_headlight import GPIOHeadlight


# ── LightAnalyser tests ───────────────────────────────────────────────────────

class TestLightAnalyser:

    def _bright_frame(self, value: int = 180) -> np.ndarray:
        return np.full((480, 640), value, dtype=np.uint8)

    def _dark_frame(self, value: int = 5) -> np.ndarray:
        return np.full((480, 640), value, dtype=np.uint8)

    def test_initial_mode_default(self):
        la = LightAnalyser()
        assert la.mode == 'day'

    def test_bright_frame_stays_day(self):
        la = LightAnalyser()
        for _ in range(10):
            mode, _ = la.update(self._bright_frame(180))
        assert mode == 'day'

    def test_dark_frame_transitions_to_ir_only(self):
        la = LightAnalyser()
        for _ in range(10):
            mode, _ = la.update(self._dark_frame(3))
        assert mode == 'ir_only'

    def test_medium_intensity_twilight(self):
        la = LightAnalyser()
        frame = np.full((480, 640), 75, dtype=np.uint8)
        for _ in range(10):
            mode, _ = la.update(frame)
        assert mode == 'twilight'

    def test_night_range(self):
        la = LightAnalyser()
        frame = np.full((480, 640), 25, dtype=np.uint8)
        for _ in range(10):
            mode, _ = la.update(frame)
        assert mode == 'night'

    def test_intensity_value_correct(self):
        la = LightAnalyser()
        frame = np.full((60, 80), 128, dtype=np.uint8)
        _, intensity = la.update(frame)
        assert intensity == pytest.approx(128.0, abs=0.5)

    def test_rgb_frame_accepted(self):
        la = LightAnalyser()
        frame = np.full((480, 640, 3), 60, dtype=np.uint8)
        mode, intensity = la.update(frame)
        assert isinstance(mode, str)
        assert 0.0 <= intensity <= 255.0

    def test_hysteresis_prevents_oscillation(self):
        """Frame right at threshold should not cause mode flip on each tick."""
        la = LightAnalyser()
        # Start from day, push to just below twilight threshold
        frame = np.full((480, 640), 100, dtype=np.uint8)
        modes: list[str] = []
        for _ in range(20):
            mode, _ = la.update(frame)
            modes.append(mode)
        # Should not oscillate — all same mode
        assert len(set(modes[-10:])) == 1

    def test_recovery_to_day(self):
        la = LightAnalyser(initial_mode='ir_only')
        bright = np.full((480, 640), 200, dtype=np.uint8)
        for _ in range(20):
            mode, _ = la.update(bright)
        assert mode == 'day'


# ── GPIOHeadlight tests (simulated — no hardware) ─────────────────────────────

class TestGPIOHeadlightSim:
    """
    Tests that GPIOHeadlight correctly tracks brightness internally
    even when running in simulation mode (no hardware GPIO).
    """

    def test_default_brightness_zero(self):
        hl = GPIOHeadlight()
        assert hl._brightness == pytest.approx(0.0)

    def test_set_brightness_clamps_high(self):
        hl = GPIOHeadlight()
        hl.set_brightness(2.5)
        assert hl._brightness == pytest.approx(1.0)

    def test_set_brightness_clamps_low(self):
        hl = GPIOHeadlight()
        hl.set_brightness(-0.5)
        assert hl._brightness == pytest.approx(0.0)

    def test_set_level_off(self):
        hl = GPIOHeadlight()
        hl.set_brightness(0.8)
        hl.set_level('off')
        assert hl._brightness == pytest.approx(0.0)

    def test_set_level_full(self):
        hl = GPIOHeadlight()
        hl.set_level('full')
        assert hl._brightness == pytest.approx(1.0)

    def test_set_level_dim(self):
        hl = GPIOHeadlight()
        hl.set_level('dim')
        assert hl._brightness == pytest.approx(0.25)

    def test_off_method(self):
        hl = GPIOHeadlight()
        hl.set_brightness(0.75)
        hl.off()
        assert hl._brightness == pytest.approx(0.0)

    def test_cleanup_sets_zero(self):
        hl = GPIOHeadlight()
        hl.set_brightness(0.9)
        hl.cleanup()
        assert hl._brightness == pytest.approx(0.0)

    def test_no_op_on_same_brightness(self):
        """Repeated set of same value should not raise."""
        hl = GPIOHeadlight()
        hl.set_brightness(0.5)
        hl.set_brightness(0.5)   # should be no-op, no error
        assert hl._brightness == pytest.approx(0.5)


if __name__ == '__main__':
    pytest.main([__file__, '-v'])
