"""
conftest.py — Shared fixtures for MemPalace tests.

Provides isolated palace and knowledge graph instances so tests never
touch the user's real data or leak temp files on failure.

HOME is redirected to a temp directory at module load time — before any
mempalace imports — so that module-level initialisations (e.g.
``_kg = KnowledgeGraph()`` in mcp_server) write to a throwaway location
instead of the real user profile.
"""

import hashlib
import math
import os
import re
import shutil
import tempfile

# ── Isolate HOME before any mempalace imports ──────────────────────────
_original_env = {}
_session_tmp = tempfile.mkdtemp(prefix="mempalace_session_")

for _var in ("HOME", "USERPROFILE", "HOMEDRIVE", "HOMEPATH"):
    _original_env[_var] = os.environ.get(_var)

os.environ["HOME"] = _session_tmp
os.environ["USERPROFILE"] = _session_tmp
os.environ["HOMEDRIVE"] = os.path.splitdrive(_session_tmp)[0] or "C:"
os.environ["HOMEPATH"] = os.path.splitdrive(_session_tmp)[1] or _session_tmp

# Set disk-budget thresholds to 1 byte for the test session so existing backup/watcher
# tests pass on CI machines regardless of available disk space. Individual tests that
# verify disk-budget behavior mock free_bytes directly.
os.environ.setdefault("MEMPALACE_DISK_MIN_FREE_BYTES", "1")

# Now it is safe to import mempalace modules that trigger initialisation.
import pytest  # noqa: E402

from mempalace_code.config import MempalaceConfig  # noqa: E402
from mempalace_code.knowledge_graph import KnowledgeGraph  # noqa: E402
from mempalace_code.storage import open_store  # noqa: E402


class _DeterministicTestEmbedder:
    """Small deterministic embedder for tests that do not explicitly fetch HF models."""

    _DIM = 384

    def ndims(self):
        return self._DIM

    def compute_source_embeddings(self, texts):
        return [self._embed(text) for text in texts]

    def _embed(self, text):
        vec = [0.0] * self._DIM
        for token in re.findall(r"[A-Za-z0-9_]+", text.lower()):
            digest = hashlib.blake2b(token.encode("utf-8"), digest_size=4).digest()
            idx = int.from_bytes(digest[:2], "little") % self._DIM
            vec[idx] += 1.0 if digest[2] & 1 else -1.0

        norm = math.sqrt(sum(v * v for v in vec)) or 1.0
        return [v / norm for v in vec]


@pytest.fixture(autouse=True)
def _use_deterministic_test_embedder(monkeypatch, request):
    """Keep ordinary tests offline and fast while leaving needs_network tests real."""
    if request.node.get_closest_marker("needs_network"):
        return

    from mempalace_code.storage import LanceStore

    monkeypatch.setenv("HF_HUB_OFFLINE", "1")
    monkeypatch.setenv("TRANSFORMERS_OFFLINE", "1")
    monkeypatch.setattr(LanceStore, "_get_embedder", lambda self: _DeterministicTestEmbedder())


@pytest.fixture(scope="session", autouse=True)
def _isolate_home():
    """Ensure HOME points to a temp dir for the entire test session.

    The env vars were already set at module level (above) so that
    module-level initialisations are captured.  This fixture simply
    restores the originals on teardown and cleans up the temp dir.
    """
    yield
    for var, orig in _original_env.items():
        if orig is None:
            os.environ.pop(var, None)
        else:
            os.environ[var] = orig
    shutil.rmtree(_session_tmp, ignore_errors=True)


@pytest.fixture
def tmp_dir():
    """Create and auto-cleanup a temporary directory."""
    d = tempfile.mkdtemp(prefix="mempalace_test_")
    yield d
    shutil.rmtree(d, ignore_errors=True)


@pytest.fixture
def palace_path(tmp_dir):
    """Path to an empty palace directory inside tmp_dir."""
    p = os.path.join(tmp_dir, "palace")
    os.makedirs(p)
    return p


@pytest.fixture
def config(tmp_dir, palace_path):
    """A MempalaceConfig pointing at the temp palace."""
    cfg_dir = os.path.join(tmp_dir, "config")
    os.makedirs(cfg_dir)
    import json

    with open(os.path.join(cfg_dir, "config.json"), "w") as f:
        json.dump({"palace_path": palace_path}, f)
    return MempalaceConfig(config_dir=cfg_dir)


@pytest.fixture
def collection(palace_path):
    """A drawer store in the temp palace."""
    return open_store(palace_path, create=True)


@pytest.fixture
def seeded_collection(collection):
    """Collection with a handful of representative drawers."""
    collection.add(
        ids=[
            "drawer_proj_backend_aaa",
            "drawer_proj_backend_bbb",
            "drawer_proj_frontend_ccc",
            "drawer_notes_planning_ddd",
        ],
        documents=[
            "The authentication module uses JWT tokens for session management. "
            "Tokens expire after 24 hours. Refresh tokens are stored in HttpOnly cookies.",
            "Database migrations are handled by Alembic. We use PostgreSQL 15 "
            "with connection pooling via pgbouncer.",
            "The React frontend uses TanStack Query for server state management. "
            "All API calls go through a centralized fetch wrapper.",
            "Sprint planning: migrate auth to passkeys by Q3. "
            "Evaluate ChromaDB alternatives for vector search.",
        ],
        metadatas=[
            {
                "wing": "project",
                "room": "backend",
                "source_file": "auth.py",
                "chunk_index": 0,
                "added_by": "miner",
                "filed_at": "2026-01-01T00:00:00",
            },
            {
                "wing": "project",
                "room": "backend",
                "source_file": "db.py",
                "chunk_index": 0,
                "added_by": "miner",
                "filed_at": "2026-01-02T00:00:00",
            },
            {
                "wing": "project",
                "room": "frontend",
                "source_file": "App.tsx",
                "chunk_index": 0,
                "added_by": "miner",
                "filed_at": "2026-01-03T00:00:00",
            },
            {
                "wing": "notes",
                "room": "planning",
                "source_file": "sprint.md",
                "chunk_index": 0,
                "added_by": "miner",
                "filed_at": "2026-01-04T00:00:00",
            },
        ],
    )
    return collection


@pytest.fixture
def code_seeded_collection(collection):
    """Collection with code drawers that carry symbol metadata."""
    collection.add(
        ids=[
            "code_py_func_detect",
            "code_py_class_lance",
            "code_go_func_handle",
            "code_ts_func_detect_user",
            "code_rs_struct_config",
        ],
        documents=[
            "def detect_language(filepath): detect programming language from file extension",
            "class LanceStore: vector storage backend for managing palace drawers in LanceDB",
            "func handleRequest(w http.ResponseWriter, r *http.Request): process HTTP requests",
            "function detectUser(token: string): TypeScript authentication user detection",
            "struct Config { palace_path: String, embed_model: String } application configuration",
        ],
        metadatas=[
            {
                "wing": "mempalace",
                "room": "backend",
                "source_file": "/project/mempalace/mempalace/miner.py",
                "language": "python",
                "symbol_name": "detect_language",
                "symbol_type": "function",
                "chunk_index": 0,
                "added_by": "miner",
                "filed_at": "2026-01-01T00:00:00",
            },
            {
                "wing": "mempalace",
                "room": "backend",
                "source_file": "/project/mempalace/mempalace/storage.py",
                "language": "python",
                "symbol_name": "LanceStore",
                "symbol_type": "class",
                "chunk_index": 0,
                "added_by": "miner",
                "filed_at": "2026-01-02T00:00:00",
            },
            {
                "wing": "other_project",
                "room": "backend",
                "source_file": "/project/other_project/main.go",
                "language": "go",
                "symbol_name": "handleRequest",
                "symbol_type": "function",
                "chunk_index": 0,
                "added_by": "miner",
                "filed_at": "2026-01-03T00:00:00",
            },
            {
                "wing": "frontend",
                "room": "frontend",
                "source_file": "/project/frontend/src/auth.ts",
                "language": "typescript",
                "symbol_name": "detectUser",
                "symbol_type": "function",
                "chunk_index": 0,
                "added_by": "miner",
                "filed_at": "2026-01-04T00:00:00",
            },
            {
                "wing": "rust_project",
                "room": "backend",
                "source_file": "/project/rust_project/src/config.rs",
                "language": "rust",
                "symbol_name": "Config",
                "symbol_type": "struct",
                "chunk_index": 0,
                "added_by": "miner",
                "filed_at": "2026-01-05T00:00:00",
            },
        ],
    )
    return collection


@pytest.fixture
def kg(tmp_dir):
    """An isolated KnowledgeGraph using a temp SQLite file."""
    db_path = os.path.join(tmp_dir, "test_kg.sqlite3")
    return KnowledgeGraph(db_path=db_path)


@pytest.fixture
def seeded_kg(kg):
    """KnowledgeGraph pre-loaded with sample triples."""
    kg.add_entity("Alice", entity_type="person")
    kg.add_entity("Max", entity_type="person")
    kg.add_entity("swimming", entity_type="activity")
    kg.add_entity("chess", entity_type="activity")

    kg.add_triple("Alice", "parent_of", "Max", valid_from="2015-04-01")
    kg.add_triple("Max", "does", "swimming", valid_from="2025-01-01")
    kg.add_triple("Max", "does", "chess", valid_from="2024-06-01")
    kg.add_triple("Alice", "works_at", "Acme Corp", valid_from="2020-01-01", valid_to="2024-12-31")
    kg.add_triple("Alice", "works_at", "NewCo", valid_from="2025-01-01")

    return kg
