import json
import logging
import os
import random
import re
import string
import time
import uuid
from datetime import datetime, timedelta, timezone
from typing import List
from unittest.mock import AsyncMock, Mock, patch

import pytest
from _pytest.python_api import approx
from anthropic.types.beta import BetaMessage
from anthropic.types.beta.messages import BetaMessageBatchIndividualResponse, BetaMessageBatchSucceededResult

# Import shared fixtures and constants from conftest
from conftest import (
    CREATE_DELAY_SQLITE,
    DEFAULT_EMBEDDING_CONFIG,
    USING_SQLITE,
)
from openai.types.chat.chat_completion_message_tool_call import ChatCompletionMessageToolCall as OpenAIToolCall, Function as OpenAIFunction
from sqlalchemy import func, select
from sqlalchemy.exc import IntegrityError, InvalidRequestError
from sqlalchemy.orm.exc import StaleDataError

from letta.config import LettaConfig
from letta.constants import (
    BASE_MEMORY_TOOLS,
    BASE_SLEEPTIME_TOOLS,
    BASE_TOOLS,
    BASE_VOICE_SLEEPTIME_CHAT_TOOLS,
    BASE_VOICE_SLEEPTIME_TOOLS,
    BUILTIN_TOOLS,
    DEFAULT_ORG_ID,
    DEFAULT_ORG_NAME,
    FILES_TOOLS,
    LETTA_TOOL_EXECUTION_DIR,
    LETTA_TOOL_SET,
    LOCAL_ONLY_MULTI_AGENT_TOOLS,
    MCP_TOOL_TAG_NAME_PREFIX,
    MULTI_AGENT_TOOLS,
)
from letta.data_sources.redis_client import NoopAsyncRedisClient, get_redis_client
from letta.errors import LettaAgentNotFoundError
from letta.functions.functions import derive_openai_json_schema, parse_source_code
from letta.functions.mcp_client.types import MCPTool
from letta.helpers import ToolRulesSolver
from letta.helpers.datetime_helpers import AsyncTimer
from letta.jobs.types import ItemUpdateInfo, RequestStatusUpdateInfo, StepStatusUpdateInfo
from letta.orm import Base, Block
from letta.orm.block_history import BlockHistory
from letta.orm.errors import NoResultFound, UniqueConstraintViolationError
from letta.orm.file import FileContent as FileContentModel, FileMetadata as FileMetadataModel
from letta.schemas.agent import AgentRelationships, AgentState, CreateAgent, UpdateAgent
from letta.schemas.block import Block as PydanticBlock, BlockUpdate, CreateBlock
from letta.schemas.embedding_config import EmbeddingConfig
from letta.schemas.enums import (
    ActorType,
    AgentStepStatus,
    FileProcessingStatus,
    JobStatus,
    JobType,
    MessageRole,
    ProviderType,
    SandboxType,
    StepStatus,
    TagMatchMode,
    ToolType,
    VectorDBProvider,
)
from letta.schemas.environment_variables import SandboxEnvironmentVariableCreate, SandboxEnvironmentVariableUpdate
from letta.schemas.file import FileMetadata, FileMetadata as PydanticFileMetadata
from letta.schemas.identity import IdentityCreate, IdentityProperty, IdentityPropertyType, IdentityType, IdentityUpdate, IdentityUpsert
from letta.schemas.job import BatchJob, Job, Job as PydanticJob, JobUpdate, LettaRequestConfig
from letta.schemas.letta_message import UpdateAssistantMessage, UpdateReasoningMessage, UpdateSystemMessage, UpdateUserMessage
from letta.schemas.letta_message_content import TextContent
from letta.schemas.letta_stop_reason import LettaStopReason, StopReasonType
from letta.schemas.llm_batch_job import AgentStepState, LLMBatchItem
from letta.schemas.llm_config import LLMConfig
from letta.schemas.message import Message as PydanticMessage, MessageCreate, MessageUpdate
from letta.schemas.openai.chat_completion_response import UsageStatistics
from letta.schemas.organization import Organization, Organization as PydanticOrganization, OrganizationUpdate
from letta.schemas.passage import Passage as PydanticPassage
from letta.schemas.pip_requirement import PipRequirement
from letta.schemas.run import Run as PydanticRun
from letta.schemas.sandbox_config import E2BSandboxConfig, LocalSandboxConfig, SandboxConfigCreate, SandboxConfigUpdate
from letta.schemas.source import Source as PydanticSource, SourceUpdate
from letta.schemas.tool import Tool as PydanticTool, ToolCreate, ToolUpdate
from letta.schemas.tool_rule import InitToolRule
from letta.schemas.user import User as PydanticUser, UserUpdate
from letta.server.db import db_registry
from letta.server.server import SyncServer
from letta.services.block_manager import BlockManager
from letta.services.helpers.agent_manager_helper import calculate_base_tools, calculate_multi_agent_tools, validate_agent_exists_async
from letta.services.step_manager import FeedbackType
from letta.settings import settings, tool_settings
from letta.utils import calculate_file_defaults_based_on_context_window
from tests.helpers.utils import comprehensive_agent_checks, validate_context_window_overview
from tests.utils import random_string


# ======================================================================================================================
# Archive Manager Tests
# ======================================================================================================================
@pytest.mark.asyncio
async def test_archive_manager_delete_archive_async(server: SyncServer, default_user):
    """Test the delete_archive_async function."""
    archive = await server.archive_manager.create_archive_async(
        name="test_archive_to_delete",
        description="This archive will be deleted",
        embedding_config=DEFAULT_EMBEDDING_CONFIG,
        actor=default_user,
    )

    retrieved_archive = await server.archive_manager.get_archive_by_id_async(archive_id=archive.id, actor=default_user)
    assert retrieved_archive.id == archive.id

    await server.archive_manager.delete_archive_async(archive_id=archive.id, actor=default_user)

    with pytest.raises(Exception):
        await server.archive_manager.get_archive_by_id_async(archive_id=archive.id, actor=default_user)


@pytest.mark.asyncio
async def test_archive_manager_get_agents_for_archive_async(server: SyncServer, default_user, sarah_agent):
    """Test getting all agents that have access to an archive."""
    archive = await server.archive_manager.create_archive_async(
        name="shared_archive",
        description="Archive shared by multiple agents",
        embedding_config=DEFAULT_EMBEDDING_CONFIG,
        actor=default_user,
    )

    agent2 = await server.agent_manager.create_agent_async(
        agent_create=CreateAgent(
            name="test_agent_2",
            agent_type="memgpt_v2_agent",
            memory_blocks=[],
            llm_config=LLMConfig.default_config("gpt-4o-mini"),
            embedding_config=EmbeddingConfig.default_config(provider="openai"),
            include_base_tools=False,
        ),
        actor=default_user,
    )

    await server.archive_manager.attach_agent_to_archive_async(
        agent_id=sarah_agent.id, archive_id=archive.id, is_owner=True, actor=default_user
    )

    await server.archive_manager.attach_agent_to_archive_async(
        agent_id=agent2.id, archive_id=archive.id, is_owner=False, actor=default_user
    )

    agents = await server.archive_manager.get_agents_for_archive_async(archive_id=archive.id, actor=default_user)

    assert len(agents) == 2
    agent_ids = [a.id for a in agents]
    assert sarah_agent.id in agent_ids
    assert agent2.id in agent_ids

    # Cleanup
    await server.agent_manager.delete_agent_async(agent2.id, actor=default_user)
    await server.archive_manager.delete_archive_async(archive.id, actor=default_user)


@pytest.mark.asyncio
async def test_archive_manager_race_condition_handling(server: SyncServer, default_user, sarah_agent):
    """Test that the race condition fix in get_or_create_default_archive_for_agent_async works."""
    from unittest.mock import patch

    from sqlalchemy.exc import IntegrityError

    agent = await server.agent_manager.create_agent_async(
        agent_create=CreateAgent(
            name="test_agent_race_condition",
            agent_type="memgpt_v2_agent",
            memory_blocks=[],
            llm_config=LLMConfig.default_config("gpt-4o-mini"),
            embedding_config=EmbeddingConfig.default_config(provider="openai"),
            include_base_tools=False,
        ),
        actor=default_user,
    )

    created_archives = []
    original_create = server.archive_manager.create_archive_async

    async def track_create(*args, **kwargs):
        result = await original_create(*args, **kwargs)
        created_archives.append(result)
        return result

    # First, create an archive that will be attached by a "concurrent" request
    concurrent_archive = await server.archive_manager.create_archive_async(
        name=f"{agent.name}'s Archive",
        description="Default archive created automatically",
        embedding_config=DEFAULT_EMBEDDING_CONFIG,
        actor=default_user,
    )

    call_count = 0
    original_attach = server.archive_manager.attach_agent_to_archive_async

    async def failing_attach(*args, **kwargs):
        nonlocal call_count
        call_count += 1
        if call_count == 1:
            # Simulate another request already attached the agent to an archive
            await original_attach(agent_id=agent.id, archive_id=concurrent_archive.id, is_owner=True, actor=default_user)
            # Now raise the IntegrityError as if our attempt failed
            raise IntegrityError("duplicate key value violates unique constraint", None, None)
        # This shouldn't be called since we already have an archive
        raise Exception("Should not reach here")

    with patch.object(server.archive_manager, "create_archive_async", side_effect=track_create):
        with patch.object(server.archive_manager, "attach_agent_to_archive_async", side_effect=failing_attach):
            archive = await server.archive_manager.get_or_create_default_archive_for_agent_async(agent_state=agent, actor=default_user)

    assert archive is not None
    assert archive.id == concurrent_archive.id  # Should return the existing archive
    assert archive.name == f"{agent.name}'s Archive"

    # One archive was created in our attempt (but then deleted)
    assert len(created_archives) == 1

    # Verify only one archive is attached to the agent
    archive_ids = await server.agent_manager.get_agent_archive_ids_async(agent_id=agent.id, actor=default_user)
    assert len(archive_ids) == 1
    assert archive_ids[0] == concurrent_archive.id

    # Cleanup
    await server.agent_manager.delete_agent_async(agent.id, actor=default_user)
    await server.archive_manager.delete_archive_async(concurrent_archive.id, actor=default_user)


@pytest.mark.asyncio
async def test_archive_manager_get_agent_from_passage_async(server: SyncServer, default_user, sarah_agent):
    """Test getting the agent ID that owns a passage through its archive."""
    archive = await server.archive_manager.get_or_create_default_archive_for_agent_async(agent_state=sarah_agent, actor=default_user)

    passage = await server.passage_manager.create_agent_passage_async(
        PydanticPassage(
            text="Test passage for agent ownership",
            archive_id=archive.id,
            organization_id=default_user.organization_id,
            embedding=[0.1],
            embedding_config=DEFAULT_EMBEDDING_CONFIG,
        ),
        actor=default_user,
    )

    agent_id = await server.archive_manager.get_agent_from_passage_async(passage_id=passage.id, actor=default_user)

    assert agent_id == sarah_agent.id

    orphan_archive = await server.archive_manager.create_archive_async(
        name="orphan_archive", description="Archive with no agents", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user
    )

    orphan_passage = await server.passage_manager.create_agent_passage_async(
        PydanticPassage(
            text="Orphan passage",
            archive_id=orphan_archive.id,
            organization_id=default_user.organization_id,
            embedding=[0.1],
            embedding_config=DEFAULT_EMBEDDING_CONFIG,
        ),
        actor=default_user,
    )

    agent_id = await server.archive_manager.get_agent_from_passage_async(passage_id=orphan_passage.id, actor=default_user)
    assert agent_id is None

    # Cleanup
    await server.passage_manager.delete_passage_by_id_async(passage.id, actor=default_user)
    await server.passage_manager.delete_passage_by_id_async(orphan_passage.id, actor=default_user)
    await server.archive_manager.delete_archive_async(orphan_archive.id, actor=default_user)


@pytest.mark.asyncio
async def test_archive_manager_create_archive_async(server: SyncServer, default_user):
    """Test creating a new archive with various parameters."""
    # test creating with name and description
    archive = await server.archive_manager.create_archive_async(
        name="test_archive_basic", description="Test archive description", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user
    )

    assert archive.name == "test_archive_basic"
    assert archive.description == "Test archive description"
    assert archive.organization_id == default_user.organization_id
    assert archive.id is not None

    # test creating without description
    archive2 = await server.archive_manager.create_archive_async(
        name="test_archive_no_desc", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user
    )

    assert archive2.name == "test_archive_no_desc"
    assert archive2.description is None
    assert archive2.organization_id == default_user.organization_id

    # cleanup
    await server.archive_manager.delete_archive_async(archive.id, actor=default_user)
    await server.archive_manager.delete_archive_async(archive2.id, actor=default_user)


@pytest.mark.asyncio
async def test_archive_manager_get_archive_by_id_async(server: SyncServer, default_user):
    """Test retrieving an archive by its ID."""
    # create an archive
    archive = await server.archive_manager.create_archive_async(
        name="test_get_by_id", description="Archive to test get_by_id", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user
    )

    # retrieve the archive
    retrieved = await server.archive_manager.get_archive_by_id_async(archive_id=archive.id, actor=default_user)

    assert retrieved.id == archive.id
    assert retrieved.name == "test_get_by_id"
    assert retrieved.description == "Archive to test get_by_id"
    assert retrieved.organization_id == default_user.organization_id

    # cleanup
    await server.archive_manager.delete_archive_async(archive.id, actor=default_user)

    # test getting non-existent archive should raise
    with pytest.raises(Exception):
        await server.archive_manager.get_archive_by_id_async(archive_id=str(uuid.uuid4()), actor=default_user)


@pytest.mark.asyncio
async def test_archive_manager_update_archive_async(server: SyncServer, default_user):
    """Test updating archive name and description."""
    # create an archive
    archive = await server.archive_manager.create_archive_async(
        name="original_name", description="original description", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user
    )

    # update name only
    updated = await server.archive_manager.update_archive_async(archive_id=archive.id, name="updated_name", actor=default_user)

    assert updated.id == archive.id
    assert updated.name == "updated_name"
    assert updated.description == "original description"

    # update description only
    updated = await server.archive_manager.update_archive_async(
        archive_id=archive.id, description="updated description", actor=default_user
    )

    assert updated.name == "updated_name"
    assert updated.description == "updated description"

    # update both
    updated = await server.archive_manager.update_archive_async(
        archive_id=archive.id, name="final_name", description="final description", actor=default_user
    )

    assert updated.name == "final_name"
    assert updated.description == "final description"

    # verify changes persisted
    retrieved = await server.archive_manager.get_archive_by_id_async(archive_id=archive.id, actor=default_user)

    assert retrieved.name == "final_name"
    assert retrieved.description == "final description"

    # cleanup
    await server.archive_manager.delete_archive_async(archive.id, actor=default_user)


@pytest.mark.asyncio
async def test_archive_manager_list_archives_async(server: SyncServer, default_user, sarah_agent):
    """Test listing archives with various filters and pagination."""
    # create test archives
    archives = []
    for i in range(5):
        archive = await server.archive_manager.create_archive_async(
            name=f"list_test_archive_{i}", description=f"Description {i}", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user
        )
        archives.append(archive)

    # test basic listing
    result = await server.archive_manager.list_archives_async(actor=default_user, limit=10)
    assert len(result) >= 5

    # test with limit
    result = await server.archive_manager.list_archives_async(actor=default_user, limit=3)
    assert len(result) == 3

    # test filtering by name
    result = await server.archive_manager.list_archives_async(actor=default_user, name="list_test_archive_2")
    assert len(result) == 1
    assert result[0].name == "list_test_archive_2"

    # attach an archive to agent and test agent_id filter
    await server.archive_manager.attach_agent_to_archive_async(
        agent_id=sarah_agent.id, archive_id=archives[0].id, is_owner=True, actor=default_user
    )

    result = await server.archive_manager.list_archives_async(actor=default_user, agent_id=sarah_agent.id)
    assert len(result) >= 1
    assert archives[0].id in [a.id for a in result]

    # test pagination with after
    all_archives = await server.archive_manager.list_archives_async(actor=default_user, limit=100)
    if len(all_archives) > 2:
        first_batch = await server.archive_manager.list_archives_async(actor=default_user, limit=2)
        second_batch = await server.archive_manager.list_archives_async(actor=default_user, after=first_batch[-1].id, limit=2)
        assert len(second_batch) <= 2
        assert first_batch[-1].id not in [a.id for a in second_batch]

    # cleanup
    for archive in archives:
        await server.archive_manager.delete_archive_async(archive.id, actor=default_user)


@pytest.mark.asyncio
async def test_archive_manager_attach_agent_to_archive_async(server: SyncServer, default_user, sarah_agent):
    """Test attaching agents to archives with ownership settings."""
    # create archives
    archive1 = await server.archive_manager.create_archive_async(
        name="archive_for_attachment_1", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user
    )
    archive2 = await server.archive_manager.create_archive_async(
        name="archive_for_attachment_2", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user
    )

    # create another agent
    agent2 = await server.agent_manager.create_agent_async(
        agent_create=CreateAgent(
            name="test_attach_agent",
            agent_type="memgpt_v2_agent",
            memory_blocks=[],
            llm_config=LLMConfig.default_config("gpt-4o-mini"),
            embedding_config=EmbeddingConfig.default_config(provider="openai"),
            include_base_tools=False,
        ),
        actor=default_user,
    )

    # attach agent as owner
    await server.archive_manager.attach_agent_to_archive_async(
        agent_id=sarah_agent.id, archive_id=archive1.id, is_owner=True, actor=default_user
    )

    # verify attachment
    agents = await server.archive_manager.get_agents_for_archive_async(archive_id=archive1.id, actor=default_user)
    assert sarah_agent.id in [a.id for a in agents]

    # attach agent as non-owner
    await server.archive_manager.attach_agent_to_archive_async(
        agent_id=agent2.id, archive_id=archive1.id, is_owner=False, actor=default_user
    )

    agents = await server.archive_manager.get_agents_for_archive_async(archive_id=archive1.id, actor=default_user)
    assert len(agents) == 2
    assert agent2.id in [a.id for a in agents]

    # test updating ownership (attach again with different is_owner)
    await server.archive_manager.attach_agent_to_archive_async(
        agent_id=agent2.id, archive_id=archive1.id, is_owner=True, actor=default_user
    )

    # verify still only 2 agents (no duplicate)
    agents = await server.archive_manager.get_agents_for_archive_async(archive_id=archive1.id, actor=default_user)
    assert len(agents) == 2

    # cleanup
    await server.agent_manager.delete_agent_async(agent2.id, actor=default_user)
    await server.archive_manager.delete_archive_async(archive1.id, actor=default_user)
    await server.archive_manager.delete_archive_async(archive2.id, actor=default_user)


@pytest.mark.asyncio
async def test_archive_manager_detach_agent_from_archive_async(server: SyncServer, default_user):
    """Test detaching agents from archives."""
    # create archive and agents
    archive = await server.archive_manager.create_archive_async(
        name="archive_for_detachment",
        description="Test archive for detachment",
        embedding_config=DEFAULT_EMBEDDING_CONFIG,
        actor=default_user,
    )

    agent1 = await server.agent_manager.create_agent_async(
        agent_create=CreateAgent(
            name="test_detach_agent_1",
            agent_type="memgpt_v2_agent",
            memory_blocks=[],
            llm_config=LLMConfig.default_config("gpt-4o-mini"),
            embedding_config=EmbeddingConfig.default_config(provider="openai"),
            include_base_tools=False,
        ),
        actor=default_user,
    )

    agent2 = await server.agent_manager.create_agent_async(
        agent_create=CreateAgent(
            name="test_detach_agent_2",
            agent_type="memgpt_v2_agent",
            memory_blocks=[],
            llm_config=LLMConfig.default_config("gpt-4o-mini"),
            embedding_config=EmbeddingConfig.default_config(provider="openai"),
            include_base_tools=False,
        ),
        actor=default_user,
    )

    # attach both agents
    await server.archive_manager.attach_agent_to_archive_async(agent_id=agent1.id, archive_id=archive.id, is_owner=True, actor=default_user)
    await server.archive_manager.attach_agent_to_archive_async(
        agent_id=agent2.id, archive_id=archive.id, is_owner=False, actor=default_user
    )

    # verify both are attached
    agents = await server.archive_manager.get_agents_for_archive_async(archive_id=archive.id, actor=default_user)
    assert len(agents) == 2
    agent_ids = [a.id for a in agents]
    assert agent1.id in agent_ids
    assert agent2.id in agent_ids

    # detach agent1
    await server.archive_manager.detach_agent_from_archive_async(agent_id=agent1.id, archive_id=archive.id, actor=default_user)

    # verify only agent2 remains
    agents = await server.archive_manager.get_agents_for_archive_async(archive_id=archive.id, actor=default_user)
    assert len(agents) == 1
    agent_ids = [a.id for a in agents]
    assert agent2.id in agent_ids
    assert agent1.id not in agent_ids

    # test idempotency - detach agent1 again (should not error)
    await server.archive_manager.detach_agent_from_archive_async(agent_id=agent1.id, archive_id=archive.id, actor=default_user)

    # verify still only agent2
    agents = await server.archive_manager.get_agents_for_archive_async(archive_id=archive.id, actor=default_user)
    assert len(agents) == 1
    assert agent2.id in [a.id for a in agents]

    # detach agent2
    await server.archive_manager.detach_agent_from_archive_async(agent_id=agent2.id, archive_id=archive.id, actor=default_user)

    # verify archive has no agents
    agents = await server.archive_manager.get_agents_for_archive_async(archive_id=archive.id, actor=default_user)
    assert len(agents) == 0

    # cleanup
    await server.agent_manager.delete_agent_async(agent1.id, actor=default_user)
    await server.agent_manager.delete_agent_async(agent2.id, actor=default_user)
    await server.archive_manager.delete_archive_async(archive.id, actor=default_user)


@pytest.mark.asyncio
async def test_archive_manager_attach_detach_idempotency(server: SyncServer, default_user):
    """Test that attach and detach operations are idempotent."""
    # create archive and agent
    archive = await server.archive_manager.create_archive_async(
        name="idempotency_test_archive", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user
    )

    agent = await server.agent_manager.create_agent_async(
        agent_create=CreateAgent(
            name="idempotency_test_agent",
            agent_type="memgpt_v2_agent",
            memory_blocks=[],
            llm_config=LLMConfig.default_config("gpt-4o-mini"),
            embedding_config=EmbeddingConfig.default_config(provider="openai"),
            include_base_tools=False,
        ),
        actor=default_user,
    )

    # test multiple attachments - should be idempotent
    await server.archive_manager.attach_agent_to_archive_async(agent_id=agent.id, archive_id=archive.id, is_owner=False, actor=default_user)
    await server.archive_manager.attach_agent_to_archive_async(agent_id=agent.id, archive_id=archive.id, is_owner=False, actor=default_user)

    # verify only one relationship exists
    agents = await server.archive_manager.get_agents_for_archive_async(archive_id=archive.id, actor=default_user)
    assert len(agents) == 1
    assert agent.id in [a.id for a in agents]

    # test ownership update through re-attachment
    await server.archive_manager.attach_agent_to_archive_async(agent_id=agent.id, archive_id=archive.id, is_owner=True, actor=default_user)

    # still only one relationship
    agents = await server.archive_manager.get_agents_for_archive_async(archive_id=archive.id, actor=default_user)
    assert len(agents) == 1

    # test detaching non-existent relationship (should be idempotent)
    non_existent_agent = await server.agent_manager.create_agent_async(
        agent_create=CreateAgent(
            name="never_attached_agent",
            agent_type="memgpt_v2_agent",
            memory_blocks=[],
            llm_config=LLMConfig.default_config("gpt-4o-mini"),
            embedding_config=EmbeddingConfig.default_config(provider="openai"),
            include_base_tools=False,
        ),
        actor=default_user,
    )

    # this should not error
    await server.archive_manager.detach_agent_from_archive_async(agent_id=non_existent_agent.id, archive_id=archive.id, actor=default_user)

    # verify original agent still attached
    agents = await server.archive_manager.get_agents_for_archive_async(archive_id=archive.id, actor=default_user)
    assert len(agents) == 1
    assert agent.id in [a.id for a in agents]

    # cleanup
    await server.agent_manager.delete_agent_async(agent.id, actor=default_user)
    await server.agent_manager.delete_agent_async(non_existent_agent.id, actor=default_user)
    await server.archive_manager.delete_archive_async(archive.id, actor=default_user)


@pytest.mark.asyncio
async def test_archive_manager_detach_with_multiple_archives(server: SyncServer, default_user):
    """Test detaching an agent from one archive doesn't affect others."""
    # create two archives
    archive1 = await server.archive_manager.create_archive_async(
        name="multi_archive_1", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user
    )
    archive2 = await server.archive_manager.create_archive_async(
        name="multi_archive_2", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user
    )

    # create two agents
    agent1 = await server.agent_manager.create_agent_async(
        agent_create=CreateAgent(
            name="multi_test_agent_1",
            agent_type="memgpt_v2_agent",
            memory_blocks=[],
            llm_config=LLMConfig.default_config("gpt-4o-mini"),
            embedding_config=EmbeddingConfig.default_config(provider="openai"),
            include_base_tools=False,
        ),
        actor=default_user,
    )

    agent2 = await server.agent_manager.create_agent_async(
        agent_create=CreateAgent(
            name="multi_test_agent_2",
            agent_type="memgpt_v2_agent",
            memory_blocks=[],
            llm_config=LLMConfig.default_config("gpt-4o-mini"),
            embedding_config=EmbeddingConfig.default_config(provider="openai"),
            include_base_tools=False,
        ),
        actor=default_user,
    )

    # Note: Due to unique constraint, each agent can only be attached to one archive
    # So we'll attach different agents to different archives
    await server.archive_manager.attach_agent_to_archive_async(
        agent_id=agent1.id, archive_id=archive1.id, is_owner=True, actor=default_user
    )
    await server.archive_manager.attach_agent_to_archive_async(
        agent_id=agent2.id, archive_id=archive2.id, is_owner=True, actor=default_user
    )

    # verify initial state
    agents_archive1 = await server.archive_manager.get_agents_for_archive_async(archive_id=archive1.id, actor=default_user)
    agents_archive2 = await server.archive_manager.get_agents_for_archive_async(archive_id=archive2.id, actor=default_user)
    assert agent1.id in [a.id for a in agents_archive1]
    assert agent2.id in [a.id for a in agents_archive2]

    # detach agent1 from archive1
    await server.archive_manager.detach_agent_from_archive_async(agent_id=agent1.id, archive_id=archive1.id, actor=default_user)

    # verify agent1 is detached from archive1
    agents_archive1 = await server.archive_manager.get_agents_for_archive_async(archive_id=archive1.id, actor=default_user)
    assert agent1.id not in [a.id for a in agents_archive1]
    assert len(agents_archive1) == 0

    # verify agent2 is still attached to archive2
    agents_archive2 = await server.archive_manager.get_agents_for_archive_async(archive_id=archive2.id, actor=default_user)
    assert agent2.id in [a.id for a in agents_archive2]
    assert len(agents_archive2) == 1

    # cleanup
    await server.agent_manager.delete_agent_async(agent1.id, actor=default_user)
    await server.agent_manager.delete_agent_async(agent2.id, actor=default_user)
    await server.archive_manager.delete_archive_async(archive1.id, actor=default_user)
    await server.archive_manager.delete_archive_async(archive2.id, actor=default_user)


@pytest.mark.asyncio
async def test_archive_manager_detach_deleted_agent(server: SyncServer, default_user):
    """Test behavior when detaching a deleted agent."""
    # create archive
    archive = await server.archive_manager.create_archive_async(
        name="test_deleted_agent_archive", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user
    )

    # create and attach agent
    agent = await server.agent_manager.create_agent_async(
        agent_create=CreateAgent(
            name="agent_to_be_deleted",
            agent_type="memgpt_v2_agent",
            memory_blocks=[],
            llm_config=LLMConfig.default_config("gpt-4o-mini"),
            embedding_config=EmbeddingConfig.default_config(provider="openai"),
            include_base_tools=False,
        ),
        actor=default_user,
    )

    await server.archive_manager.attach_agent_to_archive_async(agent_id=agent.id, archive_id=archive.id, is_owner=True, actor=default_user)

    # save the agent id before deletion
    agent_id = agent.id

    # delete the agent (should cascade delete the relationship due to ondelete="CASCADE")
    await server.agent_manager.delete_agent_async(agent.id, actor=default_user)

    # verify agent is no longer attached
    agents = await server.archive_manager.get_agents_for_archive_async(archive_id=archive.id, actor=default_user)
    assert len(agents) == 0

    # attempting to detach the deleted agent
    # 2025-10-27: used to be idempotent (no error) but now we raise an error
    with pytest.raises(LettaAgentNotFoundError):
        await server.archive_manager.detach_agent_from_archive_async(agent_id=agent_id, archive_id=archive.id, actor=default_user)

    # cleanup
    await server.archive_manager.delete_archive_async(archive.id, actor=default_user)


@pytest.mark.asyncio
async def test_archive_manager_cascade_delete_on_archive_deletion(server: SyncServer, default_user):
    """Test that deleting an archive cascades to delete relationships in archives_agents table."""
    # create archive
    archive = await server.archive_manager.create_archive_async(
        name="archive_to_be_deleted",
        description="This archive will be deleted to test CASCADE",
        embedding_config=DEFAULT_EMBEDDING_CONFIG,
        actor=default_user,
    )

    # create multiple agents and attach them to the archive
    agent1 = await server.agent_manager.create_agent_async(
        agent_create=CreateAgent(
            name="cascade_test_agent_1",
            agent_type="memgpt_v2_agent",
            memory_blocks=[],
            llm_config=LLMConfig.default_config("gpt-4o-mini"),
            embedding_config=EmbeddingConfig.default_config(provider="openai"),
            include_base_tools=False,
        ),
        actor=default_user,
    )

    agent2 = await server.agent_manager.create_agent_async(
        agent_create=CreateAgent(
            name="cascade_test_agent_2",
            agent_type="memgpt_v2_agent",
            memory_blocks=[],
            llm_config=LLMConfig.default_config("gpt-4o-mini"),
            embedding_config=EmbeddingConfig.default_config(provider="openai"),
            include_base_tools=False,
        ),
        actor=default_user,
    )

    # attach both agents to the archive
    await server.archive_manager.attach_agent_to_archive_async(agent_id=agent1.id, archive_id=archive.id, is_owner=True, actor=default_user)
    await server.archive_manager.attach_agent_to_archive_async(
        agent_id=agent2.id, archive_id=archive.id, is_owner=False, actor=default_user
    )

    # verify both agents are attached
    agents = await server.archive_manager.get_agents_for_archive_async(archive_id=archive.id, actor=default_user)
    assert len(agents) == 2
    agent_ids = [a.id for a in agents]
    assert agent1.id in agent_ids
    assert agent2.id in agent_ids

    # save archive id for later
    archive_id = archive.id

    # delete the archive (should cascade delete the relationships)
    await server.archive_manager.delete_archive_async(archive.id, actor=default_user)

    # verify archive is deleted
    with pytest.raises(Exception):
        await server.archive_manager.get_archive_by_id_async(archive_id=archive_id, actor=default_user)

    # verify agents still exist but have no archives attached
    # (agents should NOT be deleted, only the relationships)
    agent1_still_exists = await server.agent_manager.get_agent_by_id_async(agent1.id, actor=default_user)
    assert agent1_still_exists is not None
    assert agent1_still_exists.id == agent1.id

    agent2_still_exists = await server.agent_manager.get_agent_by_id_async(agent2.id, actor=default_user)
    assert agent2_still_exists is not None
    assert agent2_still_exists.id == agent2.id

    # verify agents no longer have any archives
    agent1_archives = await server.agent_manager.get_agent_archive_ids_async(agent_id=agent1.id, actor=default_user)
    assert len(agent1_archives) == 0

    agent2_archives = await server.agent_manager.get_agent_archive_ids_async(agent_id=agent2.id, actor=default_user)
    assert len(agent2_archives) == 0

    # cleanup agents
    await server.agent_manager.delete_agent_async(agent1.id, actor=default_user)
    await server.agent_manager.delete_agent_async(agent2.id, actor=default_user)


@pytest.mark.asyncio
async def test_archive_manager_list_agents_with_pagination(server: SyncServer, default_user):
    """Test listing agents for an archive with pagination support."""
    # create archive
    archive = await server.archive_manager.create_archive_async(
        name="pagination_test_archive",
        description="Archive for testing pagination",
        embedding_config=DEFAULT_EMBEDDING_CONFIG,
        actor=default_user,
    )

    # create multiple agents
    agents = []
    for i in range(5):
        agent = await server.agent_manager.create_agent_async(
            agent_create=CreateAgent(
                name=f"pagination_test_agent_{i}",
                agent_type="memgpt_v2_agent",
                memory_blocks=[],
                llm_config=LLMConfig.default_config("gpt-4o-mini"),
                embedding_config=EmbeddingConfig.default_config(provider="openai"),
                include_base_tools=False,
            ),
            actor=default_user,
        )
        agents.append(agent)
        # Attach to archive
        await server.archive_manager.attach_agent_to_archive_async(
            agent_id=agent.id, archive_id=archive.id, is_owner=(i == 0), actor=default_user
        )

    # Test basic listing (should get all 5)
    all_agents = await server.archive_manager.get_agents_for_archive_async(archive_id=archive.id, actor=default_user, limit=10)
    assert len(all_agents) == 5
    all_agent_ids = [a.id for a in all_agents]
    for agent in agents:
        assert agent.id in all_agent_ids

    # Test with limit
    limited_agents = await server.archive_manager.get_agents_for_archive_async(archive_id=archive.id, actor=default_user, limit=3)
    assert len(limited_agents) == 3

    # Test that pagination parameters are accepted without errors
    paginated = await server.archive_manager.get_agents_for_archive_async(archive_id=archive.id, actor=default_user, limit=2)
    assert len(paginated) == 2
    assert all(a.id in all_agent_ids for a in paginated)

    # Test ascending/descending order by checking we get all agents in both
    ascending_agents = await server.archive_manager.get_agents_for_archive_async(
        archive_id=archive.id, actor=default_user, ascending=True, limit=10
    )
    assert len(ascending_agents) == 5

    descending_agents = await server.archive_manager.get_agents_for_archive_async(
        archive_id=archive.id, actor=default_user, ascending=False, limit=10
    )
    assert len(descending_agents) == 5

    # Verify both orders contain all agents
    assert set([a.id for a in ascending_agents]) == set([a.id for a in descending_agents])

    # Cleanup
    for agent in agents:
        await server.agent_manager.delete_agent_async(agent.id, actor=default_user)
    await server.archive_manager.delete_archive_async(archive.id, actor=default_user)


@pytest.mark.asyncio
async def test_archive_manager_get_default_archive_for_agent_async(server: SyncServer, default_user):
    """Test getting default archive for an agent."""
    # create agent without archive
    agent = await server.agent_manager.create_agent_async(
        agent_create=CreateAgent(
            name="test_default_archive_agent",
            agent_type="memgpt_v2_agent",
            memory_blocks=[],
            llm_config=LLMConfig.default_config("gpt-4o-mini"),
            embedding_config=EmbeddingConfig.default_config(provider="openai"),
            include_base_tools=False,
        ),
        actor=default_user,
    )

    # should return None when no archive exists
    archive = await server.archive_manager.get_default_archive_for_agent_async(agent_id=agent.id, actor=default_user)
    assert archive is None

    # create and attach an archive
    created_archive = await server.archive_manager.create_archive_async(
        name="default_archive", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user
    )

    await server.archive_manager.attach_agent_to_archive_async(
        agent_id=agent.id, archive_id=created_archive.id, is_owner=True, actor=default_user
    )

    # should now return the archive
    archive = await server.archive_manager.get_default_archive_for_agent_async(agent_id=agent.id, actor=default_user)
    assert archive is not None
    assert archive.id == created_archive.id

    # cleanup
    await server.agent_manager.delete_agent_async(agent.id, actor=default_user)
    await server.archive_manager.delete_archive_async(created_archive.id, actor=default_user)


@pytest.mark.asyncio
async def test_archive_manager_get_or_set_vector_db_namespace_async(server: SyncServer, default_user):
    """Test getting or setting vector database namespace for an archive."""
    # create an archive
    archive = await server.archive_manager.create_archive_async(
        name="test_vector_namespace", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user
    )

    # get/set namespace for the first time
    namespace = await server.archive_manager.get_or_set_vector_db_namespace_async(archive_id=archive.id)

    assert namespace is not None
    assert archive.id in namespace

    # verify it returns the same namespace on subsequent calls
    namespace2 = await server.archive_manager.get_or_set_vector_db_namespace_async(archive_id=archive.id)

    assert namespace == namespace2

    # cleanup
    await server.archive_manager.delete_archive_async(archive.id, actor=default_user)


@pytest.mark.asyncio
async def test_archive_manager_get_agents_with_include_parameter(server: SyncServer, default_user):
    """Test getting agents for an archive with include parameter to load relationships."""
    # create an archive
    archive = await server.archive_manager.create_archive_async(
        name="test_include_archive",
        description="Test archive for include parameter",
        embedding_config=DEFAULT_EMBEDDING_CONFIG,
        actor=default_user,
    )

    # create agent without base tools (to avoid needing tools in test DB)
    agent = await server.agent_manager.create_agent_async(
        agent_create=CreateAgent(
            name="test_include_agent",
            agent_type="memgpt_v2_agent",
            memory_blocks=[],
            llm_config=LLMConfig.default_config("gpt-4o-mini"),
            embedding_config=EmbeddingConfig.default_config(provider="openai"),
            include_base_tools=False,
        ),
        actor=default_user,
    )

    # attach agent to archive
    await server.archive_manager.attach_agent_to_archive_async(agent_id=agent.id, archive_id=archive.id, is_owner=True, actor=default_user)

    # test without include parameter (default - no relationships loaded)
    agents_no_include = await server.archive_manager.get_agents_for_archive_async(archive_id=archive.id, actor=default_user)
    assert len(agents_no_include) == 1
    # By default, tools should be empty list (not loaded)
    assert agents_no_include[0].tools == []
    # By default, tags should also be empty (not loaded)
    assert agents_no_include[0].tags == []

    # test with include parameter to load tags
    agents_with_tags = await server.archive_manager.get_agents_for_archive_async(
        archive_id=archive.id, actor=default_user, include=["agent.tags"]
    )
    assert len(agents_with_tags) == 1
    # With include, tags should be loaded (as a list, even if empty)
    assert isinstance(agents_with_tags[0].tags, list)

    # test with include parameter to load blocks
    agents_with_blocks = await server.archive_manager.get_agents_for_archive_async(
        archive_id=archive.id, actor=default_user, include=["agent.blocks"]
    )
    assert len(agents_with_blocks) == 1
    # With include, blocks should be loaded
    assert isinstance(agents_with_blocks[0].blocks, list)
    # Agent should have blocks since we passed memory_blocks=[] which creates default blocks
    assert len(agents_with_blocks[0].blocks) >= 0

    # test with multiple includes
    agents_with_multiple = await server.archive_manager.get_agents_for_archive_async(
        archive_id=archive.id, actor=default_user, include=["agent.tags", "agent.blocks", "agent.tools"]
    )
    assert len(agents_with_multiple) == 1
    # All requested relationships should be loaded
    assert isinstance(agents_with_multiple[0].tags, list)
    assert isinstance(agents_with_multiple[0].blocks, list)
    assert isinstance(agents_with_multiple[0].tools, list)

    # cleanup
    await server.agent_manager.delete_agent_async(agent.id, actor=default_user)
    await server.archive_manager.delete_archive_async(archive.id, actor=default_user)


@pytest.mark.asyncio
async def test_archive_manager_delete_passage_from_archive_async(server: SyncServer, default_user):
    """Test deleting a passage from an archive."""
    # create archive
    archive = await server.archive_manager.create_archive_async(
        name="test_passage_deletion_archive",
        description="Archive for testing passage deletion",
        embedding_config=DEFAULT_EMBEDDING_CONFIG,
        actor=default_user,
    )

    # create passages
    passage1 = await server.passage_manager.create_agent_passage_async(
        PydanticPassage(
            text="First test passage",
            archive_id=archive.id,
            organization_id=default_user.organization_id,
            embedding=[0.1, 0.2],
            embedding_config=DEFAULT_EMBEDDING_CONFIG,
        ),
        actor=default_user,
    )

    passage2 = await server.passage_manager.create_agent_passage_async(
        PydanticPassage(
            text="Second test passage",
            archive_id=archive.id,
            organization_id=default_user.organization_id,
            embedding=[0.3, 0.4],
            embedding_config=DEFAULT_EMBEDDING_CONFIG,
        ),
        actor=default_user,
    )

    # verify both passages exist
    retrieved_passage1 = await server.passage_manager.get_agent_passage_by_id_async(passage_id=passage1.id, actor=default_user)
    assert retrieved_passage1.id == passage1.id
    assert retrieved_passage1.archive_id == archive.id

    retrieved_passage2 = await server.passage_manager.get_agent_passage_by_id_async(passage_id=passage2.id, actor=default_user)
    assert retrieved_passage2.id == passage2.id

    # delete passage1 from archive
    await server.archive_manager.delete_passage_from_archive_async(archive_id=archive.id, passage_id=passage1.id, actor=default_user)

    # verify passage1 is deleted
    with pytest.raises(NoResultFound):
        await server.passage_manager.get_agent_passage_by_id_async(passage_id=passage1.id, actor=default_user)

    # verify passage2 still exists
    retrieved_passage2 = await server.passage_manager.get_agent_passage_by_id_async(passage_id=passage2.id, actor=default_user)
    assert retrieved_passage2.id == passage2.id

    # cleanup
    await server.passage_manager.delete_agent_passage_by_id_async(passage2.id, actor=default_user)
    await server.archive_manager.delete_archive_async(archive.id, actor=default_user)


@pytest.mark.asyncio
async def test_archive_manager_delete_passage_from_wrong_archive(server: SyncServer, default_user):
    """Test that deleting a passage from the wrong archive raises an error."""
    # create two archives
    archive1 = await server.archive_manager.create_archive_async(
        name="archive_1", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user
    )
    archive2 = await server.archive_manager.create_archive_async(
        name="archive_2", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user
    )

    # create passage in archive1
    passage = await server.passage_manager.create_agent_passage_async(
        PydanticPassage(
            text="Passage in archive 1",
            archive_id=archive1.id,
            organization_id=default_user.organization_id,
            embedding=[0.1, 0.2],
            embedding_config=DEFAULT_EMBEDDING_CONFIG,
        ),
        actor=default_user,
    )

    # attempt to delete passage from archive2 (wrong archive)
    with pytest.raises(ValueError, match="does not belong to archive"):
        await server.archive_manager.delete_passage_from_archive_async(archive_id=archive2.id, passage_id=passage.id, actor=default_user)

    # verify passage still exists
    retrieved_passage = await server.passage_manager.get_agent_passage_by_id_async(passage_id=passage.id, actor=default_user)
    assert retrieved_passage.id == passage.id

    # cleanup
    await server.passage_manager.delete_agent_passage_by_id_async(passage.id, actor=default_user)
    await server.archive_manager.delete_archive_async(archive1.id, actor=default_user)
    await server.archive_manager.delete_archive_async(archive2.id, actor=default_user)


@pytest.mark.asyncio
async def test_archive_manager_delete_nonexistent_passage(server: SyncServer, default_user):
    """Test that deleting a non-existent passage raises an error."""
    # create archive
    archive = await server.archive_manager.create_archive_async(
        name="test_nonexistent_passage_archive", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user
    )

    # attempt to delete non-existent passage (use valid UUID4 format)
    fake_passage_id = f"passage-{uuid.uuid4()}"
    with pytest.raises(NoResultFound):
        await server.archive_manager.delete_passage_from_archive_async(
            archive_id=archive.id, passage_id=fake_passage_id, actor=default_user
        )

    # cleanup
    await server.archive_manager.delete_archive_async(archive.id, actor=default_user)


@pytest.mark.asyncio
async def test_archive_manager_delete_passage_from_nonexistent_archive(server: SyncServer, default_user):
    """Test that deleting a passage from a non-existent archive raises an error."""
    # create archive and passage
    archive = await server.archive_manager.create_archive_async(
        name="temp_archive", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user
    )

    passage = await server.passage_manager.create_agent_passage_async(
        PydanticPassage(
            text="Test passage",
            archive_id=archive.id,
            organization_id=default_user.organization_id,
            embedding=[0.1, 0.2],
            embedding_config=DEFAULT_EMBEDDING_CONFIG,
        ),
        actor=default_user,
    )

    # attempt to delete passage from non-existent archive (use valid UUID4 format)
    fake_archive_id = f"archive-{uuid.uuid4()}"
    with pytest.raises(NoResultFound):
        await server.archive_manager.delete_passage_from_archive_async(
            archive_id=fake_archive_id, passage_id=passage.id, actor=default_user
        )

    # verify passage still exists
    retrieved_passage = await server.passage_manager.get_agent_passage_by_id_async(passage_id=passage.id, actor=default_user)
    assert retrieved_passage.id == passage.id

    # cleanup
    await server.passage_manager.delete_agent_passage_by_id_async(passage.id, actor=default_user)
    await server.archive_manager.delete_archive_async(archive.id, actor=default_user)


@pytest.mark.asyncio
async def test_archive_manager_create_passage_in_archive_async(server: SyncServer, default_user):
    """Test creating a passage in an archive."""
    # create archive
    archive = await server.archive_manager.create_archive_async(
        name="test_passage_creation_archive",
        description="Archive for testing passage creation",
        embedding_config=DEFAULT_EMBEDDING_CONFIG,
        actor=default_user,
    )

    # create a passage in the archive
    created_passage = await server.archive_manager.create_passage_in_archive_async(
        archive_id=archive.id,
        text="This is a test passage for creation",
        actor=default_user,
    )

    # verify the passage was created
    assert created_passage.id is not None
    assert created_passage.text == "This is a test passage for creation"
    assert created_passage.archive_id == archive.id
    assert created_passage.organization_id == default_user.organization_id

    # verify we can retrieve it
    retrieved_passage = await server.passage_manager.get_agent_passage_by_id_async(passage_id=created_passage.id, actor=default_user)
    assert retrieved_passage.id == created_passage.id
    assert retrieved_passage.text == created_passage.text
    assert retrieved_passage.archive_id == archive.id

    # cleanup
    await server.passage_manager.delete_agent_passage_by_id_async(created_passage.id, actor=default_user)
    await server.archive_manager.delete_archive_async(archive.id, actor=default_user)


@pytest.mark.asyncio
async def test_archive_manager_create_passage_with_metadata_and_tags(server: SyncServer, default_user):
    """Test creating a passage with metadata and tags."""
    # create archive
    archive = await server.archive_manager.create_archive_async(
        name="test_passage_metadata_archive",
        embedding_config=DEFAULT_EMBEDDING_CONFIG,
        actor=default_user,
    )

    # create passage with metadata and tags
    test_metadata = {"source": "unit_test", "version": 1}
    test_tags = ["test", "archive", "passage"]

    created_passage = await server.archive_manager.create_passage_in_archive_async(
        archive_id=archive.id,
        text="Passage with metadata and tags",
        metadata=test_metadata,
        tags=test_tags,
        actor=default_user,
    )

    # verify metadata and tags were stored
    assert created_passage.metadata == test_metadata
    assert set(created_passage.tags) == set(test_tags)  # Use set comparison to ignore order

    # retrieve and verify persistence
    retrieved_passage = await server.passage_manager.get_agent_passage_by_id_async(passage_id=created_passage.id, actor=default_user)
    assert retrieved_passage.metadata == test_metadata
    assert set(retrieved_passage.tags) == set(test_tags)

    # cleanup
    await server.passage_manager.delete_agent_passage_by_id_async(created_passage.id, actor=default_user)
    await server.archive_manager.delete_archive_async(archive.id, actor=default_user)


@pytest.mark.asyncio
async def test_archive_manager_create_passage_in_nonexistent_archive(server: SyncServer, default_user):
    """Test that creating a passage in a non-existent archive raises an error."""
    # attempt to create passage in non-existent archive
    fake_archive_id = f"archive-{uuid.uuid4()}"

    with pytest.raises(NoResultFound):
        await server.archive_manager.create_passage_in_archive_async(
            archive_id=fake_archive_id,
            text="This should fail",
            actor=default_user,
        )


@pytest.mark.asyncio
async def test_archive_manager_create_passage_inherits_embedding_config(server: SyncServer, default_user):
    """Test that created passages inherit the archive's embedding configuration."""
    # create archive with specific embedding config
    specific_embedding_config = EmbeddingConfig.default_config(provider="openai")

    archive = await server.archive_manager.create_archive_async(
        name="test_embedding_inheritance_archive",
        embedding_config=specific_embedding_config,
        actor=default_user,
    )

    # create passage
    created_passage = await server.archive_manager.create_passage_in_archive_async(
        archive_id=archive.id,
        text="Test passage for embedding config inheritance",
        actor=default_user,
    )

    # verify the passage inherited the archive's embedding config
    assert created_passage.embedding_config is not None
    assert created_passage.embedding_config.embedding_endpoint_type == specific_embedding_config.embedding_endpoint_type
    assert created_passage.embedding_config.embedding_model == specific_embedding_config.embedding_model
    assert created_passage.embedding_config.embedding_dim == specific_embedding_config.embedding_dim

    # cleanup
    await server.passage_manager.delete_agent_passage_by_id_async(created_passage.id, actor=default_user)
    await server.archive_manager.delete_archive_async(archive.id, actor=default_user)


@pytest.mark.asyncio
async def test_archive_manager_create_multiple_passages_in_archive(server: SyncServer, default_user):
    """Test creating multiple passages in the same archive."""
    # create archive
    archive = await server.archive_manager.create_archive_async(
        name="test_multiple_passages_archive",
        embedding_config=DEFAULT_EMBEDDING_CONFIG,
        actor=default_user,
    )

    # create multiple passages
    passages = []
    for i in range(3):
        passage = await server.archive_manager.create_passage_in_archive_async(
            archive_id=archive.id,
            text=f"Test passage number {i}",
            metadata={"index": i},
            tags=[f"passage_{i}"],
            actor=default_user,
        )
        passages.append(passage)

    # verify all passages were created with correct data
    for i, passage in enumerate(passages):
        assert passage.text == f"Test passage number {i}"
        assert passage.metadata["index"] == i
        assert f"passage_{i}" in passage.tags
        assert passage.archive_id == archive.id

    # cleanup
    for passage in passages:
        await server.passage_manager.delete_agent_passage_by_id_async(passage.id, actor=default_user)
    await server.archive_manager.delete_archive_async(archive.id, actor=default_user)
