"""
Validation tests:
1. EmbeddingConfig.target_column — ds_ prefix derived from key
2. validate_embedding_column — ds_ prefix in Store
3. Embedder dimension mismatch in engine._run_indexing
4. Column existence checks for EmbeddingConfig.source_column and BM25Config.source_column
5. TableConfig duplicate key validation
"""
from unittest.mock import patch

import pytest
from dot_search.types import BM25Config, EmbeddingConfig, ExactConfig, TableConfig
from dot_search.store.sqlite import SQLiteStore
from dot_search.embed import Embedder
from dot_search import SearchEngine
from conftest import fake_embed_fn


SQLITE_URL = "sqlite+aiosqlite://"
FAKE_EMBEDDER = Embedder(fn=fake_embed_fn, dimension=4)


# ---------------------------------------------------------------------------
# EmbeddingConfig target_column derivation (sync, no fixtures needed)
# ---------------------------------------------------------------------------

def test_embedding_config_target_column_from_key():
    cfg = EmbeddingConfig(key="body_emb", source_column="body")
    assert cfg.target_column == "ds_body_emb"


# ---------------------------------------------------------------------------
# TableConfig duplicate key validation
# ---------------------------------------------------------------------------

def test_table_config_rejects_duplicate_keys():
    with pytest.raises(ValueError, match="Duplicate keys"):
        TableConfig(
            table="docs",
            embeddings=[EmbeddingConfig(key="body", source_column="body")],
            exact=[ExactConfig(key="body", source_column="body")],
        )


def test_table_config_accepts_unique_keys():
    config = TableConfig(
        table="docs",
        embeddings=[EmbeddingConfig(key="body_emb", source_column="body")],
        bm25=[BM25Config(key="body_bm25", source_column="body")],
        exact=[ExactConfig(key="body_exact", source_column="body")],
    )
    assert config.table == "docs"


# ---------------------------------------------------------------------------
# SQLiteStore.add_embedding_column validation (async, needs a table)
# ---------------------------------------------------------------------------

@pytest.fixture
async def store():
    s = SQLiteStore(db_url=SQLITE_URL)
    yield s
    await s.close()


@pytest.fixture
async def table_with_data(store):
    conn = await store._get_conn()
    await conn.execute("CREATE TABLE IF NOT EXISTS docs (id TEXT PRIMARY KEY, body TEXT)")
    await conn.execute("INSERT OR IGNORE INTO docs (id, body) VALUES ('d1', 'hello world')")
    await conn.commit()
    yield
    await conn.execute("DROP TABLE IF EXISTS docs")
    await conn.commit()


async def test_add_embedding_column_rejects_column_without_ds_prefix(store, table_with_data):
    with pytest.raises(ValueError):
        await store.add_embedding_column("docs", "body_emb", dimension=4)


async def test_add_embedding_column_rejects_arbitrary_name(store, table_with_data):
    with pytest.raises(ValueError):
        await store.add_embedding_column("docs", "embedding", dimension=4)


async def test_add_embedding_column_accepts_valid_ds_prefix(store, table_with_data):
    await store.add_embedding_column("docs", "ds_body_emb", dimension=4)
    conn = await store._get_conn()
    cursor = await conn.execute("PRAGMA table_info(docs)")
    rows = await cursor.fetchall()
    columns = {row[1] for row in rows}
    assert "ds_body_emb" in columns


# ---------------------------------------------------------------------------
# Embedder dimension mismatch
# ---------------------------------------------------------------------------

async def test_index_raises_on_dimension_mismatch(store, table_with_data):
    """Embedder.dimension=4 but fn returns 2-dim vectors → ValueError."""
    async def wrong_dim_fn(texts: list[str]) -> list[list[float]]:
        return [[0.0, 1.0] for _ in texts]

    wrong_embedder = Embedder(fn=wrong_dim_fn, dimension=4)

    engine = SearchEngine(db_url="sqlite+aiosqlite://")
    engine._store = store

    config = TableConfig(
        table="docs",
        embeddings=[EmbeddingConfig(key="body_emb", source_column="body")],
    )
    await engine._ensure_config_table()

    with patch("dot_search.engine.get_embedder", return_value=wrong_embedder):
        with pytest.raises(ValueError, match="dimension mismatch"):
            await engine._run_indexing(config)


# ---------------------------------------------------------------------------
# Column existence checks
# ---------------------------------------------------------------------------

async def _make_engine(store):
    engine = SearchEngine(db_url="sqlite+aiosqlite://")
    engine._store = store
    await engine._ensure_config_table()
    return engine


async def test_index_raises_on_nonexistent_source_column(store, table_with_data):
    engine = await _make_engine(store)
    config = TableConfig(
        table="docs",
        embeddings=[EmbeddingConfig(key="body", source_column="nonexistent")],
    )
    with pytest.raises(ValueError, match="source_column 'nonexistent'.*does not exist"):
        await engine._run_indexing(config)


async def test_index_raises_on_nonexistent_bm25_column(store, table_with_data):
    engine = await _make_engine(store)
    config = TableConfig(
        table="docs",
        embeddings=[EmbeddingConfig(key="body", source_column="body")],
        bm25=[BM25Config(key="missing", source_column="nonexistent")],
    )
    with pytest.raises(ValueError, match="BM25Config source_column 'nonexistent'.*does not exist"):
        await engine._run_indexing(config)


async def test_index_accepts_valid_columns(store, table_with_data):
    engine = await _make_engine(store)
    config = TableConfig(
        table="docs",
        embeddings=[EmbeddingConfig(key="body", source_column="body")],
    )
    with patch("dot_search.engine.get_embedder", return_value=FAKE_EMBEDDER):
        await engine._run_indexing(config)
