# Copyright (c) Microsoft. All rights reserved.

from unittest.mock import AsyncMock, Mock, PropertyMock, patch

import boto3
import pytest

from semantic_kernel.agents.bedrock.action_group_utils import (
    kernel_function_to_bedrock_function_schema,
    parse_function_result_contents,
)
from semantic_kernel.agents.bedrock.bedrock_agent import BedrockAgent, BedrockAgentThread
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
from semantic_kernel.contents.function_result_content import FunctionResultContent
from semantic_kernel.exceptions.agent_exceptions import AgentInitializationException, AgentInvokeException
from semantic_kernel.functions.kernel_arguments import KernelArguments
from semantic_kernel.kernel import Kernel

# region Agent Initialization Tests


# Test case to verify BedrockAgent initialization
@patch.object(boto3, "client", return_value=Mock())
async def test_bedrock_agent_initialization(client, bedrock_agent_model_with_id):
    agent = BedrockAgent(bedrock_agent_model_with_id)

    assert agent.name == bedrock_agent_model_with_id.agent_name
    assert agent.agent_model.agent_name == bedrock_agent_model_with_id.agent_name
    assert agent.agent_model.agent_id == bedrock_agent_model_with_id.agent_id
    assert agent.agent_model.foundation_model == bedrock_agent_model_with_id.foundation_model


# Test case to verify error handling during BedrockAgent initialization with non-auto function choice
@patch.object(boto3, "client", return_value=Mock())
async def test_bedrock_agent_initialization_error_with_non_auto_function_choice(client, bedrock_agent_model_with_id):
    with pytest.raises(ValueError, match="Only FunctionChoiceType.AUTO is supported."):
        BedrockAgent(
            bedrock_agent_model_with_id,
            function_choice_behavior=FunctionChoiceBehavior.NoneInvoke(),
        )


# Test case to verify the creation of BedrockAgent
@patch.object(boto3, "client", return_value=Mock())
@pytest.mark.parametrize(
    "kernel, function_choice_behavior, arguments",
    [
        (None, None, None),
        (Kernel(), None, None),
        (Kernel(), FunctionChoiceBehavior.Auto(), None),
        (Kernel(), FunctionChoiceBehavior.Auto(), KernelArguments()),
    ],
)
async def test_bedrock_agent_create_and_prepare_agent(
    client,
    bedrock_agent_model_with_id_not_prepared_dict,
    bedrock_agent_unit_test_env,
    kernel,
    function_choice_behavior,
    arguments,
):
    with (
        patch.object(client, "create_agent") as mock_create_agent,
        patch.object(BedrockAgent, "_wait_for_agent_status", new_callable=AsyncMock),
        patch.object(BedrockAgent, "prepare_agent_and_wait_until_prepared", new_callable=AsyncMock),
    ):
        mock_create_agent.return_value = bedrock_agent_model_with_id_not_prepared_dict

        agent = await BedrockAgent.create_and_prepare_agent(
            name=bedrock_agent_model_with_id_not_prepared_dict["agent"]["agentName"],
            instructions="test_instructions",
            bedrock_client=client,
            env_file_path="fake_path",
            kernel=kernel,
            function_choice_behavior=function_choice_behavior,
            arguments=arguments,
        )

        mock_create_agent.assert_called_once_with(
            agentName=bedrock_agent_model_with_id_not_prepared_dict["agent"]["agentName"],
            foundationModel=bedrock_agent_unit_test_env["BEDROCK_AGENT_FOUNDATION_MODEL"],
            agentResourceRoleArn=bedrock_agent_unit_test_env["BEDROCK_AGENT_AGENT_RESOURCE_ROLE_ARN"],
            instruction="test_instructions",
        )
        assert agent.agent_model.agent_id == bedrock_agent_model_with_id_not_prepared_dict["agent"]["agentId"]
        assert agent.id == bedrock_agent_model_with_id_not_prepared_dict["agent"]["agentId"]
        assert agent.agent_model.agent_name == bedrock_agent_model_with_id_not_prepared_dict["agent"]["agentName"]
        assert agent.name == bedrock_agent_model_with_id_not_prepared_dict["agent"]["agentName"]
        assert (
            agent.agent_model.foundation_model
            == bedrock_agent_model_with_id_not_prepared_dict["agent"]["foundationModel"]
        )
        assert agent.kernel is not None
        assert agent.function_choice_behavior is not None
        if arguments:
            assert agent.arguments is not None


# Test case to verify the creation of BedrockAgent
@pytest.mark.parametrize(
    "exclude_list",
    [
        ["BEDROCK_AGENT_AGENT_RESOURCE_ROLE_ARN"],
        ["BEDROCK_AGENT_FOUNDATION_MODEL"],
    ],
    indirect=True,
)
@patch.object(boto3, "client", return_value=Mock())
async def test_bedrock_agent_create_and_prepare_agent_settings_validation_error(
    client,
    bedrock_agent_model_with_id_not_prepared_dict,
    bedrock_agent_unit_test_env,
):
    with pytest.raises(AgentInitializationException):
        await BedrockAgent.create_and_prepare_agent(
            name=bedrock_agent_model_with_id_not_prepared_dict["agent"]["agentName"],
            instructions="test_instructions",
            env_file_path="fake_path",
        )


# Test case to verify the creation of BedrockAgent
@patch.object(boto3, "client", return_value=Mock())
async def test_bedrock_agent_create_and_prepare_agent_service_exception(
    client,
    bedrock_agent_model_with_id_not_prepared_dict,
    bedrock_agent_unit_test_env,
):
    with (
        patch.object(client, "create_agent") as mock_create_agent,
        patch.object(BedrockAgent, "prepare_agent_and_wait_until_prepared", new_callable=AsyncMock),
    ):
        from botocore.exceptions import ClientError

        mock_create_agent.side_effect = ClientError({}, "create_agent")

        with pytest.raises(AgentInitializationException):
            await BedrockAgent.create_and_prepare_agent(
                name=bedrock_agent_model_with_id_not_prepared_dict["agent"]["agentName"],
                instructions="test_instructions",
                bedrock_client=client,
                env_file_path="fake_path",
            )


@patch.object(boto3, "client", return_value=Mock())
async def test_bedrock_agent_prepare_agent_and_wait_until_prepared(
    client,
    bedrock_agent_unit_test_env,
    bedrock_agent_model_with_id,
    bedrock_agent_model_with_id_preparing_dict,
    bedrock_agent_model_with_id_prepared_dict,
):
    agent = BedrockAgent(bedrock_agent_model_with_id, bedrock_client=client)

    with (
        patch.object(client, "get_agent") as mock_get_agent,
        patch.object(client, "prepare_agent") as mock_prepare_agent,
    ):
        mock_get_agent.side_effect = [
            bedrock_agent_model_with_id_preparing_dict,
            bedrock_agent_model_with_id_prepared_dict,
        ]

        await agent.prepare_agent_and_wait_until_prepared()

        mock_prepare_agent.assert_called_once_with(agentId=bedrock_agent_model_with_id.agent_id)
        assert mock_get_agent.call_count == 2
        assert agent.agent_model.agent_status == "PREPARED"


@patch.object(boto3, "client", return_value=Mock())
async def test_bedrock_agent_prepare_agent_and_wait_until_prepared_fail(
    client,
    bedrock_agent_unit_test_env,
    bedrock_agent_model_with_id,
    bedrock_agent_model_with_id_preparing_dict,
):
    agent = BedrockAgent(bedrock_agent_model_with_id, bedrock_client=client)

    with (
        patch.object(client, "get_agent") as mock_get_agent,
        patch.object(client, "prepare_agent"),
    ):
        mock_get_agent.side_effect = [
            bedrock_agent_model_with_id_preparing_dict,
            bedrock_agent_model_with_id_preparing_dict,
            bedrock_agent_model_with_id_preparing_dict,
            bedrock_agent_model_with_id_preparing_dict,
            bedrock_agent_model_with_id_preparing_dict,
            bedrock_agent_model_with_id_preparing_dict,
        ]

        with pytest.raises(TimeoutError):
            await agent.prepare_agent_and_wait_until_prepared()


# Test case to verify the creation of a code interpreter action group
@patch.object(boto3, "client", return_value=Mock())
async def test_create_code_interpreter_action_group(
    client,
    bedrock_agent_unit_test_env,
    bedrock_agent_model_with_id,
    bedrock_action_group_mode_dict,
):
    agent = BedrockAgent(bedrock_agent_model_with_id, bedrock_client=client)

    with (
        patch.object(client, "create_agent_action_group") as mock_create_action_group,
        patch.object(
            BedrockAgent, "prepare_agent_and_wait_until_prepared"
        ) as mock_prepare_agent_and_wait_until_prepared,
    ):
        mock_create_action_group.return_value = bedrock_action_group_mode_dict
        action_group_model = await agent.create_code_interpreter_action_group()

        mock_create_action_group.assert_called_once_with(
            agentId=agent.agent_model.agent_id,
            agentVersion=agent.agent_model.agent_version or "DRAFT",
            actionGroupName=f"{agent.agent_model.agent_name}_code_interpreter",
            actionGroupState="ENABLED",
            parentActionGroupSignature="AMAZON.CodeInterpreter",
        )
        assert action_group_model.action_group_id == bedrock_action_group_mode_dict["agentActionGroup"]["actionGroupId"]
        mock_prepare_agent_and_wait_until_prepared.assert_called_once()


# Test case to verify the creation of BedrockAgent with plugins
@patch.object(boto3, "client", return_value=Mock())
async def test_bedrock_agent_create_with_plugin_via_constructor(
    client,
    bedrock_agent_unit_test_env,
    bedrock_agent_model_with_id,
    custom_plugin_class,
):
    agent = BedrockAgent(
        bedrock_agent_model_with_id,
        plugins=[custom_plugin_class()],
        bedrock_client=client,
    )

    assert agent.kernel.plugins is not None
    assert len(agent.kernel.plugins) == 1


# Test case to verify the creation of a user input action group
@patch.object(boto3, "client", return_value=Mock())
async def test_create_user_input_action_group(
    client,
    bedrock_agent_unit_test_env,
    bedrock_agent_model_with_id,
    bedrock_action_group_mode_dict,
):
    agent = BedrockAgent(bedrock_agent_model_with_id, bedrock_client=client)

    with (
        patch.object(agent.bedrock_client, "create_agent_action_group") as mock_create_action_group,
        patch.object(
            BedrockAgent, "prepare_agent_and_wait_until_prepared"
        ) as mock_prepare_agent_and_wait_until_prepared,
    ):
        mock_create_action_group.return_value = bedrock_action_group_mode_dict
        action_group_model = await agent.create_user_input_action_group()

        mock_create_action_group.assert_called_once_with(
            agentId=agent.agent_model.agent_id,
            agentVersion=agent.agent_model.agent_version or "DRAFT",
            actionGroupName=f"{agent.agent_model.agent_name}_user_input",
            actionGroupState="ENABLED",
            parentActionGroupSignature="AMAZON.UserInput",
        )
        assert action_group_model.action_group_id == bedrock_action_group_mode_dict["agentActionGroup"]["actionGroupId"]
        mock_prepare_agent_and_wait_until_prepared.assert_called_once()


# Test case to verify the creation of a kernel function action group
@patch.object(boto3, "client", return_value=Mock())
async def test_create_kernel_function_action_group(
    client,
    kernel_with_function,
    bedrock_agent_unit_test_env,
    bedrock_agent_model_with_id,
    bedrock_action_group_mode_dict,
):
    agent = BedrockAgent(bedrock_agent_model_with_id, kernel=kernel_with_function, bedrock_client=client)

    with (
        patch.object(agent.bedrock_client, "create_agent_action_group") as mock_create_action_group,
        patch.object(
            BedrockAgent, "prepare_agent_and_wait_until_prepared"
        ) as mock_prepare_agent_and_wait_until_prepared,
    ):
        mock_create_action_group.return_value = bedrock_action_group_mode_dict

        action_group_model = await agent.create_kernel_function_action_group()

        mock_create_action_group.assert_called_once_with(
            agentId=agent.agent_model.agent_id,
            agentVersion=agent.agent_model.agent_version or "DRAFT",
            actionGroupName=f"{agent.agent_model.agent_name}_kernel_function",
            actionGroupState="ENABLED",
            actionGroupExecutor={"customControl": "RETURN_CONTROL"},
            functionSchema=kernel_function_to_bedrock_function_schema(
                agent.function_choice_behavior.get_config(kernel_with_function)
            ),
        )
        assert action_group_model.action_group_id == bedrock_action_group_mode_dict["agentActionGroup"]["actionGroupId"]
        mock_prepare_agent_and_wait_until_prepared.assert_called_once()


# Test case to verify the association of an agent with a knowledge base
@patch.object(boto3, "client", return_value=Mock())
async def test_associate_agent_knowledge_base(
    client,
    bedrock_agent_unit_test_env,
    bedrock_agent_model_with_id,
):
    agent = BedrockAgent(bedrock_agent_model_with_id, bedrock_client=client)

    with (
        patch.object(agent.bedrock_client, "associate_agent_knowledge_base") as mock_associate_knowledge_base,
        patch.object(
            BedrockAgent, "prepare_agent_and_wait_until_prepared"
        ) as mock_prepare_agent_and_wait_until_prepared,
    ):
        await agent.associate_agent_knowledge_base("test_knowledge_base_id")

        mock_associate_knowledge_base.assert_called_once_with(
            agentId=agent.agent_model.agent_id,
            agentVersion=agent.agent_model.agent_version,
            knowledgeBaseId="test_knowledge_base_id",
        )
        mock_prepare_agent_and_wait_until_prepared.assert_called_once()


# Test case to verify the disassociation of an agent with a knowledge base
@patch.object(boto3, "client", return_value=Mock())
async def test_disassociate_agent_knowledge_base(
    client,
    bedrock_agent_unit_test_env,
    bedrock_agent_model_with_id,
):
    agent = BedrockAgent(bedrock_agent_model_with_id, bedrock_client=client)

    with (
        patch.object(agent.bedrock_client, "disassociate_agent_knowledge_base") as mock_disassociate_knowledge_base,
        patch.object(
            BedrockAgent, "prepare_agent_and_wait_until_prepared"
        ) as mock_prepare_agent_and_wait_until_prepared,
    ):
        await agent.disassociate_agent_knowledge_base("test_knowledge_base_id")
        mock_disassociate_knowledge_base.assert_called_once_with(
            agentId=agent.agent_model.agent_id,
            agentVersion=agent.agent_model.agent_version,
            knowledgeBaseId="test_knowledge_base_id",
        )
        mock_prepare_agent_and_wait_until_prepared.assert_called_once()


# Test case to verify listing associated knowledge bases with an agent
@patch.object(boto3, "client", return_value=Mock())
async def test_list_associated_agent_knowledge_bases(
    client,
    bedrock_agent_unit_test_env,
    bedrock_agent_model_with_id,
):
    agent = BedrockAgent(bedrock_agent_model_with_id, bedrock_client=client)

    with patch.object(agent.bedrock_client, "list_agent_knowledge_bases") as mock_list_knowledge_bases:
        await agent.list_associated_agent_knowledge_bases()

        mock_list_knowledge_bases.assert_called_once_with(
            agentId=agent.agent_model.agent_id,
            agentVersion=agent.agent_model.agent_version,
        )


# endregion

# region Agent Deletion Tests


@patch.object(boto3, "client", return_value=Mock())
async def test_delete_agent(
    client,
    bedrock_agent_unit_test_env,
    bedrock_agent_model_with_id,
):
    agent = BedrockAgent(bedrock_agent_model_with_id, bedrock_client=client)

    agent_id = bedrock_agent_model_with_id.agent_id
    with patch.object(agent.bedrock_client, "delete_agent") as mock_delete_agent:
        await agent.delete_agent()

        mock_delete_agent.assert_called_once_with(agentId=agent_id)
        assert agent.agent_model.agent_id is None


# Test case to verify error handling when deleting an agent that does not exist
@patch.object(boto3, "client", return_value=Mock())
async def test_delete_agent_twice_error(
    client,
    bedrock_agent_unit_test_env,
    bedrock_agent_model_with_id,
):
    agent = BedrockAgent(bedrock_agent_model_with_id, bedrock_client=client)

    with patch.object(agent.bedrock_client, "delete_agent"):
        await agent.delete_agent()

        with pytest.raises(ValueError):
            await agent.delete_agent()


# Test case to verify error handling when there is a client error during agent deletion
@patch.object(boto3, "client", return_value=Mock())
async def test_delete_agent_client_error(
    client,
    bedrock_agent_unit_test_env,
    bedrock_agent_model_with_id,
):
    agent = BedrockAgent(bedrock_agent_model_with_id, bedrock_client=client)

    with patch.object(agent.bedrock_client, "delete_agent") as mock_delete_agent:
        from botocore.exceptions import ClientError

        mock_delete_agent.side_effect = ClientError({"Error": {"Code": "500"}}, "delete_agent")

        with pytest.raises(ClientError):
            await agent.delete_agent()


# endregion

# region Agent Invoke Tests


# Test case to verify the `get_response` method of BedrockAgent
@pytest.mark.parametrize(
    "thread",
    [
        None,
        BedrockAgentThread(None, session_id="test_session_id"),
    ],
)
@patch.object(boto3, "client", return_value=Mock())
async def test_bedrock_agent_get_response(
    client,
    thread,
    bedrock_agent_unit_test_env,
    bedrock_agent_model_with_id,
    bedrock_agent_non_streaming_simple_response,
    simple_response,
):
    with (
        patch.object(BedrockAgent, "_invoke_agent", new_callable=AsyncMock) as mock_invoke_agent,
        patch.object(BedrockAgentThread, "create", new_callable=AsyncMock) as mock_start,
        patch(
            "semantic_kernel.agents.bedrock.bedrock_agent.BedrockAgentThread.id",
            new_callable=PropertyMock,
        ) as mock_id,
    ):
        agent = BedrockAgent(bedrock_agent_model_with_id)
        mock_id.return_value = "mock-thread-id"

        mock_invoke_agent.return_value = bedrock_agent_non_streaming_simple_response
        mock_start.return_value = "test_session_id"

        response = await agent.get_response(messages="test_input_text", thread=thread)
        assert response.message.content == simple_response

        mock_invoke_agent.assert_called_once()


# Test case to verify the `get_response` method of BedrockAgent
@patch.object(boto3, "client", return_value=Mock())
async def test_bedrock_agent_get_response_exception(
    client,
    bedrock_agent_unit_test_env,
    bedrock_agent_model_with_id,
    bedrock_agent_non_streaming_empty_response,
):
    with (
        patch.object(BedrockAgent, "_invoke_agent", new_callable=AsyncMock) as mock_invoke_agent,
        patch.object(BedrockAgentThread, "create", new_callable=AsyncMock) as mock_start,
        patch(
            "semantic_kernel.agents.bedrock.bedrock_agent.BedrockAgentThread.id",
            new_callable=PropertyMock,
        ) as mock_id,
    ):
        agent = BedrockAgent(bedrock_agent_model_with_id)
        mock_id.return_value = "mock-thread-id"

        mock_invoke_agent.return_value = bedrock_agent_non_streaming_empty_response
        mock_start.return_value = "test_session_id"

        with pytest.raises(AgentInvokeException):
            await agent.get_response(messages="test_input_text")


# Test case to verify the invocation of BedrockAgent
@pytest.mark.parametrize(
    "thread",
    [
        None,
        BedrockAgentThread(None, session_id="test_session_id"),
    ],
)
@patch.object(boto3, "client", return_value=Mock())
async def test_bedrock_agent_invoke(
    client,
    thread,
    bedrock_agent_unit_test_env,
    bedrock_agent_model_with_id,
    bedrock_agent_non_streaming_simple_response,
    simple_response,
):
    with (
        patch.object(BedrockAgent, "_invoke_agent", new_callable=AsyncMock) as mock_invoke_agent,
        patch.object(BedrockAgentThread, "create", new_callable=AsyncMock) as mock_start,
        patch.object(BedrockAgentThread, "id", "test_session_id"),
    ):
        agent = BedrockAgent(bedrock_agent_model_with_id)

        mock_invoke_agent.return_value = bedrock_agent_non_streaming_simple_response
        mock_start.return_value = "test_session_id"

        async for response in agent.invoke(messages="test_input_text", thread=thread):
            assert response.message.content == simple_response

        mock_invoke_agent.assert_called_once_with(
            "test_session_id",
            "test_input_text",
            None,
            streamingConfigurations={"streamFinalResponse": False},
            sessionState={},
        )


# Test case to verify the streaming invocation of BedrockAgent
@pytest.mark.parametrize(
    "thread",
    [
        None,
        BedrockAgentThread(None, session_id="test_session_id"),
    ],
)
@patch.object(boto3, "client", return_value=Mock())
async def test_bedrock_agent_invoke_stream(
    client,
    thread,
    bedrock_agent_unit_test_env,
    bedrock_agent_model_with_id,
    bedrock_agent_streaming_simple_response,
    simple_response,
):
    with (
        patch.object(BedrockAgent, "_invoke_agent", new_callable=AsyncMock) as mock_invoke_agent,
        patch.object(BedrockAgentThread, "create", new_callable=AsyncMock) as mock_start,
        patch.object(BedrockAgentThread, "id", "test_session_id"),
    ):
        agent = BedrockAgent(bedrock_agent_model_with_id)

        mock_invoke_agent.return_value = bedrock_agent_streaming_simple_response
        mock_start.return_value = "test_session_id"

        full_message = ""
        async for response in agent.invoke_stream(messages="test_input_text", thread=thread):
            full_message += response.message.content

        assert full_message == simple_response
        mock_invoke_agent.assert_called_once_with(
            "test_session_id",
            "test_input_text",
            None,
            streamingConfigurations={"streamFinalResponse": True},
            sessionState={},
        )


# Test case to verify the invocation of BedrockAgent with function call
@patch.object(boto3, "client", return_value=Mock())
async def test_bedrock_agent_invoke_with_function_call(
    client,
    bedrock_agent_unit_test_env,
    bedrock_agent_model_with_id,
    bedrock_agent_function_call_response,
    bedrock_agent_non_streaming_simple_response,
):
    with (
        patch.object(BedrockAgent, "_invoke_agent", new_callable=AsyncMock) as mock_invoke_agent,
        patch.object(BedrockAgent, "_handle_function_call_contents") as mock_handle_function_call_contents,
        patch.object(BedrockAgentThread, "create", new_callable=AsyncMock) as mock_start,
        patch.object(BedrockAgentThread, "id", "test_session_id"),
    ):
        agent = BedrockAgent(bedrock_agent_model_with_id)

        function_result_contents = [
            FunctionResultContent(
                id="test_id",
                name="test_function",
                result="test_result",
                metadata={"functionInvocationInput": {"actionGroup": "test_action_group"}},
            )
        ]
        mock_handle_function_call_contents.return_value = function_result_contents
        agent.function_choice_behavior.maximum_auto_invoke_attempts = 2

        mock_invoke_agent.side_effect = [
            bedrock_agent_function_call_response,
            bedrock_agent_non_streaming_simple_response,
        ]
        mock_start.return_value = "test_session_id"
        async for _ in agent.invoke(messages="test_input_text"):
            mock_invoke_agent.assert_called_with(
                "test_session_id",
                "test_input_text",
                None,
                streamingConfigurations={"streamFinalResponse": False},
                sessionState={
                    "invocationId": "test_invocation_id",
                    "returnControlInvocationResults": parse_function_result_contents(function_result_contents),
                },
            )


# Test case to verify the streaming invocation of BedrockAgent with function call
@patch.object(boto3, "client", return_value=Mock())
async def test_bedrock_agent_invoke_stream_with_function_call(
    client,
    bedrock_agent_unit_test_env,
    bedrock_agent_model_with_id,
    bedrock_agent_function_call_response,
    bedrock_agent_streaming_simple_response,
):
    with (
        patch.object(BedrockAgent, "_invoke_agent", new_callable=AsyncMock) as mock_invoke_agent,
        patch.object(BedrockAgent, "_handle_function_call_contents") as mock_handle_function_call_contents,
        patch.object(BedrockAgentThread, "create", new_callable=AsyncMock) as mock_start,
        patch.object(BedrockAgentThread, "id", "test_session_id"),
    ):
        agent = BedrockAgent(bedrock_agent_model_with_id)

        function_result_contents = [
            FunctionResultContent(
                id="test_id",
                name="test_function",
                result="test_result",
                metadata={"functionInvocationInput": {"actionGroup": "test_action_group"}},
            )
        ]
        mock_handle_function_call_contents.return_value = function_result_contents
        agent.function_choice_behavior.maximum_auto_invoke_attempts = 2

        mock_invoke_agent.side_effect = [
            bedrock_agent_function_call_response,
            bedrock_agent_streaming_simple_response,
        ]
        mock_start.return_value = "test_session_id"
        async for _ in agent.invoke_stream(messages="test_input_text"):
            mock_invoke_agent.assert_called_with(
                "test_session_id",
                "test_input_text",
                None,
                streamingConfigurations={"streamFinalResponse": True},
                sessionState={
                    "invocationId": "test_invocation_id",
                    "returnControlInvocationResults": parse_function_result_contents(function_result_contents),
                },
            )


# endregion


# region Filename Sanitization Tests


def test_sanitize_filename_simple():
    """Test _sanitize_filename with a simple filename."""
    assert BedrockAgent._sanitize_filename("file.txt") == "file.txt"


def test_sanitize_filename_with_spaces():
    """Test _sanitize_filename with spaces in filename."""
    assert BedrockAgent._sanitize_filename("my file.txt") == "my file.txt"


def test_sanitize_filename_directory_traversal_unix():
    """Test _sanitize_filename strips Unix-style directory traversal."""
    assert BedrockAgent._sanitize_filename("../../../etc/passwd") == "passwd"
    assert BedrockAgent._sanitize_filename("../../file.txt") == "file.txt"
    assert BedrockAgent._sanitize_filename("/etc/passwd") == "passwd"


def test_sanitize_filename_directory_traversal_windows():
    """Test _sanitize_filename strips Windows-style directory traversal."""
    assert BedrockAgent._sanitize_filename("..\\..\\..\\Windows\\System32\\config") == "config"
    assert BedrockAgent._sanitize_filename("C:\\Users\\file.txt") == "file.txt"
    assert BedrockAgent._sanitize_filename("\\\\server\\share\\file.txt") == "file.txt"


def test_sanitize_filename_mixed_separators():
    """Test _sanitize_filename with mixed path separators."""
    assert BedrockAgent._sanitize_filename("../path\\to/file.txt") == "file.txt"
    assert BedrockAgent._sanitize_filename("..\\path/to\\file.txt") == "file.txt"


def test_sanitize_filename_null_byte():
    """Test _sanitize_filename removes null bytes."""
    assert BedrockAgent._sanitize_filename("file\x00.txt") == "file.txt"
    assert BedrockAgent._sanitize_filename("file.txt\x00.exe") == "file.txt.exe"


def test_sanitize_filename_empty():
    """Test _sanitize_filename returns empty string for empty result."""
    assert BedrockAgent._sanitize_filename("") == ""
    assert BedrockAgent._sanitize_filename("../") == ""
    assert BedrockAgent._sanitize_filename("..\\") == ""


def test_sanitize_filename_only_dots():
    """Test _sanitize_filename handles edge cases with dots."""
    # Note: os.path.basename("..") returns ".." which is kept as-is
    # Only "../" or "..\" patterns get stripped to empty string
    assert BedrockAgent._sanitize_filename(".") == "."


def test_sanitize_filename_logs_warning(caplog):
    """Test _sanitize_filename logs warning when filename is sanitized."""
    import logging

    with caplog.at_level(logging.WARNING):
        result = BedrockAgent._sanitize_filename("../malicious/file.txt")
        assert result == "file.txt"
        assert "potentially malicious path components" in caplog.text
        assert "../malicious/file.txt" in caplog.text
        assert "file.txt" in caplog.text


def test_sanitize_filename_no_warning_for_clean_filename(caplog):
    """Test _sanitize_filename does not log warning for clean filenames."""
    import logging

    with caplog.at_level(logging.WARNING):
        result = BedrockAgent._sanitize_filename("clean_file.txt")
        assert result == "clean_file.txt"
        assert "potentially malicious" not in caplog.text


# endregion
