"""Tests for VectorIndex (ConcurrentHNSWIndex) Python binding (phase-30 / T30.6)."""

import math
import time

import numpy as np
import pytest

from aifastdb import VectorIndex


DIM = 32


def rand_vec(dim=DIM, seed=None):
    rng = np.random.default_rng(seed)
    return rng.standard_normal(dim).astype(np.float32).tolist()


# ---------- Construction ----------

def test_create_default():
    idx = VectorIndex(DIM)
    assert idx.dimension == DIM
    assert idx.shard_count >= 1
    assert len(idx) == 0
    assert idx.is_empty()
    idx.close()


def test_create_custom():
    idx = VectorIndex(DIM, shard_count=2, m=8, ef_construction=64, max_elements=5000)
    assert idx.dimension == DIM
    assert idx.shard_count == 2
    idx.close()


def test_create_high_throughput():
    idx = VectorIndex(DIM, mode="high_throughput")
    assert idx.shard_count >= 1
    idx.close()


def test_create_low_latency():
    idx = VectorIndex(DIM, mode="low_latency")
    assert idx.shard_count >= 1
    idx.close()


def test_zero_dimension_raises():
    with pytest.raises(ValueError, match="dimension must be > 0"):
        VectorIndex(0)


# ---------- Insert & Get ----------

def test_insert_and_get():
    with VectorIndex(DIM, shard_count=2) as idx:
        vec = rand_vec(seed=42)
        idx.insert("v1", vec)
        idx.flush()
        time.sleep(0.15)

        got = idx.get("v1")
        assert got is not None
        assert len(got) == DIM
        # Values should be close (float precision)
        assert all(abs(a - b) < 1e-6 for a, b in zip(got, vec))


def test_insert_sync():
    with VectorIndex(DIM, shard_count=2) as idx:
        vec = rand_vec(seed=43)
        idx.insert_sync("vs1", vec)
        idx.flush()
        time.sleep(0.15)

        assert idx.has("vs1")
        assert not idx.has("nonexistent")


def test_insert_batch():
    with VectorIndex(DIM, shard_count=2) as idx:
        items = [(f"b{i}", rand_vec(seed=i)) for i in range(10)]
        count = idx.insert_batch(items)
        assert count == 10
        idx.flush()
        time.sleep(0.15)

        assert len(idx) == 10
        for i in range(10):
            assert idx.has(f"b{i}")


def test_delete():
    with VectorIndex(DIM, shard_count=2) as idx:
        idx.insert_sync("del1", rand_vec(seed=100))
        idx.flush()
        time.sleep(0.15)
        assert idx.has("del1")

        idx.delete("del1")
        idx.flush()
        time.sleep(0.15)
        assert not idx.has("del1")


def test_dimension_mismatch():
    with VectorIndex(DIM, shard_count=1) as idx:
        with pytest.raises(ValueError, match="dimension mismatch"):
            idx.insert("bad", [1.0, 2.0])  # wrong dim


# ---------- Search ----------

def test_search_basic():
    with VectorIndex(DIM, shard_count=2) as idx:
        vecs = [(f"s{i}", rand_vec(seed=i)) for i in range(20)]
        idx.insert_batch(vecs)
        idx.flush()
        time.sleep(0.15)

        query = vecs[0][1]  # search for the first vector itself
        results = idx.search(query, k=5)
        assert isinstance(results, list)
        assert len(results) <= 5
        # First result should be the vector itself (distance ≈ 0)
        assert results[0]["id"] == "s0"
        assert results[0]["distance"] < 0.01


def test_search_with_ef():
    with VectorIndex(DIM, shard_count=2) as idx:
        vecs = [(f"e{i}", rand_vec(seed=i + 200)) for i in range(20)]
        idx.insert_batch(vecs)
        idx.flush()
        time.sleep(0.15)

        results = idx.search_with_ef(vecs[0][1], k=5, ef=200)
        assert len(results) <= 5
        assert results[0]["id"] == "e0"


def test_search_numpy():
    with VectorIndex(DIM, shard_count=2) as idx:
        vecs = [(f"n{i}", rand_vec(seed=i + 300)) for i in range(10)]
        idx.insert_batch(vecs)
        idx.flush()
        time.sleep(0.15)

        query_np = np.array(vecs[0][1], dtype=np.float32)
        results = idx.search_numpy(query_np, k=3)
        assert len(results) <= 3
        assert results[0]["id"] == "n0"


def test_get_numpy():
    with VectorIndex(DIM, shard_count=2) as idx:
        vec = rand_vec(seed=999)
        idx.insert_sync("np1", vec)
        idx.flush()
        time.sleep(0.15)

        arr = idx.get_numpy("np1")
        assert arr is not None
        assert isinstance(arr, np.ndarray)
        assert arr.shape == (DIM,)
        assert arr.dtype == np.float32

        none_arr = idx.get_numpy("nonexistent")
        assert none_arr is None


# ---------- Management ----------

def test_metrics():
    with VectorIndex(DIM, shard_count=1) as idx:
        idx.insert_sync("m1", rand_vec(seed=50))
        idx.flush()
        time.sleep(0.15)

        m = idx.metrics()
        assert isinstance(m, dict)
        assert "total_requests" in m
        assert "successful_writes" in m
        assert "success_rate" in m


def test_queue_fill_ratio():
    with VectorIndex(DIM, shard_count=1) as idx:
        ratio = idx.queue_fill_ratio()
        assert isinstance(ratio, float)
        assert 0.0 <= ratio <= 1.0


def test_shard_stats():
    with VectorIndex(DIM, shard_count=2) as idx:
        idx.insert_batch([(f"ss{i}", rand_vec(seed=i + 500)) for i in range(10)])
        idx.flush()
        time.sleep(0.15)

        stats = idx.shard_stats()
        assert isinstance(stats, list)
        assert len(stats) == 2
        total = sum(c for _, c in stats)
        assert total == 10


def test_backpressure_status():
    with VectorIndex(DIM, shard_count=1) as idx:
        status = idx.backpressure_status()
        assert isinstance(status, str)


def test_repr():
    with VectorIndex(DIM, shard_count=1) as idx:
        r = repr(idx)
        assert "VectorIndex" in r
        assert f"dim={DIM}" in r


# ---------- Context Manager ----------

def test_context_manager():
    with VectorIndex(DIM, shard_count=1) as idx:
        idx.insert_sync("ctx1", rand_vec(seed=777))
        idx.flush()
        time.sleep(0.15)
        assert len(idx) == 1
    # After exit, flush has been called — no errors expected.


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
