"""Tests for sync change tracking triggers."""
import pytest

from greymatter_core.db import SQLiteDatabase
from greymatter_core.schema.migrations import apply_all
from greymatter_core.sync.tracking import (
    get_changes_since,
    get_node_id,
    get_sync_version,
    install_sqlite_triggers,
    uninstall_sqlite_triggers,
)


@pytest.fixture
def db():
    """Database with schema + migration + triggers installed."""
    d = SQLiteDatabase(":memory:")
    apply_all(d)
    install_sqlite_triggers(d)
    return d


class TestSyncState:
    def test_node_id_exists(self, db):
        nid = get_node_id(db)
        assert len(nid) == 16  # hex(randomblob(8))

    def test_initial_sync_version_is_zero(self):
        d = SQLiteDatabase(":memory:")
        apply_all(d)
        assert get_sync_version(d) == 0


class TestInsertTriggers:
    def test_insert_pattern_creates_log_entry(self, db):
        db.execute(
            "INSERT INTO patterns (id, type, domain, title, observation) "
            "VALUES (?, ?, ?, ?, ?)",
            ("p-1", "process", "testing", "Test Pattern", "Observed X"),
        )
        entries = get_changes_since(db, 0)
        assert len(entries) == 1
        assert entries[0]["table_name"] == "patterns"
        assert entries[0]["row_id"] == "p-1"
        assert entries[0]["operation"] == "upsert"
        assert entries[0]["sync_version"] == 1

    def test_insert_observation_creates_log_entry(self, db):
        db.execute(
            "INSERT INTO observations (id, session_id, trigger, category, title, content) "
            "VALUES (?, ?, ?, ?, ?, ?)",
            ("obs-1", "sess-1", "manual", "insight", "Test", "Content"),
        )
        entries = get_changes_since(db, 0)
        assert len(entries) == 1
        assert entries[0]["table_name"] == "observations"

    def test_insert_sets_sync_version_on_row(self, db):
        db.execute(
            "INSERT INTO patterns (id, type, domain, title, observation) "
            "VALUES (?, ?, ?, ?, ?)",
            ("p-1", "process", "testing", "Test", "Obs"),
        )
        row = db.execute_one("SELECT _sync_version, _node_id FROM patterns WHERE id = ?", ("p-1",))
        assert row["_sync_version"] == 1
        assert row["_node_id"] == get_node_id(db)

    def test_multiple_inserts_increment_version(self, db):
        for i in range(3):
            db.execute(
                "INSERT INTO patterns (id, type, domain, title, observation) "
                "VALUES (?, ?, ?, ?, ?)",
                (f"p-{i}", "process", "testing", f"P{i}", f"Obs{i}"),
            )
        assert get_sync_version(db) == 3
        entries = get_changes_since(db, 0)
        assert len(entries) == 3


class TestUpdateTriggers:
    def test_update_creates_log_entry(self, db):
        db.execute(
            "INSERT INTO patterns (id, type, domain, title, observation) "
            "VALUES (?, ?, ?, ?, ?)",
            ("p-1", "process", "testing", "Old Title", "Obs"),
        )
        db.execute(
            "UPDATE patterns SET title = ? WHERE id = ?",
            ("New Title", "p-1"),
        )
        entries = get_changes_since(db, 0)
        # Should have 2 entries: insert + update
        assert len(entries) == 2
        assert entries[1]["operation"] == "upsert"
        assert entries[1]["sync_version"] == 2

    def test_update_increments_row_sync_version(self, db):
        db.execute(
            "INSERT INTO patterns (id, type, domain, title, observation) "
            "VALUES (?, ?, ?, ?, ?)",
            ("p-1", "process", "testing", "Title", "Obs"),
        )
        db.execute("UPDATE patterns SET title = ? WHERE id = ?", ("Updated", "p-1"))
        row = db.execute_one("SELECT _sync_version FROM patterns WHERE id = ?", ("p-1",))
        assert row["_sync_version"] == 2


class TestDeleteTriggers:
    def test_delete_creates_log_entry(self, db):
        db.execute(
            "INSERT INTO patterns (id, type, domain, title, observation) "
            "VALUES (?, ?, ?, ?, ?)",
            ("p-1", "process", "testing", "Title", "Obs"),
        )
        db.execute("DELETE FROM patterns WHERE id = ?", ("p-1",))
        entries = get_changes_since(db, 0)
        assert len(entries) == 2  # insert + delete
        assert entries[1]["operation"] == "delete"
        assert entries[1]["row_id"] == "p-1"


class TestInstallUninstall:
    def test_install_returns_trigger_count(self):
        d = SQLiteDatabase(":memory:")
        apply_all(d)
        count = install_sqlite_triggers(d)
        # 7 tables × 3 triggers each = 21
        assert count == 21

    def test_uninstall_removes_triggers(self, db):
        count = uninstall_sqlite_triggers(db)
        assert count == 21
        # Inserts should no longer create log entries
        db.execute(
            "INSERT INTO patterns (id, type, domain, title, observation) "
            "VALUES (?, ?, ?, ?, ?)",
            ("p-new", "process", "testing", "Title", "Obs"),
        )
        entries = get_changes_since(db, 0)
        assert len(entries) == 0

    def test_install_idempotent(self):
        d = SQLiteDatabase(":memory:")
        apply_all(d)
        install_sqlite_triggers(d)
        install_sqlite_triggers(d)  # Should not fail


class TestGetChangesSince:
    def test_filters_by_version(self, db):
        for i in range(5):
            db.execute(
                "INSERT INTO patterns (id, type, domain, title, observation) "
                "VALUES (?, ?, ?, ?, ?)",
                (f"p-{i}", "process", "testing", f"P{i}", f"Obs{i}"),
            )
        entries = get_changes_since(db, 3)
        assert len(entries) == 2
        assert entries[0]["sync_version"] == 4
        assert entries[1]["sync_version"] == 5

    def test_cursor_by_log_id(self, db):
        """since_log_id filters by _sync_log.id (unique), not sync_version."""
        for i in range(5):
            db.execute(
                "INSERT INTO patterns (id, type, domain, title, observation) "
                "VALUES (?, ?, ?, ?, ?)",
                (f"p-{i}", "process", "testing", f"P{i}", f"Obs{i}"),
            )
        # Get all entries to find the log ids
        all_entries = get_changes_since(db, 0)
        assert len(all_entries) == 5
        third_id = all_entries[2]["id"]

        # Cursor from 3rd entry should return entries 4 and 5
        entries = get_changes_since(db, since_log_id=third_id)
        assert len(entries) == 2
        assert entries[0]["id"] > third_id
        assert entries[1]["id"] > entries[0]["id"]

    def test_limit_parameter(self, db):
        """limit restricts the number of returned entries."""
        for i in range(5):
            db.execute(
                "INSERT INTO patterns (id, type, domain, title, observation) "
                "VALUES (?, ?, ?, ?, ?)",
                (f"p-{i}", "process", "testing", f"P{i}", f"Obs{i}"),
            )
        entries = get_changes_since(db, 0, limit=2)
        assert len(entries) == 2
        # Should be the first 2 by id order
        assert entries[0]["sync_version"] == 1
        assert entries[1]["sync_version"] == 2

    def test_cursor_with_limit(self, db):
        """since_log_id + limit work together for paginated iteration."""
        for i in range(5):
            db.execute(
                "INSERT INTO patterns (id, type, domain, title, observation) "
                "VALUES (?, ?, ?, ?, ?)",
                (f"p-{i}", "process", "testing", f"P{i}", f"Obs{i}"),
            )
        # Page 1: first 2
        page1 = get_changes_since(db, 0, limit=2)
        assert len(page1) == 2

        # Page 2: next 2, using cursor from page 1
        cursor = page1[-1]["id"]
        page2 = get_changes_since(db, since_log_id=cursor, limit=2)
        assert len(page2) == 2
        assert page2[0]["id"] > cursor

        # Page 3: last 1
        cursor2 = page2[-1]["id"]
        page3 = get_changes_since(db, since_log_id=cursor2, limit=2)
        assert len(page3) == 1

    def test_orders_by_id_not_sync_version(self, db):
        """Entries are ordered by id (unique) not sync_version (non-unique)."""
        for i in range(3):
            db.execute(
                "INSERT INTO patterns (id, type, domain, title, observation) "
                "VALUES (?, ?, ?, ?, ?)",
                (f"p-{i}", "process", "testing", f"P{i}", f"Obs{i}"),
            )
        entries = get_changes_since(db, 0)
        ids = [e["id"] for e in entries]
        assert ids == sorted(ids), "entries should be ordered by id ascending"
