"""Tests for the restricted-unpickler FAISS loader (faiss_safe_load).

Covers:
- A real ``FAISS.save_local`` index round-trips through ``safe_load_faiss``
  (proves the restricted unpickler accepts legitimate docstore payloads).
- A malicious pickle that would execute code under a plain ``pickle.load`` is
  rejected before execution (proves the RCE is actually closed, including the
  realistic ".pkl-only swap" where the .faiss is left byte-identical).
"""

import datetime
import io
import pickle
from pathlib import Path

import pytest
from langchain_community.docstore.in_memory import InMemoryDocstore
from langchain_community.vectorstores import FAISS
from langchain_core.documents import Document

from local_deep_research.research_library.services.faiss_safe_load import (
    _RestrictedFaissUnpickler,
    safe_load_faiss,
)

try:  # location varies across langchain versions
    from langchain_community.embeddings import FakeEmbeddings
except Exception:  # pragma: no cover
    from langchain_core.embeddings import FakeEmbeddings


def _build_and_save(
    dir_path: Path, index_name: str = "index"
) -> FakeEmbeddings:
    """Create a small real FAISS index on disk and return its embeddings."""
    emb = FakeEmbeddings(size=16)
    texts = ["alpha chunk", "beta chunk", "gamma chunk"]
    # Metadata mirrors what the RAG service attaches: plain scalars + lists.
    metas = [
        {"source": "a.pdf", "page": 1, "word_count": 2, "authors": None},
        {"source": "b.pdf", "page": 2, "tags": ["x", "y"]},
        {"source": "c.pdf", "score": 0.9, "nested": {"k": 1}},
    ]
    vs = FAISS.from_texts(texts, emb, metadatas=metas)
    vs.save_local(str(dir_path), index_name=index_name)
    return emb


class TestSafeLoadRoundTrip:
    def test_loads_real_index(self, tmp_path):
        emb = _build_and_save(tmp_path)
        vs = safe_load_faiss(
            str(tmp_path), emb, index_name="index", normalize_L2=True
        )
        contents = {d.page_content for d in vs.docstore._dict.values()}
        assert contents == {"alpha chunk", "beta chunk", "gamma chunk"}
        assert len(vs.index_to_docstore_id) == 3
        # Metadata (scalars, list, nested dict) survives the restricted load.
        metas = [d.metadata for d in vs.docstore._dict.values()]
        assert any(m.get("tags") == ["x", "y"] for m in metas)
        # normalize_L2 is threaded into the FAISS constructor by hand — guard
        # it, since a silent drop would corrupt cosine-distance search.
        assert vs._normalize_L2 is True

    def test_normalize_l2_defaults_false(self, tmp_path):
        emb = _build_and_save(tmp_path)
        vs = safe_load_faiss(str(tmp_path), emb, index_name="index")
        assert vs._normalize_L2 is False

    def test_search_works_after_safe_load(self, tmp_path):
        emb = _build_and_save(tmp_path)
        vs = safe_load_faiss(str(tmp_path), emb, index_name="index")
        results = vs.similarity_search("alpha", k=1)
        assert len(results) == 1


# Module-level helpers so pickle can reference them by qualified name. The
# payload writes a marker file instead of doing real harm, so the test can
# prove (a) it WOULD run under plain pickle and (b) it does NOT under the
# restricted unpickler.
def _write_marker(path: str):
    Path(path).write_text("EXECUTED")
    return InMemoryDocstore({})


class _Evil:
    def __init__(self, marker: str):
        self._marker = marker

    def __reduce__(self):
        return (_write_marker, (self._marker,))


class TestSafeLoadBlocksMalicious:
    def test_restricted_unpickler_blocks_and_does_not_execute(self, tmp_path):
        marker = tmp_path / "marker.txt"
        blob = pickle.dumps((_Evil(str(marker)), {0: "id"}))

        # Sanity: a plain pickle.load DOES execute the payload. This
        # unsafe call is the point of the test — it demonstrates the very
        # RCE that safe_load_faiss prevents, on a benign marker payload.
        pickle.loads(blob)  # noqa: S301
        assert marker.read_text() == "EXECUTED"
        marker.unlink()

        # The restricted unpickler refuses and never runs the payload.
        with pytest.raises(pickle.UnpicklingError):
            _RestrictedFaissUnpickler(io.BytesIO(blob)).load()
        assert not marker.exists()

    def test_safe_load_faiss_rejects_pkl_only_swap(self, tmp_path):
        """The realistic attack: leave .faiss byte-identical, swap the .pkl."""
        emb = _build_and_save(tmp_path)  # valid .faiss + .pkl
        marker = tmp_path / "marker.txt"
        faiss_before = (tmp_path / "index.faiss").read_bytes()

        (tmp_path / "index.pkl").write_bytes(
            pickle.dumps((_Evil(str(marker)), {0: "id"}))
        )

        with pytest.raises(pickle.UnpicklingError):
            safe_load_faiss(str(tmp_path), emb, index_name="index")
        assert not marker.exists()
        # We did not touch the .faiss (mirrors the attack precondition).
        assert (tmp_path / "index.faiss").read_bytes() == faiss_before

    def test_blocks_copyreg_extension_opcode_even_when_cache_warm(
        self, tmp_path
    ):
        """copyreg EXT1/EXT2/EXT4 opcodes resolve a callable via the extension
        registry. ``find_class`` alone is NOT enough: ``get_extension``
        short-circuits on the process-global ``copyreg._extension_cache`` and
        skips ``find_class`` once a code has been resolved. So this test WARMS
        the cache first (a plain ``pickle.loads`` that runs the payload), which
        is exactly the state in which a ``find_class``-only guard would be
        bypassed — then asserts the restricted unpickler still refuses (proving
        the ``get_extension`` override, not ``find_class``, does the blocking).
        """
        import copyreg
        import pickletools

        marker = tmp_path / "marker.txt"
        code = 0xC0DE
        copyreg.add_extension(
            _write_marker.__module__, _write_marker.__name__, code
        )
        try:
            blob = pickle.dumps((_Evil(str(marker)), {0: "id"}), protocol=2)
            # Guard: the payload must actually use an EXT opcode, else this
            # test would pass for the wrong reason.
            op_names = [op.name for op, _, _ in pickletools.genops(blob)]
            assert any(n.startswith("EXT") for n in op_names), op_names

            # Warm copyreg._extension_cache for this code. A find_class-only
            # restricted unpickler would now be bypassed on the EXT path.
            pickle.loads(blob)  # noqa: S301
            assert marker.exists()  # confirms the cache is now warm
            marker.unlink()

            with pytest.raises(pickle.UnpicklingError):
                _RestrictedFaissUnpickler(io.BytesIO(blob)).load()
            assert not marker.exists()
        finally:
            copyreg.remove_extension(
                _write_marker.__module__, _write_marker.__name__, code
            )
            # Drop the warmed cache entry so it can't leak into other tests.
            copyreg._extension_cache.pop(code, None)

    def test_blocks_disallowed_class_buried_in_metadata(self):
        """The allow-list applies to EVERY global the pickle resolves, not
        just the top-level docstore class. A disallowed class hidden inside a
        Document's metadata (here a benign ``datetime``) must still be
        refused — otherwise an attacker could smuggle a gadget through a
        nested value while the outer objects look legitimate.
        """
        doc = Document(
            page_content="x",
            metadata={"created": datetime.datetime(2026, 1, 1)},
        )
        blob = pickle.dumps((InMemoryDocstore({"id1": doc}), {0: "id1"}))

        with pytest.raises(pickle.UnpicklingError, match="datetime"):
            _RestrictedFaissUnpickler(io.BytesIO(blob)).load()
