"""Unit tests for Hindsight LangGraph BaseStore adapter."""

import json
from unittest.mock import AsyncMock, MagicMock

import pytest
from hindsight_langgraph.errors import HindsightError
from hindsight_langgraph.store import (
    HindsightStore,
    _namespace_to_bank_id,
    _parse_value,
)


def _mock_client():
    client = MagicMock()
    client.aretain = AsyncMock()
    client.arecall = AsyncMock()
    client.acreate_bank = AsyncMock()
    return client


def _mock_recall_response(texts: list[str], document_ids: list[str] | None = None):
    response = MagicMock()
    results = []
    for i, t in enumerate(texts):
        r = MagicMock()
        r.text = t
        r.document_id = document_ids[i] if document_ids else None
        r.occurred_start = None
        results.append(r)
    response.results = results
    return response


class TestNamespaceMapping:
    def test_simple_namespace(self):
        assert _namespace_to_bank_id(("user", "123")) == "user.123"

    def test_single_element(self):
        assert _namespace_to_bank_id(("memories",)) == "memories"

    def test_empty_namespace(self):
        assert _namespace_to_bank_id(()) == "default"

    def test_deep_namespace(self):
        assert (
            _namespace_to_bank_id(("org", "team", "user", "123")) == "org.team.user.123"
        )


class TestParseValue:
    def test_parses_json_dict(self):
        assert _parse_value('{"name": "Alice"}') == {"name": "Alice"}

    def test_wraps_plain_text(self):
        assert _parse_value("hello world") == {"text": "hello world"}

    def test_wraps_json_non_dict(self):
        assert _parse_value("[1, 2, 3]") == {"text": "[1, 2, 3]"}


class TestHindsightStorePut:
    @pytest.mark.asyncio
    async def test_put_calls_retain(self):
        client = _mock_client()
        store = HindsightStore(client=client)

        await store.aput(("user", "123"), "pref-1", {"color": "blue"})

        client.aretain.assert_called_once()
        call_kwargs = client.aretain.call_args[1]
        assert call_kwargs["bank_id"] == "user.123"
        assert call_kwargs["document_id"] == "pref-1"
        assert json.loads(call_kwargs["content"]) == {"color": "blue"}

    @pytest.mark.asyncio
    async def test_put_passes_tags(self):
        client = _mock_client()
        store = HindsightStore(client=client, tags=["source:langgraph"])

        await store.aput(("user", "123"), "key", {"value": 1})

        call_kwargs = client.aretain.call_args[1]
        assert call_kwargs["tags"] == ["source:langgraph"]

    @pytest.mark.asyncio
    async def test_put_tracks_namespace(self):
        client = _mock_client()
        store = HindsightStore(client=client)

        await store.aput(("user", "123"), "key", {"value": 1})

        namespaces = await store.alist_namespaces()
        assert ("user", "123") in namespaces

    @pytest.mark.asyncio
    async def test_put_none_value_is_delete_noop(self):
        client = _mock_client()
        store = HindsightStore(client=client)

        await store.adelete(("user", "123"), "key")

        client.aretain.assert_not_called()

    @pytest.mark.asyncio
    async def test_put_raises_on_error(self):
        client = _mock_client()
        client.aretain.side_effect = RuntimeError("connection refused")
        store = HindsightStore(client=client)

        with pytest.raises(HindsightError, match="Store put failed"):
            await store.aput(("user", "123"), "key", {"value": 1})


class TestHindsightStoreGet:
    @pytest.mark.asyncio
    async def test_get_returns_item_by_document_id(self):
        client = _mock_client()
        client.arecall.return_value = _mock_recall_response(
            ['{"color": "blue"}'], document_ids=["pref-1"]
        )
        store = HindsightStore(client=client)

        item = await store.aget(("user", "123"), "pref-1")

        assert item is not None
        assert item.namespace == ("user", "123")
        assert item.key == "pref-1"
        assert item.value == {"color": "blue"}

    @pytest.mark.asyncio
    async def test_get_returns_none_when_empty(self):
        client = _mock_client()
        client.arecall.return_value = _mock_recall_response([])
        store = HindsightStore(client=client)

        item = await store.aget(("user", "123"), "nonexistent")

        assert item is None

    @pytest.mark.asyncio
    async def test_get_handles_error_gracefully(self):
        client = _mock_client()
        client.arecall.side_effect = RuntimeError("timeout")
        store = HindsightStore(client=client)

        item = await store.aget(("user", "123"), "key")

        assert item is None


class TestHindsightStoreSearch:
    @pytest.mark.asyncio
    async def test_search_returns_results(self):
        client = _mock_client()
        client.arecall.return_value = _mock_recall_response(
            ["User likes Python", "User is in NYC"]
        )
        store = HindsightStore(client=client)

        results = await store.asearch(("user", "123"), query="preferences")

        assert len(results) == 2
        assert results[0].value == {"text": "User likes Python"}
        assert results[1].value == {"text": "User is in NYC"}

    @pytest.mark.asyncio
    async def test_search_respects_limit(self):
        client = _mock_client()
        client.arecall.return_value = _mock_recall_response(
            ["fact1", "fact2", "fact3", "fact4", "fact5"]
        )
        store = HindsightStore(client=client)

        results = await store.asearch(("user", "123"), query="facts", limit=2)

        assert len(results) == 2

    @pytest.mark.asyncio
    async def test_search_empty_results(self):
        client = _mock_client()
        client.arecall.return_value = _mock_recall_response([])
        store = HindsightStore(client=client)

        results = await store.asearch(("user", "123"), query="anything")

        assert results == []

    @pytest.mark.asyncio
    async def test_search_with_filter(self):
        client = _mock_client()
        client.arecall.return_value = _mock_recall_response(
            [
                '{"type": "preference", "text": "likes Python"}',
                '{"type": "fact", "text": "lives in NYC"}',
            ]
        )
        store = HindsightStore(client=client)

        results = await store.asearch(
            ("user", "123"), query="info", filter={"type": "preference"}
        )

        assert len(results) == 1
        assert results[0].value["type"] == "preference"


class TestHindsightStoreListNamespaces:
    @pytest.mark.asyncio
    async def test_lists_known_namespaces(self):
        client = _mock_client()
        store = HindsightStore(client=client)

        await store.aput(("user", "123"), "k1", {"v": 1})
        await store.aput(("user", "456"), "k2", {"v": 2})

        namespaces = await store.alist_namespaces()

        assert len(namespaces) == 2
        assert ("user", "123") in namespaces
        assert ("user", "456") in namespaces

    @pytest.mark.asyncio
    async def test_list_respects_max_depth(self):
        """max_depth truncates deep namespaces and deduplicates per BaseStore contract."""
        client = _mock_client()
        store = HindsightStore(client=client)

        await store.aput(("a",), "k", {"v": 1})
        await store.aput(("a", "b", "c"), "k", {"v": 2})
        await store.aput(("x", "y"), "k", {"v": 3})

        namespaces = await store.alist_namespaces(max_depth=1)

        # ("a",) stays as-is, ("a", "b", "c") truncated to ("a",) and deduped,
        # ("x", "y") truncated to ("x",)
        assert ("a",) in namespaces
        assert ("x",) in namespaces
        assert ("a", "b", "c") not in namespaces
        assert ("x", "y") not in namespaces
        assert len(namespaces) == 2

    @pytest.mark.asyncio
    async def test_list_filters_by_prefix(self):
        client = _mock_client()
        store = HindsightStore(client=client)

        await store.aput(("user", "123"), "k1", {"v": 1})
        await store.aput(("user", "456"), "k2", {"v": 2})
        await store.aput(("org", "abc"), "k3", {"v": 3})

        namespaces = await store.alist_namespaces(prefix=("user",))

        assert ("user", "123") in namespaces
        assert ("user", "456") in namespaces
        assert ("org", "abc") not in namespaces

    @pytest.mark.asyncio
    async def test_list_filters_by_suffix(self):
        client = _mock_client()
        store = HindsightStore(client=client)

        await store.aput(("user", "prefs"), "k1", {"v": 1})
        await store.aput(("org", "prefs"), "k2", {"v": 2})
        await store.aput(("user", "history"), "k3", {"v": 3})

        namespaces = await store.alist_namespaces(suffix=("prefs",))

        assert ("user", "prefs") in namespaces
        assert ("org", "prefs") in namespaces
        assert ("user", "history") not in namespaces

    @pytest.mark.asyncio
    async def test_list_filters_by_prefix_and_suffix(self):
        client = _mock_client()
        store = HindsightStore(client=client)

        await store.aput(("user", "prefs"), "k1", {"v": 1})
        await store.aput(("org", "prefs"), "k2", {"v": 2})
        await store.aput(("user", "history"), "k3", {"v": 3})

        namespaces = await store.alist_namespaces(prefix=("user",), suffix=("prefs",))

        assert namespaces == [("user", "prefs")]

    @pytest.mark.asyncio
    async def test_list_respects_limit(self):
        client = _mock_client()
        store = HindsightStore(client=client)

        for i in range(5):
            await store.aput((f"ns-{i}",), "k", {"v": i})

        namespaces = await store.alist_namespaces(limit=2)

        assert len(namespaces) == 2
