#!/usr/bin/env python3
"""Unit tests: SQLite node count matches source nodes dict after sync_to_sqlite.

Verifies that sync_to_sqlite writes exactly len(nodes) rows to graph_nodes —
no losses, no duplicates.  Uses in-memory SQLite + patch; no production DB writes.
"""
import sqlite3
import unittest
from unittest.mock import patch


# ---------------------------------------------------------------------------
# Minimal in-memory schema matching brain_db.py graph tables
# ---------------------------------------------------------------------------

def _make_in_memory_db() -> sqlite3.Connection:
    conn = sqlite3.connect(":memory:")
    conn.row_factory = sqlite3.Row
    conn.execute("""
        CREATE TABLE graph_nodes (
            id TEXT PRIMARY KEY, type TEXT NOT NULL DEFAULT 'CONCEPT',
            text TEXT NOT NULL DEFAULT '', source TEXT DEFAULT '',
            confidence REAL DEFAULT 0.5, domain TEXT NOT NULL DEFAULT 'general',
            date TEXT DEFAULT '', degree INTEGER DEFAULT 0,
            embedding BLOB DEFAULT NULL, metadata TEXT DEFAULT '{}'
        )
    """)
    conn.execute("""
        CREATE TABLE graph_edges (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            from_node TEXT NOT NULL, to_node TEXT NOT NULL,
            edge_type TEXT NOT NULL, confidence REAL DEFAULT 0.5,
            source_node TEXT DEFAULT '', metadata TEXT DEFAULT '{}',
            UNIQUE(from_node, edge_type, to_node)
        )
    """)
    conn.commit()
    return conn


def _call_sync(conn: sqlite3.Connection, nodes: dict, edges: list):
    """Invoke sync_to_sqlite with a patched get_conn that returns our in-memory DB.

    get_conn is imported locally inside sync_to_sqlite, so we patch the source
    module (pulse.core.brain_db) rather than the graph_sqlite_sync namespace.
    """
    with patch("pulse.core.brain_db.get_conn", return_value=conn):
        from pulse.graph.graph_sqlite_sync import sync_to_sqlite
        sync_to_sqlite(nodes, edges)


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------

class TestSyncNodeCountMatches(unittest.TestCase):
    """Node count in SQLite must equal len(nodes) passed to sync_to_sqlite."""

    def setUp(self):
        self.conn = _make_in_memory_db()

    def tearDown(self):
        self.conn.close()

    # --- core count check ---

    def test_node_count_matches_source_dict(self):
        """Exact node count: SQLite rows == len(nodes) after sync."""
        nodes = {
            "n1": {"type": "CONCEPT", "text": "alpha", "confidence": 0.8},
            "n2": {"type": "LESSON",  "text": "beta",  "confidence": 0.7},
            "n3": {"type": "FAILURE", "text": "gamma", "confidence": 0.6},
        }
        _call_sync(self.conn, nodes, [])

        count = self.conn.execute("SELECT COUNT(*) FROM graph_nodes").fetchone()[0]
        self.assertEqual(count, len(nodes),
                         f"Expected {len(nodes)} nodes in SQLite, got {count}")

    def test_empty_nodes_yields_zero_rows(self):
        """Syncing an empty dict must result in zero rows."""
        _call_sync(self.conn, {}, [])

        count = self.conn.execute("SELECT COUNT(*) FROM graph_nodes").fetchone()[0]
        self.assertEqual(count, 0, "Empty sync must produce 0 rows")

    def test_single_node_count(self):
        """Single-node dict must produce exactly 1 row."""
        nodes = {"only": {"type": "CONCEPT", "text": "solo"}}
        _call_sync(self.conn, nodes, [])

        count = self.conn.execute("SELECT COUNT(*) FROM graph_nodes").fetchone()[0]
        self.assertEqual(count, 1, "Single-node sync must produce 1 row")

    def test_large_batch_count_matches(self):
        """500-node batch: count must match exactly."""
        nodes = {f"n{i}": {"type": "CONCEPT", "text": f"node {i}"} for i in range(500)}
        _call_sync(self.conn, nodes, [])

        count = self.conn.execute("SELECT COUNT(*) FROM graph_nodes").fetchone()[0]
        self.assertEqual(count, 500,
                         f"500-node sync produced {count} rows (expected 500)")

    # --- idempotency (second sync replaces, doesn't double) ---

    def test_second_sync_replaces_not_appends(self):
        """Re-syncing same nodes must not double the count (DELETE + INSERT)."""
        nodes = {f"n{i}": {"type": "CONCEPT", "text": f"t{i}"} for i in range(10)}
        _call_sync(self.conn, nodes, [])
        _call_sync(self.conn, nodes, [])  # second run

        count = self.conn.execute("SELECT COUNT(*) FROM graph_nodes").fetchone()[0]
        self.assertEqual(count, 10,
                         f"Second sync must replace rows, got {count} (expected 10)")

    def test_smaller_second_sync_shrinks_count(self):
        """Syncing fewer nodes on second call must reduce count to new set size."""
        nodes_first  = {f"n{i}": {"type": "CONCEPT", "text": f"t{i}"} for i in range(20)}
        nodes_second = {f"n{i}": {"type": "CONCEPT", "text": f"t{i}"} for i in range(5)}
        _call_sync(self.conn, nodes_first, [])
        _call_sync(self.conn, nodes_second, [])

        count = self.conn.execute("SELECT COUNT(*) FROM graph_nodes").fetchone()[0]
        self.assertEqual(count, 5,
                         f"Second sync (5 nodes) must shrink DB to 5, got {count}")

    # --- node identity preserved ---

    def test_synced_node_ids_match_dict_keys(self):
        """IDs stored in SQLite must match the keys of the nodes dict."""
        nodes = {"key_a": {"type": "CONCEPT", "text": "A"},
                 "key_b": {"type": "LESSON",  "text": "B"}}
        _call_sync(self.conn, nodes, [])

        rows = self.conn.execute("SELECT id FROM graph_nodes ORDER BY id").fetchall()
        stored_ids = {r["id"] for r in rows}
        self.assertEqual(stored_ids, set(nodes.keys()),
                         f"Stored IDs {stored_ids} don't match source keys {set(nodes.keys())}")

    # --- edges don't inflate node count ---

    def test_edges_do_not_inflate_node_count(self):
        """Adding edges must not change the node count."""
        nodes = {"a": {"type": "CONCEPT", "text": "A"},
                 "b": {"type": "CONCEPT", "text": "B"}}
        edges = [{"from": "a", "to": "b", "type": "LEADS_TO", "confidence": 0.9}]
        _call_sync(self.conn, nodes, edges)

        count = self.conn.execute("SELECT COUNT(*) FROM graph_nodes").fetchone()[0]
        self.assertEqual(count, 2,
                         f"Edges must not inflate node count; got {count} (expected 2)")


if __name__ == "__main__":
    unittest.main(verbosity=2)
