"""
Integration tests for SearchEngine.
Requires a running PostgreSQL instance with pgvector and ParadeDB extensions.
Connection: DB_URL from conftest.py
"""
from unittest.mock import patch

import pytest
import sqlalchemy as sa
from conftest import DB_URL, fake_embed_fn
from dot_search import BM25Config, EmbeddingConfig, SearchConfig, TableConfig, SearchEngine
from dot_search.types import ExactConfig
from dot_search.embed import Embedder


FAKE_EMBEDDER = Embedder(fn=fake_embed_fn, dimension=4)


@pytest.fixture
async def engine():
    e = SearchEngine(db_url=DB_URL)
    yield e
    await e.close()


@pytest.fixture
async def patient_table(engine):
    """Creates a fake 'patient' table mimicking a Django-managed table."""
    async with engine._store._engine.begin() as conn:
        await conn.execute(sa.text(
            """
            CREATE TABLE IF NOT EXISTS patient (
                id SERIAL PRIMARY KEY,
                name TEXT NOT NULL,
                age INTEGER,
                status TEXT,
                comment TEXT
            )
            """
        ))
        await conn.execute(sa.text(
            """
            INSERT INTO patient (name, age, status, comment) VALUES
            ('Dupont', 34, 'active', 'Patient calme, coopératif, suit bien les consignes.'),
            ('Martin', 52, 'active', 'Difficile à gérer, refuse souvent les soins.'),
            ('Bernard', 28, 'inactive', 'Très motivé, bonne progression thérapeutique.'),
            ('Leroy', 61, 'active', 'Patient anxieux mais de bonne volonté.'),
            ('Moreau', 45, 'active', 'Comportement imprévisible, nécessite un suivi renforcé.')
            ON CONFLICT DO NOTHING
            """
        ))
    yield
    async with engine._store._engine.begin() as conn:
        await conn.execute(sa.text("DROP TABLE IF EXISTS patient"))
        await conn.execute(sa.text("DROP TABLE IF EXISTS ds_configs"))


async def test_augment_adds_ds_column(engine, patient_table):
    config = TableConfig(
        table="patient",
        embeddings=[
            EmbeddingConfig(
                key="comment_default",
                source_column="comment",
            )
        ],
    )
    with patch("dot_search.engine.get_embedder", return_value=FAKE_EMBEDDER):
        await engine.index(config)
    async with engine._store._engine.connect() as conn:
        result = await conn.execute(sa.text(
            "SELECT ds_comment_default FROM patient WHERE ds_comment_default IS NOT NULL"
        ))
        rows = result.fetchall()
    assert len(rows) == 5


async def test_augment_does_not_alter_existing_columns(engine, patient_table):
    config = TableConfig(
        table="patient",
        embeddings=[
            EmbeddingConfig(
                key="comment_default",
                source_column="comment",
            )
        ],
    )
    with patch("dot_search.engine.get_embedder", return_value=FAKE_EMBEDDER):
        await engine.index(config)
    async with engine._store._engine.connect() as conn:
        result = await conn.execute(sa.text(
            "SELECT id, name, age, status, comment FROM patient ORDER BY id"
        ))
        rows = result.fetchall()
    assert len(rows) == 5
    assert rows[0].name == "Dupont"


async def test_search_with_where_filter(engine, patient_table):
    config = TableConfig(
        table="patient",
        embeddings=[
            EmbeddingConfig(
                key="comment_default",
                source_column="comment",
            )
        ],
    )
    with patch("dot_search.engine.get_embedder", return_value=FAKE_EMBEDDER):
        await engine.index(config)
        results = await engine.search(
            "patient facile à gérer",
            "patient",
            SearchConfig(where="status = 'active' AND age < 50"),
        )
    returned_names = {r.row["name"] for r in results}
    assert "Bernard" not in returned_names   # inactive
    assert "Martin" not in returned_names    # age 52


async def test_search_results_contain_row_data(engine, patient_table):
    config = TableConfig(
        table="patient",
        embeddings=[
            EmbeddingConfig(
                key="comment_default",
                source_column="comment",
            )
        ],
    )
    with patch("dot_search.engine.get_embedder", return_value=FAKE_EMBEDDER):
        await engine.index(config)
        results = await engine.search(
            "patient",
            "patient",
            SearchConfig(limit=3),
        )
    for r in results:
        assert "name" in r.row
        assert "comment" in r.row
        assert "ds_comment_default" not in r.row


async def test_multi_embedding_columns(engine, patient_table):
    """Two embedding targets → two ds_ columns created independently."""
    config = TableConfig(
        table="patient",
        embeddings=[
            EmbeddingConfig(
                key="comment_multilingual",
                source_column="comment",
            ),
            EmbeddingConfig(
                key="comment_longctx",
                source_column="comment",
            ),
        ],
    )
    with patch("dot_search.engine.get_embedder", return_value=FAKE_EMBEDDER):
        await engine.index(config)
    async with engine._store._engine.connect() as conn:
        result = await conn.execute(sa.text(
            "SELECT ds_comment_multilingual, ds_comment_longctx FROM patient"
        ))
        rows = result.fetchall()
    assert all(r.ds_comment_multilingual is not None for r in rows)
    assert all(r.ds_comment_longctx is not None for r in rows)


async def test_multiple_bm25_config_round_trip(engine, patient_table):
    """Config with two BM25Configs serializes and deserializes correctly."""
    config = TableConfig(
        table="patient",
        embeddings=[
            EmbeddingConfig(
                key="comment_default",
                source_column="comment",
            )
        ],
        bm25=[
            BM25Config(key="name", source_column="name"),
            BM25Config(key="comment", source_column="comment"),
        ],
    )
    await engine._ensure_config_table()
    await engine._save_config(config)

    loaded = await engine._load_config("patient")
    assert len(loaded.bm25) == 2
    assert loaded.bm25[0].source_column == "name"
    assert loaded.bm25[1].source_column == "comment"


async def test_exact_search_returns_matching_rows(engine, patient_table):
    config = TableConfig(
        table="patient",
        embeddings=[],
        exact=[ExactConfig(key="name", source_column="name")],
    )
    with patch("dot_search.engine.get_embedder", return_value=FAKE_EMBEDDER):
        await engine.index(config)
        results = await engine.search("Dupont", "patient", SearchConfig(strategy="exact"))
    assert len(results) == 1
    assert results[0].row["name"] == "Dupont"


async def test_exact_search_infix_match(engine, patient_table):
    config = TableConfig(
        table="patient",
        embeddings=[],
        exact=[ExactConfig(key="comment", source_column="comment")],
    )
    with patch("dot_search.engine.get_embedder", return_value=FAKE_EMBEDDER):
        await engine.index(config)
        results = await engine.search("suivi", "patient", SearchConfig(strategy="exact"))
    assert len(results) == 1
    assert results[0].row["name"] == "Moreau"


async def test_exact_search_no_match_returns_empty(engine, patient_table):
    config = TableConfig(
        table="patient",
        embeddings=[],
        exact=[ExactConfig(key="name", source_column="name")],
    )
    with patch("dot_search.engine.get_embedder", return_value=FAKE_EMBEDDER):
        await engine.index(config)
        results = await engine.search("zzznomatch", "patient", SearchConfig(strategy="exact"))
    assert results == []


async def test_exact_search_raises_without_exact_config(engine, patient_table):
    config = TableConfig(
        table="patient",
        embeddings=[EmbeddingConfig(key="comment_default", source_column="comment")],
    )
    with patch("dot_search.engine.get_embedder", return_value=FAKE_EMBEDDER):
        await engine.index(config)
        with pytest.raises(ValueError, match="no exact config"):
            await engine.search("Dupont", "patient", SearchConfig(strategy="exact"))


async def test_exact_config_round_trip(engine, patient_table):
    config = TableConfig(
        table="patient",
        embeddings=[],
        exact=[ExactConfig(key="name", source_column="name")],
    )
    await engine._ensure_config_table()
    await engine._save_config(config)
    loaded = await engine._load_config("patient")
    assert len(loaded.exact) == 1
    assert loaded.exact[0].source_column == "name"
