import sqlite3
import time

from mcard.model.card import MCard
from mcard.model.card_collection import CardCollection


class TestHandleVersioning:
    """Explicit tests for the handle versioning (handle_history) system."""

    def test_version_history_recording(self):
        """
        Verify that updating a handle's target creates a correct entry in the
        handle_history table with appropriate metadata.
        """
        collection = CardCollection(db_path=":memory:")

        # 1. Create initial content and handle
        card_v1 = MCard("Content Version 1")
        hash_v1 = collection.add_with_handle(card_v1, "project-readme")

        # Verify initial state
        assert collection.resolve_handle("project-readme") == hash_v1
        assert len(collection.get_handle_history("project-readme")) == 0

        # Power nap to ensure timestamp difference (SQLite resolution can be low)
        time.sleep(0.1)

        # 2. Update handle to point to new content
        card_v2 = MCard("Content Version 2: Electric Boogaloo")
        hash_v2 = collection.update_handle("project-readme", card_v2)

        # Verify handle now points to new hash
        assert collection.resolve_handle("project-readme") == hash_v2
        assert hash_v1 != hash_v2

        # 3. Verify History API
        history = collection.get_handle_history("project-readme")
        assert len(history) == 1
        entry = history[0]

        assert entry["previous_hash"] == hash_v1
        assert "changed_at" in entry
        # ensure changed_at is a valid timestamp string
        assert len(entry["changed_at"]) > 0

    def test_version_table_schema_integrity(self):
        """
        Low-level verification of the handle_history table structure using SQL.
        This ensures the actual database schema matches expectations.
        """
        # We need a persistent DB or access to the underlying connection to check SQL
        # CardCollection encapsulates the connection, so we'll use a named memory DB
        # or file to access it if possible, OR rely on the fact that SQLiteNodeEngine
        # might expose it or we can hack it.
        # Alternatively, for this test, let's use a temporary file to be safe.

        import os
        import tempfile

        with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp:
            db_path = tmp.name

        try:
            collection = CardCollection(db_path=db_path)

            # Setup data
            c1 = MCard("V1")
            c2 = MCard("V2")

            collection.add_with_handle(c1, "test-handle")
            collection.update_handle("test-handle", c2)

            # Close collection to flush/release (if it does)
            # implementation detail: CardCollection might not have explicit close,
            # but sqlite bindings usually handle it.
            # We'll open a *new* connection to the same file to inspect.

            conn = sqlite3.connect(db_path)
            cursor = conn.cursor()

            # INSPECT handle_history
            cursor.execute(
                "SELECT * FROM handle_history WHERE handle = ?", ("test-handle",)
            )
            rows = cursor.fetchall()

            # Expected schema: id, handle, previous_hash, changed_at
            assert len(rows) == 1
            row = rows[0]

            # SQLite returns tuple (id, handle, previous_hash, changed_at)
            # We verify the columns by index based on schema definition
            assert row[1] == "test-handle"
            assert row[2] == c1.hash
            assert row[3] is not None  # changed_at

            # Verify Foreign Keys exist (PRAGMA foreign_key_list)
            cursor.execute("PRAGMA foreign_key_list(handle_history)")
            fks = cursor.fetchall()
            # expect refs to handle_registry(handle) and card(hash)
            # format: id, seq, table, from, to, on_update, on_delete, match

            tables_referenced = [fk[2] for fk in fks]
            assert "handle_registry" in tables_referenced
            assert "card" in tables_referenced

            conn.close()

        finally:
            if os.path.exists(db_path):
                os.remove(db_path)

    def test_sequential_updates_lineage(self):
        """
        Verify a chain of updates (V1 -> V2 -> V3) produces a verifiable lineage.
        """
        collection = CardCollection(db_path=":memory:")

        # V1
        h1 = collection.add_with_handle(MCard("First"), "doc")
        # V2
        h2 = collection.update_handle("doc", MCard("Second"))
        # V3
        h3 = collection.update_handle("doc", MCard("Third"))

        history = collection.get_handle_history("doc")

        # History should be [V2, V1] (newest first usually, or check sort)
        # implementation says DESC usually
        assert len(history) == 2

        # Check lineage
        # Most recent history entry should point to h2 (state before V3)
        assert history[0]["previous_hash"] == h2
        # Next entry should point to h1 (state before V2)
        # Next entry should point to h1 (state before V2)
        assert history[1]["previous_hash"] == h1

    def test_history_pruning_all(self):
        """Verify deleting all history for a handle."""
        collection = CardCollection(db_path=":memory:")
        collection.add_with_handle(MCard("V1"), "doc")
        collection.update_handle("doc", MCard("V2"))
        collection.update_handle("doc", MCard("V3"))

        assert len(collection.get_handle_history("doc")) == 2

        deleted = collection.prune_handle_history("doc", delete_all=True)
        assert deleted == 2
        assert len(collection.get_handle_history("doc")) == 0

    def test_history_pruning_duration(self):
        """Verify deleting history older than a timestamp."""
        from datetime import datetime, timedelta, timezone

        collection = CardCollection(db_path=":memory:")

        # Create V1 -> V2 (Old)
        collection.add_with_handle(MCard("V1"), "doc")
        collection.update_handle("doc", MCard("V2"))

        # Manually backdate the history entry to 1 hour ago
        # Note: In a real scenario we'd wait, but for speed we hack the DB
        # Access underlying connection (dangerous but necessary for test setup)
        conn = collection.engine.connection.conn
        old_time = (datetime.now(timezone.utc) - timedelta(hours=1)).isoformat()
        conn.execute("UPDATE handle_history SET changed_at = ?", (old_time,))
        conn.commit()

        # Create V2 -> V3 (New/Now)
        collection.update_handle("doc", MCard("V3"))

        assert len(collection.get_handle_history("doc")) == 2

        # Prune older than 30 mins ago
        cutoff = (datetime.now(timezone.utc) - timedelta(minutes=30)).isoformat()
        deleted = collection.prune_handle_history("doc", older_than=cutoff)

        # Should delete the 1 hour old entry, keep the recent one
        assert deleted == 1
        history = collection.get_handle_history("doc")
        assert len(history) == 1
        # The remaining one should be the recent update (V2 -> V3) which points to V2 hash
