"""Behavior tests for the built-in memory → external provider bridge.

The bridge lives behind the MemoryManager interface
(``MemoryManager.notify_memory_tool_write``): the agent loop hands over the raw
built-in memory tool result + args, and the manager decides whether/what to
mirror to external providers. These tests drive that method with a fake
external provider and assert which ``on_memory_write`` calls land.
"""

import json

import pytest

from agent.memory_manager import MemoryManager
from agent.memory_provider import MemoryProvider


class _RecordingProvider(MemoryProvider):
    """Minimal external provider that records on_memory_write calls."""

    def __init__(self) -> None:
        self.calls = []

    @property
    def name(self) -> str:
        return "recording"

    def is_available(self) -> bool:
        return True

    def initialize(self, session_id: str, **kwargs) -> None:
        pass

    def get_tool_schemas(self):
        return []

    def shutdown(self) -> None:
        pass

    def on_memory_write(self, action, target, content, metadata=None):
        self.calls.append({
            "action": action,
            "target": target,
            "content": content,
            "metadata": dict(metadata or {}),
        })


def _manager_with_provider():
    mgr = MemoryManager()
    provider = _RecordingProvider()
    mgr.add_provider(provider)
    return mgr, provider


def test_notifies_remove_with_old_text_after_success():
    mgr, provider = _manager_with_provider()
    mgr.notify_memory_tool_write(
        json.dumps({"success": True}),
        {"action": "remove", "target": "memory", "old_text": "stale preference entry"},
    )
    assert provider.calls == [
        {
            "action": "remove",
            "target": "memory",
            "content": "",
            "metadata": {"old_text": "stale preference entry"},
        }
    ]






@pytest.mark.parametrize("tool_result", [None, [], object(), "not-json"])
def test_skips_unrecognized_tool_result_shape(tool_result):
    mgr, provider = _manager_with_provider()
    mgr.notify_memory_tool_write(
        tool_result,
        {"action": "add", "target": "memory", "content": "new fact"},
    )
    assert provider.calls == []






def test_build_metadata_callback_is_merged_per_op():
    mgr, provider = _manager_with_provider()
    mgr.notify_memory_tool_write(
        json.dumps({"success": True}),
        {"action": "add", "target": "memory", "content": "fact"},
        build_metadata=lambda: {"session_id": "s1", "tool_name": "memory"},
    )
    assert provider.calls == [
        {
            "action": "add",
            "target": "memory",
            "content": "fact",
            "metadata": {"session_id": "s1", "tool_name": "memory"},
        }
    ]


@pytest.mark.parametrize('batch,operations,previous', [
    (False, [{'action': 'remove', 'old_text': 'Prefers tea'}], ['Prefers tea']),
    (False, [{'action': 'replace', 'old_text': 'Prefers tea', 'content': 'Prefers coffee'}], ['Prefers tea']),
    (True, [{'action': 'remove', 'old_text': 'Prefers tea'}], ['Prefers tea']),
    (True, [{'action': 'replace', 'old_text': 'Prefers tea', 'new_text': 'Prefers coffee'}], ['Prefers tea']),
    (True, [
        {'action': 'add', 'content': 'Uses the blue notebook'},
        {'action': 'replace', 'old_text': 'blue notebook', 'new_text': 'Uses the green notebook'},
        {'action': 'remove', 'old_text': 'green notebook'},
    ], [None, 'Uses the blue notebook', 'Uses the green notebook']),
])
def test_committed_entry_identity_comes_from_locked_store(
    tmp_path, monkeypatch, batch, operations, previous
):
    target = 'memory'
    from contextlib import contextmanager
    from tools import memory_tool_store
    from tools.memory_tool import MemoryStore, memory_tool

    monkeypatch.setattr('tools.memory_tool.get_memory_dir', lambda: tmp_path)
    store = MemoryStore()
    store.load_from_disk()
    store.add(target, 'Prefers tea')
    store.add(target, 'Prefers tea with milk')
    manager, provider = _manager_with_provider()
    locked = False
    lock = store._file_lock
    match = memory_tool_store._find_unique_match

    @contextmanager
    def checked_lock(path):
        nonlocal locked
        with lock(path):
            locked = True
            try:
                yield
            finally:
                locked = False

    def checked_match(entries, old_text):
        assert locked
        assert provider.calls == []
        return match(entries, old_text)

    monkeypatch.setattr(store, '_file_lock', checked_lock)
    monkeypatch.setattr(memory_tool_store, '_find_unique_match', checked_match)
    args = {'target': target, **({'operations': operations} if batch else operations[0])}
    result = memory_tool(store=store, **args)
    assert json.loads(result)['success'] is True
    assert not locked
    assert provider.calls == []
    reloaded = MemoryStore()
    reloaded.load_from_disk()
    assert reloaded._entries_for(target) == store._entries_for(target)

    manager.notify_memory_tool_write(result, args, build_metadata=lambda: {'session_id': 'test'})
    assert [call['action'] for call in provider.calls] == [op['action'] for op in operations]
    assert [call['metadata'].get('previous_content') for call in provider.calls] == previous
    assert all(call['metadata']['session_id'] == 'test' for call in provider.calls)
    assert 'Prefers tea with milk' in store._entries_for(target)


def test_previous_content_cannot_come_from_uncommitted_arguments():
    manager, provider = _manager_with_provider()
    args = {'action': 'remove', 'old_text': 'partial', 'previous_content': 'Untrusted argument'}
    manager.notify_memory_tool_write(
        {'success': True}, args, build_metadata=lambda: {'previous_content': 'Uncommitted metadata'}
    )
    assert provider.calls[0]['metadata'] == {'old_text': 'partial'}
