"""Tests for semantic search module."""
from unittest.mock import MagicMock

import pytest

from greymatter_core.db import SQLiteDatabase, register_schema_extension, _schema_extensions
from greymatter_core.embeddings import serialize_f32
from greymatter_core.semantic import SemanticSearch


@pytest.fixture
def db():
    """DB with SalesOS extension — registers schema, TABLE_CONFIG entries,
    and FTS backends since SalesOS is no longer auto-loaded by core."""
    from greymatter_core.semantic import register_table, register_fts_backend, _TABLE_CONFIG, _FTS_BACKENDS
    from greymatter_salesos.schema import salesos_schema_extension
    from greymatter_salesos.semantic import SALESOS_TABLE_CONFIGS, SALESOS_FTS_BACKENDS

    register_schema_extension(salesos_schema_extension)
    for name, config in SALESOS_TABLE_CONFIGS.items():
        register_table(name, config)
    for name, backend in SALESOS_FTS_BACKENDS.items():
        register_fts_backend(name, **backend)

    try:
        db = SQLiteDatabase(":memory:")
        yield db
    finally:
        _schema_extensions.pop()
        for name in SALESOS_TABLE_CONFIGS:
            _TABLE_CONFIG.pop(name, None)
        for name in SALESOS_FTS_BACKENDS:
            _FTS_BACKENDS.pop(name, None)


@pytest.fixture
def mock_embedder():
    """Mock embedding provider that returns deterministic vectors."""
    embedder = MagicMock()
    embedder.dimensions = 768
    def fake_embed(text):
        vec = [0.0] * 768
        if "network" in text.lower():
            vec[0] = 1.0
        if "budget" in text.lower():
            vec[1] = 1.0
        if "cisco" in text.lower():
            vec[0] = 0.9
            vec[2] = 0.5
        return vec
    embedder.embed = MagicMock(side_effect=fake_embed)
    embedder.embed_batch = MagicMock(side_effect=lambda texts: [fake_embed(t) for t in texts])
    return embedder


def test_index_row_and_search(db, mock_embedder):
    """Index a meeting, then search for it by meaning."""
    search = SemanticSearch(db, mock_embedder)
    db.execute(
        "INSERT INTO salesos_meetings (id, title, summary, transcript, account, date) VALUES (?, ?, ?, ?, ?, ?)",
        ("m1", "Network Review", "Reviewed campus network", "Discussed Cisco switches", "Acme", "2026-01-15"),
    )
    search.index_row("meetings", "m1", "Network Review Reviewed campus network Discussed Cisco switches")
    results = search.search("network infrastructure", tables=["meetings"], limit=5)
    assert len(results) >= 1
    assert results[0]["id"] == "m1"
    assert "distance" in results[0]


def test_search_returns_closest_match(db, mock_embedder):
    """Search should rank results by semantic distance."""
    search = SemanticSearch(db, mock_embedder)
    db.execute(
        "INSERT INTO salesos_meetings (id, title, summary, transcript, account, date) VALUES (?, ?, ?, ?, ?, ?)",
        ("m1", "Network Review", "Reviewed network", "Network switches and routers", "Acme", "2026-01-15"),
    )
    db.execute(
        "INSERT INTO salesos_meetings (id, title, summary, transcript, account, date) VALUES (?, ?, ?, ?, ?, ?)",
        ("m2", "Budget Meeting", "Discussed budget", "Annual budget allocation for Q3", "Beta", "2026-02-01"),
    )
    search.index_row("meetings", "m1", "Network Review Reviewed network Network switches and routers")
    search.index_row("meetings", "m2", "Budget Meeting Discussed budget Annual budget allocation for Q3")
    results = search.search("network switches", tables=["meetings"], limit=5)
    assert len(results) == 2
    assert results[0]["id"] == "m1"


def test_index_table_batch(db, mock_embedder):
    """index_table should embed all unindexed rows."""
    search = SemanticSearch(db, mock_embedder)
    for i in range(3):
        db.execute(
            "INSERT INTO salesos_meetings (id, title, summary, transcript, account, date) VALUES (?, ?, ?, ?, ?, ?)",
            (f"m{i}", f"Meeting {i}", f"Summary {i}", f"Transcript {i}", "Acme", "2026-01-15"),
        )
    count = search.index_table("meetings")
    assert count == 3
    results = search.search("meeting", tables=["meetings"], limit=10)
    assert len(results) == 3


def test_status_shows_counts(db, mock_embedder):
    """status() should report document-layer and chunk-layer counts."""
    search = SemanticSearch(db, mock_embedder)
    db.execute(
        "INSERT INTO salesos_meetings (id, title, summary, transcript, account, date) VALUES (?, ?, ?, ?, ?, ?)",
        ("m1", "Test", "Summary", "Transcript", "Acme", "2026-01-15"),
    )
    search.index_row("meetings", "m1", "Test Summary Transcript")
    status = search.status()
    assert status["document_layer"]["meetings"] == 1
    assert status["document_layer"]["documents"] == 0
    assert status["document_layer"]["knowledge"] == 0
    assert status["document_layer"]["accounts"] == 0
    assert "chunk_layer" in status
    assert "total_chunks" in status["chunk_layer"]
    assert "meetings" in status["chunk_layer"]
    assert "documents" in status["chunk_layer"]


def test_search_across_all_tables(db, mock_embedder):
    """search() with no table filter should search all tables."""
    search = SemanticSearch(db, mock_embedder)
    db.execute(
        "INSERT INTO salesos_meetings (id, title, summary, transcript, account, date) VALUES (?, ?, ?, ?, ?, ?)",
        ("m1", "Network Review", "Reviewed network", "Network discussion", "Acme", "2026-01-15"),
    )
    db.execute(
        "INSERT INTO documents (id, doc_type, title, content) VALUES (?, ?, ?, ?)",
        ("d1", "research", "Network Architecture", "Network design for campus"),
    )
    search.index_row("meetings", "m1", "Network Review Reviewed network Network discussion")
    search.index_row("documents", "d1", "Network Architecture Network design for campus")
    results = search.search("network", limit=10)
    assert len(results) == 2
    source_types = {r["source_type"] for r in results}
    assert "meetings" in source_types
    assert "documents" in source_types


def test_hybrid_search_combines_fts_and_vector(db, mock_embedder):
    """hybrid_search should merge FTS5 keyword + vector results."""
    search = SemanticSearch(db, mock_embedder)

    # Insert meetings — one matches keyword, one matches semantically
    db.execute(
        "INSERT INTO salesos_meetings (id, title, summary, transcript, account, date) VALUES (?, ?, ?, ?, ?, ?)",
        ("m1", "Network Budget", "Network budget review", "Discussed network budget allocation", "Acme", "2026-01-15"),
    )
    db.execute(
        "INSERT INTO salesos_meetings (id, title, summary, transcript, account, date) VALUES (?, ?, ?, ?, ?, ?)",
        ("m2", "Cisco Infrastructure", "Cisco discussion", "Reviewed Cisco infrastructure plans", "Beta", "2026-02-01"),
    )

    search.index_row("meetings", "m1", "Network Budget Network budget review Discussed network budget allocation")
    search.index_row("meetings", "m2", "Cisco Infrastructure Cisco discussion Reviewed Cisco infrastructure plans")

    results = search.hybrid_search("network budget", tables=["meetings"], limit=5)
    assert len(results) >= 1
    # m1 should appear (matches both keyword and semantic)
    ids = [r["id"] for r in results]
    assert "m1" in ids
    # Results should have a combined score
    assert "score" in results[0]


def test_embedding_chunks_table_exists(db):
    """Migration should create embedding_chunks and vec_chunks tables."""
    row = db.execute_one(
        "SELECT name FROM sqlite_master WHERE type='table' AND name='embedding_chunks'"
    )
    assert row is not None
    row = db.execute_one(
        "SELECT name FROM sqlite_master WHERE type='table' AND name='vec_chunks'"
    )
    assert row is not None


def test_index_table_creates_chunks_for_long_meetings(db, mock_embedder):
    """index_table should create chunks for meetings with long transcripts."""
    search = SemanticSearch(db, mock_embedder)
    long_transcript = "Network infrastructure discussion. " * 100  # ~3500 chars
    db.execute(
        "INSERT INTO salesos_meetings (id, title, summary, transcript, account, date) VALUES (?, ?, ?, ?, ?, ?)",
        ("m1", "Long Meeting", "Summary of long meeting", long_transcript, "Acme", "2026-01-15"),
    )
    count = search.index_table("meetings")
    assert count >= 1  # at least document-level embedding

    # Check chunks were created in embedding_chunks
    chunks = db.execute(
        "SELECT * FROM embedding_chunks WHERE source_table='salesos_meetings' AND source_id='m1'"
    )
    assert len(chunks) >= 2  # long text should produce multiple chunks

    # Check chunk vectors exist in vec_chunks
    vec_count = db.execute_one("SELECT COUNT(*) as c FROM vec_chunks")
    assert vec_count["c"] == len(chunks)


def test_index_table_skips_chunks_for_short_text(db, mock_embedder):
    """Short meetings should get document-level embedding but no chunks."""
    search = SemanticSearch(db, mock_embedder)
    db.execute(
        "INSERT INTO salesos_meetings (id, title, summary, transcript, account, date) VALUES (?, ?, ?, ?, ?, ?)",
        ("m1", "Short Meeting", "Brief summary", "Short transcript", "Acme", "2026-01-15"),
    )
    count = search.index_table("meetings")
    assert count >= 1

    chunks = db.execute(
        "SELECT * FROM embedding_chunks WHERE source_table='salesos_meetings' AND source_id='m1'"
    )
    assert len(chunks) == 0  # short text = no chunks


def test_index_table_does_not_chunk_knowledge(db, mock_embedder):
    """Knowledge entries should not be chunked (chunked=False)."""
    search = SemanticSearch(db, mock_embedder)
    db.execute(
        "INSERT INTO knowledge_entries (id, entry_type, title, content) VALUES (?, ?, ?, ?)",
        ("k1", "expertise", "SQL Tips", "Use parameterized queries always"),
    )
    count = search.index_table("knowledge")
    assert count == 1

    chunks = db.execute(
        "SELECT * FROM embedding_chunks WHERE source_table='knowledge_entries'"
    )
    assert len(chunks) == 0


def test_search_chunks_returns_chunk_text(db, mock_embedder):
    """search(detail='chunks') should return matching chunk text with parent metadata."""
    search = SemanticSearch(db, mock_embedder)
    long_transcript = "Network infrastructure discussion. " * 100
    db.execute(
        "INSERT INTO salesos_meetings (id, title, summary, transcript, account, date) VALUES (?, ?, ?, ?, ?, ?)",
        ("m1", "Network Meeting", "Network summary", long_transcript, "Acme", "2026-01-15"),
    )
    search.index_table("meetings")

    results = search.search("network infrastructure", tables=["meetings"], limit=5, detail="chunks")
    assert len(results) >= 1
    # Should include chunk results with chunk_text
    chunk_results = [r for r in results if "chunk_text" in r]
    assert len(chunk_results) >= 1
    assert chunk_results[0]["source_type"] == "meetings"
    assert "chunk_text" in chunk_results[0]


def test_search_documents_groups_by_parent(db, mock_embedder):
    """search(detail='documents') should group by parent document."""
    search = SemanticSearch(db, mock_embedder)
    long_transcript = "Network infrastructure discussion. " * 100
    db.execute(
        "INSERT INTO salesos_meetings (id, title, summary, transcript, account, date) VALUES (?, ?, ?, ?, ?, ?)",
        ("m1", "Network Meeting", "Network summary", long_transcript, "Acme", "2026-01-15"),
    )
    search.index_table("meetings")

    results = search.search("network infrastructure", tables=["meetings"], limit=5, detail="documents")
    assert len(results) >= 1
    # All chunks from m1 should be grouped into one result
    m1_results = [r for r in results if r.get("id") == "m1"]
    assert len(m1_results) == 1
    # Should have best_chunk but NOT chunk_text
    assert "best_chunk" in m1_results[0]
    assert "chunk_text" not in m1_results[0]
    assert "chunk_id" not in m1_results[0]


def test_hybrid_search_with_detail_documents(db, mock_embedder):
    """hybrid_search should support detail='documents' mode."""
    search = SemanticSearch(db, mock_embedder)
    long_transcript = "Network budget allocation for the entire campus. " * 80
    db.execute(
        "INSERT INTO salesos_meetings (id, title, summary, transcript, account, date) VALUES (?, ?, ?, ?, ?, ?)",
        ("m1", "Network Budget", "Network budget review", long_transcript, "Acme", "2026-01-15"),
    )
    search.index_table("meetings")

    results = search.hybrid_search(
        "network budget", tables=["meetings"], limit=5, detail="documents"
    )
    assert len(results) >= 1
    ids = [r.get("id") for r in results]
    assert "m1" in ids


def test_findings_in_salesos_table_configs():
    """findings config should be defined in the SalesOS extension."""
    from greymatter_salesos.semantic import SALESOS_TABLE_CONFIGS
    assert "findings" in SALESOS_TABLE_CONFIGS
    cfg = SALESOS_TABLE_CONFIGS["findings"]
    assert "vec_table" in cfg
    assert "text_fn" in cfg
    assert cfg["source_table"] == "salesos_findings"


def test_snapshots_in_salesos_table_configs():
    """snapshots config should be defined in the SalesOS extension."""
    from greymatter_salesos.semantic import SALESOS_TABLE_CONFIGS
    assert "snapshots" in SALESOS_TABLE_CONFIGS
    cfg = SALESOS_TABLE_CONFIGS["snapshots"]
    assert "vec_table" in cfg
    assert "text_fn" in cfg
    assert cfg["source_table"] == "salesos_snapshots"


def test_search_default_is_chunks(db, mock_embedder):
    """Default detail mode should be 'chunks'."""
    search = SemanticSearch(db, mock_embedder)
    long_transcript = "Network infrastructure discussion. " * 100
    db.execute(
        "INSERT INTO salesos_meetings (id, title, summary, transcript, account, date) VALUES (?, ?, ?, ?, ?, ?)",
        ("m1", "Network Meeting", "Network summary", long_transcript, "Acme", "2026-01-15"),
    )
    search.index_table("meetings")
    # Default call (no detail param) should return chunk-style results
    results = search.search("network", tables=["meetings"], limit=5)
    assert len(results) >= 1
    # Should have chunk results (chunk_text present), not document mode (best_chunk absent)
    chunk_results = [r for r in results if "chunk_text" in r]
    assert len(chunk_results) >= 1
    assert "best_chunk" not in chunk_results[0]


def test_observations_in_table_config():
    """TABLE_CONFIG should include observations with correct settings."""
    from greymatter_core.semantic import TABLE_CONFIG
    assert "observations" in TABLE_CONFIG
    cfg = TABLE_CONFIG["observations"]
    assert cfg["vec_table"] == "vec_observations"
    assert cfg["pk_col"] == "observation_id"
    assert cfg["source_table"] == "observations"
    assert cfg["source_pk"] == "id"
    assert cfg["chunked"] is False
    assert "text_fn" in cfg


def test_vec_observations_table_created(db):
    """Migration should create the vec_observations virtual table."""
    row = db.execute_one(
        "SELECT name FROM sqlite_master WHERE type='table' AND name='vec_observations'"
    )
    assert row is not None
    assert row["name"] == "vec_observations"


def test_observation_index_and_search(db, mock_embedder):
    """Observations should be indexable and searchable via semantic search."""
    search = SemanticSearch(db, mock_embedder)
    db.execute(
        "INSERT INTO observations (id, session_id, trigger, category, title, content) VALUES (?, ?, ?, ?, ?, ?)",
        ("obs1", "sess1", "compaction", "architecture", "Network Design Pattern", "Network topology uses hub-and-spoke architecture"),
    )
    count = search.index_table("observations")
    assert count == 1

    results = search.search("network architecture", tables=["observations"], limit=5)
    assert len(results) >= 1
    assert results[0]["id"] == "obs1"
    assert results[0]["source_type"] == "observations"


def test_observation_not_chunked(db, mock_embedder):
    """Observations should not produce chunks (chunked=False)."""
    search = SemanticSearch(db, mock_embedder)
    db.execute(
        "INSERT INTO observations (id, session_id, trigger, category, title, content) VALUES (?, ?, ?, ?, ?, ?)",
        ("obs1", "sess1", "compaction", "expertise", "SQL Tips", "Always use parameterized queries"),
    )
    search.index_table("observations")

    chunks = db.execute(
        "SELECT * FROM embedding_chunks WHERE source_table='observations'"
    )
    assert len(chunks) == 0


def test_observation_text_fn():
    """text_fn should combine title and content."""
    from greymatter_core.semantic import TABLE_CONFIG
    cfg = TABLE_CONFIG["observations"]
    row = {"title": "Network Design", "content": "Hub and spoke topology"}
    text = cfg["text_fn"](row)
    assert "Network Design" in text
    assert "Hub and spoke topology" in text


def test_status_includes_observations(db, mock_embedder):
    """status() should include observations in the document layer."""
    search = SemanticSearch(db, mock_embedder)
    status = search.status()
    assert "observations" in status["document_layer"]
    assert status["document_layer"]["observations"] == 0


def test_register_table_makes_table_searchable(db, mock_embedder):
    """register_table() should make a custom table available for search."""
    from greymatter_core.semantic import register_table, _TABLE_CONFIG

    # Create a custom table in the DB
    db.execute("CREATE TABLE IF NOT EXISTS custom_items (id TEXT PRIMARY KEY, name TEXT, description TEXT)")
    db.execute("CREATE VIRTUAL TABLE IF NOT EXISTS vec_custom USING vec0(item_id TEXT PRIMARY KEY, embedding float[768])")
    db.execute("INSERT INTO custom_items (id, name, description) VALUES (?, ?, ?)",
               ("c1", "Network Switch", "Cisco Catalyst 9300 layer 3 switch"))

    register_table("custom", {
        "vec_table": "vec_custom",
        "pk_col": "item_id",
        "source_table": "custom_items",
        "source_pk": "id",
        "text_fn": lambda r: f"{r.get('name', '')} {r.get('description', '')}",
        "chunked": False,
    })

    try:
        search = SemanticSearch(db, mock_embedder)
        count = search.index_table("custom")
        assert count == 1

        results = search.search("network switch", tables=["custom"], limit=5)
        assert len(results) >= 1
        assert results[0]["source_type"] == "custom"
    finally:
        del _TABLE_CONFIG["custom"]


def test_register_fts_backend_used_in_hybrid_search(db, mock_embedder):
    """register_fts_backend() should make FTS available in hybrid_search."""
    from greymatter_core.semantic import register_fts_backend, _FTS_BACKENDS

    # meetings FTS already exists from db.py migrate()
    register_fts_backend(
        "meetings", "meetings_fts", "salesos_meetings",
        ["title", "summary", "transcript"],
    )

    try:
        search = SemanticSearch(db, mock_embedder)
        db.execute(
            "INSERT INTO salesos_meetings (id, title, summary, transcript, account, date) VALUES (?, ?, ?, ?, ?, ?)",
            ("m1", "Network Budget", "Network budget review", "Discussed budget", "Acme", "2026-01-15"),
        )
        search.index_row("meetings", "m1", "Network Budget review")

        results = search.hybrid_search("network budget", tables=["meetings"], limit=5)
        assert len(results) >= 1
        assert "score" in results[0]
    finally:
        del _FTS_BACKENDS["meetings"]
