"""Tests for delta sync."""
import pytest

from greymatter_core.db import SQLiteDatabase
from greymatter_core.schema.migrations import apply_all
from greymatter_core.sync.delta import (
    bidirectional_sync,
    delta_sync,
    _deduplicate_changes,
)
from greymatter_core.sync.tracking import (
    get_changes_since,
    get_sync_version,
    install_sqlite_triggers,
)


def _make_db():
    d = SQLiteDatabase(":memory:")
    apply_all(d)
    install_sqlite_triggers(d)
    return d


@pytest.fixture
def db_a():
    return _make_db()


@pytest.fixture
def db_b():
    return _make_db()


class TestDeltaSync:
    def test_no_changes_syncs_nothing(self, db_a, db_b):
        result = delta_sync(db_a, db_b)
        assert result.success
        assert result.rows_pushed == 0

    def test_syncs_new_insert(self, db_a, db_b):
        db_a.execute(
            "INSERT INTO patterns (id, type, domain, title, observation) "
            "VALUES (?, ?, ?, ?, ?)",
            ("p-1", "process", "testing", "Test", "Obs"),
        )
        result = delta_sync(db_a, db_b, since_version=0)
        assert result.success
        assert result.rows_pushed == 1

        row = db_b.execute_one("SELECT * FROM patterns WHERE id = ?", ("p-1",))
        assert row is not None
        assert row["title"] == "Test"

    def test_syncs_update(self, db_a, db_b):
        db_a.execute(
            "INSERT INTO patterns (id, type, domain, title, observation) "
            "VALUES (?, ?, ?, ?, ?)",
            ("p-1", "process", "testing", "Old", "Obs"),
        )
        # Sync initial state
        delta_sync(db_a, db_b, since_version=0)

        # Now update on A
        db_a.execute("UPDATE patterns SET title = ? WHERE id = ?", ("New", "p-1"))
        result = delta_sync(db_a, db_b, since_version=1)
        assert result.rows_pushed == 1

        row = db_b.execute_one("SELECT * FROM patterns WHERE id = ?", ("p-1",))
        assert row["title"] == "New"

    def test_syncs_delete(self, db_a, db_b):
        db_a.execute(
            "INSERT INTO patterns (id, type, domain, title, observation) "
            "VALUES (?, ?, ?, ?, ?)",
            ("p-1", "process", "testing", "Title", "Obs"),
        )
        delta_sync(db_a, db_b, since_version=0)

        # Delete on A
        db_a.execute("DELETE FROM patterns WHERE id = ?", ("p-1",))
        result = delta_sync(db_a, db_b, since_version=1)
        assert result.rows_pushed == 1

        row = db_b.execute_one("SELECT * FROM patterns WHERE id = ?", ("p-1",))
        assert row is None

    def test_deduplicates_multiple_changes(self, db_a, db_b):
        db_a.execute(
            "INSERT INTO patterns (id, type, domain, title, observation) "
            "VALUES (?, ?, ?, ?, ?)",
            ("p-1", "process", "testing", "V1", "Obs"),
        )
        db_a.execute("UPDATE patterns SET title = ? WHERE id = ?", ("V2", "p-1"))
        db_a.execute("UPDATE patterns SET title = ? WHERE id = ?", ("V3", "p-1"))

        # 3 log entries but should deduplicate to 1 upsert
        result = delta_sync(db_a, db_b, since_version=0)
        assert result.rows_pushed == 1

        row = db_b.execute_one("SELECT * FROM patterns WHERE id = ?", ("p-1",))
        assert row["title"] == "V3"

    def test_updates_target_sync_version(self, db_a, db_b):
        db_a.execute(
            "INSERT INTO patterns (id, type, domain, title, observation) "
            "VALUES (?, ?, ?, ?, ?)",
            ("p-1", "process", "testing", "Title", "Obs"),
        )
        delta_sync(db_a, db_b, since_version=0)
        assert get_sync_version(db_b) == get_sync_version(db_a)

    def test_sync_multiple_tables(self, db_a, db_b):
        db_a.execute(
            "INSERT INTO patterns (id, type, domain, title, observation) "
            "VALUES (?, ?, ?, ?, ?)",
            ("p-1", "process", "testing", "P1", "O1"),
        )
        db_a.execute(
            "INSERT INTO observations (id, session_id, trigger, category, title, content) "
            "VALUES (?, ?, ?, ?, ?, ?)",
            ("obs-1", "s-1", "manual", "insight", "Test", "Content"),
        )
        result = delta_sync(db_a, db_b, since_version=0)
        assert result.tables_synced == 2
        assert result.rows_pushed == 2


class TestBidirectionalSync:
    def test_both_sides_get_changes(self, db_a, db_b):
        db_a.execute(
            "INSERT INTO patterns (id, type, domain, title, observation) "
            "VALUES (?, ?, ?, ?, ?)",
            ("p-a", "process", "testing", "From A", "Obs"),
        )
        db_b.execute(
            "INSERT INTO patterns (id, type, domain, title, observation) "
            "VALUES (?, ?, ?, ?, ?)",
            ("p-b", "process", "testing", "From B", "Obs"),
        )

        a_to_b, b_to_a = bidirectional_sync(db_a, db_b)
        assert a_to_b.rows_pushed >= 1
        assert b_to_a.rows_pushed >= 1

        # Both should have both patterns
        assert db_a.execute_one("SELECT * FROM patterns WHERE id = ?", ("p-b",)) is not None
        assert db_b.execute_one("SELECT * FROM patterns WHERE id = ?", ("p-a",)) is not None


class TestConflictResolution:
    """Tests that conflict resolution is wired into delta sync."""

    def test_merge_pattern_takes_max_confirmations(self, db_a, db_b):
        """When both sides modify a pattern, MERGE strategy takes max confirmations."""
        # Insert same pattern on both sides
        db_a.execute(
            "INSERT INTO patterns (id, type, domain, title, observation, confirmations) "
            "VALUES (?, ?, ?, ?, ?, ?)",
            ("p-1", "process", "testing", "Title", "Obs", 0),
        )
        delta_sync(db_a, db_b, since_version=0)

        # Update confirmations independently on each side
        db_a.execute("UPDATE patterns SET confirmations = ? WHERE id = ?", (5, "p-1"))
        db_b.execute("UPDATE patterns SET confirmations = ? WHERE id = ?", (3, "p-1"))

        # Sync A → B: should resolve via MERGE, keeping max(5, 3) = 5
        result = delta_sync(db_a, db_b, since_version=0)
        assert result.conflicts_resolved >= 1

        row = db_b.execute_one("SELECT * FROM patterns WHERE id = ?", ("p-1",))
        assert row["confirmations"] == 5

    def test_merge_pattern_takes_higher_confidence(self, db_a, db_b):
        """When both sides modify a pattern, MERGE keeps higher confidence."""
        db_a.execute(
            "INSERT INTO patterns (id, type, domain, title, observation, confidence) "
            "VALUES (?, ?, ?, ?, ?, ?)",
            ("p-2", "process", "testing", "Title", "Obs", "low"),
        )
        delta_sync(db_a, db_b, since_version=0)

        # A sets high, B sets medium
        db_a.execute("UPDATE patterns SET confidence = ? WHERE id = ?", ("high", "p-2"))
        db_b.execute("UPDATE patterns SET confidence = ? WHERE id = ?", ("medium", "p-2"))

        delta_sync(db_a, db_b, since_version=0)

        row = db_b.execute_one("SELECT * FROM patterns WHERE id = ?", ("p-2",))
        assert row["confidence"] == "high"

    def test_conflict_count_incremented(self, db_a, db_b):
        """SyncResult.conflicts_resolved is incremented when local row exists."""
        db_a.execute(
            "INSERT INTO patterns (id, type, domain, title, observation) "
            "VALUES (?, ?, ?, ?, ?)",
            ("p-1", "process", "testing", "Title", "Obs"),
        )
        # Insert to B first, so when A syncs there's a local row
        delta_sync(db_a, db_b, since_version=0)

        # Update on A
        db_a.execute("UPDATE patterns SET title = ? WHERE id = ?", ("Updated", "p-1"))
        result = delta_sync(db_a, db_b, since_version=0)
        assert result.conflicts_resolved >= 1

    def test_new_insert_no_conflict(self, db_a, db_b):
        """Inserting a new row (no local version) should not count as conflict."""
        db_a.execute(
            "INSERT INTO patterns (id, type, domain, title, observation) "
            "VALUES (?, ?, ?, ?, ?)",
            ("p-new", "process", "testing", "Brand New", "Obs"),
        )
        result = delta_sync(db_a, db_b, since_version=0)
        assert result.rows_pushed == 1
        assert result.conflicts_resolved == 0


class TestDeduplication:
    def test_keeps_latest_change_per_key(self):
        changes = [
            {"table_name": "patterns", "row_id": "p-1", "operation": "upsert",
             "sync_version": 1, "node_id": "n1", "created_at": ""},
            {"table_name": "patterns", "row_id": "p-1", "operation": "upsert",
             "sync_version": 2, "node_id": "n1", "created_at": ""},
            {"table_name": "patterns", "row_id": "p-1", "operation": "delete",
             "sync_version": 3, "node_id": "n1", "created_at": ""},
        ]
        deduped = _deduplicate_changes(changes)
        assert len(deduped) == 1
        assert deduped[0].operation.value == "delete"
        assert deduped[0].sync_version == 3

    def test_keeps_different_rows_separate(self):
        changes = [
            {"table_name": "patterns", "row_id": "p-1", "operation": "upsert",
             "sync_version": 1, "node_id": "n1", "created_at": ""},
            {"table_name": "patterns", "row_id": "p-2", "operation": "upsert",
             "sync_version": 2, "node_id": "n1", "created_at": ""},
        ]
        deduped = _deduplicate_changes(changes)
        assert len(deduped) == 2


class TestClassificationSyncEnforcement:
    """Cognitive entries (classification_level >= 4) skip sync."""

    def test_cognitive_knowledge_entry_skipped(self, db_a, db_b):
        """classification_level=4 entries should not sync."""
        db_a.execute(
            "INSERT INTO knowledge_entries "
            "(id, entry_type, title, content, "
            "classification_level) "
            "VALUES (?, ?, ?, ?, ?)",
            ("kn-cog", "mood", "Private", "Content", 4),
        )
        delta_sync(db_a, db_b, since_version=0)
        row = db_b.execute_one(
            "SELECT * FROM knowledge_entries WHERE id = ?",
            ("kn-cog",),
        )
        assert row is None

    def test_personal_knowledge_entry_syncs(self, db_a, db_b):
        """classification_level=3 entries should sync."""
        db_a.execute(
            "INSERT INTO knowledge_entries "
            "(id, entry_type, title, content, "
            "classification_level) "
            "VALUES (?, ?, ?, ?, ?)",
            ("kn-per", "expertise", "Eligible", "C", 3),
        )
        result = delta_sync(db_a, db_b, since_version=0)
        assert result.rows_pushed >= 1
        row = db_b.execute_one(
            "SELECT * FROM knowledge_entries WHERE id = ?",
            ("kn-per",),
        )
        assert row is not None
        assert row["classification_level"] == 3

    def test_open_knowledge_entry_syncs(self, db_a, db_b):
        """classification_level=0 (Open) entries should sync."""
        db_a.execute(
            "INSERT INTO knowledge_entries "
            "(id, entry_type, title, content, "
            "classification_level) "
            "VALUES (?, ?, ?, ?, ?)",
            ("kn-open", "expertise", "Public", "C", 0),
        )
        result = delta_sync(db_a, db_b, since_version=0)
        assert result.rows_pushed >= 1
        row = db_b.execute_one(
            "SELECT * FROM knowledge_entries WHERE id = ?",
            ("kn-open",),
        )
        assert row is not None
        assert row["classification_level"] == 0
