"""AZ-398 — :class:`Clock` Protocol conformance + WallClock parity + TlogDerivedClock semantics."""

from __future__ import annotations

import time

import pytest

from gps_denied_onboard.clock import Clock
from gps_denied_onboard.clock.tlog_derived import (
    ClockOrderingError,
    TlogDerivedClock,
)
from gps_denied_onboard.clock.wall_clock import WallClock
from gps_denied_onboard.runtime_root.clock_factory import build_clock


# ---------------------------------------------------------------------------
# AC-1 — Protocol conformance.


def test_wall_clock_satisfies_clock_protocol() -> None:
    # Assert
    assert isinstance(WallClock(), Clock)


def test_tlog_derived_clock_satisfies_clock_protocol() -> None:
    # Assert
    assert isinstance(TlogDerivedClock([1, 2, 3]), Clock)


# ---------------------------------------------------------------------------
# AC-5 — WallClock parity with :mod:`time`.


def test_wall_clock_monotonic_ns_tracks_stdlib() -> None:
    # Arrange
    clock = WallClock()
    # Act
    stdlib_before = time.monotonic_ns()
    clock_now = clock.monotonic_ns()
    stdlib_after = time.monotonic_ns()
    # Assert
    assert stdlib_before <= clock_now <= stdlib_after


def test_wall_clock_time_ns_tracks_stdlib_within_1ms() -> None:
    # Arrange
    clock = WallClock()
    # Act
    stdlib = time.time_ns()
    clock_now = clock.time_ns()
    # Assert
    assert abs(clock_now - stdlib) <= 1_000_000


def test_wall_clock_sleep_until_ns_blocks_for_about_100ms() -> None:
    # Arrange
    clock = WallClock()
    # Act
    start = time.monotonic_ns()
    target = start + 100_000_000  # 100 ms in the future
    clock.sleep_until_ns(target)
    elapsed_ns = time.monotonic_ns() - start
    # Assert — AC-5 allows ±5 ms slack on a 100 ms sleep
    assert 95_000_000 <= elapsed_ns <= 200_000_000


def test_wall_clock_sleep_until_past_target_is_noop() -> None:
    # Arrange
    clock = WallClock()
    past = clock.monotonic_ns() - 10_000_000_000  # 10 s ago
    # Act
    start = time.monotonic_ns()
    clock.sleep_until_ns(past)
    elapsed_ns = time.monotonic_ns() - start
    # Assert — should return almost immediately (no negative sleep)
    assert elapsed_ns < 5_000_000  # < 5 ms


# ---------------------------------------------------------------------------
# AC-6 — TlogDerivedClock advance-on-call semantics.


def test_tlog_derived_clock_advances_per_call() -> None:
    # Arrange
    clock = TlogDerivedClock([1_000_000, 2_000_000, 3_000_000])
    # Act
    a = clock.monotonic_ns()
    b = clock.monotonic_ns()
    c = clock.monotonic_ns()
    # Assert
    assert (a, b, c) == (1_000_000, 2_000_000, 3_000_000)


def test_tlog_derived_clock_time_ns_reflects_last_advance() -> None:
    # Arrange
    clock = TlogDerivedClock([42])
    # Act
    before = clock.time_ns()
    clock.monotonic_ns()
    after = clock.time_ns()
    # Assert
    assert (before, after) == (0, 42)


def test_tlog_derived_clock_raises_on_non_monotonic_source() -> None:
    # Arrange
    clock = TlogDerivedClock([10, 5])
    clock.monotonic_ns()
    # Act + Assert
    with pytest.raises(ClockOrderingError):
        clock.monotonic_ns()


def test_tlog_derived_clock_sleep_until_ns_is_noop() -> None:
    # Arrange
    clock = TlogDerivedClock([1])
    start = time.monotonic_ns()
    # Act
    clock.sleep_until_ns(10**18)  # absurdly far in the future
    elapsed_ns = time.monotonic_ns() - start
    # Assert
    assert elapsed_ns < 5_000_000  # < 5 ms


def test_tlog_derived_clock_accepts_callable_source() -> None:
    # Arrange
    counter = {"i": 0}

    def source() -> int:
        counter["i"] += 1
        return counter["i"] * 1_000_000

    clock = TlogDerivedClock(source)
    # Act
    a = clock.monotonic_ns()
    b = clock.monotonic_ns()
    # Assert
    assert (a, b) == (1_000_000, 2_000_000)


def test_tlog_derived_clock_returns_last_value_when_source_exhausted() -> None:
    # Arrange
    clock = TlogDerivedClock([5])
    # Act
    first = clock.monotonic_ns()
    second = clock.monotonic_ns()
    # Assert — exhausted source returns the latched value, not an error
    assert (first, second) == (5, 5)


# ---------------------------------------------------------------------------
# Composition-root factory (build_clock).


def test_build_clock_wall_returns_wall_clock() -> None:
    # Assert
    assert isinstance(build_clock(kind="wall"), WallClock)


def test_build_clock_tlog_returns_tlog_derived_clock() -> None:
    # Assert
    assert isinstance(build_clock(kind="tlog", source=[1, 2]), TlogDerivedClock)


def test_build_clock_rejects_unknown_kind() -> None:
    # Act + Assert
    with pytest.raises(ValueError, match="unknown kind"):
        build_clock(kind="invalid")  # type: ignore[arg-type]


def test_build_clock_wall_rejects_source() -> None:
    with pytest.raises(ValueError, match="source must be None"):
        build_clock(kind="wall", source=[1])


def test_build_clock_tlog_requires_source() -> None:
    with pytest.raises(ValueError, match="source is required"):
        build_clock(kind="tlog")
