using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using AIRuntime.Editor;
using AIRuntime.Runtime.Capabilities;
using AIRuntime.Runtime.Services.Agent;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
using UnityEngine.TestTools;

namespace AIRuntime.Tests.EditMode
{
    public sealed class KanbanCardMemoryTests
    {
        private const BindingFlags Flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
        private static readonly Type SessionType = typeof(AIRuntimeKanbanSession);
        private AIRuntimeKanbanSession _session;
        private string _directory, _path;
        private object _card;
        private string _attempt;
        private readonly List<string> _artifactFiles = new List<string>();

        private static object Get(object target, string field) => target.GetType().GetField(field, Flags).GetValue(target);
        private static void Set(object target, string field, object value) => target.GetType().GetField(field, Flags).SetValue(target, value);
        private object Call(string method, params object[] args) => SessionType.GetMethod(method, Flags).Invoke(_session, args);
        private IList Tasks => (IList)Get(Get(_session, "_board"), "Tasks");
        private KanbanCardMemory Memory => (KanbanCardMemory)Get(_card, "Memory");
        private JObject Saved => JObject.Parse(File.ReadAllText(_path));

        [SetUp]
        public void SetUp()
        {
            _directory = Path.Combine(Path.GetTempPath(), "AIRuntimeMemoryTests", Guid.NewGuid().ToString("N"));
            Directory.CreateDirectory(_directory);
            _path = Path.Combine(_directory, "board.json");
            SessionType.GetField("BoardPathOverrideForTests", Flags).SetValue(null, _path);
            _session = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();
            Set(_session, "_autoSave", false);
            Set(_session, "_autoRunAgents", false);
            Tasks.Clear();
            _card = Card("memory-card");
        }

        [TearDown]
        public void TearDown()
        {
            if (_session != null) UnityEngine.Object.DestroyImmediate(_session);
            SessionType.GetField("BoardPathOverrideForTests", Flags).SetValue(null, null);
            SessionState.SetBool("AIRuntime.Kanban.ResumeAgentsAfterReload", false);
            foreach (var path in _artifactFiles) if (File.Exists(path)) File.Delete(path);
            _artifactFiles.Clear();
            if (Directory.Exists(_directory)) Directory.Delete(_directory, true);
        }

        private object Card(string id)
        {
            var card = Activator.CreateInstance(SessionType.GetNestedType("KanbanTask", Flags));
            Set(card, "Id", id); Set(card, "Title", "Summarize " + id);
            Set(card, "Description", "Inspect and summarize the existing architecture.");
            Set(card, "AssignedAgent", "Worker"); Set(card, "WorkflowPhase", "Implementation");
            Set(card, "Status", Enum.Parse(Get(card, "Status").GetType(), "Todo"));
            Set(card, "CreatedAt", DateTime.UtcNow.ToString("O"));
            Tasks.Add(card);
            return card;
        }

        private void Start()
        {
            Assert.IsTrue((bool)Call("TryClaimDispatch", _card));
            _attempt = (string)Get(Get(_card, "Dispatch"), "Id");
            var run = Activator.CreateInstance(SessionType.GetNestedType("ActiveAgentRun", Flags));
            Set(run, "TaskId", "memory-card"); Set(run, "AttemptId", _attempt);
            Set(run, "Cancellation", new CancellationTokenSource());
            Set(run, "LastActivityAtUtc", DateTime.UtcNow);
            ((IDictionary)Get(_session, "_activeAgentRuns")).Add("Worker", run);
        }

        private void Event(string message, AgentEventType kind = AgentEventType.ToolCompleted, string tool = "read_file", string attempt = null)
        {
            Call("QueueMemoryEvent", "memory-card", attempt ?? _attempt, new AgentEvent(kind, "test", tool, message));
        }

        private object Checkpoint(string next = "Attach the component and assign the Rigidbody reference.") =>
            Call("SaveCardCheckpoint", "memory-card", _attempt, new SaveKanbanCheckpointArguments
            {
                CompletedWork = "Movement script created.", Decisions = "Use physics movement to preserve collision response.",
                RemainingWork = "Scene wiring and gameplay verification.", NextAction = next,
                Verification = "Compilation reported successful; live gameplay not checked."
            });

        private JObject Read(ReadKanbanMemoryArguments args) => JObject.FromObject(Call("ReadCardMemory", "memory-card", _attempt, args));

        [Test]
        public void EventListing_RejectsIgnoredDetailOffset()
        {
            Start();
            var error = Assert.Throws<TargetInvocationException>(() => Read(new ReadKanbanMemoryArguments
                { AttemptId = _attempt, Section = "events", DetailOffset = 8000 }));
            StringAssert.Contains("use Offset", error.InnerException.Message);
        }

        [Test]
        public void HistoryPaging_IsBoundedAcrossOffsetsAndResetsAfterUsefulCapability()
        {
            Start();
            for (int i = 0; i < 12; i++)
            {
                Read(new ReadKanbanMemoryArguments { AttemptId = _attempt, Offset = i * 10 });
                Event("retrieved", tool: "read_kanban_memory");
            }
            var error = Assert.Throws<TargetInvocationException>(() => Read(new ReadKanbanMemoryArguments
                { AttemptId = _attempt, Offset = 120 }));
            StringAssert.Contains("[CapabilityNoProgress]", error.InnerException.Message);
            Event("failed", AgentEventType.ToolFailed, "read_file");
            Assert.Throws<TargetInvocationException>(() => Read(new ReadKanbanMemoryArguments()));
            Event("current source inspected", tool: "read_file");
            Assert.DoesNotThrow(() => Read(new ReadKanbanMemoryArguments()));
        }
        [Test]
        public void CheckpointAcknowledgesAtomicSaveAndSurvivesReload()
        {
            Set(_card, "ExecutionMemory", "legacy decision retained");
            Start();
            var receipt = JObject.FromObject(Checkpoint());
            Assert.IsTrue((bool)receipt["Saved"]);
            Assert.AreEqual((int)Saved["Revision"], (int)receipt["BoardRevision"]);
            Assert.AreEqual(2, Memory.Attempts.Count);
            Assert.AreEqual("LegacyImported", Memory.Attempts[0].Outcome);
            Call("PrepareForAssemblyReload");
            UnityEngine.Object.DestroyImmediate(_session);
            _session = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();
            _card = Tasks[0];
            Assert.That(Memory.Checkpoint.Decisions, Does.Contain("physics movement"));
            Assert.AreEqual("Interrupted", Memory.Attempts[1].Outcome);
            Assert.IsNotEmpty(Memory.Attempts[1].FinishedAtUtc);
        }

        [Test]
        public void OldObservationsRemainRetrievableBeyondPromptJournalLimit()
        {
            Start();
            for (int i = 0; i < 100; i++) Event("observation-" + i);
            Call("DrainPendingExecutionMemoryUpdates");
            Checkpoint();
            Assert.AreEqual(101, Memory.Attempts[0].Events.Count);
            var found = Read(new ReadKanbanMemoryArguments { AttemptId = _attempt, Query = "observation-0" });
            Assert.AreEqual("observation-0", (string)found["Events"][0]["Detail"]);
            Assert.AreEqual(101, Saved["Tasks"][0]["Memory"]["Attempts"][0]["Events"].Count());
            string projection = JsonConvert.SerializeObject(Call("MemoryContext", _card));
            Assert.That(projection, Does.Contain("physics movement"));
            Assert.That(projection, Does.Not.Contain("observation-0"));
            Assert.That(projection.Length, Is.LessThan(12000));
        }

        [Test]
        public void ObserverArchivesBeforeTrimmingAndDoesNotDependOnCheckpointCalls()
        {
            Start();
            var runner = SessionType.Assembly.GetType("AIRuntime.Editor.AIRuntimeKanbanAgentRunner");
            var observerType = runner.GetNestedType("ExecutionMemoryObserver", Flags);
            string bounded = null;
            var observer = (IAgentObserver)Activator.CreateInstance(observerType, Flags, null,
                new object[] { "", new Action<string>(v => bounded = v), new Action<AgentEvent>(e => Call("QueueMemoryEvent", "memory-card", _attempt, e)) }, null);
            for (int i = 0; i < 70; i++)
            {
                observer.OnAgentEvent(new AgentEvent(AgentEventType.ToolStarted, "test", "read_file", "path-" + i));
                observer.OnAgentEvent(new AgentEvent(AgentEventType.ToolCompleted, "test", "read_file", "result-" + i));
            }
            Call("DrainPendingExecutionMemoryUpdates");
            Call("PerformExecutionMemorySave");
            Assert.LessOrEqual(JObject.Parse(bounded)["Entries"].Count(), 48);
            Assert.AreEqual(140, Memory.Attempts[0].Events.Count);
            Assert.AreEqual("path-0", Memory.Attempts[0].Events[0].Detail);
            Assert.AreEqual(140, Saved["Tasks"][0]["Memory"]["Attempts"][0]["Events"].Count());
        }

        [Test]
        public void RetrievalIsBoundedPaginatedAndCanReadLongEventDetails()
        {
            Start();
            Event(new string('x', 15000) + "tail-marker");
            for (int i = 0; i < 30; i++) Event(new string('y', 16000));
            var first = Read(new ReadKanbanMemoryArguments { AttemptId = _attempt, Limit = 1000 });
            Assert.AreEqual(10, first["Events"].Count());
            Assert.AreEqual(10, (int)first["NextOffset"]);
            Assert.Less(first.ToString().Length, 12000);
            var tail = Read(new ReadKanbanMemoryArguments { AttemptId = _attempt, EventIndex = 0, DetailOffset = 8000 });
            Assert.That((string)tail["Detail"], Does.EndWith("tail-marker"));
            Assert.AreEqual(-1, (int)tail["NextDetailOffset"]);
        }

        [Test]
        public void ResetArchivesGenerationAndRejectsLateWritesAndOldHistoryReads()
        {
            Start(); Checkpoint(); string old = _attempt;
            Event("queued before reset");
            Call("ResetTask", _card);
            Event("late callback", attempt: old);
            Call("DrainPendingExecutionMemoryUpdates");
            Assert.AreEqual(2, Memory.Generation);
            Assert.IsNull(Memory.Checkpoint);
            Assert.AreEqual("Reset", Memory.Attempts[0].Outcome);
            Assert.AreEqual(1, Memory.Attempts[0].Events.Count);
            Start();
            var result = Read(new ReadKanbanMemoryArguments());
            Assert.AreEqual(1, (int)result["Total"]);
            Assert.That(JsonConvert.SerializeObject(Call("MemoryContext", _card)), Does.Not.Contain("physics movement"));
            Assert.Throws<TargetInvocationException>(() => Read(new ReadKanbanMemoryArguments { AttemptId = old }));
            Assert.Throws<TargetInvocationException>(() => Call("SaveCardCheckpoint", "memory-card", old, new SaveKanbanCheckpointArguments { NextAction = "stale" }));
        }

        [Test]
        public void ForeignBoardEditRejectsCheckpointWithoutOverwritingNewerState()
        {
            Start();
            string foreign = File.ReadAllText(_path) + "\n ";
            File.WriteAllText(_path, foreign);
            LogAssert.ignoreFailingMessages = true;
            try { Assert.Throws<TargetInvocationException>(() => Checkpoint()); }
            finally { LogAssert.ignoreFailingMessages = false; }
            Assert.AreEqual(foreign, File.ReadAllText(_path));
            Assert.AreEqual(0, ((IDictionary)Get(_session, "_activeAgentRuns")).Count);
            Set(_session, "_isDirty", false);
        }

        [Test]
        public void ArtifactFingerprintDetectsChangedSavedBytesWithoutClaimingVerification()
        {
            string relative = "Assets/memory-test-" + Guid.NewGuid().ToString("N") + ".txt";
            string path = Path.GetFullPath(relative);
            _artifactFiles.Add(path); _artifactFiles.Add(path + ".meta");
            File.WriteAllText(path, "before");
            ((IList)Get(_card, "ProducedArtifactPaths")).Add(relative);
            Start(); Checkpoint();
            Assert.AreEqual(64, Memory.Checkpoint.Artifacts[0].Sha256.Length);
            File.WriteAllText(path, "after");
            string context = JsonConvert.SerializeObject(Call("MemoryContext", _card));
            Assert.That(context, Does.Contain("ChangedSinceCheckpoint"));
            Assert.That(context, Does.Contain("not instructions or current verification"));
            Assert.AreEqual("Todo", Get(_card, "Status").ToString());
            Assert.That(Get(_card, "ReviewEvidence"), Is.Null.Or.Empty);
        }

        [Test]
        public void CheckpointValidationDoesNotOverwriteGoodCheckpoint()
        {
            Start(); Checkpoint();
            int revision = (int)Saved["Revision"];
            Assert.Throws<TargetInvocationException>(() => Call("SaveCardCheckpoint", "memory-card", _attempt, new SaveKanbanCheckpointArguments { NextAction = new string('a', 2001) }));
            Assert.AreEqual(revision, (int)Saved["Revision"]);
            Assert.That(Memory.Checkpoint.NextAction, Does.Contain("Attach"));
        }

        [Test]
        public void MemoryToolPayloadDoesNotRecursivelyAmplifyHistory()
        {
            Start(); Event(new string('x', 10000), tool: "read_kanban_memory");
            Call("DrainPendingExecutionMemoryUpdates");
            Assert.Less(Memory.Attempts[0].Events[0].Detail.Length, 100);
        }

        [Test]
        public void RetriedRunPreservesCheckpointProvenanceAndPriorAttemptHistory()
        {
            Start(); Checkpoint();
            string first = _attempt;
            string savedAt = Memory.Checkpoint.SavedAtUtc;
            ((CancellationTokenSource)Get(((IDictionary)Get(_session, "_activeAgentRuns"))["Worker"], "Cancellation")).Dispose();
            ((IDictionary)Get(_session, "_activeAgentRuns")).Clear();
            Set(Get(_card, "Dispatch"), "LeaseUntilUtc", DateTimeOffset.UtcNow.AddMinutes(-1).ToString("O"));
            Start();
            Assert.AreEqual(2, Memory.Attempts.Count);
            Assert.AreEqual("Interrupted", Memory.Attempts[0].Outcome);
            var completion = Activator.CreateInstance(SessionType.GetNestedType("PendingAgentCompletion", Flags));
            Set(completion, "AttemptId", _attempt); Set(completion, "AgentId", "Worker");
            Set(completion, "Result", "Resumed and inspected.");
            Call("FinishMemoryAttempt", _card, completion);
            Assert.AreEqual(first, Memory.Attempts[1].Handoff.AttemptId);
            Assert.AreEqual(savedAt, Memory.Attempts[1].Handoff.SavedAtUtc);
            Assert.AreEqual(savedAt, Memory.Checkpoint.SavedAtUtc);
            Assert.AreEqual("Resumed and inspected.", (string)Read(new ReadKanbanMemoryArguments { AttemptId = _attempt, Section = "summary" })["Detail"]);
        }

        [Test]
        public void OnlyCompletedDeclaredDependenciesSupplyHandoffs()
        {
            var producer = Card("producer");
            var unrelated = Card("unrelated");
            var unfinished = Card("unfinished");
            foreach (var card in new[] { producer, unrelated })
            {
                Set(card, "Status", Enum.Parse(Get(card, "Status").GetType(), "Done"));
                Set(card, "AgentResult", Get(card, "Id") + " result");
            }
            ((IList)Get(_card, "DependsOnTaskIds")).Add("producer");
            ((IList)Get(_card, "DependsOnTaskIds")).Add("unfinished");
            string context = JsonConvert.SerializeObject(Call("MemoryDependencyHandoffs", _card));
            Assert.That(context, Does.Contain("producer result"));
            Assert.That(context, Does.Not.Contain("unrelated result"));
            Assert.That(context, Does.Not.Contain("unfinished"));
            Assert.That(context, Does.Contain("notes never satisfy acceptance"));
        }

        [Test]
        public void MemoryCapabilitiesAreScopedAndAvailableInBothWorkerPhases()
        {
            var runner = SessionType.Assembly.GetType("AIRuntime.Editor.AIRuntimeKanbanAgentRunner");
            foreach (bool planning in new[] { true, false })
            {
                var names = (IEnumerable<string>)runner.GetMethod("ResolveKanbanCapabilitiesForRun", Flags).Invoke(null, new object[] { planning });
                CollectionAssert.Contains(names, "save_kanban_checkpoint");
                CollectionAssert.Contains(names, "read_kanban_memory");
            }
            Assert.IsNotNull(new SaveKanbanCheckpointCapability().Parameters["properties"]["nextAction"]);
            Assert.IsNotNull(new ReadKanbanMemoryCapability().Parameters["properties"]["attemptId"]);
            Assert.IsNull(new ReadKanbanMemoryCapability().Parameters["properties"]["taskId"], "Workers cannot select another card.");
        }

        private sealed class MemoryServices : IKanbanCardMemoryServices
        {
            public Func<SaveKanbanCheckpointArguments, object> Save;
            public Func<ReadKanbanMemoryArguments, object> Read;
            public object SaveCheckpoint(SaveKanbanCheckpointArguments args) => Save(args);
            public object ReadMemory(ReadKanbanMemoryArguments args) => Read(args);
        }

        [Test]
        public void CapabilityExecutionReturnsDurableReceiptAndRejectsCancellation()
        {
            Start();
            var services = new MemoryServices
            {
                Save = args => Call("SaveCardCheckpoint", "memory-card", _attempt, args),
                Read = args => Call("ReadCardMemory", "memory-card", _attempt, args)
            };
            ICapability save = new SaveKanbanCheckpointCapability(services);
            var receipt = JObject.FromObject(save.ExecuteObjectAsync(new SaveKanbanCheckpointArguments { NextAction = "Verify scene wiring." },
                new CapabilityContext { Capability = save }).GetAwaiter().GetResult());
            Assert.AreEqual((int)Saved["Revision"], (int)receipt["BoardRevision"]);
            ICapability read = new ReadKanbanMemoryCapability(services);
            var history = JObject.FromObject(read.ExecuteObjectAsync(new ReadKanbanMemoryArguments(), new CapabilityContext { Capability = read }).GetAwaiter().GetResult());
            Assert.AreEqual(_attempt, (string)history["Attempts"][0]["Id"]);
            int revision = (int)Saved["Revision"];
            var cancelled = save.ExecuteObjectAsync(new SaveKanbanCheckpointArguments { NextAction = "must not save" },
                new CapabilityContext { Capability = save, CancellationToken = new CancellationToken(true) });
            Assert.IsTrue(cancelled.IsCanceled);
            Assert.AreEqual(revision, (int)Saved["Revision"]);
        }

        [Test]
        public void WorkflowMemoryInvalidationCannotBeUndoneByTerminalHandoff()
        {
            Start(); Checkpoint();
            Call("ResetCardMemory", _card);
            var completion = Activator.CreateInstance(SessionType.GetNestedType("PendingAgentCompletion", Flags));
            Set(completion, "AttemptId", _attempt); Set(completion, "AgentId", "Worker");
            Set(completion, "Result", "Old planning phase completed.");
            Call("FinishMemoryAttempt", _card, completion);
            Call("SynchronizeMemoryAttempts");
            Assert.IsNull(Memory.Checkpoint);
            Assert.AreEqual("Reset", Memory.Attempts[0].Outcome);
        }

        [Test]
        public void LoadedCheckpointFromArchivedGenerationIsExcludedAndRepairedOnSave()
        {
            Start(); Checkpoint();
            Memory.Generation++;
            Assert.That(JsonConvert.SerializeObject(Call("MemoryContext", _card)), Does.Not.Contain("physics movement"));
            Call("SaveBoard", false);
            Assert.IsNull(Memory.Checkpoint);
            Assert.That(Saved["Tasks"][0]["Memory"]["Checkpoint"]?["Decisions"]?.Value<string>(), Is.Null.Or.Empty);
            Assert.AreEqual(1, Memory.Attempts.Count, "Repair retains archived observations.");
        }

        [UnityTest]
        public IEnumerator LiveWorkerCheckpointAndCompletionAreSavedWithoutExternalProvider()
        {
            var done = new TaskCompletionSource<string>();
            Set(_session, "RunnerOverrideForTests", new Func<string, CancellationToken, Task<string>>((id, token) =>
            {
                _attempt = (string)Get(Get(_card, "Dispatch"), "Id");
                Event("created movement script", AgentEventType.ToolCompleted, "write_file");
                Checkpoint();
                token.Register(() => done.TrySetCanceled());
                return done.Task;
            }));
            Assert.IsTrue((bool)Call("StartAgentTask", _card, "Worker"));
            Assert.IsNotNull(Saved["Tasks"][0]["Memory"]["Checkpoint"]);
            done.SetResult("Architecture inspection completed.");
            double deadline = EditorApplication.timeSinceStartup + 10;
            while (Memory.Attempts[0].Outcome == "Running" && EditorApplication.timeSinceStartup < deadline)
            {
                Call("DrainPendingAgentCompletions");
                yield return null;
            }
            Assert.AreEqual("Completed", Memory.Attempts[0].Outcome);
            Assert.AreEqual("Review", Memory.Attempts[0].BoardStatus);
            Assert.That(Memory.Attempts[0].Handoff.Decisions, Does.Contain("physics movement"));
            Assert.AreEqual("Completed", (string)Saved["Tasks"][0]["Memory"]["Attempts"][0]["Outcome"]);
            string evidence = Path.GetFullPath("Logs/memory-verification/live-memory.json");
            Directory.CreateDirectory(Path.GetDirectoryName(evidence));
            File.WriteAllText(evidence, Saved.ToString());
        }
    }
}
