using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

using AIRuntime.Runtime.Capabilities;
using AIRuntime.Runtime.Models;
using AIRuntime.Runtime.Providers;
using AIRuntime.Runtime.Providers.Models;
using AIRuntime.Runtime.Services.Agent;

using NUnit.Framework;

using UnityEngine;
using UnityEngine.TestTools;
using AIRuntime.Runtime.Capabilities.Permissions;

namespace AIRuntime.Tests
{
    public sealed class MaxCapabilityRoundsTests
    {
        private string _workspace;


        [SetUp]
        public void Setup()
        {
            _workspace =
                Path.Combine(
                    Application.temporaryCachePath,
                    "AIRuntimeMaxCapabilityRoundsTests");


            if (Directory.Exists(_workspace))
            {
                Directory.Delete(
                    _workspace,
                    true);
            }


            Directory.CreateDirectory(
                _workspace);
        }


        [TearDown]
        public void Cleanup()
        {
            if (Directory.Exists(_workspace))
            {
                Directory.Delete(
                    _workspace,
                    true);
            }
        }


        //--------------------------------------------------
        // Default Configuration
        //--------------------------------------------------

        [Test]
        public void MaxCapabilityRounds_DefaultsToNoFixedLimit()
        {
            AIRuntime.Runtime.AIRuntime runtime =
                new AIRuntime.Runtime.AIRuntime(
                    _workspace);


            Assert.AreEqual(
                0,
                runtime.MaxCapabilityRounds);
        }


        //--------------------------------------------------
        // Configure Before Provider
        //--------------------------------------------------

        [Test]
        public void MaxCapabilityRounds_CanBeConfiguredBeforeProvider()
        {
            AIRuntime.Runtime.AIRuntime runtime =
                new AIRuntime.Runtime.AIRuntime(
                    _workspace);


            runtime.MaxCapabilityRounds =
                12;


            Assert.AreEqual(
                12,
                runtime.MaxCapabilityRounds);


            Assert.DoesNotThrow(
                () =>
                {
                    runtime.UseProvider(
                        new SuccessfulProvider());
                });


            Assert.AreEqual(
                12,
                runtime.MaxCapabilityRounds);
        }


        //--------------------------------------------------
        // Configure After Provider
        //--------------------------------------------------

        [UnityTest]
        public IEnumerator MaxCapabilityRounds_CanBeConfiguredAfterProvider()
        {
            RepeatingCapabilityProvider provider =
                new RepeatingCapabilityProvider();


            AIRuntime.Runtime.AIRuntime runtime =
                CreateRuntime(
                    provider);


            runtime.RegisterCapability(
                new EchoCapability());


            runtime.MaxCapabilityRounds =
                2;


            Task<AgentRunResult> task =
                runtime.RunAsync(
                    "Repeat capability calls.");


            yield return WaitForTask(
                task);


            AssertTaskCompletedSuccessfully(
                task,
                "Configured maximum-round run");


            AgentRunResult result =
                task.GetAwaiter()
                    .GetResult();


            Assert.IsFalse(
                result.Succeeded);


            Assert.AreEqual(
                2,
                provider.RequestCount);


            Assert.AreEqual(
                provider.RequestCount - 1,
                provider.CapabilityResultsReceived,
                "The provider should observe the previous capability result on each request after the first.");


            StringAssert.Contains(
                "maximum of 2 capability rounds",
                result.ErrorMessage);
        }


        [UnityTest]
        public IEnumerator IdenticalCapabilityCall_IsIgnoredOnceAndRunContinues()
        {
            DuplicateOnceProvider provider =
                new DuplicateOnceProvider();

            CountingEchoCapability capability =
                new CountingEchoCapability();

            AIRuntime.Runtime.AIRuntime runtime =
                CreateRuntime(
                    provider);

            runtime.AllowPermission(
                CapabilityPermission.Write);

            runtime.RegisterCapability(
                capability);

            Task<AgentRunResult> task =
                runtime.RunAsync(
                    "Call echo, then finish.");

            yield return WaitForTask(
                task);

            AssertTaskCompletedSuccessfully(
                task,
                "Duplicate capability recovery run");

            AgentRunResult result =
                task.GetAwaiter()
                    .GetResult();

            Assert.IsTrue(
                result.Succeeded,
                result.ErrorMessage);

            Assert.AreEqual(
                1,
                capability.ExecutionCount,
                "The duplicate mutation request must not execute twice.");

            Assert.AreEqual(
                3,
                provider.RequestCount);
        }


        [UnityTest]
        public IEnumerator RecoverableCapabilityFailure_IsReturnedToProvider()
        {
            RecoveringAfterToolErrorProvider provider =
                new RecoveringAfterToolErrorProvider();

            AIRuntime.Runtime.AIRuntime runtime =
                CreateRuntime(
                    provider);

            runtime.RegisterCapability(
                new MissingDirectoryCapability());

            LogAssert.Expect(
                LogType.Error,
                "[Capability] 'fail_directory' failed: Directory not found: Assets/Settings");

            Task<AgentRunResult> task =
                runtime.RunAsync(
                    "Recover from a missing directory.");

            yield return WaitForTask(
                task);

            AssertTaskCompletedSuccessfully(
                task,
                "Recoverable capability failure run");

            AgentRunResult result =
                task.GetAwaiter()
                    .GetResult();

            Assert.IsTrue(
                result.Succeeded,
                result.ErrorMessage);

            Assert.AreEqual(
                2,
                provider.RequestCount);

            Assert.IsTrue(
                provider.ReceivedStructuredErrorResult);
        }


        //--------------------------------------------------
        // Invalid Configuration
        //--------------------------------------------------

        [Test]
        public void MaxCapabilityRounds_ZeroDisablesFixedLimit()
        {
            AIRuntime.Runtime.AIRuntime runtime =
                new AIRuntime.Runtime.AIRuntime(
                    _workspace);


            runtime.MaxCapabilityRounds =
                0;


            Assert.AreEqual(
                0,
                runtime.MaxCapabilityRounds);
        }


        [Test]
        public void MaxCapabilityRounds_RejectsNegativeValue()
        {
            AIRuntime.Runtime.AIRuntime runtime =
                new AIRuntime.Runtime.AIRuntime(
                    _workspace);


            ArgumentOutOfRangeException exception =
                Assert.Throws<ArgumentOutOfRangeException>(
                    () =>
                    {
                        runtime.MaxCapabilityRounds =
                            -1;
                    });


            StringAssert.Contains(
                "Maximum capability rounds cannot be negative.",
                exception.Message);


            Assert.AreEqual(
                0,
                runtime.MaxCapabilityRounds);
        }


        //--------------------------------------------------
        // Provider Switching
        //--------------------------------------------------

        [UnityTest]
        public IEnumerator MaxCapabilityRounds_IsPreservedAcrossProviderSwitch()
        {
            RepeatingCapabilityProvider firstProvider =
                new RepeatingCapabilityProvider();


            RepeatingCapabilityProvider secondProvider =
                new RepeatingCapabilityProvider();


            AIRuntime.Runtime.AIRuntime runtime =
                new AIRuntime.Runtime.AIRuntime(
                    _workspace);


            runtime.RegisterCapability(
                new EchoCapability());


            runtime.MaxCapabilityRounds =
                3;


            runtime.UseProvider(
                firstProvider);


            Task<AgentRunResult> firstTask =
                runtime.RunAsync(
                    "Use the first provider.");


            yield return WaitForTask(
                firstTask);


            AssertTaskCompletedSuccessfully(
                firstTask,
                "First provider capability loop");


            Assert.IsFalse(
                firstTask.Result.Succeeded);


            Assert.AreEqual(
                3,
                firstProvider.RequestCount);


            runtime.UseProvider(
                secondProvider);


            Assert.AreEqual(
                3,
                runtime.MaxCapabilityRounds);


            Assert.AreEqual(
                0,
                runtime.MessageCount);


            Task<AgentRunResult> secondTask =
                runtime.RunAsync(
                    "Use the second provider.");


            yield return WaitForTask(
                secondTask);


            AssertTaskCompletedSuccessfully(
                secondTask,
                "Second provider capability loop");


            Assert.IsFalse(
                secondTask.Result.Succeeded);


            Assert.AreEqual(
                3,
                secondProvider.RequestCount);


            StringAssert.Contains(
                "maximum of 3 capability rounds",
                secondTask.Result.ErrorMessage);
        }


        //--------------------------------------------------
        // Capability Loop Limit
        //--------------------------------------------------

        [UnityTest]
        public IEnumerator CapabilityLoop_StopsAtConfiguredMaximum()
        {
            RepeatingCapabilityProvider provider =
                new RepeatingCapabilityProvider();


            AIRuntime.Runtime.AIRuntime runtime =
                CreateRuntime(
                    provider);


            runtime.RegisterCapability(
                new EchoCapability());


            runtime.MaxCapabilityRounds =
                4;


            Task<AgentRunResult> task =
                runtime.RunAsync(
                    "Continue calling the capability.");


            yield return WaitForTask(
                task);


            AssertTaskCompletedSuccessfully(
                task,
                "Capability-loop limit run");


            AgentRunResult result =
                task.GetAwaiter()
                    .GetResult();


            Assert.IsFalse(
                result.Succeeded);


            Assert.IsFalse(
                result.Cancelled);


            Assert.IsFalse(
                result.TimedOut);


            Assert.AreEqual(
                4,
                provider.RequestCount,
                "The provider should be called once per configured round.");


            Assert.AreEqual(
                provider.RequestCount - 1,
                provider.CapabilityResultsReceived,
                "The provider should observe the previous capability result on each request after the first.");


            Assert.AreEqual(
                4,
                result.ToolCallsExecuted);


            Assert.AreEqual(
                "Conversation exceeded the maximum of 4 capability rounds.",
                result.ErrorMessage);


            Assert.IsFalse(
                runtime.IsRunning);
        }


        [UnityTest]
        public IEnumerator CapabilityLoop_DefaultAllowsProductiveProgressBeyondThirtyTwoRounds()
        {
            FiniteCapabilityProvider provider =
                new FiniteCapabilityProvider(
                    40);

            AIRuntime.Runtime.AIRuntime runtime =
                CreateRuntime(provider);

            runtime.RegisterCapability(
                new EchoCapability());

            Task<AgentRunResult> task =
                runtime.RunAsync(
                    "Complete the productive multi-step operation.");

            yield return WaitForTask(task);

            AgentRunResult result =
                task.GetAwaiter().GetResult();

            Assert.IsTrue(result.Succeeded, result.ErrorMessage);
            Assert.AreEqual(40, result.ToolCallsExecuted);
            Assert.AreEqual(41, provider.RequestCount);
        }


        //--------------------------------------------------
        // Structured Failure Result
        //--------------------------------------------------

        [UnityTest]
        public IEnumerator CapabilityLoop_ReturnsFailedStructuredResult()
        {
            AIRuntime.Runtime.AIRuntime runtime =
                CreateRuntime(
                    new RepeatingCapabilityProvider());


            runtime.RegisterCapability(
                new EchoCapability());


            runtime.MaxCapabilityRounds =
                1;


            Task<AgentRunResult> task =
                runtime.RunAsync(
                    "Reach the capability limit.");


            yield return WaitForTask(
                task);


            AssertTaskCompletedSuccessfully(
                task,
                "Structured capability-limit run");


            AgentRunResult result =
                task.GetAwaiter()
                    .GetResult();


            Assert.IsNotNull(
                result);


            Assert.IsFalse(
                result.Succeeded);


            Assert.IsFalse(
                result.Cancelled);


            Assert.IsFalse(
                result.TimedOut);


            Assert.AreEqual(
                string.Empty,
                result.Response);


            Assert.AreEqual(
                1,
                result.ToolCallsExecuted);


            Assert.IsFalse(
                string.IsNullOrWhiteSpace(
                    result.SessionId));


            Assert.AreEqual(
                "Conversation exceeded the maximum of 1 capability rounds.",
                result.ErrorMessage);
        }


        //--------------------------------------------------
        // Current Run Rollback
        //--------------------------------------------------

        [UnityTest]
        public IEnumerator CapabilityLoop_RollsBackCurrentConversation()
        {
            AIRuntime.Runtime.AIRuntime runtime =
                CreateRuntime(
                    new RepeatingCapabilityProvider());


            runtime.RegisterCapability(
                new EchoCapability());


            runtime.MaxCapabilityRounds =
                2;


            Assert.AreEqual(
                0,
                runtime.MessageCount);


            Task<AgentRunResult> task =
                runtime.RunAsync(
                    "This run should be rolled back.");


            yield return WaitForTask(
                task);


            AssertTaskCompletedSuccessfully(
                task,
                "Capability-loop rollback run");


            Assert.IsFalse(
                task.Result.Succeeded);


            Assert.AreEqual(
                0,
                runtime.MessageCount,
                "The failed run must roll back its user, assistant, and tool messages.");
        }


        //--------------------------------------------------
        // Previous History Preservation
        //--------------------------------------------------

        [UnityTest]
        public IEnumerator CapabilityLoop_PreservesPreviousConversationHistory()
        {
            SwitchableProvider provider =
                new SwitchableProvider();


            AIRuntime.Runtime.AIRuntime runtime =
                CreateRuntime(
                    provider);


            runtime.RegisterCapability(
                new EchoCapability());


            Task<AgentRunResult> successfulTask =
                runtime.RunAsync(
                    "Store this successful conversation.");


            yield return WaitForTask(
                successfulTask);


            AssertTaskCompletedSuccessfully(
                successfulTask,
                "Successful setup run");


            Assert.IsTrue(
                successfulTask.Result.Succeeded);


            Assert.AreEqual(
                2,
                runtime.MessageCount);


            provider.RepeatCapabilities =
                true;


            runtime.MaxCapabilityRounds =
                2;


            Task<AgentRunResult> failedTask =
                runtime.RunAsync(
                    "This capability loop should fail.");


            yield return WaitForTask(
                failedTask);


            AssertTaskCompletedSuccessfully(
                failedTask,
                "Failed capability-loop run");


            Assert.IsFalse(
                failedTask.Result.Succeeded);


            Assert.AreEqual(
                2,
                runtime.MessageCount,
                "The failed run must not remove earlier successful history.");
        }


        //--------------------------------------------------
        // Isolated Run Preservation
        //--------------------------------------------------

        [UnityTest]
        public IEnumerator IsolatedCapabilityLoop_DoesNotModifyPersistentHistory()
        {
            SwitchableProvider provider =
                new SwitchableProvider();


            AIRuntime.Runtime.AIRuntime runtime =
                CreateRuntime(
                    provider);


            runtime.RegisterCapability(
                new EchoCapability());


            Task<AgentRunResult> persistentTask =
                runtime.RunAsync(
                    "Store persistent history.");


            yield return WaitForTask(
                persistentTask);


            AssertTaskCompletedSuccessfully(
                persistentTask,
                "Persistent setup run");


            Assert.IsTrue(
                persistentTask.Result.Succeeded);


            Assert.AreEqual(
                2,
                runtime.MessageCount);


            provider.RepeatCapabilities =
                true;


            runtime.MaxCapabilityRounds =
                3;


            Task<AgentRunResult> isolatedTask =
                runtime.RunIsolatedAsync(
                    "Fail inside an isolated capability loop.");


            yield return WaitForTask(
                isolatedTask);


            AssertTaskCompletedSuccessfully(
                isolatedTask,
                "Isolated capability-loop run");


            AgentRunResult result =
                isolatedTask.GetAwaiter()
                    .GetResult();


            Assert.IsFalse(
                result.Succeeded);


            Assert.AreEqual(
                3,
                result.ToolCallsExecuted);


            Assert.AreEqual(
                2,
                runtime.MessageCount,
                "The isolated failure must not alter persistent history.");
        }


        //--------------------------------------------------
        // Runtime Recovery
        //--------------------------------------------------

        [UnityTest]
        public IEnumerator Runtime_IsAvailableAfterCapabilityRoundLimit()
        {
            SwitchableProvider provider =
                new SwitchableProvider
                {
                    RepeatCapabilities =
                        true
                };


            AIRuntime.Runtime.AIRuntime runtime =
                CreateRuntime(
                    provider);


            runtime.RegisterCapability(
                new EchoCapability());


            runtime.MaxCapabilityRounds =
                1;


            Task<AgentRunResult> failedTask =
                runtime.RunAsync(
                    "Reach the capability-round limit.");


            yield return WaitForTask(
                failedTask);


            AssertTaskCompletedSuccessfully(
                failedTask,
                "Capability-limit failure");


            Assert.IsFalse(
                failedTask.Result.Succeeded);


            Assert.IsFalse(
                runtime.IsRunning);


            provider.RepeatCapabilities =
                false;


            Task<AgentRunResult> recoveryTask =
                runtime.RunAsync(
                    "Complete successfully afterward.");


            yield return WaitForTask(
                recoveryTask);


            AssertTaskCompletedSuccessfully(
                recoveryTask,
                "Post-limit recovery run");


            AgentRunResult result =
                recoveryTask.GetAwaiter()
                    .GetResult();


            Assert.IsTrue(
                result.Succeeded);


            Assert.AreEqual(
                "Successful response.",
                result.Response);


            Assert.IsFalse(
                runtime.IsRunning);
        }


        //--------------------------------------------------
        // Active Run Configuration Protection
        //--------------------------------------------------

        [UnityTest]
        public IEnumerator ChangingMaxCapabilityRoundsDuringRun_IsRejected()
        {
            BlockingProvider provider =
                new BlockingProvider();


            AIRuntime.Runtime.AIRuntime runtime =
                CreateRuntime(
                    provider);


            runtime.MaxCapabilityRounds =
                8;


            runtime.ProviderTimeout =
                TimeSpan.FromSeconds(
                    10);


            runtime.RunTimeout =
                TimeSpan.FromSeconds(
                    10);


            using CancellationTokenSource cancellation =
                new CancellationTokenSource();


            Task<AgentRunResult> task =
                runtime.RunAsync(
                    "Keep this run active.",
                    cancellation.Token);


            while (!provider.HasStarted)
            {
                yield return null;
            }


            InvalidOperationException exception =
                Assert.Throws<InvalidOperationException>(
                    () =>
                    {
                        runtime.MaxCapabilityRounds =
                            16;
                    });


            Assert.AreEqual(
                "Maximum capability rounds cannot be changed while an agent run is in progress.",
                exception.Message);


            Assert.AreEqual(
                8,
                runtime.MaxCapabilityRounds);


            cancellation.Cancel();


            yield return WaitForTask(
                task);


            AssertTaskCompletedSuccessfully(
                task,
                "Cancelled active run");


            Assert.IsTrue(
                task.Result.Cancelled);
        }


        //--------------------------------------------------
        // Failure Event
        //--------------------------------------------------

        [UnityTest]
        public IEnumerator CapabilityLoop_EmitsSingleRunFailedEvent()
        {
            TestObserver observer =
                new TestObserver();


            AIRuntime.Runtime.AIRuntime runtime =
                CreateRuntime(
                    new RepeatingCapabilityProvider());


            runtime.AddAgentObserver(
                observer);


            runtime.RegisterCapability(
                new EchoCapability());


            runtime.MaxCapabilityRounds =
                2;


            Task<AgentRunResult> task =
                runtime.RunAsync(
                    "Observe the round-limit failure.");


            yield return WaitForTask(
                task);


            AssertTaskCompletedSuccessfully(
                task,
                "Observed capability-limit run");


            Assert.IsFalse(
                task.Result.Succeeded);


            Assert.AreEqual(
                1,
                observer.Count(
                    AgentEventType.RunStarted));


            Assert.AreEqual(
                2,
                observer.Count(
                    AgentEventType.ToolStarted));


            Assert.AreEqual(
                2,
                observer.Count(
                    AgentEventType.ToolCompleted));


            Assert.AreEqual(
                1,
                observer.Count(
                    AgentEventType.RunFailed));


            Assert.AreEqual(
                0,
                observer.Count(
                    AgentEventType.RunCompleted));


            Assert.AreEqual(
                0,
                observer.Count(
                    AgentEventType.RunCancelled));


            Assert.AreEqual(
                0,
                observer.Count(
                    AgentEventType.RunTimedOut));
        }


        //--------------------------------------------------
        // Runtime Creation
        //--------------------------------------------------

        private AIRuntime.Runtime.AIRuntime CreateRuntime(
            IProvider provider)
        {
            AIRuntime.Runtime.AIRuntime runtime =
                new AIRuntime.Runtime.AIRuntime(
                    _workspace);


            runtime.UseProvider(
                provider);


            return runtime;
        }


        //--------------------------------------------------
        // Test Capability
        //--------------------------------------------------

        private sealed class EchoCapability :
            CapabilityBase<EchoArguments>
        {
            public override string Name =>
                "echo";

            public override CapabilityPermission Permission =>
                CapabilityPermission.ReadOnly;
            
            public override string Description =>
                "Returns the supplied text.";


            protected override Task<object> ExecuteAsync(
                EchoArguments arguments,
                CapabilityContext context)
            {
                context.CancellationToken
                    .ThrowIfCancellationRequested();


                return Task.FromResult<object>(
                    arguments.Text ??
                    string.Empty);
            }
        }


        private sealed class EchoArguments
        {
            public string Text
            {
                get;
                set;
            }
        }


        private sealed class CountingEchoCapability :
            CapabilityBase<EchoArguments>
        {
            public int ExecutionCount { get; private set; }

            public override string Name =>
                "echo";

            public override CapabilityPermission Permission =>
                CapabilityPermission.Write;

            public override string Description =>
                "Returns the supplied text.";


            protected override Task<object> ExecuteAsync(
                EchoArguments arguments,
                CapabilityContext context)
            {
                ExecutionCount++;

                return Task.FromResult<object>(
                    arguments.Text ??
                    string.Empty);
            }
        }


        private sealed class MissingDirectoryCapability :
            CapabilityBase<EchoArguments>
        {
            public override string Name =>
                "fail_directory";

            public override CapabilityPermission Permission =>
                CapabilityPermission.ReadOnly;

            public override string Description =>
                "Simulates a missing directory.";


            protected override Task<object> ExecuteAsync(
                EchoArguments arguments,
                CapabilityContext context)
            {
                throw new DirectoryNotFoundException(
                    "Directory not found: Assets/Settings");
            }
        }


        //--------------------------------------------------
        // Successful Provider
        //--------------------------------------------------

        private sealed class SuccessfulProvider :
            IProvider
        {
            public string Name =>
                "SuccessfulProvider";


            public Task<ChatCompletion> ChatAsync(
                IReadOnlyList<ConversationMessage> messages,
                IReadOnlyList<ProviderTool> tools,
                CancellationToken cancellationToken = default)
            {
                cancellationToken
                    .ThrowIfCancellationRequested();


                return Task.FromResult(
                    new ChatCompletion
                    {
                        Content =
                            "Successful response."
                    });
            }
        }


        //--------------------------------------------------
        // Repeating Capability Provider
        //--------------------------------------------------

        private sealed class RepeatingCapabilityProvider :
            IProvider
        {
            public string Name =>
                "RepeatingCapabilityProvider";


            public int RequestCount
            {
                get;
                private set;
            }


            public int CapabilityResultsReceived
            {
                get;
                private set;
            }


            public Task<ChatCompletion> ChatAsync(
                IReadOnlyList<ConversationMessage> messages,
                IReadOnlyList<ProviderTool> tools,
                CancellationToken cancellationToken = default)
            {
                cancellationToken
                    .ThrowIfCancellationRequested();


                RequestCount++;


                if (
                    messages.Count > 0 &&
                    messages[messages.Count - 1].Role == "tool")
                {
                    CapabilityResultsReceived++;
                }


                return Task.FromResult(
                    CreateToolCallCompletion(
                        RequestCount));
            }
        }


        private sealed class FiniteCapabilityProvider :
            IProvider
        {
            private readonly int _toolRoundCount;

            public FiniteCapabilityProvider(
                int toolRoundCount)
            {
                _toolRoundCount = toolRoundCount;
            }

            public string Name =>
                "FiniteCapabilityProvider";

            public int RequestCount { get; private set; }

            public Task<ChatCompletion> ChatAsync(
                IReadOnlyList<ConversationMessage> messages,
                IReadOnlyList<ProviderTool> tools,
                CancellationToken cancellationToken = default)
            {
                cancellationToken.ThrowIfCancellationRequested();
                RequestCount++;

                return Task.FromResult(
                    RequestCount <= _toolRoundCount
                        ? CreateToolCallCompletion(RequestCount)
                        : new ChatCompletion
                        {
                            Content = "Completed after productive progress."
                        });
            }
        }


        private sealed class DuplicateOnceProvider :
            IProvider
        {
            public string Name =>
                "DuplicateOnceProvider";

            public int RequestCount { get; private set; }


            public Task<ChatCompletion> ChatAsync(
                IReadOnlyList<ConversationMessage> messages,
                IReadOnlyList<ProviderTool> tools,
                CancellationToken cancellationToken = default)
            {
                RequestCount++;

                if (RequestCount >= 3)
                {
                    return Task.FromResult(
                        new ChatCompletion
                        {
                            Content =
                                "Completed after duplicate notice."
                        });
                }

                ChatCompletion completion =
                    new ChatCompletion();

                completion.ToolCalls.Add(
                    new ToolCall
                    {
                        Id =
                            "duplicate-" + RequestCount,
                        Name =
                            "echo",
                        Arguments =
                            "{\"Text\":\"same\"}"
                    });

                return Task.FromResult(
                    completion);
            }
        }


        private sealed class RecoveringAfterToolErrorProvider :
            IProvider
        {
            public string Name =>
                "RecoveringAfterToolErrorProvider";

            public int RequestCount { get; private set; }

            public bool ReceivedStructuredErrorResult { get; private set; }


            public Task<ChatCompletion> ChatAsync(
                IReadOnlyList<ConversationMessage> messages,
                IReadOnlyList<ProviderTool> tools,
                CancellationToken cancellationToken = default)
            {
                RequestCount++;

                ConversationMessage lastMessage =
                    messages.Count > 0
                        ? messages[messages.Count - 1]
                        : null;

                if (lastMessage?.Role ==
                    "tool")
                {
                    ReceivedStructuredErrorResult =
                        lastMessage.Content?.Contains(
                            "\"IsError\":true") ==
                        true &&
                        lastMessage.Content.Contains(
                            "Assets/Settings");

                    return Task.FromResult(
                        new ChatCompletion
                        {
                            Content =
                                "Recovered after correcting the tool path."
                        });
                }


                ChatCompletion completion =
                    new ChatCompletion();

                completion.ToolCalls.Add(
                    new ToolCall
                    {
                        Id =
                            "missing-directory-call",
                        Name =
                            "fail_directory",
                        Arguments =
                            "{\"Text\":\"Assets/Settings\"}"
                    });

                return Task.FromResult(
                    completion);
            }
        }


        //--------------------------------------------------
        // Switchable Provider
        //--------------------------------------------------

        private sealed class SwitchableProvider :
            IProvider
        {
            private int _toolCallNumber;


            public string Name =>
                "SwitchableProvider";


            public bool RepeatCapabilities
            {
                get;
                set;
            }


            public Task<ChatCompletion> ChatAsync(
                IReadOnlyList<ConversationMessage> messages,
                IReadOnlyList<ProviderTool> tools,
                CancellationToken cancellationToken = default)
            {
                cancellationToken
                    .ThrowIfCancellationRequested();


                if (!RepeatCapabilities)
                {
                    return Task.FromResult(
                        new ChatCompletion
                        {
                            Content =
                                "Successful response."
                        });
                }


                _toolCallNumber++;


                return Task.FromResult(
                    CreateToolCallCompletion(
                        _toolCallNumber));
            }
        }


        //--------------------------------------------------
        // Blocking Provider
        //--------------------------------------------------

        private sealed class BlockingProvider :
            IProvider
        {
            public string Name =>
                "BlockingProvider";


            public bool HasStarted
            {
                get;
                private set;
            }


            public async Task<ChatCompletion> ChatAsync(
                IReadOnlyList<ConversationMessage> messages,
                IReadOnlyList<ProviderTool> tools,
                CancellationToken cancellationToken = default)
            {
                HasStarted =
                    true;


                await Task.Delay(
                    Timeout.Infinite,
                    cancellationToken);


                throw new InvalidOperationException(
                    "The blocking provider should never complete normally.");
            }
        }


        //--------------------------------------------------
        // Provider Completion Helper
        //--------------------------------------------------

        private static ChatCompletion CreateToolCallCompletion(
            int callNumber)
        {
            ChatCompletion completion =
                new ChatCompletion();


            completion.ToolCalls.Add(
                new ToolCall
                {
                    Id =
                        "echo_call_" +
                        callNumber,

                    Name =
                        "echo",

                    Arguments =
                        "{\"Text\":\"round-" +
                        callNumber +
                        "\"}"
                });


            return completion;
        }


        //--------------------------------------------------
        // Observer
        //--------------------------------------------------

        private sealed class TestObserver :
            IAgentObserver
        {
            private readonly List<AgentEvent> _events =
                new List<AgentEvent>();


            public void OnAgentEvent(
                AgentEvent agentEvent)
            {
                _events.Add(
                    agentEvent);
            }


            public int Count(
                AgentEventType type)
            {
                int count =
                    0;


                foreach (AgentEvent agentEvent in _events)
                {
                    if (agentEvent.Type == type)
                    {
                        count++;
                    }
                }


                return count;
            }
        }


        //--------------------------------------------------
        // Coroutine Helper
        //--------------------------------------------------

        private static IEnumerator WaitForTask(
            Task task)
        {
            while (!task.IsCompleted)
            {
                yield return null;
            }
        }


        //--------------------------------------------------
        // Assertion Helpers
        //--------------------------------------------------

        private static void AssertTaskCompletedSuccessfully(
            Task task,
            string operation)
        {
            Assert.IsNotNull(
                task,
                operation +
                " returned a null task.");


            if (task.IsCanceled)
            {
                Assert.Fail(
                    operation +
                    " was unexpectedly cancelled.");
            }


            if (!task.IsFaulted)
                return;


            Exception exception =
                UnwrapException(
                    task.Exception);


            Assert.Fail(
                operation +
                " failed:\n" +
                (exception?.ToString() ??
                 "Unknown task failure."));
        }


        private static Exception UnwrapException(
            Exception exception)
        {
            if (!(exception is AggregateException aggregate))
                return exception;


            AggregateException flattened =
                aggregate.Flatten();


            if (flattened.InnerExceptions.Count == 1)
            {
                return flattened.InnerExceptions[0];
            }


            return flattened;
        }
    }
}
