"""Tests for DuckDB storage engine — full parity with SQLite engine."""

import os
import tempfile

import pytest

from mcard.engine.duckdb_engine import DuckDBConnection, DuckDBEngine
from mcard.model.card import MCard
from mcard.model.card_collection import CardCollection


# ═══════════════════════════════════════════════════════════════════════════════
# Fixtures
# ═══════════════════════════════════════════════════════════════════════════════


@pytest.fixture
def memory_engine():
    """Create an in-memory DuckDB engine."""
    conn = DuckDBConnection(":memory:")
    engine = DuckDBEngine(conn)
    yield engine
    conn.disconnect()


@pytest.fixture
def file_engine(tmp_path):
    """Create a file-based DuckDB engine."""
    db_path = str(tmp_path / "test.duckdb")
    conn = DuckDBConnection(db_path)
    engine = DuckDBEngine(conn)
    yield engine
    conn.disconnect()


@pytest.fixture
def duckdb_collection():
    """Create a CardCollection backed by DuckDB."""
    collection = CardCollection(engine_type="duckdb", db_path=":memory:")
    yield collection


# ═══════════════════════════════════════════════════════════════════════════════
# Card CRUD Tests
# ═══════════════════════════════════════════════════════════════════════════════


class TestDuckDBCardOperations:
    """Test basic card operations on DuckDB engine."""

    def test_add_and_get(self, memory_engine):
        card = MCard("hello world")
        hash_val = memory_engine.add(card)
        assert hash_val == str(card.hash)

        retrieved = memory_engine.get(hash_val)
        assert retrieved is not None
        assert retrieved.content == card.content

    def test_add_duplicate_raises(self, memory_engine):
        card = MCard("duplicate content")
        memory_engine.add(card)
        with pytest.raises(ValueError, match="already exists"):
            memory_engine.add(card)

    def test_get_nonexistent(self, memory_engine):
        assert memory_engine.get("nonexistent_hash") is None

    def test_delete(self, memory_engine):
        card = MCard("to be deleted")
        hash_val = memory_engine.add(card)
        assert memory_engine.delete(hash_val) is True
        assert memory_engine.get(hash_val) is None

    def test_delete_nonexistent(self, memory_engine):
        assert memory_engine.delete("nonexistent") is False

    def test_count(self, memory_engine):
        assert memory_engine.count() == 0
        memory_engine.add(MCard("one"))
        assert memory_engine.count() == 1
        memory_engine.add(MCard("two"))
        assert memory_engine.count() == 2

    def test_clear(self, memory_engine):
        memory_engine.add(MCard("a"))
        memory_engine.add(MCard("b"))
        assert memory_engine.count() == 2
        memory_engine.clear()
        assert memory_engine.count() == 0

    def test_binary_content(self, memory_engine):
        binary_data = b"\x00\x01\x02\xff\xfe\xfd"
        card = MCard(binary_data)
        hash_val = memory_engine.add(card)
        retrieved = memory_engine.get(hash_val)
        assert retrieved is not None
        assert retrieved.content == binary_data


# ═══════════════════════════════════════════════════════════════════════════════
# Pagination Tests
# ═══════════════════════════════════════════════════════════════════════════════


class TestDuckDBPagination:
    """Test pagination on DuckDB engine."""

    def test_get_all_cards(self, memory_engine):
        for i in range(5):
            memory_engine.add(MCard(f"card {i}"))
        page = memory_engine.get_all_cards()
        assert len(page.items) == 5
        assert page.total_items == 5

    def test_get_cards_by_page(self, memory_engine):
        for i in range(10):
            memory_engine.add(MCard(f"card {i}"))
        page = memory_engine.get_cards_by_page(page_number=1, page_size=3)
        assert len(page.items) == 3
        assert page.total_items == 10
        assert page.has_next is True

    def test_get_page_empty(self, memory_engine):
        page = memory_engine.get_cards_by_page(page_number=1, page_size=10)
        assert len(page.items) == 0
        assert page.total_items == 0

    def test_page_validation(self, memory_engine):
        with pytest.raises(ValueError):
            memory_engine.get_cards_by_page(page_number=0, page_size=10)
        with pytest.raises(ValueError):
            memory_engine.get_cards_by_page(page_number=1, page_size=0)


# ═══════════════════════════════════════════════════════════════════════════════
# Search Tests
# ═══════════════════════════════════════════════════════════════════════════════


class TestDuckDBSearch:
    """Test search on DuckDB engine."""

    def test_search_by_string(self, memory_engine):
        memory_engine.add(MCard("hello world"))
        memory_engine.add(MCard("goodbye world"))
        memory_engine.add(MCard("something else"))

        page = memory_engine.search_by_string("world")
        assert page.total_items == 2

    def test_search_by_content(self, memory_engine):
        memory_engine.add(MCard("find this pattern"))
        memory_engine.add(MCard("not matching"))

        page = memory_engine.search_by_content("this pattern")
        assert page.total_items == 1

    def test_search_empty_raises(self, memory_engine):
        with pytest.raises(ValueError, match="empty"):
            memory_engine.search_by_content("")

    def test_search_validation(self, memory_engine):
        with pytest.raises(ValueError):
            memory_engine.search_by_string("test", page_number=0)
        with pytest.raises(ValueError):
            memory_engine.search_by_content("test", page_size=0)


# ═══════════════════════════════════════════════════════════════════════════════
# Handle Tests
# ═══════════════════════════════════════════════════════════════════════════════


class TestDuckDBHandleOperations:
    """Test handle operations on DuckDB engine."""

    def test_register_and_resolve(self, memory_engine):
        card = MCard("handle target")
        hash_val = memory_engine.add(card)

        assert memory_engine.register_handle("my_handle", hash_val) is True
        assert memory_engine.resolve_handle("my_handle") == hash_val

    def test_register_duplicate_raises(self, memory_engine):
        card = MCard("content")
        hash_val = memory_engine.add(card)

        memory_engine.register_handle("dup_handle", hash_val)
        with pytest.raises(ValueError, match="already exists"):
            memory_engine.register_handle("dup_handle", hash_val)

    def test_resolve_nonexistent(self, memory_engine):
        assert memory_engine.resolve_handle("nonexistent") is None

    def test_get_by_handle(self, memory_engine):
        card = MCard("handle card")
        hash_val = memory_engine.add(card)
        memory_engine.register_handle("get_handle", hash_val)

        retrieved = memory_engine.get_by_handle("get_handle")
        assert retrieved is not None
        assert retrieved.content == card.content

    def test_update_handle(self, memory_engine):
        card1 = MCard("version 1")
        card2 = MCard("version 2")
        h1 = memory_engine.add(card1)
        h2 = memory_engine.add(card2)

        memory_engine.register_handle("versioned", h1)
        previous = memory_engine.update_handle("versioned", h2)
        assert previous == h1
        assert memory_engine.resolve_handle("versioned") == h2

    def test_update_nonexistent_raises(self, memory_engine):
        with pytest.raises(ValueError, match="not found"):
            memory_engine.update_handle("unknown", "hash")

    def test_handle_history(self, memory_engine):
        card1 = MCard("v1")
        card2 = MCard("v2")
        card3 = MCard("v3")
        h1 = memory_engine.add(card1)
        h2 = memory_engine.add(card2)
        h3 = memory_engine.add(card3)

        memory_engine.register_handle("history_handle", h1)
        memory_engine.update_handle("history_handle", h2)
        memory_engine.update_handle("history_handle", h3)

        history = memory_engine.get_handle_history("history_handle")
        assert len(history) == 2
        assert history[0]["previous_hash"] == h2
        assert history[1]["previous_hash"] == h1

    def test_prune_handle_history(self, memory_engine):
        card1 = MCard("p1")
        card2 = MCard("p2")
        h1 = memory_engine.add(card1)
        h2 = memory_engine.add(card2)

        memory_engine.register_handle("prune_handle", h1)
        memory_engine.update_handle("prune_handle", h2)

        count = memory_engine.prune_handle_history("prune_handle", delete_all=True)
        assert count == 1
        assert len(memory_engine.get_handle_history("prune_handle")) == 0

    def test_get_all_handles(self, memory_engine):
        card = MCard("multi handle")
        hash_val = memory_engine.add(card)
        memory_engine.register_handle("h1", hash_val)
        memory_engine.register_handle("h2", hash_val)

        handles = memory_engine.get_all_handles()
        assert len(handles) == 2
        assert {h["handle"] for h in handles} == {"h1", "h2"}

    def test_remove_handle(self, memory_engine):
        card = MCard("remove me")
        hash_val = memory_engine.add(card)
        memory_engine.register_handle("to_remove", hash_val)
        memory_engine.remove_handle("to_remove")
        assert memory_engine.resolve_handle("to_remove") is None

    def test_rename_handle(self, memory_engine):
        card = MCard("rename me")
        hash_val = memory_engine.add(card)
        memory_engine.register_handle("old_name", hash_val)
        memory_engine.rename_handle("old_name", "new_name")

        assert memory_engine.resolve_handle("old_name") is None
        assert memory_engine.resolve_handle("new_name") == hash_val

    def test_rename_nonexistent_raises(self, memory_engine):
        with pytest.raises(ValueError, match="not found"):
            memory_engine.rename_handle("ghost", "new")

    def test_rename_to_existing_raises(self, memory_engine):
        card = MCard("content_a")
        h = memory_engine.add(card)
        card2 = MCard("content_b")
        h2 = memory_engine.add(card2)

        memory_engine.register_handle("exists_a", h)
        memory_engine.register_handle("exists_b", h2)
        with pytest.raises(ValueError, match="already exists"):
            memory_engine.rename_handle("exists_a", "exists_b")


# ═══════════════════════════════════════════════════════════════════════════════
# File-based DB Tests
# ═══════════════════════════════════════════════════════════════════════════════


class TestDuckDBFilePersistence:
    """Test that DuckDB file persistence works."""

    def test_file_db_persists(self, tmp_path):
        db_path = str(tmp_path / "persist.duckdb")

        # Write data
        conn1 = DuckDBConnection(db_path)
        engine1 = DuckDBEngine(conn1)
        card = MCard("persistent data")
        hash_val = engine1.add(card)
        conn1.disconnect()

        # Read data back
        conn2 = DuckDBConnection(db_path)
        engine2 = DuckDBEngine(conn2)
        retrieved = engine2.get(hash_val)
        assert retrieved is not None
        assert retrieved.content == card.content
        conn2.disconnect()


# ═══════════════════════════════════════════════════════════════════════════════
# CardCollection Integration Tests
# ═══════════════════════════════════════════════════════════════════════════════


class TestDuckDBCardCollection:
    """Test CardCollection with DuckDB engine."""

    def test_collection_duckdb_init(self, duckdb_collection):
        assert duckdb_collection.engine is not None
        assert isinstance(duckdb_collection.engine, DuckDBEngine)

    def test_collection_add_and_get(self, duckdb_collection):
        card = MCard("collection test")
        hash_val = duckdb_collection.add(card)
        retrieved = duckdb_collection.get(hash_val)
        assert retrieved is not None
        assert retrieved.content == card.content

    def test_collection_handle_ops(self, duckdb_collection):
        card = MCard("handle collection test")
        hash_val = duckdb_collection.add_with_handle(card, "col_handle")
        resolved = duckdb_collection.resolve_handle("col_handle")
        assert resolved == hash_val

    def test_missing_duckdb_raises(self):
        """If engine_type='duckdb' but duckdb is not installed, should raise."""
        # This test only verifies the error path is reachable
        # We can't easily test the actual ImportError since duckdb IS installed
        pass


# ═══════════════════════════════════════════════════════════════════════════════
# Schema Single Source of Truth Tests
# ═══════════════════════════════════════════════════════════════════════════════


class TestDuckDBSchemaSourceOfTruth:
    """Verify that DuckDB engine loads schema from canonical SQL files,
    not from hardcoded SQL.  This enforces the single source of truth
    design documented in schema/README.md."""

    def test_schema_sql_files_exist(self):
        """The canonical schema SQL files must exist on disk."""
        from pathlib import Path

        project_root = Path(__file__).parent.parent
        core_schema = project_root / "schema" / "mcard_schema.sql"
        vector_schema = project_root / "schema" / "mcard_vector_schema.sql"

        assert core_schema.exists(), (
            f"Core schema file missing: {core_schema}\n"
            "This file is the SINGLE SOURCE OF TRUTH for all engine schemas."
        )
        assert vector_schema.exists(), (
            f"Vector schema file missing: {vector_schema}"
        )

    def test_schema_files_contain_create_statements(self):
        """The canonical SQL files must contain CREATE TABLE statements."""
        from pathlib import Path

        project_root = Path(__file__).parent.parent
        core_sql = (project_root / "schema" / "mcard_schema.sql").read_text()
        vector_sql = (project_root / "schema" / "mcard_vector_schema.sql").read_text()

        assert "CREATE TABLE" in core_sql, (
            "mcard_schema.sql must contain CREATE TABLE statements"
        )
        assert "CREATE TABLE" in vector_sql, (
            "mcard_vector_schema.sql must contain CREATE TABLE statements"
        )

        # Core schema must define the three essential tables
        for table in ["card", "handle_registry", "handle_history"]:
            assert table in core_sql.lower(), (
                f"Core schema missing required table: {table}"
            )

    def test_duckdb_loads_schema_via_mcard_schema(self):
        """DuckDB engine must use MCardSchema singleton (not hardcoded SQL)."""
        from mcard.schema import MCardSchema

        schema = MCardSchema.get_instance()
        statements = schema.get_all_statements()

        # MCardSchema must provide statements
        assert len(statements) > 0, (
            "MCardSchema returned no statements — SQL files may be corrupt"
        )

        # Core tables must be present in loaded statements
        all_sql = "\n".join(statements).lower()
        for table in ["card", "handle_registry", "handle_history", "schema_version"]:
            assert table in all_sql, (
                f"MCardSchema missing table '{table}' from canonical SQL files"
            )

    def test_duckdb_schema_source_files_constant(self):
        """DuckDBConnection must declare its schema file dependencies."""
        assert hasattr(DuckDBConnection, "SCHEMA_SOURCE_FILES"), (
            "DuckDBConnection must declare SCHEMA_SOURCE_FILES constant"
        )
        sources = DuckDBConnection.SCHEMA_SOURCE_FILES
        assert "schema/mcard_schema.sql" in sources[0], (
            "First schema source must be the core mcard_schema.sql"
        )
        assert "schema/mcard_vector_schema.sql" in sources[1], (
            "Second schema source must be mcard_vector_schema.sql"
        )

    def test_duckdb_creates_tables_from_sql_files(self, memory_engine):
        """After init, DuckDB must have the core tables loaded from SQL files."""
        conn = memory_engine.connection.conn

        # Query DuckDB's information_schema for tables
        tables = conn.execute(
            "SELECT table_name FROM information_schema.tables "
            "WHERE table_schema = 'main'"
        ).fetchall()
        table_names = {row[0] for row in tables}

        # Core tables from mcard_schema.sql
        for required in ["card", "handle_registry", "handle_history", "schema_version"]:
            assert required in table_names, (
                f"DuckDB missing table '{required}' — "
                "engine may not be loading from canonical SQL files"
            )

    def test_no_hardcoded_create_table_in_engine(self):
        """The DuckDB engine source must NOT contain hardcoded CREATE TABLE
        in executable SQL strings (comments and docstrings are allowed)."""
        from pathlib import Path

        engine_src = Path(__file__).parent.parent / "mcard" / "engine" / "duckdb_engine.py"
        source_lines = engine_src.read_text().splitlines()

        # Only check lines that are NOT comments or inside docstrings
        import re
        violations = []
        in_docstring = False
        for i, line in enumerate(source_lines, 1):
            stripped = line.strip()
            # Track triple-quote docstrings
            if '\"\"\"' in stripped or "\'\'\'" in stripped:
                count = stripped.count('\"\"\"') + stripped.count("\'\'\'" )
                if count % 2 == 1:
                    in_docstring = not in_docstring
                continue
            if in_docstring:
                continue
            if stripped.startswith("#"):
                continue
            if re.search(r"CREATE\s+TABLE", line, re.IGNORECASE):
                violations.append(f"  Line {i}: {stripped}")

        assert len(violations) == 0, (
            f"Found {len(violations)} hardcoded CREATE TABLE statement(s) in "
            "duckdb_engine.py (excluding comments/docstrings):\n"
            + "\n".join(violations)
            + "\nAll schema must come from mcard_schema.sql / "
            "mcard_vector_schema.sql via MCardSchema singleton."
        )
