"""Tests for HLC timestamp wiring into synced rows.

Covers:
  1. HLCWriter: singleton management, generation, receive-and-generate
  2. hlc_write/hlc_update/hlc_upsert: timestamps stored on writes
  3. stamp_hlc: causal stamping during sync
  4. End-to-end: delta sync with HLC conflict resolution
  5. Causal ordering: events across nodes maintain happened-before
"""
import pytest

from greymatter_core.db import SQLiteDatabase
from greymatter_core.schema.migrations import apply_all
from greymatter_core.sync.conflict import flush_audit_log, resolve_conflict, _resolve_lww
from greymatter_core.sync.delta import delta_sync
from greymatter_core.sync.hlc import HLC, HLCTimestamp, HLCWriter, ClockSkewError
from greymatter_core.sync.tracking import (
    get_node_id,
    hlc_write,
    hlc_update,
    hlc_upsert,
    stamp_hlc,
    install_sqlite_triggers,
    _get_hlc_writer,
)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _make_db(node_id: str = "test-node") -> SQLiteDatabase:
    """Create an in-memory DB with full schema, triggers, and node identity."""
    db = SQLiteDatabase(":memory:")
    apply_all(db)
    install_sqlite_triggers(db)
    db.execute(
        "UPDATE _sync_state SET value = ? WHERE key = 'node_id'",
        (node_id,),
    )
    return db


# ---------------------------------------------------------------------------
# 1. HLCWriter tests
# ---------------------------------------------------------------------------

class TestHLCWriter:
    """Test the HLCWriter singleton and generation."""

    def setup_method(self):
        HLCWriter.reset_instances()

    def test_singleton_per_node(self):
        """Same node_id returns the same instance."""
        w1 = HLCWriter.get_instance("node-a")
        w2 = HLCWriter.get_instance("node-a")
        assert w1 is w2

    def test_different_nodes_different_instances(self):
        """Different node_ids get separate clocks."""
        w1 = HLCWriter.get_instance("node-a")
        w2 = HLCWriter.get_instance("node-b")
        assert w1 is not w2

    def test_generate_returns_string(self):
        w = HLCWriter("test-node")
        ts = w.generate()
        assert isinstance(ts, str)
        assert "test-node" in ts

    def test_generate_monotonically_increasing(self):
        w = HLCWriter("test-node")
        ts1 = w.generate()
        ts2 = w.generate()
        ts3 = w.generate()
        assert HLCTimestamp.from_str(ts1) < HLCTimestamp.from_str(ts2)
        assert HLCTimestamp.from_str(ts2) < HLCTimestamp.from_str(ts3)

    def test_receive_and_generate_causally_after(self):
        w = HLCWriter("local-node")
        remote_ts = "1741459200000.000005.remote-node"
        local_ts = w.receive_and_generate(remote_ts)

        local_parsed = HLCTimestamp.from_str(local_ts)
        remote_parsed = HLCTimestamp.from_str(remote_ts)
        assert local_parsed > remote_parsed

    def test_receive_and_generate_with_clock_skew(self):
        """Excessive clock skew should raise ClockSkewError."""
        w = HLCWriter("local-node", wall_clock=lambda: 1000)
        far_future = f"{1000 + 5 * 60 * 1000 + 1}.000000.remote"
        with pytest.raises(ClockSkewError):
            w.receive_and_generate(far_future)

    def test_reset_instances(self):
        HLCWriter.get_instance("x")
        HLCWriter.reset_instances()
        assert len(HLCWriter._instances) == 0

    def test_last_property(self):
        w = HLCWriter("test-node")
        assert w.last == HLCTimestamp.zero()
        ts_str = w.generate()
        assert w.last == HLCTimestamp.from_str(ts_str)


# ---------------------------------------------------------------------------
# 2. hlc_write / hlc_update / hlc_upsert tests
# ---------------------------------------------------------------------------

class TestHLCWriteHelpers:
    """Test that write helpers store HLC timestamps in the database."""

    def setup_method(self):
        HLCWriter.reset_instances()
        self.db = _make_db("writer-node")

    def test_hlc_write_stores_timestamp(self):
        """hlc_write should INSERT a row with _hlc_timestamp populated."""
        hlc_ts = hlc_write(self.db, "knowledge_entries", {
            "id": "ke-1",
            "title": "Test Entry",
            "content": "Hello world",
            "entry_type": "expertise",
            "domain": "test",
        })

        row = self.db.execute_one(
            "SELECT _hlc_timestamp FROM knowledge_entries WHERE id = ?",
            ("ke-1",),
        )
        assert row is not None
        assert row["_hlc_timestamp"] == hlc_ts
        assert "writer-node" in hlc_ts

    def test_hlc_write_successive_timestamps_increase(self):
        """Successive writes produce increasing HLC timestamps."""
        ts1 = hlc_write(self.db, "knowledge_entries", {
            "id": "ke-1",
            "title": "First",
            "content": "c1",
            "entry_type": "expertise",
            "domain": "test",
        })
        ts2 = hlc_write(self.db, "knowledge_entries", {
            "id": "ke-2",
            "title": "Second",
            "content": "c2",
            "entry_type": "expertise",
            "domain": "test",
        })
        assert HLCTimestamp.from_str(ts1) < HLCTimestamp.from_str(ts2)

    def test_hlc_update_stores_timestamp(self):
        """hlc_update should UPDATE the row's _hlc_timestamp."""
        hlc_write(self.db, "knowledge_entries", {
            "id": "ke-1",
            "title": "Original",
            "content": "c",
            "entry_type": "expertise",
            "domain": "test",
        })

        new_ts = hlc_update(
            self.db, "knowledge_entries",
            {"title": "Updated"},
            pk_col="id", pk_val="ke-1",
        )

        row = self.db.execute_one(
            "SELECT title, _hlc_timestamp FROM knowledge_entries WHERE id = ?",
            ("ke-1",),
        )
        assert row["title"] == "Updated"
        assert row["_hlc_timestamp"] == new_ts

    def test_hlc_update_timestamp_advances(self):
        """Update timestamp must be > insert timestamp."""
        insert_ts = hlc_write(self.db, "knowledge_entries", {
            "id": "ke-1",
            "title": "v1",
            "content": "c",
            "entry_type": "expertise",
            "domain": "test",
        })
        update_ts = hlc_update(
            self.db, "knowledge_entries",
            {"title": "v2"},
            pk_col="id", pk_val="ke-1",
        )
        assert HLCTimestamp.from_str(update_ts) > HLCTimestamp.from_str(insert_ts)

    def test_hlc_upsert_insert(self):
        """hlc_upsert on a new row acts like INSERT."""
        ts = hlc_upsert(self.db, "knowledge_entries", {
            "id": "ke-new",
            "title": "Upserted",
            "content": "c",
            "entry_type": "expertise",
            "domain": "test",
        })

        row = self.db.execute_one(
            "SELECT _hlc_timestamp FROM knowledge_entries WHERE id = ?",
            ("ke-new",),
        )
        assert row["_hlc_timestamp"] == ts

    def test_hlc_upsert_update(self):
        """hlc_upsert on an existing row acts like UPDATE."""
        ts1 = hlc_upsert(self.db, "knowledge_entries", {
            "id": "ke-1",
            "title": "First",
            "content": "c",
            "entry_type": "expertise",
            "domain": "test",
        })
        ts2 = hlc_upsert(self.db, "knowledge_entries", {
            "id": "ke-1",
            "title": "Replaced",
            "content": "c2",
            "entry_type": "expertise",
            "domain": "test",
        })

        row = self.db.execute_one(
            "SELECT title, _hlc_timestamp FROM knowledge_entries WHERE id = ?",
            ("ke-1",),
        )
        assert row["title"] == "Replaced"
        assert row["_hlc_timestamp"] == ts2
        assert HLCTimestamp.from_str(ts2) > HLCTimestamp.from_str(ts1)

    def test_hlc_write_does_not_mutate_input(self):
        """hlc_write should not modify the caller's dict."""
        original = {"id": "ke-1", "title": "T", "content": "c",
                     "entry_type": "expertise", "domain": "test"}
        original_copy = dict(original)
        hlc_write(self.db, "knowledge_entries", original)
        assert original == original_copy  # No _hlc_timestamp added to caller's dict


# ---------------------------------------------------------------------------
# 3. stamp_hlc tests
# ---------------------------------------------------------------------------

class TestStampHLC:
    """Test HLC stamping for sync operations."""

    def setup_method(self):
        HLCWriter.reset_instances()
        self.db = _make_db("stamp-node")

    def test_stamp_local_event(self):
        """stamp_hlc without remote HLC generates a local timestamp."""
        # Insert a row without HLC
        self.db.execute(
            "INSERT INTO knowledge_entries (id, title, content, entry_type, domain) "
            "VALUES ('ke-1', 'Test', 'c', 'expertise', 'test')"
        )

        ts = stamp_hlc(self.db, "knowledge_entries", "id", "ke-1")

        row = self.db.execute_one(
            "SELECT _hlc_timestamp FROM knowledge_entries WHERE id = ?",
            ("ke-1",),
        )
        assert row["_hlc_timestamp"] == ts
        assert "stamp-node" in ts

    def test_stamp_with_remote_hlc(self):
        """stamp_hlc with remote HLC should produce a causally-later timestamp."""
        self.db.execute(
            "INSERT INTO knowledge_entries (id, title, content, entry_type, domain) "
            "VALUES ('ke-1', 'Test', 'c', 'expertise', 'test')"
        )

        remote_hlc = "1741459200000.000010.remote-node"
        ts = stamp_hlc(self.db, "knowledge_entries", "id", "ke-1",
                        remote_hlc=remote_hlc)

        local_parsed = HLCTimestamp.from_str(ts)
        remote_parsed = HLCTimestamp.from_str(remote_hlc)
        assert local_parsed > remote_parsed

    def test_stamp_updates_clock_state(self):
        """After stamping with a remote HLC, subsequent local writes
        should have timestamps that are causally after the remote."""
        self.db.execute(
            "INSERT INTO knowledge_entries (id, title, content, entry_type, domain) "
            "VALUES ('ke-1', 'Test', 'c', 'expertise', 'test')"
        )

        remote_hlc = "1741459200000.000010.remote-node"
        stamp_hlc(self.db, "knowledge_entries", "id", "ke-1",
                   remote_hlc=remote_hlc)

        # Now do a local write — it should be after the remote
        new_ts = hlc_write(self.db, "knowledge_entries", {
            "id": "ke-2",
            "title": "After remote",
            "content": "c",
            "entry_type": "expertise",
            "domain": "test",
        })

        new_parsed = HLCTimestamp.from_str(new_ts)
        remote_parsed = HLCTimestamp.from_str(remote_hlc)
        assert new_parsed > remote_parsed


# ---------------------------------------------------------------------------
# 4. HLC comparison for conflict resolution
# ---------------------------------------------------------------------------

class TestHLCConflictResolution:
    """Test that _resolve_lww correctly uses HLC for conflict resolution."""

    def test_hlc_beats_sync_version(self):
        """HLC should take priority over _sync_version."""
        local = {
            "id": "ke-1", "title": "Local",
            "_sync_version": 10, "_node_id": "n1",
            "_hlc_timestamp": "1000.000001.n1",
        }
        remote = {
            "id": "ke-1", "title": "Remote",
            "_sync_version": 2, "_node_id": "n2",
            "_hlc_timestamp": "2000.000000.n2",
        }
        result = _resolve_lww(local, remote)
        assert result["title"] == "Remote"  # HLC 2000 > 1000

    def test_hlc_counter_tiebreaker(self):
        """Same wall time — counter breaks the tie."""
        local = {
            "id": "ke-1", "title": "Local",
            "_sync_version": 1, "_node_id": "n1",
            "_hlc_timestamp": "5000.000003.n1",
        }
        remote = {
            "id": "ke-1", "title": "Remote",
            "_sync_version": 1, "_node_id": "n2",
            "_hlc_timestamp": "5000.000007.n2",
        }
        result = _resolve_lww(local, remote)
        assert result["title"] == "Remote"  # counter 7 > 3

    def test_hlc_node_id_final_tiebreaker(self):
        """Same wall time and counter — node_id is the deterministic tiebreaker."""
        local = {
            "id": "ke-1", "title": "Local",
            "_sync_version": 1, "_node_id": "aaa",
            "_hlc_timestamp": "5000.000003.aaa",
        }
        remote = {
            "id": "ke-1", "title": "Remote",
            "_sync_version": 1, "_node_id": "zzz",
            "_hlc_timestamp": "5000.000003.zzz",
        }
        result = _resolve_lww(local, remote)
        assert result["title"] == "Remote"  # "zzz" > "aaa"

    def test_empty_hlc_falls_back_to_sync_version(self):
        """Empty _hlc_timestamp fields should fall back to _sync_version."""
        local = {
            "id": "ke-1", "title": "Local",
            "_sync_version": 5, "_node_id": "n1",
            "_hlc_timestamp": "",
        }
        remote = {
            "id": "ke-1", "title": "Remote",
            "_sync_version": 3, "_node_id": "n2",
            "_hlc_timestamp": "",
        }
        result = _resolve_lww(local, remote)
        assert result["title"] == "Local"  # sync_version 5 > 3

    def test_one_side_has_hlc_falls_back(self):
        """If only one side has HLC, fall back to sync_version."""
        local = {
            "id": "ke-1", "title": "Local",
            "_sync_version": 1, "_node_id": "n1",
            "_hlc_timestamp": "9999.000001.n1",
        }
        remote = {
            "id": "ke-1", "title": "Remote",
            "_sync_version": 5, "_node_id": "n2",
            "_hlc_timestamp": "",
        }
        result = _resolve_lww(local, remote)
        # Empty string is falsy, so falls back to sync_version
        assert result["title"] == "Remote"  # sync_version 5 > 1


# ---------------------------------------------------------------------------
# 5. Causal ordering across nodes
# ---------------------------------------------------------------------------

class TestCausalOrdering:
    """Test that HLC timestamps maintain causal ordering across nodes."""

    def setup_method(self):
        HLCWriter.reset_instances()

    def test_write_then_sync_preserves_order(self):
        """A write on node A, synced to node B, then a write on B:
        the B write must have a later HLC than the A write."""
        db_a = _make_db("node-a")
        db_b = _make_db("node-b")

        # Write on node A
        ts_a = hlc_write(db_a, "knowledge_entries", {
            "id": "ke-1",
            "title": "From A",
            "content": "c",
            "entry_type": "expertise",
            "domain": "test",
        })

        # Simulate sync: stamp the row on node B with A's HLC
        db_b.execute(
            "INSERT INTO knowledge_entries (id, title, content, entry_type, domain) "
            "VALUES ('ke-1', 'From A', 'c', 'expertise', 'test')"
        )
        ts_b_synced = stamp_hlc(db_b, "knowledge_entries", "id", "ke-1",
                                 remote_hlc=ts_a)

        # Now write a new row on node B — it must be after A's write
        ts_b_local = hlc_write(db_b, "knowledge_entries", {
            "id": "ke-2",
            "title": "From B",
            "content": "c2",
            "entry_type": "expertise",
            "domain": "test",
        })

        assert HLCTimestamp.from_str(ts_b_synced) > HLCTimestamp.from_str(ts_a)
        assert HLCTimestamp.from_str(ts_b_local) > HLCTimestamp.from_str(ts_a)

    def test_three_node_causal_chain(self):
        """A -> B -> C: each hop preserves causal ordering."""
        HLCWriter.reset_instances()
        db_a = _make_db("node-a")
        db_b = _make_db("node-b")
        db_c = _make_db("node-c")

        # Write on A
        ts_a = hlc_write(db_a, "patterns", {
            "id": "p-1",
            "type": "process",
            "domain": "test",
            "title": "Pattern from A",
            "observation": "obs",
            "confirmations": 1,
            "confidence": "medium",
        })

        # Sync A -> B
        db_b.execute(
            "INSERT INTO patterns (id, type, domain, title, observation, "
            "confirmations, confidence) "
            "VALUES ('p-1', 'process', 'test', 'Pattern from A', 'obs', 1, 'medium')"
        )
        ts_b = stamp_hlc(db_b, "patterns", "id", "p-1", remote_hlc=ts_a)

        # Sync B -> C
        db_c.execute(
            "INSERT INTO patterns (id, type, domain, title, observation, "
            "confirmations, confidence) "
            "VALUES ('p-1', 'process', 'test', 'Pattern from A', 'obs', 1, 'medium')"
        )
        ts_c = stamp_hlc(db_c, "patterns", "id", "p-1", remote_hlc=ts_b)

        # Causal chain: A < B < C
        assert HLCTimestamp.from_str(ts_a) < HLCTimestamp.from_str(ts_b)
        assert HLCTimestamp.from_str(ts_b) < HLCTimestamp.from_str(ts_c)

    def test_concurrent_writes_are_deterministically_ordered(self):
        """Concurrent writes on different nodes produce different timestamps
        that are deterministically comparable (no ambiguity)."""
        HLCWriter.reset_instances()
        db_a = _make_db("node-a")
        db_b = _make_db("node-b")

        ts_a = hlc_write(db_a, "knowledge_entries", {
            "id": "ke-concurrent",
            "title": "From A",
            "content": "c",
            "entry_type": "expertise",
            "domain": "test",
        })
        ts_b = hlc_write(db_b, "knowledge_entries", {
            "id": "ke-concurrent",
            "title": "From B",
            "content": "c",
            "entry_type": "expertise",
            "domain": "test",
        })

        # They must be different (different node_ids ensure this)
        assert ts_a != ts_b

        # They must be comparable (total order)
        parsed_a = HLCTimestamp.from_str(ts_a)
        parsed_b = HLCTimestamp.from_str(ts_b)
        assert (parsed_a < parsed_b) or (parsed_a > parsed_b)


# ---------------------------------------------------------------------------
# 6. End-to-end delta sync with HLC
# ---------------------------------------------------------------------------

class TestDeltaSyncWithHLC:
    """Test that delta_sync uses HLC timestamps for conflict resolution."""

    def setup_method(self):
        HLCWriter.reset_instances()
        flush_audit_log()

    def test_delta_sync_stamps_hlc_on_target(self):
        """When delta_sync applies a row with _hlc_timestamp, the target
        gets a causally-later HLC stamped on the synced row."""
        from greymatter_core.sync.outbox import create_outbox_table

        source = _make_db("source-node")
        target = _make_db("target-node")
        create_outbox_table(source)
        create_outbox_table(target)

        # Write with HLC on source
        hlc_write(source, "knowledge_entries", {
            "id": "ke-sync-1",
            "title": "Sync Me",
            "content": "data",
            "entry_type": "expertise",
            "domain": "test",
        })

        source_row = source.execute_one(
            "SELECT _hlc_timestamp FROM knowledge_entries WHERE id = ?",
            ("ke-sync-1",),
        )
        source_hlc = source_row["_hlc_timestamp"]
        assert source_hlc  # Non-empty

        # Run delta sync
        result = delta_sync(source, target)
        assert result.rows_pushed >= 1

        # Check target has the row with a stamped HLC
        target_row = target.execute_one(
            "SELECT _hlc_timestamp, title FROM knowledge_entries WHERE id = ?",
            ("ke-sync-1",),
        )
        assert target_row is not None
        assert target_row["title"] == "Sync Me"
        target_hlc = target_row["_hlc_timestamp"]

        # Target's HLC should be causally after source's
        if target_hlc and target_hlc != source_hlc:
            assert HLCTimestamp.from_str(target_hlc) > HLCTimestamp.from_str(source_hlc)

    def test_conflict_resolved_by_hlc(self):
        """When both source and target have the same row with HLC timestamps,
        the later HLC should win during conflict resolution."""
        from greymatter_core.sync.outbox import create_outbox_table

        source = _make_db("source-node")
        target = _make_db("target-node")
        create_outbox_table(source)
        create_outbox_table(target)

        # Write on target FIRST with an earlier wall clock
        target_writer = HLCWriter("target-node", wall_clock=lambda: 1000000000000)
        HLCWriter._instances["target-node"] = target_writer

        hlc_write(target, "knowledge_entries", {
            "id": "ke-conflict",
            "title": "Target Loses",
            "content": "target data",
            "entry_type": "expertise",
            "domain": "test",
        })

        # Write on source with a later wall clock
        source_writer = HLCWriter("source-node", wall_clock=lambda: 2000000000000)
        HLCWriter._instances["source-node"] = source_writer

        hlc_write(source, "knowledge_entries", {
            "id": "ke-conflict",
            "title": "Source Wins",
            "content": "source data",
            "entry_type": "expertise",
            "domain": "test",
        })

        # Reset target sync version so delta_sync picks up source changes
        # (In production, the target would track the source's version separately)
        target.execute(
            "UPDATE _sync_state SET value = '0' WHERE key = 'sync_version'"
        )

        # Sync source -> target: source's row has later HLC, should win
        result = delta_sync(source, target, since_version=0)
        assert result.rows_pushed >= 1

        resolved_row = target.execute_one(
            "SELECT title FROM knowledge_entries WHERE id = ?",
            ("ke-conflict",),
        )
        assert resolved_row["title"] == "Source Wins"


# ---------------------------------------------------------------------------
# 7. _get_hlc_writer integration
# ---------------------------------------------------------------------------

class TestGetHLCWriter:
    """Test the _get_hlc_writer helper used by tracking functions."""

    def setup_method(self):
        HLCWriter.reset_instances()

    def test_writer_uses_db_node_id(self):
        """The HLC writer should use the node_id from _sync_state."""
        db = _make_db("my-special-node")
        writer = _get_hlc_writer(db)
        assert writer.node_id == "my-special-node"

    def test_writer_cached_on_db(self):
        """Second call should return cached instance."""
        db = _make_db("cached-node")
        w1 = _get_hlc_writer(db)
        w2 = _get_hlc_writer(db)
        assert w1 is w2
