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

using AIRuntime.Runtime;
using AIRuntime.Runtime.Capabilities;
using AIRuntime.Runtime.Capabilities.Permissions;
using AIRuntime.Runtime.Core;
using AIRuntime.Runtime.Models;
using AIRuntime.Runtime.Providers;
using AIRuntime.Runtime.Providers.Models;
using AIRuntime.Runtime.Providers.Testing;
using AIRuntime.Runtime.Filesystem;
using AIRuntime.Runtime.Services.Agent;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using System.Linq;
using UnityEngine;
using UnityEngine.TestTools;



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


        private AIRuntime.Runtime.AIRuntime _runtime;

        [TestCase("I'll now inspect the scene.", true)]
        [TestCase("Let me read Assets/Game/Player.cs.", true)]
        [TestCase("I’ll run the tests…", true)]
        [TestCase("The scene is ready and the tests passed.", false)]
        [TestCase("I'll run the tests if you approve.", false)]
        [TestCase("Let me check: which scene should I use?", false)]
        [TestCase("I will use this convention in future responses.", false)]
        [TestCase("I'll inspect the scene. The current blocker is missing access.", false)]
        public void ActionAnnouncement_RecognizesOnlyShortUnconditionalIntent(string content, bool expected)
        {
            MethodInfo method = typeof(ConversationEngine).GetMethod(
                "IsUnfulfilledActionAnnouncement", BindingFlags.NonPublic | BindingFlags.Static);
            Assert.AreEqual(expected, method.Invoke(null, new object[] { content }));
        }

        [UnityTest]
        public IEnumerator ActionAnnouncement_ContinuesWithToolsAndRetainedContext()
        {
            var provider = new ActionAnnouncementProvider(false);
            var observer = new TestAgentObserver();
            _runtime.UseProvider(provider);
            _runtime.AddAgentObserver(observer);
            Task<AgentRunResult> task = _runtime.RunAsync("Read test.txt.");
            while (!task.IsCompleted) yield return null;

            Assert.IsFalse(task.IsFaulted, task.Exception?.ToString());
            Assert.IsTrue(task.Result.Succeeded, task.Result.ErrorMessage);
            Assert.AreEqual("Read and verified.", task.Result.Response);
            Assert.AreEqual(3, provider.RequestCount);
            Assert.IsTrue(provider.SawContinuationWithTools);
            Assert.AreEqual(1, task.Result.ToolCallsExecuted);
            Assert.AreEqual(1, observer.Events.Count(e => e.Type == AgentEventType.ModelResponseRepair));
            Assert.AreEqual(1, observer.Events.Count(e => e.Type == AgentEventType.RunCompleted));
        }

        [UnityTest]
        public IEnumerator ActionAnnouncement_RepeatedPromisesFailWithoutFalseCompletion()
        {
            var provider = new ActionAnnouncementProvider(true);
            var observer = new TestAgentObserver();
            _runtime.UseProvider(provider);
            _runtime.AddAgentObserver(observer);
            Task<AgentRunResult> task = _runtime.RunAsync("Read test.txt.");
            while (!task.IsCompleted) yield return null;

            Assert.IsFalse(task.IsFaulted, task.Exception?.ToString());
            Assert.AreEqual(AgentRunStatus.Failed, task.Result.Status);
            StringAssert.Contains("[ActionContinuationExhausted]", task.Result.ErrorMessage);
            Assert.AreEqual(3, provider.RequestCount);
            Assert.AreEqual(0, task.Result.ToolCallsExecuted);
            Assert.AreEqual(2, observer.Events.Count(e => e.Type == AgentEventType.ModelResponseRepair));
            Assert.IsFalse(observer.Events.Any(e => e.Type == AgentEventType.RunCompleted));
        }

        private sealed class ActionAnnouncementProvider : IProvider
        {
            private readonly bool _repeat;
            public string Name => "ActionAnnouncementProvider";
            public int RequestCount { get; private set; }
            public bool SawContinuationWithTools { get; private set; }

            public ActionAnnouncementProvider(bool repeat) { _repeat = repeat; }

            public Task<ChatCompletion> ChatAsync(
                IReadOnlyList<ConversationMessage> messages,
                IReadOnlyList<ProviderTool> tools,
                CancellationToken cancellationToken = default)
            {
                RequestCount++;
                if (RequestCount == 1 || _repeat)
                    return Task.FromResult(new ChatCompletion { Content = "I'll now read test.txt." });
                if (RequestCount == 2)
                {
                    SawContinuationWithTools = tools.Any(t => t.Function.Name == "read_file") &&
                        messages.Any(m => m.Role == "assistant" && m.Content == "I'll now read test.txt.") &&
                        messages.Any(m => m.Role == "user" && m.Content.Contains("announced an action"));
                    var completion = new ChatCompletion();
                    completion.ToolCalls.Add(new ToolCall("intent-read", "read_file", "{\"path\":\"test.txt\"}"));
                    return Task.FromResult(completion);
                }
                return Task.FromResult(new ChatCompletion { Content = "Read and verified." });
            }
        }


        [Test]
        public void CapabilityArgumentValidation_IsRecoverable()
        {
            MethodInfo method =
                typeof(ConversationEngine).GetMethod(
                    "IsRecoverableCapabilityFailure",
                    BindingFlags.NonPublic |
                    BindingFlags.Static);

            Assert.NotNull(method);

            Assert.IsTrue(
                (bool)method.Invoke(
                    null,
                    new object[]
                    {
                        new ValidationException(
                            "Correct this capability argument.")
                    }));
        }


        [Test]
        public void OptedInMutationBatch_PreservesEverySameCapabilityCall()
        {
            ChatCompletion completion =
                new ChatCompletion();

            completion.ToolCalls.Add(
                new ToolCall("batch-1", "batchable_mutation", "{}"));
            completion.ToolCalls.Add(
                new ToolCall("batch-2", "batchable_mutation", "{}"));

            MethodInfo method =
                typeof(ConversationEngine).GetMethod(
                    "EnforceSequentialMutationCalls",
                    BindingFlags.NonPublic |
                    BindingFlags.Static);

            Assert.NotNull(method);

            method.Invoke(
                null,
                new object[]
                {
                    completion,
                    new ICapability[]
                    {
                        new BatchableMutationCapability()
                    }
                });

            Assert.AreEqual(2, completion.ToolCalls.Count);
        }


        [Test]
        public void ConversationEngine_DoesNotReferenceApplicationCapabilityIds()
        {
            string sourcePath =
                TestAssetPaths.Resolve("Assets/Runtime/Core/ConversationEngine.cs");


            Assert.IsTrue(
                File.Exists(
                    sourcePath),
                $"Conversation engine source was not found at '{sourcePath}'.");


            string source =
                File.ReadAllText(
                    sourcePath);


            string[] applicationCapabilityIds =
            {
                "create_kanban_child_task",
                "run_edit_mode_tests",
                "run_play_mode_tests"
            };


            foreach (string capabilityId in applicationCapabilityIds)
            {
                StringAssert.DoesNotContain(
                    capabilityId,
                    source,
                    $"Runtime core must derive execution behavior from capability semantics instead of application capability ID '{capabilityId}'.");
            }
        }


        [Test]
        public void CapabilityResultConversationEnvelope_BoundsLargeResultsAndPreservesBothEnds()
        {
            MethodInfo method =
                typeof(ConversationEngine).GetMethod(
                    "BoundCapabilityResultForConversation",
                    BindingFlags.NonPublic |
                    BindingFlags.Static);

            Assert.NotNull(method);

            string largeResult =
                "BEGIN-" +
                new string('x', 200000) +
                "-END";

            string bounded =
                (string)method.Invoke(
                    null,
                    new object[]
                    {
                        "read_file",
                        largeResult
                    });

            Assert.LessOrEqual(
                bounded.Length,
                8192);
            StringAssert.Contains(
                "original characters=200010",
                bounded);
            StringAssert.Contains(
                "BEGIN-",
                bounded);
            StringAssert.Contains(
                "-END",
                bounded);
            StringAssert.Contains(
                "...[bounded]...",
                bounded);

            Assert.AreEqual(
                "small result",
                method.Invoke(
                    null,
                    new object[]
                    {
                        "read_file",
                        "small result"
                    }));
        }



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


            Directory.CreateDirectory(
                _workspace);



            File.WriteAllText(
                Path.Combine(
                    _workspace,
                    "test.txt"),
                "Hello from Fake Agent");



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



            _runtime.RegisterCapability(
                new ReadFileCapability());



            _runtime.UseProvider(
                new FakeProvider());
        }




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


        [Test]
        public void AgentLifecycle_EmitsEvents()
        {
            AgentRunContext context =
                new AgentRunContext();


            AgentObserverCollection observers =
                new AgentObserverCollection();


            TestAgentObserver observer =
                new TestAgentObserver();


            observers.Add(observer);


            observers.Publish(
                new AgentEvent(
                    AgentEventType.RunStarted,
                    context.SessionId));


            observers.Publish(
                new AgentEvent(
                    AgentEventType.ToolStarted,
                    context.SessionId,
                    "read_file"));


            observers.Publish(
                new AgentEvent(
                    AgentEventType.ToolCompleted,
                    context.SessionId,
                    "read_file"));


            observers.Publish(
                new AgentEvent(
                    AgentEventType.RunCompleted,
                    context.SessionId));


            Assert.AreEqual(
                4,
                observer.Events.Count);


            Assert.AreEqual(
                AgentEventType.RunStarted,
                observer.Events[0].Type);


            Assert.AreEqual(
                AgentEventType.ToolStarted,
                observer.Events[1].Type);


            Assert.AreEqual(
                AgentEventType.ToolCompleted,
                observer.Events[2].Type);


            Assert.AreEqual(
                AgentEventType.RunCompleted,
                observer.Events[3].Type);
        }

        [UnityTest]
        public IEnumerator AgentLoop_ExecutesToolAndReturnsFinalAnswer()
        {
            Task<string> task =
                _runtime.ChatAsync(
                    "Read test.txt");


            while(!task.IsCompleted)
            {
                yield return null;
            }



            if(task.IsFaulted)
            {
                throw task.Exception;
            }



            string result =
                task.Result;



            Assert.AreEqual(
                "FakeProvider successfully read the file.",
                result);
        }


        [UnityTest]
        public IEnumerator MalformedToolCall_ReceivesOneSchemaRepairAndRecovers()
        {
            RepairingMalformedToolCallProvider provider =
                new RepairingMalformedToolCallProvider();

            TestAgentObserver observer =
                new TestAgentObserver();

            _runtime.UseProvider(
                provider);

            _runtime.AddAgentObserver(
                observer);


            Task<string> task =
                _runtime.ChatAsync(
                    "Read test.txt and report its contents.");


            while (!task.IsCompleted)
            {
                yield return null;
            }


            Assert.IsFalse(
                task.IsFaulted,
                task.Exception?.ToString());

            Assert.AreEqual(
                "recovered after schema correction",
                task.Result);

            Assert.AreEqual(
                3,
                provider.RequestCount);

            Assert.IsTrue(
                provider.SawSchemaCorrectionPrompt);

            Assert.IsTrue(
                observer.Events.Any(
                    agentEvent =>
                        agentEvent.Type ==
                        AgentEventType.ModelResponseRepair));
        }


        [UnityTest]
        public IEnumerator RepeatedMalformedToolCall_StopsAfterBoundedRepair()
        {
            RepeatingMalformedToolCallProvider provider =
                new RepeatingMalformedToolCallProvider();

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

            runtime.RegisterCapability(
                new ReadFileCapability());

            runtime.UseProvider(
                provider);


            Task<AgentRunResult> task =
                runtime.RunAsync(
                    "Read test.txt.");


            while (!task.IsCompleted)
            {
                yield return null;
            }


            Assert.IsFalse(
                task.IsFaulted,
                task.Exception?.ToString());

            Assert.AreEqual(
                AgentRunStatus.Failed,
                task.Result.Status);

            Assert.AreEqual(
                2,
                provider.RequestCount);

            StringAssert.Contains(
                "repeated the same malformed tool call",
                task.Result.ErrorMessage);
        }


        [UnityTest]
        public IEnumerator OptedInMutationBatch_ExecutesEveryDistinctCall()
        {
            DistinctBatchProvider provider =
                new DistinctBatchProvider();

            BatchableMutationCapability capability =
                new BatchableMutationCapability();

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


            runtime.RegisterCapability(
                capability);

            runtime.AllowPermission(
                CapabilityPermission.Write);

            runtime.UseProvider(
                provider);


            Task<string> task =
                runtime.ChatAsync(
                    "Create both independently validated items.");


            while (!task.IsCompleted)
            {
                yield return null;
            }


            Assert.IsFalse(
                task.IsFaulted,
                task.Exception?.ToString());

            Assert.AreEqual(
                "verified",
                task.Result);

            CollectionAssert.AreEqual(
                new[]
                {
                    "first",
                    "second"
                },
                capability.ExecutedValues);

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


        [UnityTest]
        public IEnumerator MutationAndVerificationBatch_IsSequencedAcrossRounds()
        {
            string path =
                Path.Combine(
                    _workspace,
                    "workflow.txt");

            File.WriteAllText(
                path,
                "start\n");

            BatchedMutationProvider provider =
                new BatchedMutationProvider();

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

            runtime.RegisterCapability(
                new AppendFileCapability());

            runtime.RegisterCapability(
                new ReadFileCapability());

            runtime.AllowPermission(
                CapabilityPermission.Write);

            runtime.UseProvider(
                provider);


            Task<string> task =
                runtime.ChatAsync(
                    "Append and verify.");


            while (!task.IsCompleted)
            {
                yield return null;
            }


            if (task.IsFaulted)
            {
                throw task.Exception;
            }


            Assert.AreEqual(
                "verified",
                task.Result);

            Assert.AreEqual(
                "start\nfinish\n",
                File.ReadAllText(
                    path));

            CollectionAssert.AreEqual(
                new[]
                {
                    "append_file"
                },
                provider.SecondRequestToolResults);

            CollectionAssert.AreEqual(
                new[]
                {
                    "append_file",
                    "read_file"
                },
                provider.ThirdRequestToolResults);
        }


        [UnityTest]
        public IEnumerator RepeatedStableReadOnlyInspection_ReplaysAndPreservesTools()
        {
            string path =
                Path.Combine(
                    _workspace,
                    "verification.txt");

            File.WriteAllText(
                path,
                "verified contents");

            File.WriteAllText(
                Path.Combine(
                    _workspace,
                    "second-verification.txt"),
                "additional verified contents");

            RepeatingReadOnlyProvider provider =
                new RepeatingReadOnlyProvider();

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

            runtime.RegisterCapability(
                new ReadFileCapability());

            runtime.AllowPermission(
                CapabilityPermission.ReadOnly);

            runtime.UseProvider(
                provider);

            Task<string> task =
                runtime.ChatAsync(
                    "Verify the file.");

            while (!task.IsCompleted)
            {
                yield return null;
            }

            Assert.IsFalse(
                task.IsFaulted);

            Assert.AreEqual(
                "verified",
                task.Result);

            Assert.AreEqual(
                4,
                provider.RequestCount);

            Assert.IsTrue(
                provider.ToolsRemainedAvailableAfterDuplicate,
                "A repeated inspection must not remove unrelated capabilities from the model's next turn.");

            Assert.IsTrue(
                provider.RepeatedInspectionWasReplayed,
                "A stable read in an unchanged mutation epoch must reuse its authoritative result instead of executing again.");
        }


        [UnityTest]
        public IEnumerator RepeatedStableReadAfterReplay_StopsAsSemanticNoProgress()
        {
            File.WriteAllText(
                Path.Combine(
                    _workspace,
                    "looping-read.txt"),
                "authoritative contents");

            LoopingStableReadProvider provider =
                new LoopingStableReadProvider();

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

            runtime.RegisterCapability(
                new ReadFileCapability());

            runtime.AllowPermission(
                CapabilityPermission.ReadOnly);

            runtime.UseProvider(
                provider);

            Task<string> task =
                runtime.ChatAsync(
                    "Inspect the same unchanged file forever.");

            while (!task.IsCompleted)
                yield return null;

            Assert.IsTrue(
                task.IsFaulted,
                "The third identical stable observation must terminate rather than executing indefinitely.");

            StringAssert.Contains(
                "[CapabilityNoProgress]",
                task.Exception?.ToString());

            StringAssert.Contains(
                "repeated the identical capability call 'read_file'",
                task.Exception?.ToString());

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


        [UnityTest]
        public IEnumerator LiveSelection_CanBeReadAgainAfterInterveningToolWork()
        {
            string path =
                Path.Combine(
                    _workspace,
                    "verification.txt");

            File.WriteAllText(
                path,
                "verified contents");

            SelectionVerificationProvider provider =
                new SelectionVerificationProvider();

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

            runtime.RegisterCapability(
                new AIRuntime.Editor.GetEditorSelectionCapability());

            runtime.RegisterCapability(
                new LiveSelectionInterveningCapability());

            runtime.AllowPermission(
                CapabilityPermission.ReadOnly);

            runtime.UseProvider(
                provider);

            Task<string> task =
                runtime.ChatAsync(
                    "Inspect, perform related tool work, then verify the live selection again.");

            while (!task.IsCompleted)
            {
                yield return null;
            }

            if (task.IsFaulted)
            {
                throw task.Exception;
            }

            Assert.AreEqual(
                "verified",
                task.Result);

            Assert.AreEqual(
                4,
                provider.RequestCount);

            Assert.AreEqual(
                2,
                provider.SelectionResultCount);
        }


        [UnityTest]
        public IEnumerator RepeatedConvergentVerification_CompletesWithoutRerunning()
        {
            RepeatingConvergentProvider provider =
                new RepeatingConvergentProvider();

            CountingConvergentCapability capability =
                new CountingConvergentCapability();

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

            runtime.RegisterCapability(
                capability);

            runtime.AllowPermission(
                CapabilityPermission.Write);

            runtime.UseProvider(
                provider);

            Task<string> task =
                runtime.ChatAsync(
                    "Run the expensive verification once.");

            while (!task.IsCompleted)
            {
                yield return null;
            }

            Assert.IsFalse(
                task.IsFaulted,
                task.Exception?.ToString());

            StringAssert.Contains(
                "already-finished verification capability 'expensive_verification'",
                task.Result);

            int resultJsonStart =
                task.Result.IndexOf('{');

            Assert.GreaterOrEqual(
                resultJsonStart,
                0,
                "The reused authoritative test result must include its JSON payload.");

            JObject reusedResult =
                JObject.Parse(
                    task.Result.Substring(
                        resultJsonStart));

            Assert.AreEqual(
                0,
                (int)reusedResult["FailCount"]);

            Assert.AreEqual(
                1,
                capability.ExecutionCount,
                "The completed test run must be reused, not executed again.");

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


        [UnityTest]
        public IEnumerator FailedConvergentVerification_IsNeverReplayedAsSuccess()
        {
            RepeatingFailedConvergentProvider provider =
                new RepeatingFailedConvergentProvider();

            FailingConvergentCapability capability =
                new FailingConvergentCapability();

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

            runtime.RegisterCapability(
                capability);

            runtime.AllowPermission(
                CapabilityPermission.Write);

            runtime.UseProvider(
                provider);

            LogAssert.Expect(
                LogType.Error,
                "[Capability] 'failing_verification' failed: Verification could not run.");

            Task<string> task =
                runtime.ChatAsync(
                    "Run verification and handle a failed result.");

            while (!task.IsCompleted)
            {
                yield return null;
            }

            Assert.IsFalse(
                task.IsFaulted,
                task.Exception?.ToString());

            Assert.AreEqual(
                "failure handled",
                task.Result,
                "A failed verification must not trigger convergent successful completion.");

            Assert.AreEqual(
                1,
                capability.ExecutionCount);

            Assert.AreEqual(
                3,
                provider.RequestCount);

            Assert.IsTrue(
                provider.DuplicateWasReportedAsFailure);
        }


        [UnityTest]
        public IEnumerator RepeatedRecoverableFailure_WithVariedArguments_StopsAsNoProgress()
        {
            VaryingFailedMutationProvider provider =
                new VaryingFailedMutationProvider();

            VaryingFailedMutationCapability capability =
                new VaryingFailedMutationCapability();

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

            runtime.RegisterCapability(
                capability);

            runtime.AllowPermission(
                CapabilityPermission.Write);

            runtime.UseProvider(
                provider);

            for (int index = 0; index < 3; index++)
            {
                LogAssert.Expect(
                    LogType.Error,
                    "[Capability] 'varying_failed_mutation' failed: The target rejected every candidate value.");
            }

            Task<AgentRunResult> task =
                runtime.RunAsync(
                    "Keep changing the value even though the same target error repeats.");

            while (!task.IsCompleted)
                yield return null;

            Assert.IsFalse(
                task.IsFaulted,
                task.Exception?.ToString());

            Assert.AreEqual(
                AgentRunStatus.Failed,
                task.Result.Status);

            StringAssert.Contains(
                "[CapabilityNoProgress]",
                task.Result.ErrorMessage);

            StringAssert.Contains(
                "superficial argument changes",
                task.Result.ErrorMessage);

            Assert.AreEqual(
                3,
                capability.ExecutionCount);

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


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

            public int RequestCount { get; private set; }

            public bool SawSchemaCorrectionPrompt { get; private set; }


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


                if (RequestCount == 1)
                {
                    ChatCompletion malformed =
                        new ChatCompletion();

                    malformed.RejectedToolCalls.Add(
                        CreateRejectedReadCall());

                    return Task.FromResult(
                        malformed);
                }


                if (RequestCount == 2)
                {
                    SawSchemaCorrectionPrompt =
                        messages.Any(
                            message =>
                                message.Role == "user" &&
                                (message.Content ?? string.Empty).Contains(
                                    "function.arguments value must be one JSON object") &&
                                (message.Content ?? string.Empty).Contains(
                                    "\"name\": \"read_file\"") &&
                                (message.Content ?? string.Empty).Contains(
                                    "argumentsSchema"));

                    ChatCompletion corrected =
                        new ChatCompletion();

                    corrected.ToolCalls.Add(
                        new ToolCall(
                            "corrected-read",
                            "read_file",
                            "{\"path\":\"test.txt\"}"));

                    return Task.FromResult(
                        corrected);
                }


                return Task.FromResult(
                    new ChatCompletion
                    {
                        Content =
                            "recovered after schema correction"
                    });
            }
        }


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

            public int RequestCount { get; private set; }


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

                ChatCompletion malformed =
                    new ChatCompletion();

                malformed.RejectedToolCalls.Add(
                    CreateRejectedReadCall());

                return Task.FromResult(
                    malformed);
            }
        }


        private static RejectedToolCall CreateRejectedReadCall()
        {
            return new RejectedToolCall
            {
                Index = 0,
                Id = "bad-read",
                Name = "read_file",
                Arguments = "[]",
                Reason = "function arguments were not a JSON object"
            };
        }


        private sealed class BatchedMutationProvider :
            IProvider
        {
            private int _requestCount;


            public string Name =>
                "BatchedMutationProvider";


            public string[] SecondRequestToolResults { get; private set; }

            public string[] ThirdRequestToolResults { get; private set; }


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


                if (_requestCount ==
                    1)
                {
                    ChatCompletion completion =
                        new ChatCompletion();

                    completion.ToolCalls.Add(
                        new ToolCall(
                            "append-1",
                            "append_file",
                            "{\"path\":\"workflow.txt\",\"content\":\"finish\\n\"}"));

                    completion.ToolCalls.Add(
                        new ToolCall(
                            "premature-read",
                            "read_file",
                            "{\"path\":\"workflow.txt\"}"));

                    return Task.FromResult(
                        completion);
                }


                if (_requestCount ==
                    2)
                {
                    SecondRequestToolResults =
                        messages
                            .Where(
                                message =>
                                    message.Role ==
                                    "tool")
                            .Select(
                                message =>
                                    message.Name)
                            .ToArray();

                    ChatCompletion completion =
                        new ChatCompletion();

                    completion.ToolCalls.Add(
                        new ToolCall(
                            "read-2",
                            "read_file",
                            "{\"path\":\"workflow.txt\"}"));

                    return Task.FromResult(
                        completion);
                }


                ThirdRequestToolResults =
                    messages
                        .Where(
                            message =>
                                message.Role ==
                                "tool")
                        .Select(
                            message =>
                                message.Name)
                        .ToArray();

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


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

            public int RequestCount { get; private set; }


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


                if (RequestCount == 1)
                {
                    ChatCompletion completion =
                        new ChatCompletion();

                    completion.ToolCalls.Add(
                        new ToolCall(
                            "batch-first",
                            "batchable_mutation",
                            "{\"value\":\"first\"}"));

                    completion.ToolCalls.Add(
                        new ToolCall(
                            "batch-second",
                            "batchable_mutation",
                            "{\"value\":\"second\"}"));

                    return Task.FromResult(
                        completion);
                }


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


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

            public int RequestCount { get; private set; }

            public bool ToolsRemainedAvailableAfterDuplicate { get; private set; }

            public bool RepeatedInspectionWasReplayed { get; private set; }


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

                ChatCompletion completion =
                    new ChatCompletion();

                if (RequestCount >= 4)
                {
                    completion.Content =
                        "verified";

                    return Task.FromResult(
                        completion);
                }

                if (RequestCount == 3)
                {
                    ToolsRemainedAvailableAfterDuplicate =
                        tools.Any(
                            tool =>
                                string.Equals(
                                    tool?.Function?.Name,
                                    "read_file",
                                    System.StringComparison.Ordinal));

                    RepeatedInspectionWasReplayed =
                        messages.Count(
                            message =>
                                message.Role == "tool" &&
                                message.Name == "read_file") == 2 &&
                        messages.Any(
                            message =>
                                message.Role == "tool" &&
                                (message.Content ?? string.Empty).Contains(
                                    "DuplicateIgnored"));

                    completion.ToolCalls.Add(
                        new ToolCall(
                            "read-distinct",
                            "read_file",
                            "{\"path\":\"second-verification.txt\"}"));
                }
                else
                {
                    completion.ToolCalls.Add(
                        new ToolCall(
                            $"read-{RequestCount}",
                            "read_file",
                            "{\"path\":\"verification.txt\"}"));
                }

                return Task.FromResult(
                    completion);
            }
        }


        private sealed class EvidenceSequenceProvider : IProvider
        {
            private readonly string[] _arguments;
            public int RequestCount;
            public EvidenceSequenceProvider(params string[] arguments) { _arguments = arguments; }
            public string Name => "EvidenceSequence";
            public Task<ChatCompletion> ChatAsync(IReadOnlyList<ConversationMessage> messages,
                IReadOnlyList<ProviderTool> tools, CancellationToken cancellationToken = default)
            {
                var completion = new ChatCompletion();
                int step = RequestCount++;
                if (step < _arguments.Length)
                    completion.ToolCalls.Add(new ToolCall("evidence-" + step, "read_file", _arguments[step]));
                else completion.Content = "verified";
                return Task.FromResult(completion);
            }
        }

        private IEnumerator VerifyEvidenceSequence(bool shouldFail, params string[] arguments)
        {
            File.WriteAllText(Path.Combine(_workspace, "evidence-a.txt"), "first authoritative result");
            File.WriteAllText(Path.Combine(_workspace, "evidence-b.txt"), "different authoritative result");
            var provider = new EvidenceSequenceProvider(arguments);
            var runtime = new AIRuntime.Runtime.AIRuntime(_workspace);
            runtime.RegisterCapability(new ReadFileCapability());
            runtime.AllowPermission(CapabilityPermission.ReadOnly);
            runtime.UseProvider(provider);
            var task = runtime.ChatAsync("Compare the evidence and finish.");
            double deadline = UnityEditor.EditorApplication.timeSinceStartup + 10;
            while (!task.IsCompleted && UnityEditor.EditorApplication.timeSinceStartup < deadline) yield return null;
            Assert.IsTrue(task.IsCompleted, "Evidence sequence did not complete in time.");
            Assert.AreEqual(shouldFail, task.IsFaulted, task.Exception?.ToString());
            if (shouldFail) StringAssert.Contains("CapabilityNoProgress", task.Exception.ToString());
            else Assert.AreEqual("verified", task.Result);
        }

        [UnityTest]
        public IEnumerator NewReadEvidenceAllowsRevisitingAnEarlierFile()
        {
            yield return VerifyEvidenceSequence(false,
                "{\"path\":\"evidence-a.txt\"}", "{\"path\":\"evidence-a.txt\"}",
                "{\"path\":\"evidence-b.txt\"}", "{\"path\":\"evidence-a.txt\"}");
        }

        [UnityTest]
        public IEnumerator CyclingAlreadySeenReadResultsStillStops()
        {
            yield return VerifyEvidenceSequence(true,
                "{\"path\":\"evidence-a.txt\"}", "{\"path\":\"evidence-b.txt\"}",
                "{\"path\":\"evidence-a.txt\"}", "{\"path\":\"evidence-b.txt\"}",
                "{\"path\":\"evidence-a.txt\"}");
        }

        [UnityTest]
        public IEnumerator ArgumentVariantsReturningSameEvidenceDoNotResetGuard()
        {
            yield return VerifyEvidenceSequence(true,
                "{\"path\":\"evidence-a.txt\"}", "{\"path\":\"evidence-a.txt\"}",
                "{\"path\":\"evidence-a.txt\",\"requireExactPath\":true}", "{\"path\":\"evidence-a.txt\"}");
        }

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

            public int RequestCount { get; private set; }


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

                ChatCompletion completion =
                    new ChatCompletion();

                completion.ToolCalls.Add(
                    new ToolCall(
                        $"looping-read-{RequestCount}",
                        "read_file",
                        "{\"path\":\"looping-read.txt\"}"));

                return Task.FromResult(
                    completion);
            }
        }


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

            public int RequestCount { get; private set; }

            public int SelectionResultCount { get; private set; }


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

                SelectionResultCount =
                    messages.Count(
                        message =>
                            message.Role == "tool" &&
                            message.Name == "get_editor_selection");

                ChatCompletion completion =
                    new ChatCompletion();

                if (RequestCount == 1 ||
                    RequestCount == 3)
                {
                    completion.ToolCalls.Add(
                        new ToolCall(
                            $"selection-{RequestCount}",
                            "get_editor_selection",
                            "{\"includeComponents\":true}"));
                }
                else if (RequestCount == 2)
                {
                    completion.ToolCalls.Add(
                        new ToolCall(
                            "related-inspection",
                            "inspect_editor_component",
                            "{}"));
                }
                else
                {
                    completion.Content =
                        "verified";
                }

                return Task.FromResult(
                    completion);
            }
        }


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

            public int RequestCount { get; private set; }


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

                ChatCompletion completion =
                    new ChatCompletion();

                completion.ToolCalls.Add(
                    new ToolCall(
                        $"verification-{RequestCount}",
                        "expensive_verification",
                        "{}"));

                return Task.FromResult(
                    completion);
            }
        }


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

            public int RequestCount { get; private set; }

            public bool DuplicateWasReportedAsFailure { get; private set; }


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

                if (RequestCount == 3)
                {
                    ConversationMessage duplicate =
                        messages.LastOrDefault(
                            message =>
                                message.Role == "tool" &&
                                (message.Content ?? string.Empty).Contains(
                                    "DuplicateIgnored"));

                    DuplicateWasReportedAsFailure =
                        duplicate != null &&
                        (bool?)JObject.Parse(
                            duplicate.Content)["Succeeded"] == false;
                }

                if (RequestCount >= 3)
                {
                    return Task.FromResult(
                        new ChatCompletion
                        {
                            Content = "failure handled"
                        });
                }

                ChatCompletion completion =
                    new ChatCompletion();

                completion.ToolCalls.Add(
                    new ToolCall(
                        $"failed-verification-{RequestCount}",
                        "failing_verification",
                        "{}"));

                return Task.FromResult(
                    completion);
            }
        }


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

            public int RequestCount { get; private set; }


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

                ChatCompletion completion =
                    new ChatCompletion();

                completion.ToolCalls.Add(
                    new ToolCall(
                        $"varying-failure-{RequestCount}",
                        "varying_failed_mutation",
                        "{\"value\":\"candidate-" +
                        RequestCount +
                        "\"}"));

                return Task.FromResult(
                    completion);
            }
        }


        private sealed class CountingConvergentCapability :
            CapabilityBase<EmptyEditModeTestArguments>,
            ICapabilityExecutionSemantics
        {
            public override string Name =>
                "expensive_verification";

            public override string Description =>
                "Run an expensive verification";

            public override CapabilityPermission Permission =>
                CapabilityPermission.Write;


            public CapabilityBatchBehavior BatchBehavior =>
                CapabilityBatchBehavior.SingleMutationPerResponse;


            public CapabilityRepeatedCallBehavior RepeatedCallBehavior =>
                CapabilityRepeatedCallBehavior.CompleteRunFromPreviousResult;

            public int ExecutionCount { get; private set; }


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

                return Task.FromResult<object>(
                    new
                    {
                        RunScheduled = true,
                        FailCount = 0
                    });
            }
        }


        private sealed class FailingConvergentCapability :
            CapabilityBase<EmptyEditModeTestArguments>,
            ICapabilityExecutionSemantics
        {
            public override string Name =>
                "failing_verification";

            public override string Description =>
                "A verification that fails recoverably";

            public override CapabilityPermission Permission =>
                CapabilityPermission.Write;

            public CapabilityBatchBehavior BatchBehavior =>
                CapabilityBatchBehavior.SingleMutationPerResponse;

            public CapabilityRepeatedCallBehavior RepeatedCallBehavior =>
                CapabilityRepeatedCallBehavior.CompleteRunFromPreviousResult;

            public int ExecutionCount { get; private set; }


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

                return Task.FromException<object>(
                    new System.InvalidOperationException(
                        "Verification could not run."));
            }
        }


        private sealed class VaryingFailedMutationCapability :
            CapabilityBase<BatchableMutationArguments>
        {
            public override string Name =>
                "varying_failed_mutation";

            public override string Description =>
                "Attempts a mutation whose target rejects every candidate";

            public override CapabilityPermission Permission =>
                CapabilityPermission.Write;

            public int ExecutionCount { get; private set; }


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

                return Task.FromException<object>(
                    new System.InvalidOperationException(
                        "The target rejected every candidate value."));
            }
        }


        private sealed class BatchableMutationCapability :
            CapabilityBase<BatchableMutationArguments>,
            ICapabilityExecutionSemantics
        {
            public override string Name =>
                "batchable_mutation";

            public override string Description =>
                "Create one independently validated item";

            public override CapabilityPermission Permission =>
                CapabilityPermission.Write;


            public CapabilityBatchBehavior BatchBehavior =>
                CapabilityBatchBehavior.AllowSameCapabilityBatch;


            public CapabilityRepeatedCallBehavior RepeatedCallBehavior =>
                CapabilityRepeatedCallBehavior.ReplayPreviousResult;

            public List<string> ExecutedValues { get; } =
                new List<string>();

            protected override Task<object> ExecuteAsync(
                BatchableMutationArguments arguments,
                CapabilityContext context)
            {
                ExecutedValues.Add(
                    arguments.Value);

                return Task.FromResult<object>(
                    new { Succeeded = true });
            }
        }


        private sealed class LiveSelectionInterveningCapability :
            CapabilityBase<EmptyEditModeTestArguments>
        {
            public override string Name =>
                "inspect_editor_component";

            public override string Description =>
                "Inspect a selected component";

            public override CapabilityPermission Permission =>
                CapabilityPermission.ReadOnly;


            protected override Task<object> ExecuteAsync(
                EmptyEditModeTestArguments arguments,
                CapabilityContext context)
            {
                return Task.FromResult<object>(
                    new
                    {
                        Inspected = true
                    });
            }
        }


        private sealed class EmptyEditModeTestArguments
        {
        }


        private sealed class BatchableMutationArguments
        {
            public string Value;
        }
    }
}
