"""Tests for the sync outbox (crash-safe change propagation)."""
import sqlite3
import pytest

from greymatter_core.sync.outbox import (
    create_outbox_table,
    enqueue,
    get_pending,
    mark_delivered,
    mark_failed,
    cleanup_delivered,
    get_stats,
)


class FakeDB:
    """Minimal DB wrapper around sqlite3 for testing."""

    def __init__(self):
        self.conn = sqlite3.connect(":memory:")
        self.conn.row_factory = sqlite3.Row

    def execute(self, query, params=()):
        cursor = self.conn.execute(query, params)
        if query.strip().upper().startswith("SELECT") or "RETURNING" in query.upper():
            return [dict(row) for row in cursor.fetchall()]
        self.conn.commit()
        return []

    def execute_one(self, query, params=()):
        rows = self.execute(query, params)
        return rows[0] if rows else None


@pytest.fixture
def db():
    d = FakeDB()
    create_outbox_table(d)
    return d


class TestOutboxBasics:
    def test_create_table(self, db):
        """Table should exist after creation."""
        rows = db.execute(
            "SELECT name FROM sqlite_master WHERE type='table' AND name='_sync_outbox'"
        )
        assert len(rows) == 1

    def test_enqueue_returns_id(self, db):
        oid = enqueue(db, "observations", "obs-1", "upsert")
        assert oid > 0

    def test_get_pending_returns_enqueued(self, db):
        enqueue(db, "observations", "obs-1")
        enqueue(db, "patterns", "p-1")
        pending = get_pending(db)
        assert len(pending) == 2
        assert pending[0]["table_name"] == "observations"
        assert pending[1]["table_name"] == "patterns"

    def test_get_pending_respects_limit(self, db):
        for i in range(10):
            enqueue(db, "observations", f"obs-{i}")
        pending = get_pending(db, limit=3)
        assert len(pending) == 3

    def test_get_pending_oldest_first(self, db):
        enqueue(db, "observations", "obs-first")
        enqueue(db, "observations", "obs-second")
        pending = get_pending(db)
        assert pending[0]["row_id"] == "obs-first"


class TestDelivery:
    def test_mark_delivered_removes_from_pending(self, db):
        oid = enqueue(db, "observations", "obs-1")
        mark_delivered(db, [oid])
        pending = get_pending(db)
        assert len(pending) == 0

    def test_mark_delivered_empty_list(self, db):
        result = mark_delivered(db, [])
        assert result == 0

    def test_mark_failed_increments_attempts(self, db):
        oid = enqueue(db, "observations", "obs-1")
        mark_failed(db, oid, "connection refused")
        pending = get_pending(db)
        assert pending[0]["attempts"] == 1

    def test_mark_failed_twice(self, db):
        oid = enqueue(db, "observations", "obs-1")
        mark_failed(db, oid, "error 1")
        mark_failed(db, oid, "error 2")
        pending = get_pending(db)
        assert pending[0]["attempts"] == 2


class TestCleanup:
    def test_cleanup_removes_old_delivered(self, db):
        oid = enqueue(db, "observations", "obs-1")
        # Mark delivered, then backdate to 48h ago
        mark_delivered(db, [oid])
        db.execute(
            "UPDATE _sync_outbox SET delivered_at = datetime('now', '-48 hours') WHERE id = ?",
            (oid,),
        )
        removed = cleanup_delivered(db, retain_hours=24)
        assert removed == 1

    def test_cleanup_keeps_recent_delivered(self, db):
        oid = enqueue(db, "observations", "obs-1")
        mark_delivered(db, [oid])
        removed = cleanup_delivered(db, retain_hours=24)
        assert removed == 0  # just delivered, too recent


class TestStats:
    def test_initial_stats_all_zero(self, db):
        stats = get_stats(db)
        assert stats["pending"] == 0
        assert stats["failed"] == 0
        assert stats["delivered"] == 0

    def test_stats_counts_pending(self, db):
        enqueue(db, "observations", "obs-1")
        enqueue(db, "observations", "obs-2")
        stats = get_stats(db)
        assert stats["pending"] == 2

    def test_stats_counts_failed(self, db):
        oid = enqueue(db, "observations", "obs-1")
        mark_failed(db, oid, "timeout")
        stats = get_stats(db)
        assert stats["pending"] == 1  # still pending
        assert stats["failed"] == 1   # also counted as failed

    def test_stats_counts_delivered(self, db):
        oid = enqueue(db, "observations", "obs-1")
        mark_delivered(db, [oid])
        stats = get_stats(db)
        assert stats["pending"] == 0
        assert stats["delivered"] == 1
