"""Unit suite for the PRIMARY-store backend selector in common/pipeline.py.

The PRIMARY (savvy) store can be either the existing S3/Garage client OR a box-local
FILESYSTEM rooted at SAVVY_LOCAL_DIR, picked SILENTLY by config (operator 2026-06-23).
This suite proves BOTH halves of the contract:

  * FOLDER mode (SAVVY_LOCAL_DIR set, temp-dir backed): put / get / head / list /
    exists round-trip on the real filesystem, atoms stored verbatim with the S3 key
    layout mirrored on disk, and copy-in pulls GLOBAL S3 → a local file.

  * S3 mode (SAVVY_LOCAL_DIR UNSET): the store is the boto3 client and EVERY primary
    write/read lands as the exact boto3 call the pre-existing direct code made — i.e.
    the S3 path is byte-identical / untouched. boto3 is mocked so we assert the calls.

pipeline.py reads its env knobs at IMPORT time, so each mode is exercised in a FRESH
import of the module under a controlled environment + a controlled boto3 stub. We never
touch a live S3/Garage/Redis.
"""
from __future__ import annotations

import importlib
import io
import sys
import types

import pytest


# ── A recording fake boto3.client, good enough for the S3 store surface ──────────

class _FakeS3Client:
    """Records every call so the test can assert the S3 path is the same boto3 surface
    the old direct code used. In-memory bucket keyed by (Bucket, Key)."""

    def __init__(self):
        self.objects: dict[tuple, dict] = {}
        self.calls: list[tuple] = []

    def put_object(self, Bucket, Key, Body, ContentType="application/octet-stream", **kw):
        self.calls.append(("put_object", Bucket, Key, ContentType, kw))
        self.objects[(Bucket, Key)] = {
            "Body": Body,
            "ContentType": ContentType,
            "Metadata": kw.get("Metadata"),
        }

    def head_object(self, Bucket, Key):
        self.calls.append(("head_object", Bucket, Key))
        if (Bucket, Key) not in self.objects:
            raise _make_clienterror("NoSuchKey")
        return {"ContentLength": len(self.objects[(Bucket, Key)]["Body"])}

    def get_object(self, Bucket, Key):
        self.calls.append(("get_object", Bucket, Key))
        if (Bucket, Key) not in self.objects:
            raise _make_clienterror("NoSuchKey")
        o = self.objects[(Bucket, Key)]
        return {"Body": io.BytesIO(o["Body"]),
                "ContentType": o["ContentType"], "Metadata": o["Metadata"]}

    def delete_object(self, Bucket, Key):
        self.calls.append(("delete_object", Bucket, Key))
        self.objects.pop((Bucket, Key), None)

    def upload_fileobj(self, Fileobj, Bucket, Key, ExtraArgs=None):
        self.calls.append(("upload_fileobj", Bucket, Key, ExtraArgs))
        ExtraArgs = ExtraArgs or {}
        self.objects[(Bucket, Key)] = {
            "Body": Fileobj.read(),
            "ContentType": ExtraArgs.get("ContentType", "application/octet-stream"),
            "Metadata": ExtraArgs.get("Metadata"),
        }

    def download_fileobj(self, Bucket, Key, Fileobj):
        self.calls.append(("download_fileobj", Bucket, Key))
        if (Bucket, Key) not in self.objects:
            raise _make_clienterror("NoSuchKey")
        Fileobj.write(self.objects[(Bucket, Key)]["Body"])


def _make_clienterror(code: str):
    err = Exception(code)
    err.response = {"Error": {"Code": code}}
    return err


def _install_fake_boto3(client: _FakeS3Client):
    """Replace boto3 in sys.modules with a stub whose .client always returns `client`."""
    boto3 = types.ModuleType("boto3")
    boto3.client = lambda *a, **k: client
    sys.modules["boto3"] = boto3


def _fresh_pipeline(monkeypatch, env: dict, boto_client: _FakeS3Client | None):
    """Import a FRESH common/pipeline.py under the given env + boto3 stub.

    Returns the freshly-imported module. The standing conftest stub for botocore is
    reused; only boto3 is swapped to a recording client (when provided)."""
    for k in ("SAVVY_LOCAL_DIR", "GARAGE_URL", "GLOBAL_S3_URL",
              "GARAGE_GITLAB_KEY_ID", "GARAGE_GITLAB_SECRET",
              "GLOBAL_KEY_ID", "GLOBAL_SECRET"):
        monkeypatch.delenv(k, raising=False)
    for k, v in env.items():
        monkeypatch.setenv(k, v)
    if boto_client is not None:
        _install_fake_boto3(boto_client)
    # drop any cached import so module-scope env reads re-run
    sys.modules.pop("common.pipeline", None)
    sys.modules.pop("pipeline", None)
    mod = importlib.import_module("pipeline")
    return importlib.reload(mod)


# ── FOLDER mode ──────────────────────────────────────────────────────────────────

def test_folder_mode_selected_and_savvy_enabled(monkeypatch, tmp_path):
    s3 = _FakeS3Client()
    pl = _fresh_pipeline(monkeypatch, {"SAVVY_LOCAL_DIR": str(tmp_path)}, s3)
    assert pl.SAVVY_LOCAL_DIR == str(tmp_path)
    # folder mode flips SAVVY on so savvy-first + copy-in still apply
    assert pl.SAVVY_ENABLED is True
    store = pl.get_primary_store()
    assert store.__class__.__name__ == "_FolderStore"
    # GLOBAL store is ALWAYS S3
    assert pl.get_global_store().__class__.__name__ == "_S3Store"


def test_folder_put_get_head_list_exists(monkeypatch, tmp_path):
    s3 = _FakeS3Client()
    pl = _fresh_pipeline(monkeypatch, {"SAVVY_LOCAL_DIR": str(tmp_path)}, s3)
    store = pl.get_primary_store()

    key = "pipeline/ctx0/doc1/stage.raw.json"
    payload = b'{"hello":"world"}'
    store.put_bytes(key, payload, content_type="application/json")

    # exists / head / get_bytes
    assert store.exists(key) is True
    assert store.exists("pipeline/ctx0/doc1/stage.tokenized.json") is False
    assert store.head(key) == len(payload)
    assert store.get_bytes(key) == payload

    # mirrors the S3 key layout on disk, bytes stored VERBATIM
    on_disk = tmp_path / "pipeline" / "ctx0" / "doc1" / "stage.raw.json"
    assert on_disk.is_file()
    assert on_disk.read_bytes() == payload

    # list under a prefix returns S3-style keys; sidecars/partials excluded
    store.put_bytes("pipeline/ctx0/doc2/stage.raw.json", b"x")
    keys = sorted(store.list("pipeline/ctx0"))
    assert keys == [
        "pipeline/ctx0/doc1/stage.raw.json",
        "pipeline/ctx0/doc2/stage.raw.json",
    ]


def test_folder_get_stream_and_missing(monkeypatch, tmp_path):
    s3 = _FakeS3Client()
    pl = _fresh_pipeline(monkeypatch, {"SAVVY_LOCAL_DIR": str(tmp_path)}, s3)
    store = pl.get_primary_store()

    key = "pipeline/ctx/doc/stage.hidden.json"
    store.put_stream(key, io.BytesIO(b"abc123"))
    buf = io.BytesIO()
    store.get_stream(key, buf)
    assert buf.getvalue() == b"abc123"

    # a miss surfaces as a not-found (mapped by _is_not_found → savvy-first fallthrough)
    with pytest.raises(Exception) as ei:
        store.get_bytes("pipeline/ctx/doc/nope.json")
    assert pl._is_not_found(ei.value) is True


def test_folder_metadata_sidecar(monkeypatch, tmp_path):
    s3 = _FakeS3Client()
    pl = _fresh_pipeline(monkeypatch, {"SAVVY_LOCAL_DIR": str(tmp_path)}, s3)
    store = pl.get_primary_store()
    key = "pipeline/ctx/doc/atom.npy"
    store.put_bytes(key, b"NPYBYTES", content_type="application/octet-stream",
                    metadata={"content-sha256": "deadbeef"})
    # atom bytes are verbatim; metadata lives in the sidecar, not framing the atom
    assert (tmp_path / "pipeline" / "ctx" / "doc" / "atom.npy").read_bytes() == b"NPYBYTES"
    sidecar = tmp_path / "pipeline" / "ctx" / "doc" / "atom.npy.meta.json"
    assert sidecar.is_file()
    import json as _json
    side = _json.loads(sidecar.read_text())
    assert side["Metadata"]["content-sha256"] == "deadbeef"
    # the sidecar is NOT itself listed as an atom
    assert list(store.list("pipeline/ctx/doc")) == ["pipeline/ctx/doc/atom.npy"]


def test_folder_copy_in_pulls_global_s3_to_local_file(monkeypatch, tmp_path):
    """In folder mode copy-in must pull GLOBAL S3 → a local file (savvy=fs, global=S3)."""
    s3 = _FakeS3Client()
    pl = _fresh_pipeline(
        monkeypatch,
        {"SAVVY_LOCAL_DIR": str(tmp_path),
         "GARAGE_URL": "http://savvy:3900",
         "GLOBAL_S3_URL": "http://global:3900"},
        s3,
    )
    key = "pipeline/ctx/doc/stage.hidden.json"
    # seed the atom ONLY in global S3 (with content-type + metadata to verify preservation)
    s3.objects[(pl.GARAGE_BUCKET, key)] = {
        "Body": b'{"big":"atom"}',
        "ContentType": "application/json",
        "Metadata": {"content-sha256": "cafef00d"},
    }
    assert pl.get_primary_store().exists(key) is False     # not yet local
    assert pl._copy_in_object(key) is True                 # copies global → local
    # now resident on the local filesystem, bytes verbatim
    local = tmp_path / "pipeline" / "ctx" / "doc" / "stage.hidden.json"
    assert local.is_file() and local.read_bytes() == b'{"big":"atom"}'
    # metadata preserved via sidecar (graduation content-sha256 verify)
    side_path = tmp_path / "pipeline" / "ctx" / "doc" / "stage.hidden.json.meta.json"
    import json as _json
    assert _json.loads(side_path.read_text())["Metadata"]["content-sha256"] == "cafef00d"
    # idempotent: a second copy-in short-circuits on exists, no extra global get
    n_get_before = sum(1 for c in s3.calls if c[0] == "get_object")
    assert pl._copy_in_object(key) is True
    assert sum(1 for c in s3.calls if c[0] == "get_object") == n_get_before


def test_folder_read_stage_savvy_first_then_global_copy_in(monkeypatch, tmp_path):
    """End-to-end read_stage in folder mode: miss-local → read global S3 → copy-in local."""
    s3 = _FakeS3Client()
    pl = _fresh_pipeline(
        monkeypatch,
        {"SAVVY_LOCAL_DIR": str(tmp_path),
         "GARAGE_URL": "http://savvy:3900",
         "GLOBAL_S3_URL": "http://global:3900"},
        s3,
    )
    key = pl.storage_key("ctx", "doc", "raw")
    s3.objects[(pl.GARAGE_BUCKET, key)] = {
        "Body": b'{"v":1}', "ContentType": "application/json", "Metadata": None}
    out = pl.read_stage(key)
    assert out == {"v": 1}
    # copy-in landed the atom locally for the next reader
    assert pl.get_primary_store().exists(key) is True


# ── S3 mode (SAVVY_LOCAL_DIR UNSET): the S3 path is untouched ────────────────────

def test_s3_mode_default_no_folder(monkeypatch, tmp_path):
    s3 = _FakeS3Client()
    pl = _fresh_pipeline(monkeypatch, {}, s3)         # SAVVY_LOCAL_DIR unset
    assert pl.SAVVY_LOCAL_DIR == ""
    # global == savvy by default → SAVVY off (pure no-op), primary is the S3 store
    assert pl.SAVVY_ENABLED is False
    assert pl.get_primary_store().__class__.__name__ == "_S3Store"


def test_s3_write_stage_is_exact_put_object(monkeypatch, tmp_path):
    s3 = _FakeS3Client()
    pl = _fresh_pipeline(monkeypatch, {}, s3)
    key = pl.write_stage("ctx", "doc", "raw", {"a": 1})
    assert key == "pipeline/ctx/doc/stage.raw.json"
    # exactly one put_object, to the data bucket, application/json — byte-identical path
    puts = [c for c in s3.calls if c[0] == "put_object"]
    assert len(puts) == 1
    _, bucket, k, ctype, kw = puts[0]
    assert (bucket, k, ctype) == (pl.GARAGE_BUCKET, key, "application/json")
    assert "Metadata" not in kw                       # no Metadata kwarg when none given
    # bytes are the JSON payload
    assert s3.objects[(pl.GARAGE_BUCKET, key)]["Body"] == b'{"a": 1}'


def test_s3_read_stage_is_exact_get_object(monkeypatch, tmp_path):
    s3 = _FakeS3Client()
    pl = _fresh_pipeline(monkeypatch, {}, s3)
    key = "pipeline/ctx/doc/stage.raw.json"
    s3.objects[(pl.GARAGE_BUCKET, key)] = {
        "Body": b'{"x":42}', "ContentType": "application/json", "Metadata": None}
    assert pl.read_stage(key) == {"x": 42}
    assert ("get_object", pl.GARAGE_BUCKET, key) in s3.calls


def test_s3_stage_exists_and_head_size(monkeypatch, tmp_path):
    s3 = _FakeS3Client()
    pl = _fresh_pipeline(monkeypatch, {}, s3)
    key = pl.storage_key("ctx", "doc", "raw")
    assert pl.stage_exists("ctx", "doc", "raw") is False
    s3.objects[(pl.GARAGE_BUCKET, key)] = {
        "Body": b"123456", "ContentType": "application/json", "Metadata": None}
    assert pl.stage_exists("ctx", "doc", "raw") is True
    assert pl.head_atom(key) is True
    assert pl.head_atom_size(key) == 6


def test_s3_path_makes_no_filesystem_access(monkeypatch, tmp_path):
    """With SAVVY_LOCAL_DIR unset the primary store never touches the filesystem — every
    op is a recorded boto3 call. (Sanity: the _FolderStore is never constructed.)"""
    s3 = _FakeS3Client()
    pl = _fresh_pipeline(monkeypatch, {}, s3)
    pl.write_stage("ctx", "doc", "raw", {"a": 1})
    pl.write_meta("ctx", "doc", {"title": "t"})
    # every primary mutation went through boto3
    assert all(c[0] in ("put_object", "get_object", "head_object",
                        "upload_fileobj", "download_fileobj", "delete_object")
               for c in s3.calls)
    # tmp_path stayed empty — no stray folder backend writes
    assert list(tmp_path.iterdir()) == []
