"""Tests for RBSP filtered vector search.

Covers:
    - FilterExpression DSL: FieldCondition operators, And, Or, Not, composition
    - Metadata storage: insert, update, set, get, remove
    - Filtered search across all modes: exact, hnsw, binary, hybrid
    - Edge cases: empty filters, no matches, all filtered out, missing metadata
    - HNSW filter_fn integration
    - Thread safety of filtered operations
"""

from __future__ import annotations

import math
import os
import random
import sys
import threading
import time
import unittest

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))

from rbsp.core.hnsw import HNSWConfig, HNSWIndex, cosine_distance, l2_distance
from rbsp.indexer.vector_store import (
    And,
    FieldCondition,
    FilterExpression,
    Not,
    Or,
    VectorStore,
)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _random_vector(dim, rng):
    """Generate a random unit-ish vector."""
    v = [rng.gauss(0, 1) for _ in range(dim)]
    norm = math.sqrt(sum(x * x for x in v))
    if norm > 0:
        v = [x / norm for x in v]
    return v


# ---------------------------------------------------------------------------
# FilterExpression DSL tests
# ---------------------------------------------------------------------------


class TestFieldConditionEq(unittest.TestCase):
    """FieldCondition with 'eq' operator."""

    def test_match(self):
        fc = FieldCondition("color", "eq", "red")
        self.assertTrue(fc.match({"color": "red"}))

    def test_no_match(self):
        fc = FieldCondition("color", "eq", "red")
        self.assertFalse(fc.match({"color": "blue"}))

    def test_missing_field(self):
        fc = FieldCondition("color", "eq", "red")
        self.assertFalse(fc.match({"size": 10}))

    def test_numeric_eq(self):
        fc = FieldCondition("count", "eq", 5)
        self.assertTrue(fc.match({"count": 5}))
        self.assertFalse(fc.match({"count": 6}))


class TestFieldConditionNe(unittest.TestCase):
    """FieldCondition with 'ne' operator."""

    def test_match(self):
        fc = FieldCondition("color", "ne", "red")
        self.assertTrue(fc.match({"color": "blue"}))

    def test_no_match(self):
        fc = FieldCondition("color", "ne", "red")
        self.assertFalse(fc.match({"color": "red"}))

    def test_missing_field(self):
        fc = FieldCondition("color", "ne", "red")
        self.assertFalse(fc.match({}))


class TestFieldConditionGt(unittest.TestCase):
    """FieldCondition with 'gt' operator."""

    def test_match(self):
        fc = FieldCondition("price", "gt", 10.0)
        self.assertTrue(fc.match({"price": 15.0}))

    def test_no_match_equal(self):
        fc = FieldCondition("price", "gt", 10.0)
        self.assertFalse(fc.match({"price": 10.0}))

    def test_no_match_less(self):
        fc = FieldCondition("price", "gt", 10.0)
        self.assertFalse(fc.match({"price": 5.0}))


class TestFieldConditionGte(unittest.TestCase):
    """FieldCondition with 'gte' operator."""

    def test_match_greater(self):
        fc = FieldCondition("price", "gte", 10.0)
        self.assertTrue(fc.match({"price": 15.0}))

    def test_match_equal(self):
        fc = FieldCondition("price", "gte", 10.0)
        self.assertTrue(fc.match({"price": 10.0}))

    def test_no_match(self):
        fc = FieldCondition("price", "gte", 10.0)
        self.assertFalse(fc.match({"price": 5.0}))


class TestFieldConditionLt(unittest.TestCase):
    """FieldCondition with 'lt' operator."""

    def test_match(self):
        fc = FieldCondition("price", "lt", 10.0)
        self.assertTrue(fc.match({"price": 5.0}))

    def test_no_match_equal(self):
        fc = FieldCondition("price", "lt", 10.0)
        self.assertFalse(fc.match({"price": 10.0}))

    def test_no_match_greater(self):
        fc = FieldCondition("price", "lt", 10.0)
        self.assertFalse(fc.match({"price": 15.0}))


class TestFieldConditionLte(unittest.TestCase):
    """FieldCondition with 'lte' operator."""

    def test_match_less(self):
        fc = FieldCondition("price", "lte", 10.0)
        self.assertTrue(fc.match({"price": 5.0}))

    def test_match_equal(self):
        fc = FieldCondition("price", "lte", 10.0)
        self.assertTrue(fc.match({"price": 10.0}))

    def test_no_match(self):
        fc = FieldCondition("price", "lte", 10.0)
        self.assertFalse(fc.match({"price": 15.0}))


class TestFieldConditionIn(unittest.TestCase):
    """FieldCondition with 'in' operator."""

    def test_match(self):
        fc = FieldCondition("color", "in", ["red", "blue", "green"])
        self.assertTrue(fc.match({"color": "red"}))

    def test_no_match(self):
        fc = FieldCondition("color", "in", ["red", "blue", "green"])
        self.assertFalse(fc.match({"color": "yellow"}))

    def test_in_set(self):
        fc = FieldCondition("color", "in", {"red", "blue"})
        self.assertTrue(fc.match({"color": "red"}))

    def test_missing_field(self):
        fc = FieldCondition("color", "in", ["red"])
        self.assertFalse(fc.match({}))


class TestFieldConditionContains(unittest.TestCase):
    """FieldCondition with 'contains' operator."""

    def test_match_list(self):
        fc = FieldCondition("tags", "contains", "python")
        self.assertTrue(fc.match({"tags": ["python", "java"]}))

    def test_no_match_list(self):
        fc = FieldCondition("tags", "contains", "rust")
        self.assertFalse(fc.match({"tags": ["python", "java"]}))

    def test_match_set(self):
        fc = FieldCondition("tags", "contains", "x")
        self.assertTrue(fc.match({"tags": {"x", "y"}}))

    def test_non_iterable_returns_false(self):
        fc = FieldCondition("val", "contains", "x")
        self.assertFalse(fc.match({"val": 42}))

    def test_match_string(self):
        fc = FieldCondition("name", "contains", "hello")
        self.assertTrue(fc.match({"name": "say hello world"}))


class TestFieldConditionInvalidOp(unittest.TestCase):
    """Invalid operator raises ValueError."""

    def test_invalid_operator(self):
        with self.assertRaises(ValueError):
            FieldCondition("x", "invalid_op", 1)


class TestFieldConditionRepr(unittest.TestCase):
    """FieldCondition __repr__."""

    def test_repr(self):
        fc = FieldCondition("color", "eq", "red")
        r = repr(fc)
        self.assertIn("FieldCondition", r)
        self.assertIn("color", r)
        self.assertIn("eq", r)


# ---------------------------------------------------------------------------
# Composite expressions
# ---------------------------------------------------------------------------


class TestAnd(unittest.TestCase):
    """And expression."""

    def test_both_true(self):
        expr = And(
            FieldCondition("a", "eq", 1),
            FieldCondition("b", "eq", 2),
        )
        self.assertTrue(expr.match({"a": 1, "b": 2}))

    def test_first_false(self):
        expr = And(
            FieldCondition("a", "eq", 1),
            FieldCondition("b", "eq", 2),
        )
        self.assertFalse(expr.match({"a": 99, "b": 2}))

    def test_second_false(self):
        expr = And(
            FieldCondition("a", "eq", 1),
            FieldCondition("b", "eq", 2),
        )
        self.assertFalse(expr.match({"a": 1, "b": 99}))

    def test_operator_shorthand(self):
        a = FieldCondition("a", "eq", 1)
        b = FieldCondition("b", "eq", 2)
        expr = a & b
        self.assertIsInstance(expr, And)
        self.assertTrue(expr.match({"a": 1, "b": 2}))

    def test_repr(self):
        expr = And(
            FieldCondition("a", "eq", 1),
            FieldCondition("b", "eq", 2),
        )
        self.assertIn("And", repr(expr))


class TestOr(unittest.TestCase):
    """Or expression."""

    def test_both_true(self):
        expr = Or(
            FieldCondition("a", "eq", 1),
            FieldCondition("b", "eq", 2),
        )
        self.assertTrue(expr.match({"a": 1, "b": 2}))

    def test_first_true(self):
        expr = Or(
            FieldCondition("a", "eq", 1),
            FieldCondition("b", "eq", 2),
        )
        self.assertTrue(expr.match({"a": 1, "b": 99}))

    def test_neither_true(self):
        expr = Or(
            FieldCondition("a", "eq", 1),
            FieldCondition("b", "eq", 2),
        )
        self.assertFalse(expr.match({"a": 99, "b": 99}))

    def test_operator_shorthand(self):
        a = FieldCondition("a", "eq", 1)
        b = FieldCondition("b", "eq", 2)
        expr = a | b
        self.assertIsInstance(expr, Or)
        self.assertTrue(expr.match({"a": 1, "b": 99}))

    def test_repr(self):
        expr = Or(
            FieldCondition("a", "eq", 1),
            FieldCondition("b", "eq", 2),
        )
        self.assertIn("Or", repr(expr))


class TestNot(unittest.TestCase):
    """Not expression."""

    def test_negates_true(self):
        expr = Not(FieldCondition("a", "eq", 1))
        self.assertFalse(expr.match({"a": 1}))

    def test_negates_false(self):
        expr = Not(FieldCondition("a", "eq", 1))
        self.assertTrue(expr.match({"a": 2}))

    def test_operator_shorthand(self):
        a = FieldCondition("a", "eq", 1)
        expr = ~a
        self.assertIsInstance(expr, Not)
        self.assertTrue(expr.match({"a": 2}))

    def test_repr(self):
        expr = Not(FieldCondition("a", "eq", 1))
        self.assertIn("Not", repr(expr))


class TestComplexExpression(unittest.TestCase):
    """Complex nested filter expressions."""

    def test_and_or_combination(self):
        # (color == "red" AND price > 10) OR category == "sale"
        expr = (
            FieldCondition("color", "eq", "red")
            & FieldCondition("price", "gt", 10)
        ) | FieldCondition("category", "eq", "sale")

        self.assertTrue(expr.match({"color": "red", "price": 15}))
        self.assertTrue(expr.match({"category": "sale"}))
        self.assertFalse(expr.match({"color": "red", "price": 5}))

    def test_not_and_combination(self):
        # NOT (color == "red") AND price < 20
        expr = ~FieldCondition("color", "eq", "red") & FieldCondition(
            "price", "lt", 20
        )
        self.assertTrue(expr.match({"color": "blue", "price": 10}))
        self.assertFalse(expr.match({"color": "red", "price": 10}))

    def test_triple_and(self):
        expr = (
            FieldCondition("a", "eq", 1)
            & FieldCondition("b", "eq", 2)
            & FieldCondition("c", "eq", 3)
        )
        self.assertTrue(expr.match({"a": 1, "b": 2, "c": 3}))
        self.assertFalse(expr.match({"a": 1, "b": 2, "c": 99}))


# ---------------------------------------------------------------------------
# Metadata storage on VectorStore
# ---------------------------------------------------------------------------


class TestMetadataStorage(unittest.TestCase):
    """Metadata CRUD operations on VectorStore."""

    def test_insert_with_metadata(self):
        vs = VectorStore(dim=3)
        vs.insert("a", [1, 0, 0], metadata={"color": "red"})
        meta = vs.get_metadata("a")
        self.assertEqual(meta, {"color": "red"})

    def test_insert_without_metadata(self):
        vs = VectorStore(dim=3)
        vs.insert("a", [1, 0, 0])
        meta = vs.get_metadata("a")
        self.assertEqual(meta, {})

    def test_set_metadata(self):
        vs = VectorStore(dim=3)
        vs.insert("a", [1, 0, 0])
        vs.set_metadata("a", {"color": "blue", "size": 10})
        self.assertEqual(vs.get_metadata("a"), {"color": "blue", "size": 10})

    def test_set_metadata_replaces(self):
        vs = VectorStore(dim=3)
        vs.insert("a", [1, 0, 0], metadata={"old": True})
        vs.set_metadata("a", {"new": True})
        self.assertEqual(vs.get_metadata("a"), {"new": True})

    def test_set_metadata_nonexistent_raises(self):
        vs = VectorStore(dim=3)
        with self.assertRaises(KeyError):
            vs.set_metadata("ghost", {"x": 1})

    def test_update_metadata(self):
        vs = VectorStore(dim=3)
        vs.insert("a", [1, 0, 0], metadata={"color": "red"})
        vs.update_metadata("a", {"size": 10})
        self.assertEqual(vs.get_metadata("a"), {"color": "red", "size": 10})

    def test_update_metadata_overwrites_key(self):
        vs = VectorStore(dim=3)
        vs.insert("a", [1, 0, 0], metadata={"color": "red"})
        vs.update_metadata("a", {"color": "blue"})
        self.assertEqual(vs.get_metadata("a")["color"], "blue")

    def test_update_metadata_nonexistent_raises(self):
        vs = VectorStore(dim=3)
        with self.assertRaises(KeyError):
            vs.update_metadata("ghost", {"x": 1})

    def test_get_metadata_nonexistent(self):
        vs = VectorStore(dim=3)
        self.assertIsNone(vs.get_metadata("ghost"))

    def test_remove_clears_metadata(self):
        vs = VectorStore(dim=3)
        vs.insert("a", [1, 0, 0], metadata={"color": "red"})
        vs.remove("a")
        self.assertIsNone(vs.get_metadata("a"))

    def test_batch_insert_with_metadata(self):
        vs = VectorStore(dim=2)
        ids = ["a", "b", "c"]
        vecs = [[1, 0], [0, 1], [1, 1]]
        metas = [{"t": "x"}, None, {"t": "z"}]
        vs.batch_insert(ids, vecs, metadata_list=metas)
        self.assertEqual(vs.get_metadata("a"), {"t": "x"})
        self.assertEqual(vs.get_metadata("b"), {})
        self.assertEqual(vs.get_metadata("c"), {"t": "z"})

    def test_batch_insert_metadata_length_mismatch(self):
        vs = VectorStore(dim=2)
        with self.assertRaises(ValueError):
            vs.batch_insert(["a"], [[1, 0]], metadata_list=[{"t": 1}, {"t": 2}])

    def test_insert_update_preserves_metadata(self):
        """Re-inserting a vector without metadata keeps existing metadata."""
        vs = VectorStore(dim=2)
        vs.insert("a", [1, 0], metadata={"color": "red"})
        vs.insert("a", [0, 1])  # Update vector, no new metadata
        self.assertEqual(vs.get_metadata("a"), {"color": "red"})

    def test_insert_update_replaces_metadata(self):
        """Re-inserting with new metadata replaces old metadata."""
        vs = VectorStore(dim=2)
        vs.insert("a", [1, 0], metadata={"color": "red"})
        vs.insert("a", [0, 1], metadata={"color": "blue"})
        self.assertEqual(vs.get_metadata("a"), {"color": "blue"})

    def test_get_metadata_returns_copy(self):
        """Modifying the returned dict does not affect internal state."""
        vs = VectorStore(dim=2)
        vs.insert("a", [1, 0], metadata={"color": "red"})
        meta = vs.get_metadata("a")
        meta["color"] = "blue"
        self.assertEqual(vs.get_metadata("a")["color"], "red")


# ---------------------------------------------------------------------------
# Filtered search: exact mode
# ---------------------------------------------------------------------------


class TestFilteredExactSearch(unittest.TestCase):
    """Filtered search using exact (brute-force) mode."""

    def setUp(self):
        self.vs = VectorStore(dim=3, distance="l2")
        self.vs.insert("red1", [1, 0, 0], metadata={"color": "red", "price": 10})
        self.vs.insert("red2", [0.9, 0.1, 0], metadata={"color": "red", "price": 20})
        self.vs.insert("blue1", [0, 1, 0], metadata={"color": "blue", "price": 5})
        self.vs.insert("blue2", [0, 0.9, 0.1], metadata={"color": "blue", "price": 30})
        self.vs.insert("green", [0, 0, 1], metadata={"color": "green", "price": 15})

    def test_filter_eq(self):
        f = FieldCondition("color", "eq", "red")
        results = self.vs.search([1, 0, 0], k=5, mode="exact", filter_expr=f)
        ids = [id_ for id_, _ in results]
        self.assertTrue(all("red" in id_ for id_ in ids))
        self.assertEqual(len(results), 2)

    def test_filter_gt(self):
        f = FieldCondition("price", "gt", 15)
        results = self.vs.search([0, 0, 0], k=5, mode="exact", filter_expr=f)
        ids = {id_ for id_, _ in results}
        self.assertEqual(ids, {"red2", "blue2"})

    def test_filter_lte(self):
        f = FieldCondition("price", "lte", 10)
        results = self.vs.search([1, 0, 0], k=5, mode="exact", filter_expr=f)
        ids = {id_ for id_, _ in results}
        self.assertEqual(ids, {"red1", "blue1"})

    def test_combined_and_filter(self):
        f = FieldCondition("color", "eq", "red") & FieldCondition("price", "gt", 15)
        results = self.vs.search([1, 0, 0], k=5, mode="exact", filter_expr=f)
        ids = [id_ for id_, _ in results]
        self.assertEqual(ids, ["red2"])

    def test_or_filter(self):
        f = FieldCondition("color", "eq", "red") | FieldCondition("color", "eq", "green")
        results = self.vs.search([1, 0, 0], k=5, mode="exact", filter_expr=f)
        ids = {id_ for id_, _ in results}
        self.assertEqual(ids, {"red1", "red2", "green"})

    def test_not_filter(self):
        f = ~FieldCondition("color", "eq", "blue")
        results = self.vs.search([0, 1, 0], k=5, mode="exact", filter_expr=f)
        ids = {id_ for id_, _ in results}
        self.assertNotIn("blue1", ids)
        self.assertNotIn("blue2", ids)
        self.assertEqual(len(results), 3)

    def test_no_filter_returns_all(self):
        results = self.vs.search([1, 0, 0], k=5, mode="exact")
        self.assertEqual(len(results), 5)

    def test_filter_matches_none(self):
        f = FieldCondition("color", "eq", "purple")
        results = self.vs.search([1, 0, 0], k=5, mode="exact", filter_expr=f)
        self.assertEqual(results, [])

    def test_filter_with_k_limit(self):
        f = FieldCondition("price", "gt", 0)
        results = self.vs.search([1, 0, 0], k=2, mode="exact", filter_expr=f)
        self.assertEqual(len(results), 2)

    def test_filter_results_sorted_by_distance(self):
        f = FieldCondition("color", "eq", "red")
        results = self.vs.search([1, 0, 0], k=5, mode="exact", filter_expr=f)
        dists = [d for _, d in results]
        self.assertEqual(dists, sorted(dists))


# ---------------------------------------------------------------------------
# Filtered search: HNSW mode
# ---------------------------------------------------------------------------


class TestFilteredHNSWSearch(unittest.TestCase):
    """Filtered search using HNSW mode."""

    def setUp(self):
        cfg = HNSWConfig(M=8, ef_construction=100, ef_search=50, distance="l2", seed=42)
        self.vs = VectorStore(dim=4, distance="l2", hnsw_config=cfg)
        # Insert 50 vectors: half "category A", half "category B"
        rng = random.Random(42)
        for i in range(50):
            cat = "A" if i < 25 else "B"
            vec = _random_vector(4, rng)
            self.vs.insert(f"v_{i}", vec, metadata={"category": cat, "idx": i})

    def test_filter_category(self):
        f = FieldCondition("category", "eq", "A")
        results = self.vs.search([1, 0, 0, 0], k=10, mode="hnsw", filter_expr=f)
        for id_, _ in results:
            meta = self.vs.get_metadata(id_)
            self.assertEqual(meta["category"], "A")

    def test_filter_numeric_range(self):
        f = FieldCondition("idx", "lt", 10)
        results = self.vs.search([1, 0, 0, 0], k=5, mode="hnsw", filter_expr=f)
        for id_, _ in results:
            meta = self.vs.get_metadata(id_)
            self.assertLess(meta["idx"], 10)

    def test_filter_returns_fewer_than_k(self):
        """When few items match, we get fewer than k results."""
        f = FieldCondition("idx", "eq", 0)
        results = self.vs.search([1, 0, 0, 0], k=10, mode="hnsw", filter_expr=f)
        self.assertLessEqual(len(results), 1)

    def test_no_filter_returns_full_results(self):
        results = self.vs.search([1, 0, 0, 0], k=10, mode="hnsw")
        self.assertEqual(len(results), 10)

    def test_default_mode_with_filter(self):
        """Default mode (hnsw) with filter should work."""
        f = FieldCondition("category", "eq", "B")
        results = self.vs.search([1, 0, 0, 0], k=5, filter_expr=f)
        for id_, _ in results:
            meta = self.vs.get_metadata(id_)
            self.assertEqual(meta["category"], "B")


# ---------------------------------------------------------------------------
# Filtered search: binary mode
# ---------------------------------------------------------------------------


class TestFilteredBinarySearch(unittest.TestCase):
    """Filtered search using binary quantization mode."""

    def test_filter_binary(self):
        vs = VectorStore(dim=8, distance="l2")
        vs.insert("a", [1, 1, 1, 1, 0, 0, 0, 0], metadata={"type": "pos"})
        vs.insert("b", [0, 0, 0, 0, 1, 1, 1, 1], metadata={"type": "neg"})
        vs.insert("c", [1, 1, 1, 0, 0, 0, 0, 0], metadata={"type": "pos"})

        f = FieldCondition("type", "eq", "pos")
        results = vs.search(
            [1, 1, 1, 1, 0, 0, 0, 0], k=5, mode="binary", filter_expr=f
        )
        ids = {id_ for id_, _ in results}
        self.assertIn("a", ids)
        self.assertNotIn("b", ids)

    def test_binary_no_filter(self):
        vs = VectorStore(dim=4)
        vs.insert("a", [1, 1, 0, 0], metadata={"x": 1})
        vs.insert("b", [0, 0, 1, 1], metadata={"x": 2})
        results = vs.search([1, 1, 0, 0], k=2, mode="binary")
        self.assertEqual(len(results), 2)


# ---------------------------------------------------------------------------
# Filtered search: hybrid mode
# ---------------------------------------------------------------------------


class TestFilteredHybridSearch(unittest.TestCase):
    """Filtered search using hybrid mode."""

    def test_filter_hybrid(self):
        vs = VectorStore(dim=8, distance="l2")
        rng = random.Random(42)

        target = _random_vector(8, rng)
        vs.insert("target", target, metadata={"keep": True})
        for i in range(50):
            vs.insert(f"noise_{i}", _random_vector(8, rng), metadata={"keep": False})

        f = FieldCondition("keep", "eq", True)
        results = vs.search(target, k=5, mode="hybrid", filter_expr=f)
        ids = {id_ for id_, _ in results}
        self.assertEqual(ids, {"target"})

    def test_hybrid_filter_empty_result(self):
        vs = VectorStore(dim=4, distance="l2")
        vs.insert("a", [1, 0, 0, 0], metadata={"x": 1})
        f = FieldCondition("x", "eq", 999)
        results = vs.search([1, 0, 0, 0], k=5, mode="hybrid", filter_expr=f)
        self.assertEqual(results, [])


# ---------------------------------------------------------------------------
# HNSW filter_fn integration tests
# ---------------------------------------------------------------------------


class TestHNSWFilterFn(unittest.TestCase):
    """Direct HNSW filter_fn tests (without VectorStore)."""

    def test_filter_fn_basic(self):
        idx = HNSWIndex(HNSWConfig(distance="l2", seed=42))
        idx.insert("a", [1, 0])
        idx.insert("b", [0, 1])
        idx.insert("c", [1, 1])

        # Only accept "b" and "c"
        results = idx.search([0, 1], k=3, filter_fn=lambda nid: nid != "a")
        ids = {nid for nid, _ in results}
        self.assertNotIn("a", ids)
        self.assertIn("b", ids)

    def test_filter_fn_none(self):
        idx = HNSWIndex(HNSWConfig(distance="l2", seed=42))
        idx.insert("a", [1, 0])
        idx.insert("b", [0, 1])
        results = idx.search([1, 0], k=2, filter_fn=None)
        self.assertEqual(len(results), 2)

    def test_filter_fn_all_rejected(self):
        idx = HNSWIndex(HNSWConfig(distance="l2", seed=42))
        idx.insert("a", [1, 0])
        idx.insert("b", [0, 1])
        results = idx.search([1, 0], k=2, filter_fn=lambda _: False)
        self.assertEqual(results, [])

    def test_filter_fn_preserves_nearest(self):
        """Filtered result should still be the nearest among passing nodes."""
        idx = HNSWIndex(HNSWConfig(distance="l2", seed=42))
        idx.insert("near", [0.1, 0])  # Closest to query but will be filtered
        idx.insert("mid", [0.5, 0])
        idx.insert("far", [1.0, 0])
        results = idx.search(
            [0, 0], k=1, filter_fn=lambda nid: nid != "near"
        )
        self.assertEqual(results[0][0], "mid")

    def test_filter_fn_with_larger_dataset(self):
        """Filter on a larger dataset to exercise expanded ef."""
        rng = random.Random(42)
        idx = HNSWIndex(HNSWConfig(distance="l2", ef_search=30, seed=42))
        keep_ids = set()
        for i in range(100):
            vec = [rng.gauss(0, 1) for _ in range(8)]
            nid = f"v_{i}"
            idx.insert(nid, vec)
            if i % 3 == 0:
                keep_ids.add(nid)

        results = idx.search(
            [0] * 8, k=5, filter_fn=lambda nid: nid in keep_ids
        )
        for nid, _ in results:
            self.assertIn(nid, keep_ids)


# ---------------------------------------------------------------------------
# Edge cases
# ---------------------------------------------------------------------------


class TestFilteredSearchEdgeCases(unittest.TestCase):
    """Edge cases for filtered search."""

    def test_empty_store(self):
        vs = VectorStore(dim=3)
        f = FieldCondition("x", "eq", 1)
        results = vs.search([1, 0, 0], k=5, mode="exact", filter_expr=f)
        self.assertEqual(results, [])

    def test_no_metadata_set(self):
        """Vectors without metadata should not match any filter."""
        vs = VectorStore(dim=2, distance="l2")
        vs.insert("a", [1, 0])  # No metadata provided, defaults to {}
        f = FieldCondition("color", "eq", "red")
        results = vs.search([1, 0], k=5, mode="exact", filter_expr=f)
        self.assertEqual(results, [])

    def test_in_operator_with_empty_collection(self):
        fc = FieldCondition("x", "in", [])
        self.assertFalse(fc.match({"x": "anything"}))

    def test_filter_on_empty_metadata(self):
        """Filter expression on {} metadata returns False."""
        fc = FieldCondition("missing_field", "eq", "val")
        self.assertFalse(fc.match({}))

    def test_filter_with_none_value_in_metadata(self):
        """A field with None value should not match eq comparisons."""
        fc = FieldCondition("x", "eq", None)
        # None is treated as field missing
        self.assertFalse(fc.match({"x": None}))


# ---------------------------------------------------------------------------
# Thread safety
# ---------------------------------------------------------------------------


class TestFilteredThreadSafety(unittest.TestCase):
    """Concurrent filtered operations should not crash."""

    def test_concurrent_insert_and_filtered_search(self):
        vs = VectorStore(dim=4, distance="l2")
        rng = random.Random(42)
        errors = []

        # Pre-populate
        for i in range(30):
            vs.insert(
                f"pre_{i}",
                _random_vector(4, rng),
                metadata={"group": i % 3},
            )

        def inserter():
            try:
                local_rng = random.Random(77)
                for i in range(30):
                    vs.insert(
                        f"new_{i}",
                        _random_vector(4, local_rng),
                        metadata={"group": i % 3},
                    )
            except Exception as e:
                errors.append(e)

        def searcher():
            try:
                f = FieldCondition("group", "eq", 1)
                local_rng = random.Random(99)
                for _ in range(20):
                    vs.search(
                        _random_vector(4, local_rng),
                        k=3,
                        mode="exact",
                        filter_expr=f,
                    )
            except Exception as e:
                errors.append(e)

        threads = [
            threading.Thread(target=inserter),
            threading.Thread(target=searcher),
            threading.Thread(target=searcher),
        ]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        self.assertEqual(errors, [], f"Errors: {errors}")

    def test_concurrent_metadata_update_and_search(self):
        vs = VectorStore(dim=4, distance="l2")
        rng = random.Random(42)
        errors = []

        for i in range(50):
            vs.insert(
                f"v_{i}",
                _random_vector(4, rng),
                metadata={"val": i},
            )

        def updater():
            try:
                for i in range(50):
                    vs.update_metadata(f"v_{i}", {"val": i + 100})
            except Exception as e:
                errors.append(e)

        def searcher():
            try:
                f = FieldCondition("val", "gte", 0)
                local_rng = random.Random(88)
                for _ in range(20):
                    vs.search(
                        _random_vector(4, local_rng),
                        k=5,
                        mode="exact",
                        filter_expr=f,
                    )
            except Exception as e:
                errors.append(e)

        threads = [
            threading.Thread(target=updater),
            threading.Thread(target=searcher),
        ]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        self.assertEqual(errors, [], f"Errors: {errors}")


# ---------------------------------------------------------------------------
# Adaptive fallback
# ---------------------------------------------------------------------------


class TestAdaptiveFallback(unittest.TestCase):
    """Adaptive 3-tier HNSW fallback for highly selective filters."""

    def test_fallback_to_exact_on_very_selective_filter(self):
        """When only 1 of 100 vectors matches, adaptive fallback finds it."""
        rng = random.Random(42)
        cfg = HNSWConfig(M=4, ef_construction=50, ef_search=10, distance="l2", seed=42)
        vs = VectorStore(dim=4, distance="l2", hnsw_config=cfg)

        target_vec = [1.0, 0.0, 0.0, 0.0]
        vs.insert("target", target_vec, metadata={"special": True})
        for i in range(99):
            vs.insert(f"other_{i}", _random_vector(4, rng), metadata={"special": False})

        f = FieldCondition("special", "eq", True)
        results = vs.search(target_vec, k=1, mode="hnsw", filter_expr=f)
        self.assertEqual(len(results), 1)
        self.assertEqual(results[0][0], "target")

    def test_no_fallback_when_enough_results(self):
        """When the filter is not very selective, normal HNSW is sufficient."""
        rng = random.Random(42)
        cfg = HNSWConfig(M=8, ef_construction=100, ef_search=50, distance="l2", seed=42)
        vs = VectorStore(dim=4, distance="l2", hnsw_config=cfg)

        for i in range(50):
            vs.insert(f"v_{i}", _random_vector(4, rng), metadata={"group": i % 2})

        f = FieldCondition("group", "eq", 0)
        results = vs.search([1, 0, 0, 0], k=5, mode="hnsw", filter_expr=f)
        # Should get 5 results from the 25 matching vectors
        self.assertEqual(len(results), 5)
        for id_, _ in results:
            meta = vs.get_metadata(id_)
            self.assertEqual(meta["group"], 0)


# ---------------------------------------------------------------------------
# Performance
# ---------------------------------------------------------------------------


class TestFilterPerformance(unittest.TestCase):
    """Performance benchmarks for filtered search."""

    def test_filtered_search_100_vectors(self):
        """Filtered search on 100 vectors should complete quickly."""
        rng = random.Random(42)
        cfg = HNSWConfig(distance="cosine", seed=42)
        vs = VectorStore(dim=32, distance="cosine", hnsw_config=cfg)

        for i in range(100):
            vs.insert(
                f"v_{i}",
                _random_vector(32, rng),
                metadata={"category": "A" if i % 2 == 0 else "B", "idx": i},
            )

        f = FieldCondition("category", "eq", "A")
        query = _random_vector(32, rng)

        t0 = time.perf_counter()
        for _ in range(10):
            results = vs.search(query, k=5, mode="hnsw", filter_expr=f)
        elapsed = time.perf_counter() - t0

        self.assertLess(elapsed, 2.0, f"10 filtered searches took {elapsed:.3f}s")
        # Verify results are correct
        for id_, _ in results:
            meta = vs.get_metadata(id_)
            self.assertEqual(meta["category"], "A")

    def test_exact_filtered_search_1000_vectors(self):
        """Exact filtered search on 1000 vectors should complete in < 2s."""
        rng = random.Random(42)
        vs = VectorStore(dim=16, distance="l2")

        for i in range(1000):
            vs.insert(
                f"v_{i}",
                _random_vector(16, rng),
                metadata={"tier": i % 5},
            )

        f = FieldCondition("tier", "eq", 0)
        query = _random_vector(16, rng)

        t0 = time.perf_counter()
        results = vs.search(query, k=10, mode="exact", filter_expr=f)
        elapsed = time.perf_counter() - t0

        self.assertLess(elapsed, 2.0, f"Exact filtered search took {elapsed:.3f}s")
        self.assertEqual(len(results), 10)
        for id_, _ in results:
            meta = vs.get_metadata(id_)
            self.assertEqual(meta["tier"], 0)


if __name__ == "__main__":
    unittest.main()
