// Copyright (c) Microsoft Corporation. All rights reserved.
// MiddlewareTest.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.Extensions.AI;
using Xunit;

namespace AutoGen.Tests;

[Trait("Category", "UnitV1")]
public partial class MiddlewareTest
{
    [Function]
    public async Task<string> Echo(string message)
    {
        return $"[FUNC] {message}";
    }

    [Fact]
    public async Task HumanInputMiddlewareTestAsync()
    {
        var agent = new EchoAgent("echo");
        var neverAskUserInputMW = new HumanInputMiddleware(mode: HumanInputMode.NEVER);

        var neverInputAgent = agent.RegisterMiddleware(neverAskUserInputMW);
        var reply = await neverInputAgent.SendAsync("hello");
        reply.GetContent()!.Should().Be("hello");
        reply.From.Should().Be("echo");

        var alwaysAskUserInputMW = new HumanInputMiddleware(
            mode: HumanInputMode.ALWAYS,
            getInput: () => "input");

        var alwaysInputAgent = agent.RegisterMiddleware(alwaysAskUserInputMW);
        reply = await alwaysInputAgent.SendAsync("hello");
        reply.GetContent()!.Should().Be("input");
        reply.From.Should().Be("echo");

        // test auto mode
        // if the reply from echo is not terminate message, return the original reply
        var autoAskUserInputMW = new HumanInputMiddleware(
            mode: HumanInputMode.AUTO,
            isTermination: async (messages, ct) => messages.Last()?.GetContent() == "terminate",
            getInput: () => "input",
            exitKeyword: "exit");
        var autoInputAgent = agent.RegisterMiddleware(autoAskUserInputMW);
        reply = await autoInputAgent.SendAsync("hello");
        reply.GetContent()!.Should().Be("hello");

        // if the reply from echo is terminate message, asking user for input
        reply = await autoInputAgent.SendAsync("terminate");
        reply.GetContent()!.Should().Be("input");

        // if the reply from echo is terminate message, and user input is exit, return the TERMINATE message
        autoAskUserInputMW = new HumanInputMiddleware(
            mode: HumanInputMode.AUTO,
            isTermination: async (messages, ct) => messages.Last().GetContent() == "terminate",
            getInput: () => "exit",
            exitKeyword: "exit");
        autoInputAgent = agent.RegisterMiddleware(autoAskUserInputMW);

        reply = await autoInputAgent.SendAsync("terminate");
        reply.IsGroupChatTerminateMessage().Should().BeTrue();
    }

    [Fact]
    public async Task FunctionCallMiddlewareTestAsync()
    {
        var agent = new EchoAgent("echo");
        var args = new AutoGen.Tests.MiddlewareTest.EchoSchema { message = "hello" }; // make the format check happy on linux
        var argsJson = JsonSerializer.Serialize(args) ?? throw new InvalidOperationException("Failed to serialize args");
        var functionCall = new ToolCall("Echo", argsJson);
        var functionCallAgent = agent.RegisterMiddleware(async (messages, options, agent, ct) =>
        {
            if (options?.Functions is null)
            {
                return await agent.GenerateReplyAsync(messages, options, ct);
            }

            return new ToolCallMessage(functionCall.FunctionName, functionCall.FunctionArguments, from: agent.Name);
        });

        // test 1
        // middleware should invoke function call if the message is a function call message
        var mw = new FunctionCallMiddleware(
            functionMap: new Dictionary<string, Func<string, Task<string>>> { { "Echo", EchoWrapper } });

        var testAgent = agent.RegisterMiddleware(mw);
        var functionCallMessage = new ToolCallMessage(functionCall.FunctionName, functionCall.FunctionArguments, from: "user");
        var reply = await testAgent.SendAsync(functionCallMessage);
        reply.Should().BeOfType<ToolCallResultMessage>();
        reply.GetContent()!.Should().Be("[FUNC] hello");
        reply.From.Should().Be("echo");

        // test 2
        // middleware should work with AIFunction from M.E.A.I
        var getWeatherTool = AIFunctionFactory.Create(this.Echo);
        mw = new FunctionCallMiddleware([getWeatherTool]);
        testAgent = agent.RegisterMiddleware(mw);
        reply = await testAgent.SendAsync(functionCallMessage);
        reply.GetContent()!.Should().Be("[FUNC] hello");

        // test 3
        // middleware should invoke function call if agent reply is a function call message
        mw = new FunctionCallMiddleware(
            functions: [this.EchoFunctionContract],
            functionMap: new Dictionary<string, Func<string, Task<string>>> { { "Echo", EchoWrapper } });
        testAgent = functionCallAgent.RegisterMiddleware(mw);
        reply = await testAgent.SendAsync("hello");
        reply.GetContent()!.Should().Be("[FUNC] hello");
        reply.From.Should().Be("echo");

        // test 4
        // middleware should return original reply if the reply from agent is not a function call message
        mw = new FunctionCallMiddleware(
            functionMap: new Dictionary<string, Func<string, Task<string>>> { { "Echo", EchoWrapper } });
        testAgent = agent.RegisterMiddleware(mw);
        reply = await testAgent.SendAsync("hello");
        reply.GetContent()!.Should().Be("hello");
        reply.From.Should().Be("echo");

        // test 5
        // middleware should return an error message if the function name is not available when invoking the function from previous agent reply
        mw = new FunctionCallMiddleware(
            functionMap: new Dictionary<string, Func<string, Task<string>>> { { "Echo2", EchoWrapper } });
        testAgent = agent.RegisterMiddleware(mw);
        reply = await testAgent.SendAsync(functionCallMessage);
        reply.GetContent()!.Should().Be("Function Echo is not available. Available functions are: Echo2");
    }
}
