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

using AIRuntime.Editor;
using AIRuntime.Runtime.Services.Agent;

using NUnit.Framework;

using UnityEngine;

using RuntimeFacade =
    AIRuntime.Runtime.AIRuntime;


namespace AIRuntime.Tests
{
    public sealed class AgentActivityPluginTests
    {
        private static string Explain(AIRuntimeAgentActivityStore store, string field)
        {
            var entries = store.Snapshot();
            var type = typeof(AIRuntimeAgentActivityStore).Assembly.GetType("AIRuntime.Editor.AIRuntimeAgentActivityExplanation");
            var view = type.GetMethod("Build", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic)
                .Invoke(null, new object[] { entries.Last(), entries });
            return (string)type.GetField(field).GetValue(view);
        }

        [Test]
        public void Explanation_PlanningNamesCardAndDistinguishesProposalFromCompletion()
        {
            var store = new AIRuntimeAgentActivityStore();
            var context = new AIRuntimeEditorRuntimePluginContext("Kanban", "Architect", "Build game");
            store.Publish(context, AIRuntimeAgentActivityKind.ToolStarted, "Calling",
                "Title=Goal and Victory System", "session", "create_kanban_child_task");
            Assert.AreEqual("Planning", Explain(store, "Action"));
            Assert.AreEqual("Goal and Victory System", Explain(store, "Target"));
            store.Publish(context, AIRuntimeAgentActivityKind.ToolCompleted, "Completed",
                "Succeeded=True, ChildTaskTitle=Goal and Victory System", "session", "create_kanban_child_task");
            Assert.AreEqual("Recorded plan for", Explain(store, "Action"));
            Assert.AreEqual("Goal and Victory System", Explain(store, "Target"));
            StringAssert.Contains("implementation and review are still required", Explain(store, "Outcome"));
        }

        [Test]
        public void ReadingModeHoldsSnapshotWhileNewEventsAreRecorded()
        {
            var store = AIRuntimeAgentActivityStore.Shared;
            store.Clear();
            var window = UnityEngine.ScriptableObject.CreateInstance<AIRuntimeAgentActivityWindow>();
            var flags = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic;
            var type = typeof(AIRuntimeAgentActivityWindow);
            try
            {
                type.GetField("_autoScroll", flags).SetValue(window, false);
                object before = type.GetField("_runs", flags).GetValue(window);
                store.Publish(new AIRuntimeEditorRuntimePluginContext("Kanban", "Architect", "Planning"),
                    AIRuntimeAgentActivityKind.RunStarted, "Started");
                type.GetMethod("Update", flags).Invoke(window, null);
                Assert.AreSame(before, type.GetField("_runs", flags).GetValue(window));
                Assert.AreEqual(1, store.Snapshot().Count, "Reading mode must not discard incoming evidence.");
                Assert.AreEqual(1, type.GetField("_pendingRefresh", flags).GetValue(window));
                type.GetMethod("RefreshRuns", flags).Invoke(window, null);
                Assert.AreNotSame(before, type.GetField("_runs", flags).GetValue(window));
                Assert.AreEqual(0, type.GetField("_pendingRefresh", flags).GetValue(window));
            }
            finally { UnityEngine.Object.DestroyImmediate(window); store.Clear(); }
        }
        [Test]
        public void Explanation_RunFailureShowsProviderLimitWithoutPayload()
        {
            var store = new AIRuntimeAgentActivityStore();
            var context = new AIRuntimeEditorRuntimePluginContext("Kanban", "Developer", "UI");
            store.Publish(context, AIRuntimeAgentActivityKind.RunFailed, "Run failed",
                "Provider [RateLimited]: HTTP 429\n{\"content\":\"SECRET-PAYLOAD\"}");
            StringAssert.Contains("provider limited requests", Explain(store, "Outcome"));
            StringAssert.DoesNotContain("SECRET-PAYLOAD", Explain(store, "Outcome"));
        }

        [Test]
        public void Explanation_ResolvesObservedObjectNameAndComponentWithinSession()
        {
            var store = new AIRuntimeAgentActivityStore();
            var context = new AIRuntimeEditorRuntimePluginContext("Kanban", "Developer", "Minimal UI System");
            store.Publish(context, AIRuntimeAgentActivityKind.ToolCompleted, "Inspected",
                "GameObjectInstanceId=-3144, GameObjectName=RestartText, HierarchyPath=Canvas/RestartText, ScenePath=Assets/Game/Main.unity",
                "session", "inspect_editor_component");
            store.Publish(context, AIRuntimeAgentActivityKind.ToolStarted, "Calling inspect_editor_component.",
                "gameObjectInstanceId=-3144, componentType=UnityEngine.UI.Text", "session", "inspect_editor_component");
            StringAssert.Contains("Canvas/RestartText", Explain(store, "Target"));
            StringAssert.Contains("UI.Text", Explain(store, "Target"));
            Assert.AreEqual("Inspect component settings", Explain(store, "Action"));
            StringAssert.Contains("current values", Explain(store, "Purpose"));
        }

        [Test]
        public void Observer_PreservesNestedObjectIdentityWithoutCopyingPayloadBodies()
        {
            var store = new AIRuntimeAgentActivityStore();
            var context = new AIRuntimeEditorRuntimePluginContext("Kanban", "Developer", "UI");
            using (var observer = new AIRuntimeAgentActivityObserver(store, context))
            {
                observer.OnAgentEvent(new AgentEvent(AgentEventType.ToolCompleted, "session", "inspect_game_object",
                    "{\"GameObject\":{\"InstanceId\":42,\"Name\":\"RestartText\",\"HierarchyPath\":\"Canvas/RestartText\",\"ScenePath\":\"Assets/Main.unity\",\"Content\":\"PRIVATE-BODY\"}}"));
                observer.OnAgentEvent(new AgentEvent(AgentEventType.ToolStarted, "session", "inspect_editor_component",
                    "{\"gameObjectInstanceId\":42,\"componentType\":\"UnityEngine.UI.Text\"}"));
            }
            StringAssert.Contains("Canvas/RestartText", Explain(store, "Target"));
            StringAssert.DoesNotContain("PRIVATE-BODY", string.Join(" ", store.Snapshot().Select(e => e.Detail)));
        }

        [Test]
        public void Explanation_DoesNotBorrowObjectIdentityFromAnotherSession()
        {
            var store = new AIRuntimeAgentActivityStore();
            var context = new AIRuntimeEditorRuntimePluginContext("Kanban", "Developer", "UI");
            store.Publish(context, AIRuntimeAgentActivityKind.ToolCompleted, "Inspected",
                "GameObjectInstanceId=42, GameObjectName=OldObject", "old", "inspect_editor_component");
            store.Publish(context, AIRuntimeAgentActivityKind.ToolStarted, "Inspect",
                "gameObjectInstanceId=42", "new", "inspect_editor_component");
            StringAssert.DoesNotContain("OldObject", Explain(store, "Target"));
            StringAssert.Contains("name not recorded", Explain(store, "Target"));
        }

        [Test]
        public void Explanation_FailedSaveRetainsTargetAndExplainsOwnership()
        {
            var store = new AIRuntimeAgentActivityStore();
            var context = new AIRuntimeEditorRuntimePluginContext("Kanban", "Developer", "UI");
            store.Publish(context, AIRuntimeAgentActivityKind.ToolStarted, "Save",
                "scenePath=Assets/Game/Main.unity", "session", "save_editor_scene");
            store.Publish(context, AIRuntimeAgentActivityKind.ToolFailed, "Failed",
                "[ArtifactOwnership] Not an owned scene. password=secret-value", "session", "save_editor_scene");
            Assert.AreEqual("Assets/Game/Main.unity", Explain(store, "Target"));
            StringAssert.Contains("Blocked by task ownership", Explain(store, "Outcome"));
            StringAssert.DoesNotContain("secret-value", Explain(store, "Outcome"));
        }

        [Test]
        public void Explanation_DoesNotGuessBetweenOverlappingCalls()
        {
            var store = new AIRuntimeAgentActivityStore();
            var context = new AIRuntimeEditorRuntimePluginContext("Kanban", "Developer", "UI");
            store.Publish(context, AIRuntimeAgentActivityKind.ToolStarted, "Save", "scenePath=Assets/First.unity", "session", "save_editor_scene");
            store.Publish(context, AIRuntimeAgentActivityKind.ToolStarted, "Save", "scenePath=Assets/Second.unity", "session", "save_editor_scene");
            store.Publish(context, AIRuntimeAgentActivityKind.ToolFailed, "Failed", "No details", "session", "save_editor_scene");
            Assert.AreEqual("Target not recorded in this event", Explain(store, "Target"));
        }

        [Test]
        public void Explanation_CompletedEventDoesNotHideUnsuccessfulOperation()
        {
            var store = new AIRuntimeAgentActivityStore();
            var context = new AIRuntimeEditorRuntimePluginContext("Kanban", "Developer", "UI");
            store.Publish(context, AIRuntimeAgentActivityKind.ToolCompleted, "Completed",
                "Succeeded=True, ActiveRequestSucceeded=False, ScenePath=Assets/Main.unity", "session", "open_editor_scene");
            StringAssert.Contains("not fully successful", Explain(store, "Outcome"));
        }

        [Test]
        public void Store_RedactsSecretsBoundsTextAndRetainsOnlyCapacity()
        {
            AIRuntimeAgentActivityStore store =
                new AIRuntimeAgentActivityStore(
                    maximumEntries: 3);

            AIRuntimeEditorRuntimePluginContext context =
                new AIRuntimeEditorRuntimePluginContext(
                    "Kanban password=objective-secret",
                    "DeveloperAgent",
                    "Use apiKey=objective-key to inspect the task.",
                    "Implementation",
                    "task-redaction");

            for (int index = 0; index < 5; index++)
            {
                store.Publish(
                    context,
                    AIRuntimeAgentActivityKind.Status,
                    $"Update {index} authorization=Bearer summary-secret",
                    "{\"apiKey\":\"json-secret\",\"password\":\"password-secret\",\"message\":\"Bearer bearer-secret\",\"content\":\"" +
                    new string(
                        'x',
                        4000) +
                    "\"}");
            }

            IReadOnlyList<AIRuntimeAgentActivityEntry> entries =
                store.Snapshot();

            Assert.AreEqual(
                3,
                entries.Count);

            string serializedView =
                string.Join(
                    "\n",
                    entries.Select(
                        entry =>
                            string.Join(
                                "|",
                                entry.Source,
                                entry.Objective,
                                entry.Summary,
                                entry.Detail)));

            StringAssert.DoesNotContain(
                "objective-secret",
                serializedView);
            StringAssert.DoesNotContain(
                "objective-key",
                serializedView);
            StringAssert.DoesNotContain(
                "summary-secret",
                serializedView);
            StringAssert.DoesNotContain(
                "json-secret",
                serializedView);
            StringAssert.DoesNotContain(
                "password-secret",
                serializedView);
            StringAssert.DoesNotContain(
                "bearer-secret",
                serializedView);
            StringAssert.Contains(
                "[REDACTED]",
                serializedView);
            StringAssert.Contains(
                "[bounded; original characters=",
                entries.Last().Detail);
        }


        [Test]
        public void Observer_StoresSafeToolSummariesWithoutPayloadBodies()
        {
            AIRuntimeAgentActivityStore store =
                new AIRuntimeAgentActivityStore();

            AIRuntimeEditorRuntimePluginContext context =
                new AIRuntimeEditorRuntimePluginContext(
                    "Kanban",
                    "DeveloperAgent",
                    "Update Assets/Game/Player.cs",
                    "Implementation",
                    "task-tool-summary");

            using (
                AIRuntimeAgentActivityObserver observer =
                    new AIRuntimeAgentActivityObserver(
                        store,
                        context))
            {
                observer.OnAgentEvent(
                    new AgentEvent(
                        AgentEventType.ToolStarted,
                        "session-1",
                        "write_file",
                        "{\"path\":\"Assets/Game/Player.cs\",\"content\":\"RAW-SOURCE-BODY\",\"apiKey\":\"tool-secret\"}"));

                observer.OnAgentEvent(
                    new AgentEvent(
                        AgentEventType.ToolCompleted,
                        "session-1",
                        "write_file",
                        "{\"Succeeded\":true,\"Path\":\"Assets/Game/Player.cs\",\"Content\":\"RAW-RESULT-BODY\"}"));
            }

            IReadOnlyList<AIRuntimeAgentActivityEntry> entries =
                store.Snapshot();

            Assert.AreEqual(
                2,
                entries.Count);
            Assert.AreEqual(
                AIRuntimeAgentActivityKind.ToolStarted,
                entries[0].Kind);
            Assert.AreEqual(
                AIRuntimeAgentActivityKind.ToolCompleted,
                entries[1].Kind);
            Assert.AreEqual(
                "session-1",
                entries[0].SessionId);
            Assert.AreEqual(
                "write_file",
                entries[0].ToolName);
            StringAssert.Contains(
                "Assets/Game/Player.cs",
                entries[0].Detail);
            StringAssert.Contains(
                "Succeeded=",
                entries[1].Detail);
            StringAssert.DoesNotContain(
                "RAW-SOURCE-BODY",
                entries[0].Detail);
            StringAssert.DoesNotContain(
                "RAW-RESULT-BODY",
                entries[1].Detail);
            StringAssert.DoesNotContain(
                "tool-secret",
                entries[0].Detail);
        }


        [Test]
        public void Store_OmitsProviderResponseAndReasoningFromFailures()
        {
            AIRuntimeAgentActivityStore store =
                new AIRuntimeAgentActivityStore();

            AIRuntimeEditorRuntimePluginContext context =
                new AIRuntimeEditorRuntimePluginContext(
                    "Chat Console",
                    "Assistant",
                    "Handle a provider failure",
                    "Conversation",
                    "provider-error-redaction");

            store.Publish(
                context,
                AIRuntimeAgentActivityKind.RunFailed,
                "Provider request failed. HTTP 500.\n{\"reasoning\":\"PRIVATE-THOUGHT\",\"content\":\"RAW-RESPONSE\"}",
                "Response body: {\"content\":\"SECOND-RAW-RESPONSE\"}");

            AIRuntimeAgentActivityEntry entry =
                store.Snapshot()
                    .Single();

            string visible =
                entry.Summary +
                "|" +
                entry.Detail;

            StringAssert.Contains(
                "Provider request failed. HTTP 500.",
                visible);
            StringAssert.Contains(
                "payload details omitted",
                visible);
            StringAssert.DoesNotContain(
                "PRIVATE-THOUGHT",
                visible);
            StringAssert.DoesNotContain(
                "RAW-RESPONSE",
                visible);
            StringAssert.DoesNotContain(
                "SECOND-RAW-RESPONSE",
                visible);
        }


        [Test]
        public void ToolSummary_DoesNotParseOrRetainOversizedPayloadDetails()
        {
            AIRuntimeAgentActivityStore store =
                new AIRuntimeAgentActivityStore();

            AIRuntimeEditorRuntimePluginContext context =
                new AIRuntimeEditorRuntimePluginContext(
                    "Kanban",
                    "DeveloperAgent",
                    "Write a large file",
                    "Implementation",
                    "large-tool-payload");

            string privateBody =
                new string(
                    'x',
                    100000);

            using (
                AIRuntimeAgentActivityObserver observer =
                    new AIRuntimeAgentActivityObserver(
                        store,
                        context))
            {
                observer.OnAgentEvent(
                    new AgentEvent(
                        AgentEventType.ToolStarted,
                        "session-large",
                        "write_file",
                        "{\"path\":\"Assets/Large.cs\",\"content\":\"" +
                        privateBody +
                        "\"}"));
            }

            string summary =
                store.Snapshot()
                    .Single()
                    .Detail;

            StringAssert.Contains(
                "details omitted",
                summary);
            StringAssert.DoesNotContain(
                "Assets/Large.cs",
                summary);
            StringAssert.DoesNotContain(
                privateBody.Substring(
                    0,
                    128),
                summary);
        }


        [Test]
        public void ContextReports_AreIsolatedAndCaptureUpdatedStage()
        {
            AIRuntimeAgentActivityStore store =
                new AIRuntimeAgentActivityStore();

            AIRuntimeEditorRuntimePluginContext context =
                new AIRuntimeEditorRuntimePluginContext(
                    "Kanban",
                    "ReviewAgent",
                    "Review the implementation",
                    "Preparing",
                    "task-stage");

            context.ActivityReported +=
                (ignoredContext, ignoredReport) =>
                    throw new InvalidOperationException(
                        "A broken diagnostics subscriber must be isolated.");

            using (
                AIRuntimeAgentActivityObserver observer =
                    new AIRuntimeAgentActivityObserver(
                        store,
                        context))
            {
                Assert.DoesNotThrow(
                    () =>
                        context.UpdateStage(
                            "Review",
                            "Reviewing acceptance criteria."));
            }

            AIRuntimeAgentActivityEntry entry =
                store.Snapshot()
                    .Single();

            Assert.AreEqual(
                "Review",
                entry.Stage);
            Assert.AreEqual(
                "Reviewing acceptance criteria.",
                entry.Summary);

            using (
                AIRuntimeAgentActivityObserver observer =
                    new AIRuntimeAgentActivityObserver(
                        store,
                        context))
            {
                context.UpdateRun(
                    "SecondAgent",
                    "Handle the next prompt",
                    "Implementation",
                    correlationId: "task-stage-2");
            }

            AIRuntimeAgentActivityEntry updated =
                store.Snapshot()
                    .Last();

            Assert.AreEqual(
                "SecondAgent",
                updated.Agent);
            Assert.AreEqual(
                "Handle the next prompt",
                updated.Objective);
            Assert.AreEqual(
                "Implementation",
                updated.Stage);
            Assert.AreEqual(
                "task-stage-2",
                updated.CorrelationId);
        }


        [Test]
        public void Store_IsThreadSafeAndSequenceRemainsOrderedWhenBounded()
        {
            const int capacity = 64;

            AIRuntimeAgentActivityStore store =
                new AIRuntimeAgentActivityStore(
                    capacity);

            AIRuntimeEditorRuntimePluginContext context =
                new AIRuntimeEditorRuntimePluginContext(
                    "Concurrent test",
                    "Agent",
                    "Record activity",
                    "Working",
                    "task-concurrent");

            Parallel.For(
                0,
                500,
                index =>
                    store.Publish(
                        context,
                        AIRuntimeAgentActivityKind.Status,
                        $"Update {index}"));

            IReadOnlyList<AIRuntimeAgentActivityEntry> entries =
                store.Snapshot();

            Assert.AreEqual(
                capacity,
                entries.Count);
            Assert.AreEqual(
                capacity,
                entries.Select(
                        entry =>
                            entry.Sequence)
                    .Distinct()
                    .Count());
            CollectionAssert.IsOrdered(
                entries.Select(
                    entry =>
                        entry.Sequence));
        }


        [Test]
        public void Projection_GroupsLifecycleAndReopensAfterLaterRunStart()
        {
            AIRuntimeAgentActivityStore store =
                new AIRuntimeAgentActivityStore();

            AIRuntimeEditorRuntimePluginContext context =
                new AIRuntimeEditorRuntimePluginContext(
                    "Kanban",
                    "DeveloperAgent",
                    "Build a feature",
                    "Implementation",
                    "task-projection");

            store.Publish(
                context,
                AIRuntimeAgentActivityKind.RunStarted,
                "Run started.",
                sessionId: "session-1");
            store.Publish(
                context,
                AIRuntimeAgentActivityKind.ToolCompleted,
                "read_file completed.",
                sessionId: "session-1",
                toolName: "read_file");

            context.UpdateStage(
                "Review");

            store.Publish(
                context,
                AIRuntimeAgentActivityKind.Review,
                "Reviewing acceptance criteria.");
            store.Publish(
                context,
                AIRuntimeAgentActivityKind.RunCompleted,
                "Run completed.",
                sessionId: "session-1");

            AIRuntimeAgentActivityRun completed =
                AIRuntimeAgentActivityProjection.BuildRuns(
                        store.Snapshot())
                    .Single();

            Assert.IsFalse(
                completed.IsActive);
            Assert.IsTrue(
                completed.HasStarted);
            Assert.IsTrue(
                completed.HasToolActivity);
            Assert.IsTrue(
                completed.HasReviewActivity);
            Assert.IsTrue(
                completed.HasFinished);

            store.Publish(
                context,
                AIRuntimeAgentActivityKind.Retry,
                "Retrying after new evidence.");
            store.Publish(
                context,
                AIRuntimeAgentActivityKind.RunStarted,
                "Run started.",
                sessionId: "session-2");

            AIRuntimeAgentActivityRun retried =
                AIRuntimeAgentActivityProjection.BuildRuns(
                        store.Snapshot())
                    .Single();

            Assert.IsTrue(
                retried.IsActive);
            Assert.AreEqual(
                6,
                retried.Entries.Count);
        }


        [Test]
        public void Projection_StaysActiveDuringPostRunVerificationAndClosesOnOutcome()
        {
            AIRuntimeAgentActivityStore store =
                new AIRuntimeAgentActivityStore();

            AIRuntimeEditorRuntimePluginContext context =
                new AIRuntimeEditorRuntimePluginContext(
                    "Kanban",
                    "DeveloperAgent",
                    "Verify a card",
                    "Implementation",
                    "task-post-run-verification");

            using (
                AIRuntimeAgentActivityObserver observer =
                    new AIRuntimeAgentActivityObserver(
                        store,
                        context))
            {
                observer.OnAgentEvent(
                    new AgentEvent(
                        AgentEventType.RunStarted,
                        "session"));

                observer.OnAgentEvent(
                    new AgentEvent(
                        AgentEventType.RunCompleted,
                        "session"));

                context.UpdateStage(
                    "Verification",
                    "Checking completion evidence.");

                Assert.IsTrue(
                    AIRuntimeAgentActivityProjection.BuildRuns(
                            store.Snapshot())
                        .Single()
                        .IsActive);

                context.Report(
                    AIRuntimeAgentActivityKind.Error,
                    "Verification failed.");
            }

            AIRuntimeAgentActivityRun failed =
                AIRuntimeAgentActivityProjection.BuildRuns(
                        store.Snapshot())
                    .Single();

            Assert.IsFalse(
                failed.IsActive);
            Assert.IsTrue(
                failed.HasFinished);
            Assert.IsTrue(
                failed.HasError);
        }


        [Test]
        public void Registry_HasBuiltInAndIsolatesPluginAttachAndDisposeFailures()
        {
            string suffix =
                Guid.NewGuid().ToString("N");

            ThrowingPlugin throwing =
                new ThrowingPlugin(
                    "aaa.throwing." +
                    suffix);

            CountingPlugin counting =
                new CountingPlugin(
                    "zzz.counting." +
                    suffix);

            int revisionBeforeRegistration =
                AIRuntimeEditorRuntimePluginRegistry.Revision;

            using (
                IDisposable throwingRegistration =
                    AIRuntimeEditorRuntimePluginRegistry.Register(
                        throwing))
            using (
                IDisposable countingRegistration =
                    AIRuntimeEditorRuntimePluginRegistry.Register(
                        counting))
            {
                Assert.Greater(
                    AIRuntimeEditorRuntimePluginRegistry.Revision,
                    revisionBeforeRegistration);

                Assert.IsTrue(
                    AIRuntimeEditorRuntimePluginRegistry
                        .GetRegisteredPlugins()
                        .Any(
                            plugin =>
                                string.Equals(
                                    plugin.Id,
                                    AIRuntimeAgentActivityRuntimePlugin.PluginId,
                                    StringComparison.Ordinal)));

                string projectRoot =
                    Directory.GetParent(
                            Application.dataPath)
                        ?.FullName ??
                    Application.dataPath;

                RuntimeFacade runtime =
                    new RuntimeFacade(
                        projectRoot);

                AIRuntimeEditorRuntimePluginContext context =
                    new AIRuntimeEditorRuntimePluginContext(
                        "Registry test",
                        "Agent",
                        "Attach plugins",
                        "Preparing",
                        "task-registry-" +
                        suffix);

                IDisposable attachment =
                    null;

                Assert.DoesNotThrow(
                    () =>
                        attachment =
                            AIRuntimeEditorRuntimePluginRegistry.Attach(
                                runtime,
                                context));

                Assert.AreEqual(
                    1,
                    throwing.AttachCount);
                Assert.AreEqual(
                    1,
                    counting.AttachCount);

                Assert.DoesNotThrow(
                    () =>
                        attachment.Dispose());

                Assert.AreEqual(
                    1,
                    counting.DisposeCount);
            }

            Assert.IsFalse(
                AIRuntimeEditorRuntimePluginRegistry
                    .GetRegisteredPlugins()
                    .Any(
                        plugin =>
                            string.Equals(
                                plugin.Id,
                                    counting.Id,
                                    StringComparison.Ordinal)));

            Assert.Greater(
                AIRuntimeEditorRuntimePluginRegistry.Revision,
                revisionBeforeRegistration + 1);
        }


        private sealed class ThrowingPlugin :
            IAIRuntimeEditorRuntimePlugin
        {
            public ThrowingPlugin(
                string id)
            {
                Id = id;
            }


            public string Id
            {
                get;
            }


            public int AttachCount
            {
                get;
                private set;
            }


            public IDisposable Attach(
                RuntimeFacade runtime,
                AIRuntimeEditorRuntimePluginContext context)
            {
                AttachCount++;

                throw new InvalidOperationException(
                    "apiKey=plugin-attach-secret");
            }
        }


        private sealed class CountingPlugin :
            IAIRuntimeEditorRuntimePlugin
        {
            public CountingPlugin(
                string id)
            {
                Id = id;
            }


            public string Id
            {
                get;
            }


            public int AttachCount
            {
                get;
                private set;
            }


            public int DisposeCount
            {
                get;
                private set;
            }


            public IDisposable Attach(
                RuntimeFacade runtime,
                AIRuntimeEditorRuntimePluginContext context)
            {
                AttachCount++;

                return new CallbackDisposable(
                    () =>
                        DisposeCount++);
            }
        }


        private sealed class CallbackDisposable :
            IDisposable
        {
            private Action _callback;


            public CallbackDisposable(
                Action callback)
            {
                _callback = callback;
            }


            public void Dispose()
            {
                Action callback =
                    Interlocked.Exchange(
                        ref _callback,
                        null);

                callback?.Invoke();
            }
        }
    }
}
