"""Tests for the database layer."""
import sqlite3
import threading

import pytest
from greymatter_core.db import (
    SQLiteDatabase,
    register_schema_extension,
    _schema_extensions,
    get_shared_db,
    _reset_shared_db,
)


@pytest.fixture
def db():
    """DB with SalesOS extension registered — for tests that need SalesOS tables."""
    from greymatter_salesos.schema import salesos_schema_extension

    register_schema_extension(salesos_schema_extension)
    try:
        db = SQLiteDatabase(":memory:")
        yield db
    finally:
        _schema_extensions.pop()


@pytest.fixture
def core_db():
    """DB with core schema only — no extensions."""
    return SQLiteDatabase(":memory:")


def test_sqlite_creates_tables(core_db):
    """Database creates all required core tables on init."""
    tables = core_db.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
    table_names = [row["name"] for row in tables]
    assert "patterns" in table_names
    assert "knowledge_entries" in table_names
    assert "work_items" in table_names
    assert "sessions" in table_names
    # SalesOS tables should NOT be present in core-only DB
    assert "salesos_meetings" not in table_names
    assert "salesos_accounts" not in table_names


def test_sqlite_execute_returns_dicts(core_db):
    """Execute returns list of dicts, not tuples."""
    core_db.execute("INSERT INTO sessions (id, summary) VALUES (?, ?)", ("s1", "test"))
    rows = core_db.execute("SELECT * FROM sessions WHERE id = ?", ("s1",))
    assert len(rows) == 1
    assert rows[0]["id"] == "s1"
    assert rows[0]["summary"] == "test"


def test_sqlite_execute_one(core_db):
    """execute_one returns a single dict or None."""
    result = core_db.execute_one("SELECT * FROM sessions WHERE id = ?", ("nonexistent",))
    assert result is None

    core_db.execute("INSERT INTO sessions (id, summary) VALUES (?, ?)", ("s1", "test"))
    result = core_db.execute_one("SELECT * FROM sessions WHERE id = ?", ("s1",))
    assert result is not None
    assert result["id"] == "s1"


def test_sqlite_transaction_commits(core_db):
    """Transaction context manager commits on success."""
    with core_db.transaction():
        core_db.execute("INSERT INTO sessions (id, summary) VALUES (?, ?)", ("s1", "test"))
    rows = core_db.execute("SELECT * FROM sessions WHERE id = ?", ("s1",))
    assert len(rows) == 1


def test_sqlite_transaction_rollback(core_db):
    """Transaction context manager rolls back on exception."""
    with pytest.raises(ValueError):
        with core_db.transaction():
            core_db.execute("INSERT INTO sessions (id, summary) VALUES (?, ?)", ("s1", "test"))
            raise ValueError("intentional")
    rows = core_db.execute("SELECT * FROM sessions WHERE id = ?", ("s1",))
    assert len(rows) == 0


def test_core_tables_exist(core_db):
    """All core tables should exist after migration."""
    tables = core_db.execute(
        "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
    )
    table_names = {t["name"] for t in tables}
    expected_core = {
        "patterns", "pattern_evidence", "knowledge_entries",
        "work_items", "sessions", "session_checkpoints",
        "llm_completions", "quality_standards", "build_runs",
        "quality_scores", "review_issues", "documents",
        "context_requests", "embedding_chunks",
    }
    assert expected_core.issubset(table_names), f"Missing: {expected_core - table_names}"


def test_new_tables_exist(db):
    """All tables (core + SalesOS) should exist when extension is registered."""
    tables = db.execute(
        "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
    )
    table_names = {t["name"] for t in tables}
    expected = {
        "patterns", "pattern_evidence", "knowledge_entries",
        "work_items", "sessions", "llm_completions",
        "quality_standards", "build_runs", "quality_scores",
        "review_issues", "documents", "context_requests",
        "embedding_chunks",
        # SalesOS tables (from extension)
        "salesos_meetings", "salesos_accounts", "influence_map",
        "entity_connections", "hunting_list",
        "salesos_findings", "salesos_contacts", "salesos_vendor_stack",
        "salesos_funding_sources", "salesos_procurement_history",
        "salesos_compliance_records", "salesos_legislative_items",
        "salesos_broadband_projects", "salesos_meddpicc_scores",
        "salesos_snapshots",
    }
    assert expected.issubset(table_names), f"Missing: {expected - table_names}"


def test_behavior_database_persists_across_operations(core_db):
    """
    Given: A fresh database
    When: Data is inserted, then queried in a separate call
    Then: Data persists and is retrievable
    """
    # When
    core_db.execute("INSERT INTO patterns (id, type, domain, title, observation) VALUES (?, ?, ?, ?, ?)",
               ("pat-001", "process", "testing", "TDD First", "Always write tests first"))

    # Then
    result = core_db.execute_one("SELECT * FROM patterns WHERE id = ?", ("pat-001",))
    assert result is not None
    assert result["title"] == "TDD First"
    assert result["confidence"] == "low"
    assert result["confirmations"] == 0


def test_fts5_meetings_table_exists(db):
    """FTS5 virtual table for meeting search should be created by extension."""
    db.execute(
        "INSERT INTO salesos_meetings (id, title, summary, transcript, account, date) VALUES (?, ?, ?, ?, ?, ?)",
        ("m1", "Q1 Review", "Discussed pipeline", "Full transcript about pipeline targets", "Acme Corp", "2026-01-15"),
    )
    results = db.execute(
        "SELECT m.* FROM meetings_fts f JOIN salesos_meetings m ON f.rowid = m.rowid WHERE meetings_fts MATCH ? ORDER BY rank LIMIT 5",
        ("pipeline",),
    )
    assert len(results) == 1
    assert results[0]["title"] == "Q1 Review"


def test_fts5_bm25_ranking(db):
    """FTS5 should rank results by relevance."""
    db.execute(
        "INSERT INTO salesos_meetings (id, title, summary, transcript, account, date) VALUES (?, ?, ?, ?, ?, ?)",
        ("m1", "Budget Meeting", "Budget discussion", "Brief mention of network budget", "Acme", "2026-01-01"),
    )
    db.execute(
        "INSERT INTO salesos_meetings (id, title, summary, transcript, account, date) VALUES (?, ?, ?, ?, ?, ?)",
        ("m2", "Network Budget Deep Dive", "Network budget analysis", "Detailed network budget breakdown with network costs and network planning", "Acme", "2026-01-02"),
    )
    results = db.execute(
        "SELECT m.id FROM meetings_fts f JOIN salesos_meetings m ON f.rowid = m.rowid WHERE meetings_fts MATCH ? ORDER BY rank LIMIT 5",
        ("network budget",),
    )
    assert len(results) == 2
    assert results[0]["id"] == "m2"


def test_salesos_accounts_table_exists(db):
    """salesos_accounts table should exist after extension registration."""
    db.execute(
        "INSERT INTO salesos_accounts (id, name, segment, state, status) VALUES (?, ?, ?, ?, ?)",
        ("a1", "Acme Corp", "K-12", "NV", "Customer"),
    )
    result = db.execute_one("SELECT * FROM salesos_accounts WHERE id = ?", ("a1",))
    assert result["name"] == "Acme Corp"


def test_influence_map_table_exists(db):
    """influence_map table should exist after extension registration."""
    db.execute(
        "INSERT INTO influence_map (account_name, entity_name, entity_type, relationship, region) VALUES (?, ?, ?, ?, ?)",
        ("Acme Corp", "Cisco", "Incumbent", "Existing vendor", "Rocky Mountain"),
    )
    results = db.execute("SELECT * FROM influence_map WHERE account_name = ?", ("Acme Corp",))
    assert len(results) == 1
    assert results[0]["entity_type"] == "Incumbent"


def test_sqlite_vec_extension_loaded(core_db):
    """sqlite-vec extension should be loaded and functional."""
    version = core_db.execute_one("SELECT vec_version() as v")
    assert version is not None
    assert version["v"] is not None


def test_vec0_core_tables_created(core_db):
    """Core vec0 virtual tables should be created by migrate."""
    tables = core_db.execute(
        "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'vec_%' ORDER BY name"
    )
    table_names = [r["name"] for r in tables]
    assert "vec_documents" in table_names
    assert "vec_knowledge" in table_names
    assert "vec_observations" in table_names
    assert "vec_chunks" in table_names
    # SalesOS vec tables should NOT be in core
    assert "vec_meetings" not in table_names
    assert "vec_accounts" not in table_names


def test_vec0_tables_with_extension(db):
    """All vec0 tables (core + SalesOS) should exist when extension is registered."""
    tables = db.execute(
        "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'vec_%' ORDER BY name"
    )
    table_names = [r["name"] for r in tables]
    assert "vec_documents" in table_names
    assert "vec_knowledge" in table_names
    assert "vec_meetings" in table_names
    assert "vec_accounts" in table_names


# --- SalesOS 4 table tests (require extension) ---


def test_salesos4_findings_table_exists(db):
    """SalesOS 4 findings table should be created by extension."""
    result = db.execute(
        "SELECT name FROM sqlite_master WHERE type='table' AND name='salesos_findings'"
    )
    assert len(result) == 1


def test_salesos4_contacts_table_exists(db):
    result = db.execute(
        "SELECT name FROM sqlite_master WHERE type='table' AND name='salesos_contacts'"
    )
    assert len(result) == 1


def test_salesos4_vendor_stack_table_exists(db):
    result = db.execute(
        "SELECT name FROM sqlite_master WHERE type='table' AND name='salesos_vendor_stack'"
    )
    assert len(result) == 1


def test_salesos4_funding_sources_table_exists(db):
    result = db.execute(
        "SELECT name FROM sqlite_master WHERE type='table' AND name='salesos_funding_sources'"
    )
    assert len(result) == 1


def test_salesos4_meddpicc_scores_table_exists(db):
    result = db.execute(
        "SELECT name FROM sqlite_master WHERE type='table' AND name='salesos_meddpicc_scores'"
    )
    assert len(result) == 1


def test_salesos4_snapshots_table_exists(db):
    result = db.execute(
        "SELECT name FROM sqlite_master WHERE type='table' AND name='salesos_snapshots'"
    )
    assert len(result) == 1


def test_salesos4_findings_insert_and_query(db):
    """Verify findings table supports all required columns."""
    db.execute(
        "INSERT INTO salesos_accounts (id, name, segment, state) VALUES (?, ?, ?, ?)",
        ("acct-001", "Test County", "COUNTY", "NV"),
    )
    db.execute(
        """INSERT INTO salesos_findings
           (finding_id, account_id, domain, category, title, detail,
            confidence, source_url, source_type, discovered_by,
            discovered_at, freshness_days, meddpicc_elements)
           VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
        ("f-001", "acct-001", "infrastructure", "vendor_stack",
         "Cisco switching deployed", "Core switches are Catalyst 9300",
         "CONFIRMED", "https://example.com", "web", "infra_analyst",
         "2026-03-02T10:00:00", 30, '["D1","I"]'),
    )
    row = db.execute_one(
        "SELECT * FROM salesos_findings WHERE finding_id = ?", ("f-001",)
    )
    assert row is not None
    assert row["domain"] == "infrastructure"
    assert row["confidence"] == "CONFIRMED"


def test_salesos_accounts_expanded_columns(db):
    """Verify salesos_accounts has the new SalesOS 4 columns."""
    db.execute(
        """INSERT INTO salesos_accounts
           (id, name, segment, state, status, population, annual_budget,
            fy_start_month, current_stage)
           VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
        ("acct-001", "Test County", "COUNTY", "NV", "HUNTING",
         50000, "$2M", 7, 2),
    )
    row = db.execute_one(
        "SELECT population, fy_start_month, current_stage FROM salesos_accounts WHERE id = ?",
        ("acct-001",),
    )
    assert row["population"] == 50000
    assert row["fy_start_month"] == 7
    assert row["current_stage"] == 2


def test_migrate_adds_missing_columns_to_legacy_tables(tmp_path):
    """Verify migrate() adds missing columns to existing tables with legacy schema."""
    db_path = str(tmp_path / "legacy.db")

    # Create a "legacy" DB with old schema (no confirmations, no entry_type)
    conn = sqlite3.connect(db_path)
    conn.execute("""
        CREATE TABLE patterns (
            id TEXT PRIMARY KEY,
            type TEXT NOT NULL,
            domain TEXT NOT NULL,
            title TEXT NOT NULL,
            observation TEXT NOT NULL,
            confidence TEXT DEFAULT 'low',
            applications INTEGER DEFAULT 0,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
    """)
    conn.execute("""
        CREATE TABLE knowledge_entries (
            id TEXT PRIMARY KEY,
            type TEXT NOT NULL,
            scope TEXT DEFAULT 'cross-cutting',
            domain TEXT,
            title TEXT NOT NULL,
            content TEXT NOT NULL,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            pruned_at TEXT
        )
    """)
    conn.execute(
        "INSERT INTO patterns (id, type, domain, title, observation, confidence, applications) "
        "VALUES ('p1', 'process', 'testing', 'Legacy Pattern', 'obs', 'low', 5)"
    )
    conn.execute(
        "INSERT INTO knowledge_entries (id, type, scope, domain, title, content) "
        "VALUES ('k1', 'reference', 'cross-cutting', 'testing', 'Legacy Knowledge', 'content')"
    )
    conn.commit()
    conn.close()

    # Now run migrate via SQLiteDatabase constructor
    db = SQLiteDatabase(db_path)

    # Verify new columns exist and data was backfilled
    row = db.execute_one("SELECT confirmations FROM patterns WHERE id = 'p1'")
    assert row is not None
    assert row["confirmations"] == 5  # backfilled from applications

    row = db.execute_one("SELECT entry_type, tags, applied_count, confidence FROM knowledge_entries WHERE id = 'k1'")
    assert row is not None
    assert row["entry_type"] == "reference"  # backfilled from type
    assert row["applied_count"] == 0
    # tags should be None (not set)
    assert row["tags"] is None


def test_register_schema_extension_creates_tables():
    """register_schema_extension callbacks should run during DB init."""
    original_len = len(_schema_extensions)

    def my_extension(conn):
        conn.execute("CREATE TABLE IF NOT EXISTS ext_test (id TEXT PRIMARY KEY, name TEXT)")

    register_schema_extension(my_extension)
    try:
        db = SQLiteDatabase(":memory:")
        tables = db.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='ext_test'")
        assert len(tables) == 1
        assert tables[0]["name"] == "ext_test"
    finally:
        # Clean up global state
        _schema_extensions.pop()


def test_register_schema_extension_runs_before_migrate():
    """Extension tables should exist before migrate() runs, so FK constraints work."""
    execution_order = []

    def tracking_extension(conn):
        execution_order.append("extension")
        conn.execute("CREATE TABLE IF NOT EXISTS ext_order_test (id TEXT PRIMARY KEY)")

    register_schema_extension(tracking_extension)
    try:
        db = SQLiteDatabase(":memory:")
        # Extension ran (table exists)
        tables = db.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='ext_order_test'")
        assert len(tables) == 1
        assert "extension" in execution_order
    finally:
        _schema_extensions.pop()


def test_backward_compat_schema_alias():
    """SCHEMA should be an alias for CORE_SCHEMA for backward compatibility."""
    from greymatter_core.db import SCHEMA, CORE_SCHEMA
    assert SCHEMA is CORE_SCHEMA


# ---------------------------------------------------------------------------
# Shared singleton tests
# ---------------------------------------------------------------------------


class TestGetSharedDb:
    """Tests for the get_shared_db() singleton."""

    def teardown_method(self):
        _reset_shared_db()

    def test_returns_sqlite_database(self, tmp_path):
        """get_shared_db returns a SQLiteDatabase instance."""
        db = get_shared_db(str(tmp_path / "shared.db"))
        assert isinstance(db, SQLiteDatabase)

    def test_returns_same_instance(self, tmp_path):
        """Subsequent calls return the exact same object."""
        db1 = get_shared_db(str(tmp_path / "shared.db"))
        db2 = get_shared_db()
        assert db1 is db2

    def test_ignores_path_after_first_call(self, tmp_path):
        """Path argument is only used on the first call."""
        db1 = get_shared_db(str(tmp_path / "first.db"))
        db2 = get_shared_db(str(tmp_path / "second.db"))
        assert db1 is db2

    def test_reset_allows_new_instance(self, tmp_path):
        """_reset_shared_db clears the singleton so a new one is created."""
        db1 = get_shared_db(str(tmp_path / "a.db"))
        _reset_shared_db()
        db2 = get_shared_db(str(tmp_path / "b.db"))
        assert db1 is not db2

    def test_thread_safety(self, tmp_path):
        """Multiple threads calling get_shared_db get the same instance."""
        path = str(tmp_path / "threaded.db")
        results: list[SQLiteDatabase] = []
        barrier = threading.Barrier(4)

        def worker():
            barrier.wait()
            results.append(get_shared_db(path))

        threads = [threading.Thread(target=worker) for _ in range(4)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert len(results) == 4
        assert all(r is results[0] for r in results)
