"""Tests for brynq.vector_store — Phase 61 simple TF-IDF vector store."""

import json
import pytest
from pathlib import Path


@pytest.fixture(autouse=True)
def _isolate_vectors(tmp_path, monkeypatch):
    """Redirect vector storage to a temp directory."""
    vectors_dir = tmp_path / "vectors"
    vectors_dir.mkdir()
    monkeypatch.setattr("brynq.vector_store.VECTORS_DIR", vectors_dir)
    monkeypatch.setattr("brynq.vector_store.VOCAB_PATH", vectors_dir / "vocab.json")


# ── Tokenization ──────────────────────────────────────────────────────────


class TestTokenize:
    def test_basic(self):
        from brynq.vector_store import _tokenize
        assert _tokenize("Hello World") == ["hello", "world"]

    def test_underscores_preserved(self):
        from brynq.vector_store import _tokenize
        assert "my_func" in _tokenize("my_func is great")

    def test_empty(self):
        from brynq.vector_store import _tokenize
        assert _tokenize("") == []

    def test_numbers(self):
        from brynq.vector_store import _tokenize
        assert "42" in _tokenize("answer is 42")


# ── TF / IDF / Cosine ────────────────────────────────────────────────────


class TestMath:
    def test_compute_tf(self):
        from brynq.vector_store import _compute_tf
        tf = _compute_tf(["a", "b", "a"])
        assert tf["a"] == pytest.approx(2 / 3)
        assert tf["b"] == pytest.approx(1 / 3)

    def test_compute_tf_empty(self):
        from brynq.vector_store import _compute_tf
        assert _compute_tf([]) == {}

    def test_compute_idf(self):
        from brynq.vector_store import _compute_idf
        docs = [{"a": 0.5}, {"a": 0.5, "b": 0.5}, {"b": 0.5}]
        idf = _compute_idf(docs)
        assert "a" in idf
        assert "b" in idf

    def test_cosine_identical(self):
        from brynq.vector_store import _cosine_similarity
        v = {"a": 1.0, "b": 2.0}
        assert _cosine_similarity(v, v) == pytest.approx(1.0)

    def test_cosine_orthogonal(self):
        from brynq.vector_store import _cosine_similarity
        assert _cosine_similarity({"a": 1.0}, {"b": 1.0}) == 0.0

    def test_cosine_empty(self):
        from brynq.vector_store import _cosine_similarity
        assert _cosine_similarity({}, {"a": 1.0}) == 0.0


# ── add_document ──────────────────────────────────────────────────────────


class TestAddDocument:
    def test_returns_doc_id(self):
        from brynq.vector_store import add_document
        did = add_document("doc1", "hello world test")
        assert did == "doc1"

    def test_creates_file(self):
        from brynq.vector_store import add_document, VECTORS_DIR
        add_document("doc2", "some text here")
        assert (VECTORS_DIR / "doc2.json").exists()

    def test_stores_text(self):
        from brynq.vector_store import add_document, get_document
        add_document("doc3", "the quick brown fox")
        doc = get_document("doc3")
        assert doc["text"] == "the quick brown fox"

    def test_stores_metadata(self):
        from brynq.vector_store import add_document, get_document
        add_document("doc4", "content", metadata={"tag": "test"})
        doc = get_document("doc4")
        assert doc["metadata"]["tag"] == "test"

    def test_empty_id_raises(self):
        from brynq.vector_store import add_document
        with pytest.raises(ValueError, match="doc_id"):
            add_document("", "text")

    def test_empty_text_raises(self):
        from brynq.vector_store import add_document
        with pytest.raises(ValueError, match="text"):
            add_document("x", "   ")


# ── get_document ──────────────────────────────────────────────────────────


class TestGetDocument:
    def test_found(self):
        from brynq.vector_store import add_document, get_document
        add_document("g1", "hello")
        assert get_document("g1") is not None

    def test_not_found(self):
        from brynq.vector_store import get_document
        assert get_document("nonexistent") is None

    def test_has_timestamps(self):
        from brynq.vector_store import add_document, get_document
        add_document("g2", "hello")
        doc = get_document("g2")
        assert doc["created_at"] is not None


# ── delete_document ───────────────────────────────────────────────────────


class TestDeleteDocument:
    def test_delete_existing(self):
        from brynq.vector_store import add_document, delete_document, get_document
        add_document("d1", "hello")
        assert delete_document("d1") is True
        assert get_document("d1") is None

    def test_delete_nonexistent(self):
        from brynq.vector_store import delete_document
        assert delete_document("no_such") is False


# ── search ────────────────────────────────────────────────────────────────


class TestSearch:
    def test_finds_relevant(self):
        from brynq.vector_store import add_document, search
        add_document("s1", "python programming language")
        add_document("s2", "java virtual machine")
        add_document("s3", "python web framework flask")
        results = search("python programming")
        assert len(results) >= 1
        assert results[0]["doc_id"] in ("s1", "s3")

    def test_empty_query(self):
        from brynq.vector_store import add_document, search
        add_document("s4", "hello")
        assert search("") == []

    def test_no_match(self):
        from brynq.vector_store import add_document, search
        add_document("s5", "alpha beta gamma")
        results = search("xyzzy_nonexistent_term")
        assert results == []

    def test_respects_limit(self):
        from brynq.vector_store import add_document, search
        for i in range(10):
            add_document(f"lim{i}", f"common word number {i}")
        results = search("common word", limit=3)
        assert len(results) <= 3

    def test_result_has_score(self):
        from brynq.vector_store import add_document, search
        add_document("sc1", "test document content")
        results = search("test content")
        assert len(results) >= 1
        assert "score" in results[0]
        assert results[0]["score"] > 0

    def test_scores_are_sorted(self):
        from brynq.vector_store import add_document, search
        add_document("sort1", "machine learning deep neural network")
        add_document("sort2", "machine learning basics")
        add_document("sort3", "cooking recipes salad")
        results = search("machine learning neural network")
        if len(results) > 1:
            assert results[0]["score"] >= results[1]["score"]

    def test_no_docs_returns_empty(self):
        from brynq.vector_store import search
        assert search("anything") == []


# ── get_stats ─────────────────────────────────────────────────────────────


class TestGetStats:
    def test_empty(self):
        from brynq.vector_store import get_stats
        stats = get_stats()
        assert stats["doc_count"] == 0
        assert stats["vocab_size"] == 0

    def test_after_adds(self):
        from brynq.vector_store import add_document, get_stats
        add_document("st1", "hello world")
        add_document("st2", "python code")
        stats = get_stats()
        assert stats["doc_count"] == 2
        assert stats["vocab_size"] >= 4
        assert stats["total_tokens"] >= 4

    def test_avg_length(self):
        from brynq.vector_store import add_document, get_stats
        add_document("av1", "one two three")
        stats = get_stats()
        assert stats["avg_doc_length"] > 0
