"""Tests for Hybrid Logical Clock."""
import pytest

from greymatter_core.sync.hlc import HLC, HLCTimestamp, ClockSkewError, MAX_CLOCK_SKEW_MS


class TestHLCTimestamp:
    def test_string_roundtrip(self):
        ts = HLCTimestamp(wall_ms=1741459200000, counter=42, node_id="node-1")
        s = str(ts)
        parsed = HLCTimestamp.from_str(s)
        assert parsed == ts

    def test_string_format(self):
        ts = HLCTimestamp(wall_ms=1741459200000, counter=1, node_id="n1")
        assert str(ts) == "1741459200000.000001.n1"

    def test_int_roundtrip(self):
        ts = HLCTimestamp(wall_ms=1741459200000, counter=42, node_id="n1")
        val = ts.to_int()
        recovered = HLCTimestamp.from_int(val, node_id="n1")
        assert recovered.wall_ms == ts.wall_ms
        assert recovered.counter == ts.counter

    def test_ordering(self):
        a = HLCTimestamp(wall_ms=100, counter=0, node_id="n1")
        b = HLCTimestamp(wall_ms=100, counter=1, node_id="n1")
        c = HLCTimestamp(wall_ms=101, counter=0, node_id="n1")
        assert a < b < c

    def test_ordering_by_node_id_tiebreaker(self):
        a = HLCTimestamp(wall_ms=100, counter=0, node_id="aaa")
        b = HLCTimestamp(wall_ms=100, counter=0, node_id="zzz")
        assert a < b

    def test_zero(self):
        z = HLCTimestamp.zero()
        assert z.wall_ms == 0
        assert z.counter == 0

    def test_from_str_no_node(self):
        ts = HLCTimestamp.from_str("1000.000005")
        assert ts.wall_ms == 1000
        assert ts.counter == 5
        assert ts.node_id == ""

    def test_from_str_invalid(self):
        with pytest.raises(ValueError):
            HLCTimestamp.from_str("garbage")


class TestHLCLocal:
    """Test local event timestamping."""

    def test_monotonically_increasing(self):
        """Successive now() calls always increase."""
        clock = HLC("n1", wall_clock=lambda: 1000)
        a = clock.now()
        b = clock.now()
        c = clock.now()
        assert a < b < c

    def test_wall_clock_advance_resets_counter(self):
        """When wall clock advances, counter resets to 0."""
        t = [1000]
        clock = HLC("n1", wall_clock=lambda: t[0])

        ts1 = clock.now()
        assert ts1.counter == 0

        # Same ms — counter increments
        ts2 = clock.now()
        assert ts2.counter == 1

        # Wall clock advances — counter resets
        t[0] = 1001
        ts3 = clock.now()
        assert ts3.wall_ms == 1001
        assert ts3.counter == 0

    def test_wall_clock_backward_keeps_monotonic(self):
        """If wall clock goes backward, HLC stays monotonic."""
        t = [1000]
        clock = HLC("n1", wall_clock=lambda: t[0])

        ts1 = clock.now()
        assert ts1.wall_ms == 1000

        # Clock goes backward!
        t[0] = 999
        ts2 = clock.now()
        # Should NOT go backward — keeps wall_ms=1000, increments counter
        assert ts2.wall_ms == 1000
        assert ts2.counter == ts1.counter + 1
        assert ts2 > ts1

    def test_node_id_stamped(self):
        clock = HLC("my-node")
        ts = clock.now()
        assert ts.node_id == "my-node"


class TestHLCReceive:
    """Test receive() for causal ordering across nodes."""

    def test_remote_behind_local(self):
        """Remote is behind local — just increment local."""
        t = [1000]
        clock = HLC("n1", wall_clock=lambda: t[0])
        clock.now()  # wall_ms=1000, counter=0

        remote = HLCTimestamp(wall_ms=999, counter=5, node_id="n2")
        ts = clock.receive(remote)
        # Local wall is at 1000 which is > remote's 999
        assert ts.wall_ms == 1000
        assert ts > remote

    def test_remote_ahead_of_local(self):
        """Remote is ahead — adopt remote wall time."""
        clock = HLC("n1", wall_clock=lambda: 1000)
        clock.now()  # wall_ms=1000, counter=0

        remote = HLCTimestamp(wall_ms=1005, counter=3, node_id="n2")
        ts = clock.receive(remote)
        # Should adopt remote's wall_ms, counter = remote.counter + 1
        assert ts.wall_ms == 1005
        assert ts.counter == 4
        assert ts > remote

    def test_same_wall_time(self):
        """Both at same wall time — counter = max(both) + 1."""
        clock = HLC("n1", wall_clock=lambda: 1000)
        clock.now()   # counter=0
        clock.now()   # counter=1

        remote = HLCTimestamp(wall_ms=1000, counter=5, node_id="n2")
        ts = clock.receive(remote)
        assert ts.wall_ms == 1000
        assert ts.counter == 6  # max(1, 5) + 1

    def test_receive_advances_past_both(self):
        """Result must be > both local last and remote."""
        clock = HLC("n1", wall_clock=lambda: 1000)
        local = clock.now()

        remote = HLCTimestamp(wall_ms=1000, counter=10, node_id="n2")
        ts = clock.receive(remote)
        assert ts > local
        assert ts > remote

    def test_clock_skew_detected(self):
        """Excessive clock skew raises ClockSkewError."""
        clock = HLC("n1", wall_clock=lambda: 1000)

        # Remote is way in the future
        remote = HLCTimestamp(
            wall_ms=1000 + MAX_CLOCK_SKEW_MS + 1,
            counter=0,
            node_id="n2",
        )
        with pytest.raises(ClockSkewError, match="ahead of local clock"):
            clock.receive(remote)

    def test_clock_skew_within_tolerance(self):
        """Clock skew within tolerance is fine."""
        clock = HLC("n1", wall_clock=lambda: 1000)

        remote = HLCTimestamp(
            wall_ms=1000 + MAX_CLOCK_SKEW_MS - 1,
            counter=0,
            node_id="n2",
        )
        ts = clock.receive(remote)  # Should not raise
        assert ts > remote


class TestHLCCausalChain:
    """Test causal ordering across multiple nodes."""

    def test_three_node_causal_chain(self):
        """A → B → C: timestamps reflect causal ordering."""
        clock_a = HLC("a", wall_clock=lambda: 1000)
        clock_b = HLC("b", wall_clock=lambda: 1000)
        clock_c = HLC("c", wall_clock=lambda: 1000)

        # A sends to B
        ts_a = clock_a.now()
        ts_b = clock_b.receive(ts_a)
        assert ts_b > ts_a

        # B sends to C
        ts_c = clock_c.receive(ts_b)
        assert ts_c > ts_b > ts_a

    def test_concurrent_events_are_comparable(self):
        """Concurrent events on different nodes have different timestamps."""
        clock_a = HLC("a", wall_clock=lambda: 1000)
        clock_b = HLC("b", wall_clock=lambda: 1000)

        ts_a = clock_a.now()
        ts_b = clock_b.now()

        # They're at the same wall time and counter, but different node_id
        # So they ARE ordered (by node_id) — no ambiguity
        assert ts_a != ts_b
        assert (ts_a < ts_b) or (ts_a > ts_b)

    def test_skewed_clocks_still_causal(self):
        """Even with clock skew, causal ordering is maintained."""
        clock_a = HLC("a", wall_clock=lambda: 1000)
        clock_b = HLC("b", wall_clock=lambda: 1005)  # 5ms ahead

        ts_a = clock_a.now()  # wall_ms=1000
        ts_b = clock_b.receive(ts_a)

        # B's clock is ahead, so B's timestamp uses its own wall time
        assert ts_b.wall_ms == 1005
        assert ts_b > ts_a  # Causal ordering preserved
