// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using AGUI.Client;
using FluentAssertions;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests;

public sealed class ToolCallingTests : IAsyncDisposable
{
    private WebApplication? _app;
    private HttpClient? _client;
    private readonly ITestOutputHelper _output;

    public ToolCallingTests(ITestOutputHelper output)
    {
        this._output = output;
    }

    [Fact]
    public async Task ServerTriggersSingleFunctionCallAsync()
    {
        // Arrange
        int callCount = 0;
        AIFunction serverTool = AIFunctionFactory.Create(() =>
        {
            callCount++;
            return "Server function result";
        }, "ServerFunction", "A function on the server");

        await this.SetupTestServerAsync(serverTools: [serverTool]);
        var chatClient = new AGUIChatClient(new(this._client!, ""));
        AIAgent agent = chatClient.AsAIAgent(instructions: null, name: "assistant", description: "Test assistant", tools: []);
        AgentSession session = await agent.CreateSessionAsync();
        ChatMessage userMessage = new(ChatRole.User, "Call the server function");

        List<AgentResponseUpdate> updates = [];

        // Act
        await foreach (AgentResponseUpdate update in agent.RunStreamingAsync([userMessage], session, new AgentRunOptions(), CancellationToken.None))
        {
            updates.Add(update);
        }

        // Assert
        callCount.Should().Be(1, "server function should be called once");
        updates.Should().Contain(u => u.Contents.Any(c => c is FunctionCallContent), "should contain function call");
        updates.Should().Contain(u => u.Contents.Any(c => c is FunctionResultContent), "should contain function result");

        var functionCallUpdates = updates.Where(u => u.Contents.Any(c => c is FunctionCallContent)).ToList();
        functionCallUpdates.Should().HaveCount(1);

        var functionResultUpdates = updates.Where(u => u.Contents.Any(c => c is FunctionResultContent)).ToList();
        functionResultUpdates.Should().HaveCount(1);

        var resultContent = functionResultUpdates[0].Contents.OfType<FunctionResultContent>().First();
        resultContent.Result.Should().NotBeNull();
    }

    [Fact]
    public async Task ServerTriggersMultipleFunctionCallsAsync()
    {
        // Arrange
        int getWeatherCallCount = 0;
        int getTimeCallCount = 0;

        AIFunction getWeatherTool = AIFunctionFactory.Create(() =>
        {
            getWeatherCallCount++;
            return "Sunny, 75°F";
        }, "GetWeather", "Gets the current weather");

        AIFunction getTimeTool = AIFunctionFactory.Create(() =>
        {
            getTimeCallCount++;
            return "3:45 PM";
        }, "GetTime", "Gets the current time");

        await this.SetupTestServerAsync(serverTools: [getWeatherTool, getTimeTool]);
        var chatClient = new AGUIChatClient(new(this._client!, ""));
        AIAgent agent = chatClient.AsAIAgent(instructions: null, name: "assistant", description: "Test assistant", tools: []);
        AgentSession session = await agent.CreateSessionAsync();
        ChatMessage userMessage = new(ChatRole.User, "What's the weather and time?");

        List<AgentResponseUpdate> updates = [];

        // Act
        await foreach (AgentResponseUpdate update in agent.RunStreamingAsync([userMessage], session, new AgentRunOptions(), CancellationToken.None))
        {
            updates.Add(update);
        }

        // Assert
        getWeatherCallCount.Should().Be(1, "GetWeather should be called once");
        getTimeCallCount.Should().Be(1, "GetTime should be called once");

        var functionCallUpdates = updates.Where(u => u.Contents.Any(c => c is FunctionCallContent)).ToList();
        functionCallUpdates.Should().NotBeEmpty("should contain function calls");

        var functionCalls = updates.SelectMany(u => u.Contents.OfType<FunctionCallContent>()).ToList();
        functionCalls.Should().HaveCount(2, "should have 2 function calls");
        functionCalls.Should().Contain(fc => fc.Name == "GetWeather");
        functionCalls.Should().Contain(fc => fc.Name == "GetTime");

        var functionResults = updates.SelectMany(u => u.Contents.OfType<FunctionResultContent>()).ToList();
        functionResults.Should().HaveCount(2, "should have 2 function results");
    }

    [Fact]
    public async Task ClientTriggersSingleFunctionCallAsync()
    {
        // Arrange
        int callCount = 0;
        AIFunction clientTool = AIFunctionFactory.Create(() =>
        {
            callCount++;
            return "Client function result";
        }, "ClientFunction", "A function on the client");

        await this.SetupTestServerAsync();
        var chatClient = new AGUIChatClient(new(this._client!, ""));
        AIAgent agent = chatClient.AsAIAgent(instructions: null, name: "assistant", description: "Test assistant", tools: [clientTool]);
        AgentSession session = await agent.CreateSessionAsync();
        ChatMessage userMessage = new(ChatRole.User, "Call the client function");

        List<AgentResponseUpdate> updates = [];

        // Act
        await foreach (AgentResponseUpdate update in agent.RunStreamingAsync([userMessage], session, new AgentRunOptions(), CancellationToken.None))
        {
            updates.Add(update);
        }

        // Assert
        callCount.Should().Be(1, "client function should be called once");
        updates.Should().Contain(u => u.Contents.Any(c => c is FunctionCallContent), "should contain function call");
        updates.Should().Contain(u => u.Contents.Any(c => c is FunctionResultContent), "should contain function result");

        var functionCallUpdates = updates.Where(u => u.Contents.Any(c => c is FunctionCallContent)).ToList();
        functionCallUpdates.Should().HaveCount(1);

        var functionResultUpdates = updates.Where(u => u.Contents.Any(c => c is FunctionResultContent)).ToList();
        functionResultUpdates.Should().HaveCount(1);

        var resultContent = functionResultUpdates[0].Contents.OfType<FunctionResultContent>().First();
        resultContent.Result.Should().NotBeNull();
    }

    [Fact]
    public async Task ClientTriggersMultipleFunctionCallsAsync()
    {
        // Arrange
        int calculateCallCount = 0;
        int formatCallCount = 0;

        AIFunction calculateTool = AIFunctionFactory.Create((int a, int b) =>
        {
            calculateCallCount++;
            return a + b;
        }, "Calculate", "Calculates sum of two numbers");

        AIFunction formatTool = AIFunctionFactory.Create((string text) =>
        {
            formatCallCount++;
            return text.ToUpperInvariant();
        }, "FormatText", "Formats text to uppercase");

        await this.SetupTestServerAsync();
        var chatClient = new AGUIChatClient(new(this._client!, ""));
        AIAgent agent = chatClient.AsAIAgent(instructions: null, name: "assistant", description: "Test assistant", tools: [calculateTool, formatTool]);
        AgentSession session = await agent.CreateSessionAsync();
        ChatMessage userMessage = new(ChatRole.User, "Calculate 5 + 3 and format 'hello'");

        List<AgentResponseUpdate> updates = [];

        // Act
        await foreach (AgentResponseUpdate update in agent.RunStreamingAsync([userMessage], session, new AgentRunOptions(), CancellationToken.None))
        {
            updates.Add(update);
        }

        // Assert
        calculateCallCount.Should().Be(1, "Calculate should be called once");
        formatCallCount.Should().Be(1, "FormatText should be called once");

        var functionCallUpdates = updates.Where(u => u.Contents.Any(c => c is FunctionCallContent)).ToList();
        functionCallUpdates.Should().NotBeEmpty("should contain function calls");

        var functionCalls = updates.SelectMany(u => u.Contents.OfType<FunctionCallContent>()).ToList();
        functionCalls.Should().HaveCount(2, "should have 2 function calls");
        functionCalls.Should().Contain(fc => fc.Name == "Calculate");
        functionCalls.Should().Contain(fc => fc.Name == "FormatText");

        var functionResults = updates.SelectMany(u => u.Contents.OfType<FunctionResultContent>()).ToList();
        functionResults.Should().HaveCount(2, "should have 2 function results");
    }

    [Fact]
    public async Task ServerAndClientTriggerFunctionCallsSimultaneouslyAsync()
    {
        // Arrange
        int serverCallCount = 0;
        int clientCallCount = 0;

        AIFunction serverTool = AIFunctionFactory.Create(() =>
        {
            System.Diagnostics.Debug.Assert(true, "Server function is being called!");
            serverCallCount++;
            return "Server data";
        }, "GetServerData", "Gets data from the server");

        AIFunction clientTool = AIFunctionFactory.Create(() =>
        {
            System.Diagnostics.Debug.Assert(true, "Client function is being called!");
            clientCallCount++;
            return "Client data";
        }, "GetClientData", "Gets data from the client");

        await this.SetupTestServerAsync(serverTools: [serverTool]);
        var chatClient = new AGUIChatClient(new(this._client!, ""));
        AIAgent agent = chatClient.AsAIAgent(instructions: null, name: "assistant", description: "Test assistant", tools: [clientTool]);
        AgentSession session = await agent.CreateSessionAsync();
        ChatMessage userMessage = new(ChatRole.User, "Get both server and client data");

        List<AgentResponseUpdate> updates = [];

        // Act
        await foreach (AgentResponseUpdate update in agent.RunStreamingAsync([userMessage], session, new AgentRunOptions(), CancellationToken.None))
        {
            updates.Add(update);
            this._output.WriteLine($"Update: {update.Contents.Count} contents");
            foreach (var content in update.Contents)
            {
                this._output.WriteLine($"  Content: {content.GetType().Name}");
                if (content is FunctionCallContent fc)
                {
                    this._output.WriteLine($"    FunctionCall: {fc.Name}");
                }
                if (content is FunctionResultContent fr)
                {
                    this._output.WriteLine($"    FunctionResult: {fr.CallId} - {fr.Result}");
                }
            }
        }

        // Assert
        this._output.WriteLine($"serverCallCount={serverCallCount}, clientCallCount={clientCallCount}");

        // Verify both the server and client tools executed and both results round-tripped through
        // the streaming pipeline. This is now correct behavior thanks to
        // ConfigureForMixedInvocation in the AGUI.Hosting.AspNetCore package.

        serverCallCount.Should().Be(1, "server function should execute on server");
        clientCallCount.Should().Be(1, "client function should execute on client");

        var functionCallUpdates = updates.Where(u => u.Contents.Any(c => c is FunctionCallContent)).ToList();
        functionCallUpdates.Should().NotBeEmpty("should contain function calls");

        var functionCalls = updates.SelectMany(u => u.Contents.OfType<FunctionCallContent>()).ToList();
        functionCalls.Should().HaveCount(2, "should have 2 function calls");
        functionCalls.Should().Contain(fc => fc.Name == "GetServerData");
        functionCalls.Should().Contain(fc => fc.Name == "GetClientData");

        var functionResults = updates.SelectMany(u => u.Contents.OfType<FunctionResultContent>()).ToList();
        functionResults.Should().HaveCount(2, "both server and client function results should be present");

        var serverResult = functionResults.FirstOrDefault(fr =>
            functionCalls.Any(fc => fc.Name == "GetServerData" && fc.CallId == fr.CallId));
        serverResult.Should().NotBeNull("server function call should have a result");
        serverResult!.Result?.ToString().Should().Contain("Server data");

        var clientResult = functionResults.FirstOrDefault(fr =>
            functionCalls.Any(fc => fc.Name == "GetClientData" && fc.CallId == fr.CallId));
        clientResult.Should().NotBeNull("client function call should have a result");
        clientResult!.Result?.ToString().Should().Contain("Client data");
    }

    [Fact]
    public async Task FunctionCallsPreserveCallIdAndNameAsync()
    {
        // Arrange
        AIFunction testTool = AIFunctionFactory.Create(() => "Test result", "TestFunction", "A test function");

        await this.SetupTestServerAsync(serverTools: [testTool]);
        var chatClient = new AGUIChatClient(new(this._client!, ""));
        AIAgent agent = chatClient.AsAIAgent(instructions: null, name: "assistant", description: "Test assistant", tools: []);
        AgentSession session = await agent.CreateSessionAsync();
        ChatMessage userMessage = new(ChatRole.User, "Call the test function");

        List<AgentResponseUpdate> updates = [];

        // Act
        await foreach (AgentResponseUpdate update in agent.RunStreamingAsync([userMessage], session, new AgentRunOptions(), CancellationToken.None))
        {
            updates.Add(update);
        }

        // Assert
        var functionCallContent = updates.SelectMany(u => u.Contents.OfType<FunctionCallContent>()).FirstOrDefault();
        functionCallContent.Should().NotBeNull();
        functionCallContent!.CallId.Should().NotBeNullOrEmpty();
        functionCallContent.Name.Should().Be("TestFunction");

        var functionResultContent = updates.SelectMany(u => u.Contents.OfType<FunctionResultContent>()).FirstOrDefault();
        functionResultContent.Should().NotBeNull();
        functionResultContent!.CallId.Should().Be(functionCallContent.CallId, "result should have same call ID as the call");
    }

    [Fact]
    public async Task ParallelFunctionCallsFromServerAreHandledCorrectlyAsync()
    {
        // Arrange
        int func1CallCount = 0;
        int func2CallCount = 0;

        AIFunction func1 = AIFunctionFactory.Create(() =>
        {
            func1CallCount++;
            return "Result 1";
        }, "Function1", "First function");

        AIFunction func2 = AIFunctionFactory.Create(() =>
        {
            func2CallCount++;
            return "Result 2";
        }, "Function2", "Second function");

        await this.SetupTestServerAsync(serverTools: [func1, func2], triggerParallelCalls: true);
        var chatClient = new AGUIChatClient(new(this._client!, ""));
        AIAgent agent = chatClient.AsAIAgent(instructions: null, name: "assistant", description: "Test assistant", tools: []);
        AgentSession session = await agent.CreateSessionAsync();
        ChatMessage userMessage = new(ChatRole.User, "Call both functions in parallel");

        List<AgentResponseUpdate> updates = [];

        // Act
        await foreach (AgentResponseUpdate update in agent.RunStreamingAsync([userMessage], session, new AgentRunOptions(), CancellationToken.None))
        {
            updates.Add(update);
        }

        // Assert
        func1CallCount.Should().Be(1, "Function1 should be called once");
        func2CallCount.Should().Be(1, "Function2 should be called once");

        var functionCalls = updates.SelectMany(u => u.Contents.OfType<FunctionCallContent>()).ToList();
        functionCalls.Should().HaveCount(2);
        functionCalls.Select(fc => fc.Name).Should().Contain(s_expectedFunctionNames);

        var functionResults = updates.SelectMany(u => u.Contents.OfType<FunctionResultContent>()).ToList();
        functionResults.Should().HaveCount(2);

        // Each result should match its corresponding call ID
        foreach (var call in functionCalls)
        {
            functionResults.Should().Contain(r => r.CallId == call.CallId);
        }
    }

    private static readonly string[] s_expectedFunctionNames = ["Function1", "Function2"];

    [Fact]
    public async Task AGUIChatClientCombinesCustomJsonSerializerOptionsAsync()
    {
        // This test verifies that custom JSON contexts work correctly with AGUIChatClient by testing
        // that a client-defined type can be serialized successfully using the combined options

        // Arrange
        await this.SetupTestServerAsync();

        // Client uses custom JSON context
        var clientJsonOptions = new JsonSerializerOptions();
        clientJsonOptions.TypeInfoResolverChain.Add(ClientJsonContext.Default);

        _ = new AGUIChatClient(new(this._client!, "") { JsonSerializerOptions = clientJsonOptions });

        // Act - Verify that both AG-UI types and custom types can be serialized
        // The AGUIChatClient should have combined AGUIJsonSerializerContext with ClientJsonContext

        // Try to serialize a custom type using the ClientJsonContext
        var testResponse = new ClientForecastResponse(75, 60, "Rainy");
        var json = JsonSerializer.Serialize(testResponse, ClientJsonContext.Default.ClientForecastResponse);

        // Assert
        var jsonElement = JsonElement.Parse(json);
        jsonElement.GetProperty("MaxTemp").GetInt32().Should().Be(75);
        jsonElement.GetProperty("MinTemp").GetInt32().Should().Be(60);
        jsonElement.GetProperty("Outlook").GetString().Should().Be("Rainy");

        this._output.WriteLine("Successfully serialized custom type: " + json);

        // The actual integration is tested by the ClientToolCallWithCustomArgumentsAsync test
        // which verifies that AG-UI protocol works end-to-end with custom types
    }

    [Fact]
    public async Task ServerToolCallWithCustomArgumentsAsync()
    {
        // Arrange
        int callCount = 0;
        AIFunction serverTool = AIFunctionFactory.Create(
            (ServerForecastRequest request) =>
            {
                callCount++;
                return new ServerForecastResponse(
                    Temperature: 72,
                    Condition: request.Location == "Seattle" ? "Rainy" : "Sunny",
                    Humidity: 65);
            },
            "GetServerForecast",
            "Gets the weather forecast from server",
            ServerJsonContext.Default.Options);

        await this.SetupTestServerAsync(serverTools: [serverTool], jsonSerializerOptions: ServerJsonContext.Default.Options);
        var chatClient = new AGUIChatClient(new(this._client!, "") { JsonSerializerOptions = ServerJsonContext.Default.Options });
        AIAgent agent = chatClient.AsAIAgent(instructions: null, name: "assistant", description: "Test assistant", tools: []);
        AgentSession session = await agent.CreateSessionAsync();
        ChatMessage userMessage = new(ChatRole.User, "Get server forecast for Seattle for 5 days");

        List<AgentResponseUpdate> updates = [];

        // Act
        await foreach (AgentResponseUpdate update in agent.RunStreamingAsync([userMessage], session, new AgentRunOptions(), CancellationToken.None))
        {
            updates.Add(update);
        }

        // Assert
        callCount.Should().Be(1, "server function with custom arguments should be called once");
        updates.Should().Contain(u => u.Contents.Any(c => c is FunctionCallContent), "should contain function call");
        updates.Should().Contain(u => u.Contents.Any(c => c is FunctionResultContent), "should contain function result");

        var functionCallContent = updates.SelectMany(u => u.Contents.OfType<FunctionCallContent>()).FirstOrDefault();
        functionCallContent.Should().NotBeNull();
        functionCallContent!.Name.Should().Be("GetServerForecast");

        var functionResultContent = updates.SelectMany(u => u.Contents.OfType<FunctionResultContent>()).FirstOrDefault();
        functionResultContent.Should().NotBeNull();
        functionResultContent!.Result.Should().NotBeNull();
    }

    [Fact]
    public async Task ClientToolCallWithCustomArgumentsAsync()
    {
        // Arrange
        int callCount = 0;
        AIFunction clientTool = AIFunctionFactory.Create(
            (ClientForecastRequest request) =>
            {
                callCount++;
                return new ClientForecastResponse(
                    MaxTemp: request.City == "Portland" ? 68 : 75,
                    MinTemp: 55,
                    Outlook: "Partly Cloudy");
            },
            "GetClientForecast",
            "Gets the weather forecast from client",
            ClientJsonContext.Default.Options);

        await this.SetupTestServerAsync();
        var chatClient = new AGUIChatClient(new(this._client!, "") { JsonSerializerOptions = ClientJsonContext.Default.Options });
        AIAgent agent = chatClient.AsAIAgent(instructions: null, name: "assistant", description: "Test assistant", tools: [clientTool]);
        AgentSession session = await agent.CreateSessionAsync();
        ChatMessage userMessage = new(ChatRole.User, "Get client forecast for Portland with hourly data");

        List<AgentResponseUpdate> updates = [];

        // Act
        await foreach (AgentResponseUpdate update in agent.RunStreamingAsync([userMessage], session, new AgentRunOptions(), CancellationToken.None))
        {
            updates.Add(update);
        }

        // Assert
        callCount.Should().Be(1, "client function with custom arguments should be called once");
        updates.Should().Contain(u => u.Contents.Any(c => c is FunctionCallContent), "should contain function call");
        updates.Should().Contain(u => u.Contents.Any(c => c is FunctionResultContent), "should contain function result");

        var functionCallContent = updates.SelectMany(u => u.Contents.OfType<FunctionCallContent>()).FirstOrDefault();
        functionCallContent.Should().NotBeNull();
        functionCallContent!.Name.Should().Be("GetClientForecast");

        var functionResultContent = updates.SelectMany(u => u.Contents.OfType<FunctionResultContent>()).FirstOrDefault();
        functionResultContent.Should().NotBeNull();
        functionResultContent!.Result.Should().NotBeNull();
    }

    private async Task SetupTestServerAsync(
        IList<AITool>? serverTools = null,
        bool triggerParallelCalls = false,
        JsonSerializerOptions? jsonSerializerOptions = null)
    {
        WebApplicationBuilder builder = WebApplication.CreateBuilder();
        builder.Services.AddAGUIServer();
        builder.WebHost.UseTestServer();

        // Configure HTTP JSON options if custom serializer options provided
        if (jsonSerializerOptions?.TypeInfoResolver != null)
        {
            builder.Services.ConfigureHttpJsonOptions(options =>
                options.SerializerOptions.TypeInfoResolverChain.Add(jsonSerializerOptions.TypeInfoResolver));
        }

        this._app = builder.Build();
        // FakeChatClient will receive options.Tools containing both server and client tools (merged by framework)
        var fakeChatClient = new FakeToolCallingChatClient(triggerParallelCalls, this._output, jsonSerializerOptions: jsonSerializerOptions);
        AIAgent baseAgent = fakeChatClient.AsAIAgent(instructions: null, name: "base-agent", description: "A base agent for tool testing", tools: serverTools ?? []);
        this._app.MapAGUIServer("/agent", baseAgent);

        await this._app.StartAsync();

        TestServer testServer = this._app.Services.GetRequiredService<IServer>() as TestServer
            ?? throw new InvalidOperationException("TestServer not found");

        this._client = testServer.CreateClient();
        this._client.BaseAddress = new Uri("http://localhost/agent");
    }

    public async ValueTask DisposeAsync()
    {
        this._client?.Dispose();
        if (this._app != null)
        {
            await this._app.DisposeAsync();
        }
    }
}

internal sealed class FakeToolCallingChatClient : IChatClient
{
    private readonly bool _triggerParallelCalls;
    private readonly ITestOutputHelper? _output;
    public FakeToolCallingChatClient(bool triggerParallelCalls = false, ITestOutputHelper? output = null, JsonSerializerOptions? jsonSerializerOptions = null)
    {
        this._triggerParallelCalls = triggerParallelCalls;
        this._output = output;
    }

    public ChatClientMetadata Metadata => new("fake-tool-calling-chat-client");

    public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
        IEnumerable<ChatMessage> messages,
        ChatOptions? options = null,
        [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        string messageId = Guid.NewGuid().ToString("N");

        var messageList = messages.ToList();
        this._output?.WriteLine($"[FakeChatClient] Received {messageList.Count} messages");

        // Check if there are function results in the messages - if so, we've already done the function call loop
        var hasFunctionResults = messageList.Any(m => m.Contents.Any(c => c is FunctionResultContent));

        if (hasFunctionResults)
        {
            this._output?.WriteLine("[FakeChatClient] Function results present, returning final response");
            // Function results are present, return a final response
            yield return new ChatResponseUpdate
            {
                MessageId = messageId,
                Role = ChatRole.Assistant,
                Contents = [new TextContent("Function calls completed successfully")]
            };
            yield break;
        }

        // options?.Tools contains all tools (server + client merged by framework)
        var allTools = (options?.Tools ?? []).ToList();
        this._output?.WriteLine($"[FakeChatClient] Received {allTools.Count} tools to advertise");

        if (allTools.Count == 0)
        {
            // No tools available, just return a simple message
            yield return new ChatResponseUpdate
            {
                MessageId = messageId,
                Role = ChatRole.Assistant,
                Contents = [new TextContent("No tools available")]
            };
            yield break;
        }

        // Determine which tools to call based on the scenario
        var toolsToCall = new List<AITool>();

        // Check message content to determine what to call
        var lastUserMessage = messageList.LastOrDefault(m => m.Role == ChatRole.User)?.Text ?? "";

        if (this._triggerParallelCalls)
        {
            // Call all available tools in parallel
            toolsToCall.AddRange(allTools);
        }
        else if (lastUserMessage.Contains("both", StringComparison.OrdinalIgnoreCase) ||
                 lastUserMessage.Contains("all", StringComparison.OrdinalIgnoreCase))
        {
            // Call all available tools
            toolsToCall.AddRange(allTools);
        }
        else
        {
            // Default: call all available tools
            // The fake LLM doesn't distinguish between server and client tools - it just requests them all
            // The FunctionInvokingChatClient layers will handle executing what they can
            toolsToCall.AddRange(allTools);
        }

        // Assert: Should have tools to call
        System.Diagnostics.Debug.Assert(toolsToCall.Count > 0, "Should have at least one tool to call");

        // Generate function calls
        // Server's FunctionInvokingChatClient will execute server tools
        // Client tool calls will be sent back to client, and client's FunctionInvokingChatClient will execute them
        this._output?.WriteLine($"[FakeChatClient] Generating {toolsToCall.Count} function calls");
        foreach (var tool in toolsToCall)
        {
            string callId = $"call_{Guid.NewGuid():N}";
            var functionName = tool.Name ?? "UnknownFunction";
            this._output?.WriteLine($"[FakeChatClient]   Calling: {functionName} (type: {tool.GetType().Name})");

            // Generate sample arguments based on the function signature
            var arguments = GenerateArgumentsForTool(functionName);

            yield return new ChatResponseUpdate
            {
                MessageId = messageId,
                Role = ChatRole.Assistant,
                Contents = [new FunctionCallContent(callId, functionName, arguments)]
            };

            await Task.Yield();
        }
    }

    private static Dictionary<string, object?> GenerateArgumentsForTool(string functionName)
    {
        // Generate sample arguments based on the function name
        return functionName switch
        {
            "GetWeather" => new Dictionary<string, object?> { ["location"] = "Seattle" },
            "GetTime" => [], // No parameters
            "Calculate" => new Dictionary<string, object?> { ["a"] = 5, ["b"] = 3 },
            "FormatText" => new Dictionary<string, object?> { ["text"] = "hello" },
            "GetServerData" => [], // No parameters
            "GetClientData" => [], // No parameters
            // For custom types, the parameter name is "request" and the value is an instance of the request type
            "GetServerForecast" => new Dictionary<string, object?> { ["request"] = new ServerForecastRequest("Seattle", 5) },
            "GetClientForecast" => new Dictionary<string, object?> { ["request"] = new ClientForecastRequest("Portland", true) },
            _ => [] // Default: no parameters
        };
    }

    public Task<ChatResponse> GetResponseAsync(
        IEnumerable<ChatMessage> messages,
        ChatOptions? options = null,
        CancellationToken cancellationToken = default)
    {
        throw new NotImplementedException();
    }

    public void Dispose()
    {
    }

    public object? GetService(Type serviceType, object? serviceKey = null) => null;
}

// Custom types and serialization contexts for testing cross-boundary serialization
public record ServerForecastRequest(string Location, int Days);
public record ServerForecastResponse(int Temperature, string Condition, int Humidity);

public record ClientForecastRequest(string City, bool IncludeHourly);
public record ClientForecastResponse(int MaxTemp, int MinTemp, string Outlook);

[JsonSourceGenerationOptions(WriteIndented = false)]
[JsonSerializable(typeof(ServerForecastRequest))]
[JsonSerializable(typeof(ServerForecastResponse))]
internal sealed partial class ServerJsonContext : JsonSerializerContext;

[JsonSourceGenerationOptions(WriteIndented = false)]
[JsonSerializable(typeof(ClientForecastRequest))]
[JsonSerializable(typeof(ClientForecastResponse))]
internal sealed partial class ClientJsonContext : JsonSerializerContext;
