"""
BIBIVIDI — Matcher tests (TODO-12)
Tests for FAISS-backed and numpy-fallback index paths.
"""
from __future__ import annotations

import numpy as np
import pytest

from bvd.engine.library import Script
from bvd.engine.matcher import (
    _FAISS_IVF_THRESHOLD,
    Matcher,
    _build_faiss_index,
    _cosine,
    _l2_normalise,
)

_DIM = 384


def _make_script(sid: str, vec: np.ndarray) -> tuple[str, np.ndarray, Script]:
    s = Script(
        id=sid,
        name=sid,
        description=f"Test script {sid}",
        code="",
        embedding=vec.astype(np.float32).tobytes(),
    )
    return sid, vec.astype(np.float32), s


def _rand_vec() -> np.ndarray:
    v = np.random.randn(_DIM).astype(np.float32)
    return v / np.linalg.norm(v)


# ── unit helpers ──────────────────────────────────────────────────────────────

def test_l2_normalise_unit():
    v = np.array([3.0, 4.0], dtype=np.float32)
    n = _l2_normalise(v)
    assert abs(np.linalg.norm(n) - 1.0) < 1e-6


def test_l2_normalise_zero():
    v = np.zeros(_DIM, dtype=np.float32)
    n = _l2_normalise(v)
    assert np.all(n == 0)


def test_cosine_identical():
    v = _rand_vec()
    assert abs(_cosine(v, v) - 1.0) < 1e-5


def test_cosine_orthogonal():
    a = np.zeros(_DIM, dtype=np.float32)
    b = np.zeros(_DIM, dtype=np.float32)
    a[0] = 1.0
    b[1] = 1.0
    assert abs(_cosine(a, b)) < 1e-6


# ── FAISS flat index (< threshold) ───────────────────────────────────────────

def test_faiss_flat_finds_nearest():
    """FAISS FlatIP returns the most similar vector."""
    pytest.importorskip("faiss")
    target = _rand_vec()
    entries = [_make_script(f"s{i}", _rand_vec()) for i in range(10)]
    entries[5] = _make_script("target", target)

    idx = _build_faiss_index(entries)
    query = _l2_normalise(target).reshape(1, -1).astype(np.float32)
    scores, ids = idx.search(query, 1)
    assert ids[0][0] == 5
    assert abs(float(scores[0][0]) - 1.0) < 1e-4


def test_faiss_flat_identical_score_is_one():
    """Searching with itself as query gives score ≈ 1.0."""
    pytest.importorskip("faiss")
    v = _rand_vec()
    entries = [_make_script("only", v)]
    idx = _build_faiss_index(entries)
    query = _l2_normalise(v).reshape(1, -1).astype(np.float32)
    scores, _ = idx.search(query, 1)
    assert abs(float(scores[0][0]) - 1.0) < 1e-4


# ── FAISS IVF index (≥ threshold) ────────────────────────────────────────────

def test_faiss_ivf_built_for_large_index():
    """IVF index is used when len(index) >= _FAISS_IVF_THRESHOLD."""
    faiss = pytest.importorskip("faiss")
    entries = [_make_script(f"s{i}", _rand_vec()) for i in range(_FAISS_IVF_THRESHOLD)]
    idx = _build_faiss_index(entries)
    # IVFFlat is a subclass of IndexIVF
    assert isinstance(idx, faiss.IndexIVFFlat)


def test_faiss_ivf_finds_nearest():
    """IVF index returns the correct nearest neighbour."""
    pytest.importorskip("faiss")
    target = _rand_vec()
    entries = [_make_script(f"s{i}", _rand_vec()) for i in range(_FAISS_IVF_THRESHOLD)]
    entries[0] = _make_script("target", target)

    idx = _build_faiss_index(entries)
    query = _l2_normalise(target).reshape(1, -1).astype(np.float32)
    scores, ids = idx.search(query, 1)
    assert ids[0][0] == 0
    assert float(scores[0][0]) > 0.95


# ── Matcher._match_numpy ──────────────────────────────────────────────────────

@pytest.mark.asyncio
async def test_matcher_numpy_returns_best_match(monkeypatch):
    """_match_numpy finds the closest script above threshold."""
    import bvd.engine.matcher as _mod

    target = _rand_vec()
    entries = [_make_script(f"s{i}", _rand_vec()) for i in range(5)]
    entries[2] = _make_script("winner", target)

    m = Matcher()
    m._index = entries
    m._index_dirty = False

    monkeypatch.setattr(_mod, "_FAISS_AVAILABLE", False)
    m._faiss_index = None

    result = m._match_numpy(target)
    assert result is not None
    assert result.script_id == "winner"
    assert abs(result.score - 1.0) < 1e-4


@pytest.mark.asyncio
async def test_matcher_numpy_returns_none_below_threshold(monkeypatch):
    """_match_numpy returns None when best score is below threshold."""
    import bvd.engine.matcher as _mod
    from bvd.config import settings

    m = Matcher()
    # Put a vector that is nearly orthogonal to the query
    far = _rand_vec()
    query = _rand_vec()
    # Force orthogonality
    far -= np.dot(far, query) * query
    far /= np.linalg.norm(far)

    m._index = [_make_script("far", far)]
    m._index_dirty = False
    monkeypatch.setattr(_mod, "_FAISS_AVAILABLE", False)
    m._faiss_index = None

    # Raise threshold so the low score doesn't qualify
    old = settings.match_threshold
    settings.match_threshold = 0.99
    try:
        result = m._match_numpy(query)
        assert result is None
    finally:
        settings.match_threshold = old


# ── Matcher._match_faiss ──────────────────────────────────────────────────────

@pytest.mark.asyncio
async def test_matcher_faiss_returns_best_match():
    """_match_faiss finds the closest script above threshold."""
    pytest.importorskip("faiss")
    from bvd.config import settings

    target = _rand_vec()
    entries = [_make_script(f"s{i}", _rand_vec()) for i in range(5)]
    entries[3] = _make_script("winner", target)

    m = Matcher()
    m._index = entries
    m._faiss_index = _build_faiss_index(entries)
    m._index_dirty = False

    old = settings.match_threshold
    settings.match_threshold = 0.5
    try:
        result = m._match_faiss(target)
        assert result is not None
        assert result.script_id == "winner"
        assert abs(result.score - 1.0) < 1e-4
    finally:
        settings.match_threshold = old
