"""Tests for robomotion.lmo module-level singleton functions (pack_value, resolve_blob_ref_value, etc.)."""
import json
import os
import shutil
import tempfile

import pytest

import robomotion.lmo as lmo_mod
from robomotion.lmo import (
    MAGIC,
    THRESHOLD,
    Store,
    _hash_ref,
    is_blob_ref,
)


@pytest.fixture(autouse=True)
def reset_lmo_singleton():
    """Ensure the module-level _store is reset before and after each test."""
    lmo_mod._store = None
    yield
    if lmo_mod._store is not None:
        lmo_mod._store.purge()
        lmo_mod._store = None


@pytest.fixture
def tmp_dir():
    d = tempfile.mkdtemp()
    yield d
    shutil.rmtree(d, ignore_errors=True)


def _setup_store(tmp_dir, rel_path="test/rel"):
    """Create and install a module-level store for testing."""
    s = Store(tmp_dir)
    s.set_rel_path(rel_path)
    lmo_mod._store = s
    return s


def _large_string(n=THRESHOLD):
    return "x" * n


# ── lmo_active ──


class TestLmoActive:
    def test_inactive_by_default(self):
        assert lmo_mod.lmo_active() is False

    def test_active_after_store_set(self, tmp_dir):
        _setup_store(tmp_dir)
        assert lmo_mod.lmo_active() is True


# ── pack_value ──


class TestPackValue:
    def test_returns_false_when_no_store(self):
        result, ok = lmo_mod.pack_value({"big": _large_string()})
        assert ok is False
        assert result is None

    def test_returns_false_when_rel_path_empty(self, tmp_dir):
        lmo_mod._store = Store(tmp_dir)  # no set_rel_path
        result, ok = lmo_mod.pack_value({"big": _large_string()})
        assert ok is False
        assert result is None

    def test_returns_false_for_small_value(self, tmp_dir):
        _setup_store(tmp_dir)
        result, ok = lmo_mod.pack_value({"small": "hi"})
        assert ok is False

    def test_packs_large_value(self, tmp_dir):
        _setup_store(tmp_dir)
        value = {"big": _large_string()}
        packed, ok = lmo_mod.pack_value(value)
        assert ok is True
        assert isinstance(packed, dict)
        assert is_blob_ref(packed["big"])

    def test_pack_value_recursive(self, tmp_dir):
        """Only the large child is extracted, small stays inline."""
        _setup_store(tmp_dir)
        value = {
            "big": _large_string(THRESHOLD + 500),
            "small": "inline",
        }
        packed, ok = lmo_mod.pack_value(value)
        assert ok is True
        assert is_blob_ref(packed["big"])
        assert packed["small"] == "inline"


# ── resolve_blob_ref_value ──


class TestResolveBlobRefValue:
    def test_returns_none_when_no_store(self):
        blob_ref = {"__ref": "xxh3:deadbeef", "__magic": MAGIC, "__path": "p"}
        assert lmo_mod.resolve_blob_ref_value(blob_ref) is None

    def test_resolves_blob(self, tmp_dir):
        store = _setup_store(tmp_dir)
        data = json.dumps({"resolved": True}).encode("utf-8")
        ref = store.put_blob(data)
        blob_ref = {
            "__ref": ref,
            "__magic": MAGIC,
            "__path": store.rel_path,
        }
        result = lmo_mod.resolve_blob_ref_value(blob_ref)
        assert result == {"resolved": True}

    def test_learns_rel_path_from_blobref(self, tmp_dir):
        """If store has no rel_path, it should learn from the BlobRef."""
        # Write blob with a store that has rel_path
        writer = Store(tmp_dir)
        writer.set_rel_path("learned/path")
        data = json.dumps("learned value").encode("utf-8")
        ref = writer.put_blob(data)

        # Module store has no rel_path
        lmo_mod._store = Store(tmp_dir)
        assert lmo_mod._store.rel_path == ""

        blob_ref = {"__ref": ref, "__magic": MAGIC, "__path": "learned/path"}
        result = lmo_mod.resolve_blob_ref_value(blob_ref)
        assert result == "learned value"
        assert lmo_mod._store.rel_path == "learned/path"

    def test_uses_blobref_path_over_store_path(self, tmp_dir):
        """Even if store has a rel_path, BlobRef __path should be used for reading."""
        writer = Store(tmp_dir)
        writer.set_rel_path("writer/path")
        data = json.dumps(42).encode("utf-8")
        ref = writer.put_blob(data)

        _setup_store(tmp_dir, rel_path="reader/path")
        blob_ref = {"__ref": ref, "__magic": MAGIC, "__path": "writer/path"}
        result = lmo_mod.resolve_blob_ref_value(blob_ref)
        assert result == 42


# ── lmo_pack ──


class TestLmoPack:
    def test_noop_when_no_store(self):
        payload = b'{"key":"value"}'
        assert lmo_mod.lmo_pack(payload) is payload

    def test_packs_large_fields(self, tmp_dir):
        _setup_store(tmp_dir)
        original = {"big": _large_string(), "id": 1}
        payload = json.dumps(original, separators=(",", ":")).encode("utf-8")
        result = lmo_mod.lmo_pack(payload)
        msg = json.loads(result)
        assert is_blob_ref(msg["big"])
        assert msg["id"] == 1


# ── lmo_resolve ──


class TestLmoResolve:
    def test_noop_when_no_store(self):
        assert lmo_mod.lmo_resolve(b'{"a":1}', "a") is None

    def test_resolves_field(self, tmp_dir):
        store = _setup_store(tmp_dir)
        big = _large_string()
        payload = json.dumps({"big": big}, separators=(",", ":")).encode("utf-8")
        packed = store.pack(payload)
        result = lmo_mod.lmo_resolve(packed, "big")
        assert result == big


# ── lmo_resolve_all ──


class TestLmoResolveAll:
    def test_noop_when_no_store(self):
        data = b'{"a":1}'
        assert lmo_mod.lmo_resolve_all(data) is data

    def test_resolves_all(self, tmp_dir):
        store = _setup_store(tmp_dir)
        big = _large_string()
        original = {"big": big, "small": "ok"}
        payload = json.dumps(original, separators=(",", ":")).encode("utf-8")
        packed = store.pack(payload)
        resolved = lmo_mod.lmo_resolve_all(packed)
        assert json.loads(resolved) == original


# ── close_lmo ──


class TestCloseLmo:
    def test_close_sets_none(self, tmp_dir):
        _setup_store(tmp_dir)
        assert lmo_mod.lmo_active() is True
        lmo_mod.close_lmo()
        assert lmo_mod.lmo_active() is False

    def test_close_idempotent(self):
        lmo_mod.close_lmo()
        lmo_mod.close_lmo()  # should not raise
