"""Tests for persistent agent memory."""

from pathlib import Path
from typing import Any

from pydantic_ai.models.test import TestModel
from pydantic_ai.tools import RunContext
from pydantic_ai.workspaces import LocalWorkspaceBackend, Workspace
from pydantic_ai_harness.memory import FileStore

from pydantic_deep import (
    DEFAULT_MAX_MEMORY_LINES,
    DEFAULT_MEMORY_DIR,
    DEFAULT_MEMORY_FILENAME,
    AgentMemoryToolset,
    DeepAgentDeps,
    MemoryFile,
    create_deep_agent,
    format_memory_prompt,
    get_memory_path,
    load_memory,
)
from tests.workspaces import Document, as_workspace, run_context

TEST_MODEL = TestModel()


def _make_ctx(backend: Document | Workspace | None = None) -> RunContext[DeepAgentDeps]:
    """Create a RunContext whose workspace is `backend` (or holds its files)."""
    workspace = backend if isinstance(backend, Workspace) else as_workspace(backend or Document())
    return run_context(DeepAgentDeps(), workspace)


class TestMemoryFile:
    """Tests for MemoryFile dataclass."""

    def test_create(self):
        """Test creating a MemoryFile."""
        mf = MemoryFile(
            agent_name="main",
            path="/.deep/memory/main/MEMORY.md",
            content="# Memory",
        )
        assert mf.agent_name == "main"
        assert mf.path == "/.deep/memory/main/MEMORY.md"
        assert mf.content == "# Memory"

    def test_create_subagent(self):
        """Test creating a MemoryFile for a subagent."""
        mf = MemoryFile(
            agent_name="code-reviewer",
            path="/.deep/memory/code-reviewer/MEMORY.md",
            content="Prefer Python 3.12",
        )
        assert mf.agent_name == "code-reviewer"


class TestGetMemoryPath:
    """Tests for get_memory_path function."""

    def test_default_dir(self):
        """Test path with default memory directory."""
        path = get_memory_path("/.deep/memory", "main")
        assert path == "/.deep/memory/main/MEMORY.md"

    def test_custom_dir(self):
        """Test path with custom memory directory."""
        path = get_memory_path("/workspace/.memory", "main")
        assert path == "/workspace/.memory/main/MEMORY.md"

    def test_subagent_name(self):
        """Test path with subagent name."""
        path = get_memory_path("/.deep/memory", "code-reviewer")
        assert path == "/.deep/memory/code-reviewer/MEMORY.md"

    def test_trailing_slash(self):
        """Test that trailing slash is handled correctly."""
        path = get_memory_path("/.deep/memory/", "main")
        assert path == "/.deep/memory/main/MEMORY.md"


class TestLoadMemory:
    """Tests for load_memory function."""

    async def test_load_existing(self):
        """Test loading an existing memory file."""
        backend = Document()
        backend.write("/.deep/memory/main/MEMORY.md", "# Memory\n- item 1")

        mem = await load_memory(as_workspace(backend), "/.deep/memory/main/MEMORY.md", "main")
        assert mem is not None
        assert mem.agent_name == "main"
        assert mem.path == "/.deep/memory/main/MEMORY.md"
        assert "item 1" in mem.content

    async def test_load_missing(self):
        """Test loading a missing memory file returns None."""
        backend = Document()
        mem = await load_memory(as_workspace(backend), "/.deep/memory/main/MEMORY.md", "main")
        assert mem is None

    async def test_load_utf8(self):
        """Test loading memory with non-ASCII content."""
        backend = Document()
        backend.write("/.deep/memory/main/MEMORY.md", "Polskie znaki: ąęćżź")

        mem = await load_memory(as_workspace(backend), "/.deep/memory/main/MEMORY.md", "main")
        assert mem is not None
        assert "ąęćżź" in mem.content

    async def test_load_default_agent_name(self):
        """Test load_memory with default agent_name."""
        backend = Document()
        backend.write("/mem/MEMORY.md", "content")

        mem = await load_memory(as_workspace(backend), "/mem/MEMORY.md")
        assert mem is not None
        assert mem.agent_name == "main"


class TestFormatMemoryPrompt:
    """Tests for format_memory_prompt function."""

    def test_short_content(self):
        """Test formatting memory that fits within max_lines."""
        mem = MemoryFile(agent_name="main", path="/m", content="line1\nline2\nline3")
        result = format_memory_prompt(mem, max_lines=200)

        assert "## Agent Memory (main)" in result
        assert "line1" in result
        assert "line2" in result
        assert "line3" in result
        assert "more lines" not in result

    def test_truncation_keeps_most_recent_lines(self):
        """Long memory keeps the most recent lines (the tail), not the oldest.

        Regression for #157: `write_memory` appends, so the newest entries are
        at the bottom of the file and must survive truncation.
        """
        lines = [f"line {i}" for i in range(300)]
        content = "\n".join(lines)
        mem = MemoryFile(agent_name="main", path="/m", content=content)
        result = format_memory_prompt(mem, max_lines=100)

        assert "## Agent Memory (main)" in result
        assert "line 299" in result  # newest kept
        assert "line 200" in result  # start of the kept tail (last 100 lines)
        assert "line 0" not in result  # oldest dropped
        assert "line 199" not in result  # just before the tail, dropped
        assert "200 more lines in memory" in result

    def test_pinned_head_survives_truncation(self):
        """Content above the pin marker is always injected; the body is tailed."""
        from pydantic_deep.features.memory import DEFAULT_PIN_END_MARKER

        head = f"# Identity\nfoundational note\n{DEFAULT_PIN_END_MARKER}\n"
        body = "\n".join(f"obs {i}" for i in range(50))
        mem = MemoryFile(agent_name="main", path="/m", content=head + body)
        result = format_memory_prompt(mem, max_lines=3)

        assert "# Identity" in result  # pinned head kept
        assert "foundational note" in result
        assert "obs 49" in result  # newest body line kept
        assert "obs 0" not in result  # oldest body line dropped
        assert DEFAULT_PIN_END_MARKER not in result  # marker itself not injected
        assert "47 more lines in memory" in result

    def test_pinned_head_only_empty_body(self):
        """A file that is only a pinned head injects the head with no marker."""
        from pydantic_deep.features.memory import DEFAULT_PIN_END_MARKER

        mem = MemoryFile(
            agent_name="main",
            path="/m",
            content=f"# Identity\nonly head here\n{DEFAULT_PIN_END_MARKER}",
        )
        result = format_memory_prompt(mem, max_lines=3)

        assert "# Identity" in result
        assert "only head here" in result
        assert "more lines in memory" not in result

    def test_max_tokens_takes_precedence(self):
        """`max_tokens` budgets the body and overrides `max_lines`."""
        lines = [f"line {i}" for i in range(300)]
        mem = MemoryFile(agent_name="main", path="/m", content="\n".join(lines))
        # ~7 chars/line, NUM_CHARS_PER_TOKEN=4 → 10 tokens ≈ 40 chars ≈ 5 lines.
        result = format_memory_prompt(mem, max_lines=300, max_tokens=10)

        assert "line 299" in result  # newest kept
        assert "line 0" not in result  # oldest dropped despite max_lines=300
        assert "more lines in memory" in result

    def test_max_tokens_large_budget_keeps_all(self):
        """A generous `max_tokens` keeps the whole body untruncated."""
        mem = MemoryFile(agent_name="main", path="/m", content="a\nb\nc")
        result = format_memory_prompt(mem, max_lines=2, max_tokens=10_000)

        assert "a" in result and "b" in result and "c" in result
        assert "more lines in memory" not in result

    def test_max_tokens_keeps_at_least_one_line(self):
        """A single line larger than the token budget is still kept."""
        mem = MemoryFile(
            agent_name="main",
            path="/m",
            content="old\n" + "x" * 1000,
        )
        result = format_memory_prompt(mem, max_lines=200, max_tokens=1)

        assert "x" * 1000 in result  # newest line kept even though it overflows
        assert "1 more lines in memory" in result

    def test_zero_max_lines_drops_all_body(self):
        """`max_lines=0` keeps no body lines, only the dropped-count marker."""
        mem = MemoryFile(agent_name="main", path="/m", content="a\nb\nc")
        result = format_memory_prompt(mem, max_lines=0)

        assert "3 more lines in memory" in result
        assert "## Agent Memory (main)" in result

    def test_exact_limit(self):
        """Test content at exactly max_lines."""
        lines = [f"line {i}" for i in range(10)]
        content = "\n".join(lines)
        mem = MemoryFile(agent_name="main", path="/m", content=content)
        result = format_memory_prompt(mem, max_lines=10)

        assert "more lines" not in result
        assert "line 9" in result

    def test_subagent_label(self):
        """Test that subagent name appears in prompt."""
        mem = MemoryFile(agent_name="code-reviewer", path="/m", content="review notes")
        result = format_memory_prompt(mem, max_lines=200)
        assert "## Agent Memory (code-reviewer)" in result

    def test_empty_content(self):
        """Test formatting empty memory content."""
        mem = MemoryFile(agent_name="main", path="/m", content="")
        result = format_memory_prompt(mem, max_lines=200)
        assert "## Agent Memory (main)" in result


class TestAgentMemoryToolset:
    """Tests for AgentMemoryToolset class."""

    def test_has_tools(self):
        """Test that toolset has read, write, update tools."""
        toolset = AgentMemoryToolset()
        tool_names = set(toolset.tools.keys())
        assert "read_memory" in tool_names
        assert "write_memory" in tool_names
        assert "update_memory" in tool_names

    def test_id(self):
        """Test toolset ID."""
        toolset = AgentMemoryToolset()
        assert toolset.id == "deep-memory"

    def test_default_params(self):
        """Test default constructor parameters."""
        toolset = AgentMemoryToolset()
        assert toolset._agent_name == "main"
        assert toolset._memory_dir == DEFAULT_MEMORY_DIR
        assert toolset._max_lines == DEFAULT_MAX_MEMORY_LINES

    def test_custom_params(self):
        """Test custom constructor parameters."""
        toolset = AgentMemoryToolset(
            agent_name="reviewer",
            memory_dir="/custom/mem",
            max_lines=50,
        )
        assert toolset._agent_name == "reviewer"
        assert toolset._memory_dir == "/custom/mem"
        assert toolset._max_lines == 50
        assert toolset._path == "/custom/mem/reviewer/MEMORY.md"

    async def test_get_instructions_no_memory(self):
        """Test get_instructions when no memory file exists."""
        ctx = _make_ctx()
        toolset = AgentMemoryToolset()
        result = await toolset.get_instructions(ctx)
        assert result is None

    async def test_get_instructions_with_memory(self):
        """Test get_instructions injects existing memory."""
        backend = Document()
        backend.write("/.deep/memory/main/MEMORY.md", "# Notes\n- Important item")
        ctx = _make_ctx(backend)

        toolset = AgentMemoryToolset()
        result = await toolset.get_instructions(ctx)

        assert result is not None
        joined = "\n\n".join(p.content for p in result)
        assert "## Agent Memory (main)" in joined
        assert "Important item" in joined

    async def test_get_instructions_truncated(self):
        """Test get_instructions truncates long memory."""
        backend = Document()
        lines = [f"line {i}" for i in range(300)]
        backend.write("/.deep/memory/main/MEMORY.md", "\n".join(lines))
        ctx = _make_ctx(backend)

        toolset = AgentMemoryToolset(max_lines=50)
        result = await toolset.get_instructions(ctx)

        assert result is not None
        joined = "\n\n".join(p.content for p in result)
        assert "250 more lines in memory" in joined


class TestMemoryTools:
    """Tests for individual memory tool functions."""

    async def test_read_memory_empty(self):
        """Test read_memory when no memory exists."""
        ctx = _make_ctx()
        toolset = AgentMemoryToolset()
        result = await toolset.tools["read_memory"].function(ctx)
        assert result == "No memory saved yet."

    async def test_read_memory_existing(self):
        """Test read_memory with existing memory."""
        backend = Document()
        backend.write("/.deep/memory/main/MEMORY.md", "# My Notes\nImportant")
        ctx = _make_ctx(backend)

        toolset = AgentMemoryToolset()
        result = await toolset.tools["read_memory"].function(ctx)
        assert "My Notes" in result
        assert "Important" in result

    async def test_write_memory_new(self):
        """Test write_memory creates new memory file."""
        backend = Document()
        ctx = _make_ctx(backend)

        toolset = AgentMemoryToolset()
        result = await toolset.tools["write_memory"].function(ctx, "# First entry")

        assert "Memory updated" in result
        # Verify file was created
        raw = backend.read_bytes("/.deep/memory/main/MEMORY.md")
        assert raw is not None
        assert b"First entry" in raw

    async def test_write_memory_append(self):
        """Test write_memory appends to existing memory."""
        backend = Document()
        backend.write("/.deep/memory/main/MEMORY.md", "# Existing")
        ctx = _make_ctx(backend)

        toolset = AgentMemoryToolset()
        result = await toolset.tools["write_memory"].function(ctx, "New entry")

        assert "Memory updated" in result
        raw = backend.read_bytes("/.deep/memory/main/MEMORY.md")
        assert raw is not None
        content = raw.decode("utf-8")
        assert "Existing" in content
        assert "New entry" in content
        # Should be separated by double newline
        assert "# Existing\n\nNew entry" in content

    async def test_update_memory_success(self):
        """Test update_memory replaces text."""
        backend = Document()
        backend.write("/.deep/memory/main/MEMORY.md", "Use Python 3.11")
        ctx = _make_ctx(backend)

        toolset = AgentMemoryToolset()
        result = await toolset.tools["update_memory"].function(ctx, "Python 3.11", "Python 3.12")

        assert "Memory updated" in result
        raw = backend.read_bytes("/.deep/memory/main/MEMORY.md")
        assert raw is not None
        assert b"Python 3.12" in raw
        assert b"Python 3.11" not in raw

    async def test_update_memory_not_found(self):
        """Test update_memory when old_text is not found."""
        backend = Document()
        backend.write("/.deep/memory/main/MEMORY.md", "Some content")
        ctx = _make_ctx(backend)

        toolset = AgentMemoryToolset()
        result = await toolset.tools["update_memory"].function(ctx, "nonexistent", "replacement")

        assert "Text not found" in result

    async def test_update_memory_no_file(self):
        """Test update_memory when no memory file exists."""
        ctx = _make_ctx()
        toolset = AgentMemoryToolset()
        result = await toolset.tools["update_memory"].function(ctx, "old", "new")
        assert "No memory exists yet" in result

    async def test_update_memory_multiple_matches(self):
        """Test update_memory refuses to replace a non-unique old_text."""
        backend = Document()
        backend.write("/.deep/memory/main/MEMORY.md", "foo\nfoo\nbar")
        ctx = _make_ctx(backend)

        toolset = AgentMemoryToolset()
        result = await toolset.tools["update_memory"].function(ctx, "foo", "baz")

        assert "appears 2 times" in result
        assert "must be" in result
        # Memory is left unchanged.
        raw = backend.read_bytes("/.deep/memory/main/MEMORY.md")
        assert raw == b"foo\nfoo\nbar"

    async def test_write_memory_custom_path(self):
        """Test write_memory with custom agent name and dir."""
        backend = Document()
        ctx = _make_ctx(backend)

        toolset = AgentMemoryToolset(
            agent_name="reviewer",
            memory_dir="/custom",
        )
        await toolset.tools["write_memory"].function(ctx, "Review notes")

        raw = backend.read_bytes("/custom/reviewer/MEMORY.md")
        assert raw is not None
        assert b"Review notes" in raw


class TestCreateDeepAgentMemory:
    """Tests for create_deep_agent with memory parameters."""

    def test_create_with_include_memory(self):
        """Test creating an agent with include_memory=True."""
        agent = create_deep_agent(
            model=TEST_MODEL,
            include_memory=True,
        )
        assert agent is not None

    def test_create_with_custom_memory_dir(self):
        """Test creating an agent with custom memory_dir."""
        agent = create_deep_agent(
            model=TEST_MODEL,
            include_memory=True,
            memory_dir="/workspace/.memory",
        )
        assert agent is not None

    def test_create_without_memory(self):
        """Test default agent has no memory toolset."""
        agent = create_deep_agent(model=TEST_MODEL)
        assert agent is not None

    def test_create_with_memory_and_output_type(self):
        """Test memory works with structured output."""
        from pydantic import BaseModel

        class Result(BaseModel):
            summary: str

        agent = create_deep_agent(
            model=TEST_MODEL,
            include_memory=True,
            output_type=Result,
        )
        assert agent is not None


class TestPerSubagentMemory:
    """A subagent built by its own factory gets the memory tools as a toolset."""

    @staticmethod
    def _memory_toolsets(config: Any) -> list[Any]:
        from pydantic_ai_harness.memory import MemoryToolset

        return [t for t in config.get("toolsets", []) if isinstance(t, MemoryToolset)]

    def test_subagent_gets_its_own_memory_tools(self):
        from pydantic_deep.agent import _inject_subagent_memory_toolset
        from pydantic_deep.types import SubAgentConfig

        config = SubAgentConfig(name="reviewer", description="Code reviewer", instructions="Review")
        _inject_subagent_memory_toolset(config, FileStore(".deep/memory"), "")

        [toolset] = self._memory_toolsets(config)
        assert toolset._capability.agent_name == "reviewer"

    def test_subagent_memory_disabled_via_extra(self):
        from pydantic_deep.agent import _inject_subagent_memory_toolset
        from pydantic_deep.types import SubAgentConfig

        config = SubAgentConfig(
            name="worker", description="Worker", instructions="Do work", extra={"memory": False}
        )
        _inject_subagent_memory_toolset(config, FileStore(".deep/memory"), "")

        assert self._memory_toolsets(config) == []

    def test_subagent_memory_custom_max_lines(self):
        from pydantic_deep.agent import _inject_subagent_memory_toolset
        from pydantic_deep.types import SubAgentConfig

        config = SubAgentConfig(
            name="analyst",
            description="Analyst",
            instructions="Analyze",
            extra={"memory_max_lines": 50},
        )
        _inject_subagent_memory_toolset(config, FileStore(".deep/memory"), "")

        [toolset] = self._memory_toolsets(config)
        assert toolset._capability.max_lines == 50

    def test_subagent_preserves_existing_toolsets(self):
        from pydantic_ai.toolsets import FunctionToolset

        from pydantic_deep.agent import _inject_subagent_memory_toolset
        from pydantic_deep.types import SubAgentConfig

        existing_toolset = FunctionToolset(id="custom")
        config = SubAgentConfig(
            name="worker", description="Worker", instructions="Work", toolsets=[existing_toolset]
        )
        _inject_subagent_memory_toolset(config, FileStore(".deep/memory"), "")

        assert config["toolsets"][0] is existing_toolset
        assert len(self._memory_toolsets(config)) == 1

    def test_no_subagent_memory_when_include_memory_false(self):
        """create_deep_agent(include_memory=False) does not inject subagent memory
        and never mutates the caller's config dict."""
        from pydantic_deep.types import SubAgentConfig

        config = SubAgentConfig(
            name="worker",
            description="Worker",
            instructions="Work",
        )
        create_deep_agent(
            model=TEST_MODEL,
            subagents=[config],
            include_memory=False,
        )

        # Caller's dict is left untouched (no toolsets key injected at all).
        if "toolsets" in config:
            toolset_types = [type(t).__name__ for t in config["toolsets"]]
            assert "AgentMemoryToolset" not in toolset_types


class TestMemoryConstants:
    """Tests for memory module constants."""

    def test_default_memory_dir(self):
        """Test DEFAULT_MEMORY_DIR value."""
        assert DEFAULT_MEMORY_DIR == ".deep/memory"

    def test_default_memory_filename(self):
        """Test DEFAULT_MEMORY_FILENAME value."""
        assert DEFAULT_MEMORY_FILENAME == "MEMORY.md"

    def test_default_max_memory_lines(self):
        """Test DEFAULT_MAX_MEMORY_LINES value."""
        assert DEFAULT_MAX_MEMORY_LINES == 200


class TestMemoryExports:
    """Tests for memory exports from the package."""

    def test_agent_memory_toolset_exported(self):
        """Test AgentMemoryToolset is importable from package."""
        from pydantic_deep import AgentMemoryToolset as exported

        assert exported is AgentMemoryToolset

    def test_memory_file_exported(self):
        """Test MemoryFile is importable from package."""
        from pydantic_deep import MemoryFile as exported

        assert exported is MemoryFile

    def test_load_memory_exported(self):
        """Test load_memory is importable from package."""
        from pydantic_deep import load_memory as exported

        assert exported is load_memory

    def test_get_memory_path_exported(self):
        """Test get_memory_path is importable from package."""
        from pydantic_deep import get_memory_path as exported

        assert exported is get_memory_path

    def test_format_memory_prompt_exported(self):
        """Test format_memory_prompt is importable from package."""
        from pydantic_deep import format_memory_prompt as exported

        assert exported is format_memory_prompt

    def test_constants_exported(self):
        """Test all memory constants are importable."""
        from pydantic_deep import (
            DEFAULT_MAX_MEMORY_LINES,
            DEFAULT_MEMORY_DIR,
            DEFAULT_MEMORY_FILENAME,
        )

        assert DEFAULT_MEMORY_DIR is not None
        assert DEFAULT_MEMORY_FILENAME is not None
        assert DEFAULT_MAX_MEMORY_LINES is not None


class _WriteDenied(Document):
    """A document whose reads work but whose writes are always refused."""

    deny = False

    def write_bytes(self, path: str, data: bytes) -> None:
        if self.deny:
            raise OSError("disk quota exceeded")
        super().write_bytes(path, data)


def _write_deny_backend(seed_path: str | None = None, seed: bytes = b"") -> _WriteDenied:
    """Isolates "memory is readable but the workspace refuses the write" (issue #135)."""
    backend = _WriteDenied()
    if seed_path is not None:
        backend.write(seed_path, seed)
    backend.deny = True
    return backend


def _denied_backend_and_dir(tmp_path: Path) -> tuple[Workspace, str]:
    """A local workspace whose memory file exists but cannot be read."""
    memory_dir = str(tmp_path / "memory")
    memory_file = Path(get_memory_path(memory_dir, "main"))
    memory_file.parent.mkdir(parents=True)
    memory_file.write_text("secret")
    memory_file.chmod(0)
    return Workspace(LocalWorkspaceBackend(tmp_path)), memory_dir


class TestMemoryFailureSurfacing:
    """Issue #135 — backend write/permission failures must be visible."""

    async def test_load_memory_raises_on_denied_path(self, tmp_path):
        """A denied path raises MemoryAccessError, not a silent None."""
        from pydantic_deep import MemoryAccessError

        workspace, memory_dir = _denied_backend_and_dir(tmp_path)
        path = get_memory_path(memory_dir, "main")
        try:
            await load_memory(workspace, path, "main")
            raise AssertionError("expected MemoryAccessError")
        except MemoryAccessError as exc:
            assert "denied" in str(exc).lower()

    async def test_load_memory_raises_on_a_directory(self, tmp_path):
        from pydantic_deep import MemoryAccessError

        Path(get_memory_path(str(tmp_path), "main")).mkdir(parents=True)
        workspace = Workspace(LocalWorkspaceBackend(tmp_path))
        try:
            await load_memory(workspace, get_memory_path(str(tmp_path), "main"), "main")
            raise AssertionError("expected MemoryAccessError")
        except MemoryAccessError:
            pass

    async def test_load_memory_empty_existing_file_returns_none(self):
        """An empty (but accessible) file is missing/empty memory, not an error."""
        backend = Document()
        path = get_memory_path(DEFAULT_MEMORY_DIR, "main")
        backend.write(path, b"")
        assert await load_memory(as_workspace(backend), path, "main") is None

    async def test_read_memory_surfaces_denied_access(self, tmp_path):
        """read_memory reports an error instead of 'No memory saved yet.'."""
        backend, memory_dir = _denied_backend_and_dir(tmp_path)
        toolset = AgentMemoryToolset(agent_name="main", memory_dir=memory_dir)
        ctx = _make_ctx(backend)
        result = await toolset.tools["read_memory"].function(ctx)
        assert result.startswith("Error:")
        assert "No memory saved yet" not in result

    async def test_write_memory_surfaces_denied_access(self, tmp_path):
        """write_memory reports the access failure instead of phantom success."""
        backend, memory_dir = _denied_backend_and_dir(tmp_path)
        toolset = AgentMemoryToolset(agent_name="main", memory_dir=memory_dir)
        ctx = _make_ctx(backend)
        result = await toolset.tools["write_memory"].function(ctx, content="note")
        assert result.startswith("Error:")
        assert "Memory updated" not in result

    async def test_write_memory_surfaces_write_result_error(self):
        """A rejected backend write is not reported as a successful update."""
        backend = _write_deny_backend()
        toolset = AgentMemoryToolset(agent_name="main", memory_dir=DEFAULT_MEMORY_DIR)
        ctx = _make_ctx(backend)
        result = await toolset.tools["write_memory"].function(ctx, content="note")
        assert result.startswith("Error:")
        assert "disk quota exceeded" in result

    async def test_update_memory_surfaces_denied_access(self, tmp_path):
        """update_memory reports the access failure instead of normal-looking output."""
        backend, memory_dir = _denied_backend_and_dir(tmp_path)
        toolset = AgentMemoryToolset(agent_name="main", memory_dir=memory_dir)
        ctx = _make_ctx(backend)
        result = await toolset.tools["update_memory"].function(ctx, old_text="a", new_text="b")
        assert result.startswith("Error:")

    async def test_update_memory_surfaces_write_result_error(self):
        """update_memory does not report success when the backend rejects the write."""
        path = get_memory_path(DEFAULT_MEMORY_DIR, "main")
        # Seed readable content first, then make subsequent writes fail.
        backend = _write_deny_backend(seed_path=path, seed=b"hello world")
        toolset = AgentMemoryToolset(agent_name="main", memory_dir=DEFAULT_MEMORY_DIR)
        ctx = _make_ctx(backend)
        result = await toolset.tools["update_memory"].function(
            ctx, old_text="hello", new_text="bye"
        )
        assert result.startswith("Error:")
        assert "disk quota exceeded" in result

    async def test_get_instructions_skips_denied_memory(self, tmp_path):
        """A denied memory path must not abort the run; instructions are skipped."""
        backend, memory_dir = _denied_backend_and_dir(tmp_path)
        toolset = AgentMemoryToolset(agent_name="main", memory_dir=memory_dir)
        ctx = _make_ctx(backend)
        assert await toolset.get_instructions(ctx) is None

    def test_memory_access_error_exported(self):
        """MemoryAccessError is importable from the package root."""
        from pydantic_deep import MemoryAccessError
        from pydantic_deep.features.memory import MemoryAccessError as direct

        assert MemoryAccessError is direct
