"""Tests for SyncClockRuntime — thread-safe timing, pause/resume/reset."""

import threading
import time


from wled_controller.core.processing.sync_clock_runtime import SyncClockRuntime


class TestSyncClockRuntimeInit:
    def test_default_speed(self):
        rt = SyncClockRuntime()
        assert rt.speed == 1.0

    def test_custom_speed(self):
        rt = SyncClockRuntime(speed=2.5)
        assert rt.speed == 2.5

    def test_starts_running(self):
        rt = SyncClockRuntime()
        assert rt.is_running is True

    def test_initial_time_near_zero(self):
        rt = SyncClockRuntime()
        t = rt.get_time()
        assert 0.0 <= t < 0.1  # should be very small right after creation


class TestSyncClockRuntimeSpeed:
    def test_set_speed(self):
        rt = SyncClockRuntime()
        rt.speed = 3.0
        assert rt.speed == 3.0

    def test_speed_zero(self):
        rt = SyncClockRuntime()
        rt.speed = 0.0
        assert rt.speed == 0.0

    def test_speed_negative(self):
        """Negative speed is allowed at the runtime level (clamping is store-level)."""
        rt = SyncClockRuntime()
        rt.speed = -1.0
        assert rt.speed == -1.0


class TestSyncClockRuntimeGetTime:
    def test_time_advances(self):
        rt = SyncClockRuntime()
        t1 = rt.get_time()
        time.sleep(0.05)
        t2 = rt.get_time()
        assert t2 > t1

    def test_time_is_real_seconds(self):
        """get_time returns real elapsed seconds, NOT speed-scaled."""
        rt = SyncClockRuntime(speed=5.0)
        time.sleep(0.1)
        t = rt.get_time()
        # Should be roughly 0.1s (real time), not 0.5s (speed-scaled)
        assert 0.05 < t < 0.5


class TestSyncClockRuntimePauseResume:
    def test_pause_freezes_time(self):
        rt = SyncClockRuntime()
        time.sleep(0.05)
        rt.pause()
        t1 = rt.get_time()
        time.sleep(0.05)
        t2 = rt.get_time()
        assert t1 == t2  # time should not advance while paused

    def test_pause_sets_not_running(self):
        rt = SyncClockRuntime()
        rt.pause()
        assert rt.is_running is False

    def test_resume_unfreezes_time(self):
        rt = SyncClockRuntime()
        rt.pause()
        time.sleep(0.02)
        rt.resume()
        assert rt.is_running is True
        t1 = rt.get_time()
        time.sleep(0.05)
        t2 = rt.get_time()
        assert t2 > t1

    def test_resume_preserves_offset(self):
        """After pause+resume, time continues from where it was paused."""
        rt = SyncClockRuntime()
        time.sleep(0.05)
        rt.pause()
        paused_time = rt.get_time()
        time.sleep(0.1)
        rt.resume()
        resumed_time = rt.get_time()
        # Resumed time should be close to paused time (not reset, not including pause gap)
        assert abs(resumed_time - paused_time) < 0.05

    def test_double_pause_is_safe(self):
        rt = SyncClockRuntime()
        time.sleep(0.02)
        rt.pause()
        t1 = rt.get_time()
        rt.pause()  # second pause should be a no-op
        t2 = rt.get_time()
        assert t1 == t2

    def test_double_resume_is_safe(self):
        rt = SyncClockRuntime()
        rt.resume()  # already running, should be a no-op
        assert rt.is_running is True


class TestSyncClockRuntimeReset:
    def test_reset_sets_time_to_zero(self):
        rt = SyncClockRuntime()
        time.sleep(0.05)
        rt.reset()
        t = rt.get_time()
        assert t < 0.02  # should be very close to zero

    def test_reset_while_paused(self):
        rt = SyncClockRuntime()
        time.sleep(0.05)
        rt.pause()
        rt.reset()
        # After reset, offset is 0 but clock is still paused — is_running unchanged
        # The reset resets offset and epoch but doesn't change running state
        t = rt.get_time()
        # Time might be ~0 if paused, or very small if running
        assert t < 0.05

    def test_reset_preserves_speed(self):
        rt = SyncClockRuntime(speed=3.0)
        time.sleep(0.05)
        rt.reset()
        assert rt.speed == 3.0


class TestSyncClockRuntimeThreadSafety:
    def test_concurrent_get_time(self):
        rt = SyncClockRuntime()
        results = []
        errors = []

        def _read():
            try:
                for _ in range(100):
                    t = rt.get_time()
                    results.append(t)
            except Exception as e:
                errors.append(e)

        threads = [threading.Thread(target=_read) for _ in range(8)]
        for th in threads:
            th.start()
        for th in threads:
            th.join()

        assert len(errors) == 0
        assert len(results) == 800

    def test_concurrent_pause_resume(self):
        rt = SyncClockRuntime()
        errors = []

        def _toggle():
            try:
                for _ in range(50):
                    rt.pause()
                    rt.resume()
            except Exception as e:
                errors.append(e)

        threads = [threading.Thread(target=_toggle) for _ in range(4)]
        for th in threads:
            th.start()
        for th in threads:
            th.join()

        assert len(errors) == 0
