using System;
using Newtonsoft.Json.Linq;
using Xunit;

namespace GxMcp.Gateway.Tests
{
    public class OperationTrackerTests
    {
        [Fact]
        public void RegisterSpawnSample_ReturnsP50AndP95_AfterEnoughSamples()
        {
            var tracker = new OperationTracker(System.TimeSpan.FromMinutes(5));

            for (int i = 1; i <= 100; i++)
            {
                tracker.RegisterSpawnSample("test-kb", 100.0 + i);
            }

            var (count, p50, p95) = tracker.GetSpawnStats("test-kb");
            Assert.Equal(100, count);
            Assert.InRange(p50, 145, 155);
            Assert.InRange(p95, 190, 200);
        }

        [Fact]
        public void GetSpawnStats_ReturnsZeros_ForUnknownKb()
        {
            var tracker = new OperationTracker(System.TimeSpan.FromMinutes(5));
            var (count, p50, p95) = tracker.GetSpawnStats("never-seen");
            Assert.Equal(0, count);
            Assert.Equal(0, p50);
            Assert.Equal(0, p95);
        }

        [Fact]
        public void CompleteFromWorker_ShouldHandleArrayResultPayload()
        {
            var tracker = new OperationTracker(TimeSpan.FromMinutes(5));
            string requestId = Guid.NewGuid().ToString("N");
            string operationId = tracker.StartOperation(
                requestId,
                "genexus_list_objects",
                new JObject { ["limit"] = 20 },
                Guid.NewGuid().ToString("N"));

            var workerPayload = new JObject
            {
                ["id"] = requestId,
                ["result"] = new JArray(
                    new JObject
                    {
                        ["name"] = "ACADEMICOS",
                        ["type"] = "Folder"
                    })
            };

            tracker.CompleteFromWorker(requestId, workerPayload);
            JObject status = tracker.BuildOperationStatus(operationId);

            Assert.Equal("Completed", status["status"]?.ToString());
            Assert.False(status["timedOut"]?.Value<bool>() ?? true);
        }
        [Fact]
        public void BuildWorkerRpcRequest_IncludesMetaProgressToken_WhenOperationIdPresent()
        {
            var workerCommand = JObject.Parse(@"{
                ""module"": ""Build"",
                ""action"": ""Build"",
                ""target"": ""InvoiceProc""
            }");

            var method = typeof(GxMcp.Gateway.Program).GetMethod(
                "BuildWorkerRpcRequest",
                System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);

            Assert.NotNull(method);

            // Signature being added: BuildWorkerRpcRequest(JObject workerCommand, string requestId, string operationId = null)
            var built = (JObject)method!.Invoke(null, new object?[] { workerCommand, "req-1", "op-xyz" })!;

            Assert.Equal("op-xyz", built["_meta"]?["progressToken"]?.ToString());
            Assert.Equal("Build", built["method"]?.ToString());
        }

        // Wave-3 item 94 — heatmap block surfaces totalMs / percentOfSession / lastUsedAt.
        [Fact]
        public void BuildHeatmapBlock_RanksToolsByTotalMs_AndComputesPercent()
        {
            var tracker = new OperationTracker(TimeSpan.FromMinutes(5));
            tracker.RecordSyntheticCompletion("genexus_read", 100, isError: false);
            tracker.RecordSyntheticCompletion("genexus_read", 200, isError: false);
            tracker.RecordSyntheticCompletion("genexus_edit", 50, isError: false);

            var heat = tracker.BuildHeatmapBlock();

            Assert.Equal(2, heat.Count);
            Assert.Equal("genexus_read", heat[0]["tool"]?.ToString()); // higher totalMs first
            Assert.True(heat[0]["totalMs"]!.ToObject<long>() >= 300);
            Assert.True(heat[0]["percentOfSession"]!.ToObject<double>() > heat[1]["percentOfSession"]!.ToObject<double>());
            Assert.NotNull(heat[0]["lastUsedAt"]);
        }

        [Fact]
        public void BuildHeatmapBlock_Empty_WhenNoCompletionsRecorded()
        {
            var tracker = new OperationTracker(TimeSpan.FromMinutes(5));
            var heat = tracker.BuildHeatmapBlock();
            Assert.Empty(heat);
        }

        // Wave-3 item 36 — execution history filtered by target.
        [Fact]
        public void BuildExecutionHistory_FiltersByTarget_AndClampsLast()
        {
            var tracker = new OperationTracker(TimeSpan.FromMinutes(5));
            for (int i = 0; i < 60; i++)
            {
                tracker.RecordSyntheticCompletion("genexus_read", 10, isError: false,
                    new JObject { ["target"] = "InvoiceProc" });
            }
            tracker.RecordSyntheticCompletion("genexus_read", 10, isError: false,
                new JObject { ["target"] = "OtherObj" });

            JObject result = tracker.BuildExecutionHistory("InvoiceProc", last: 100);
            var runs = (JArray)result["runs"]!;
            Assert.Equal(50, runs.Count); // clamped to 50
            Assert.Equal(60, result["totalMatches"]?.ToObject<int>());
            foreach (var run in runs)
            {
                Assert.Equal("InvoiceProc", run["params"]?["target"]?.ToString());
                Assert.NotNull(run["outcome"]);
                Assert.NotNull(run["durationMs"]);
            }
        }

        [Fact]
        public void BuildExecutionHistory_ReturnsEmptyRuns_WhenTargetUnknown()
        {
            var tracker = new OperationTracker(TimeSpan.FromMinutes(5));
            tracker.RecordSyntheticCompletion("genexus_read", 10, isError: false,
                new JObject { ["target"] = "Existing" });

            JObject result = tracker.BuildExecutionHistory("Missing", last: 10);
            Assert.Empty((JArray)result["runs"]!);
            Assert.Equal(0, result["totalMatches"]?.ToObject<int>());
        }

        // Wave-3 item 35 — watch_event filters by target + event name in payload.
        [Fact]
        public void BuildWatchEvent_FiltersByTargetAndToolWhitelist()
        {
            var tracker = new OperationTracker(TimeSpan.FromMinutes(5));
            // matching tool + target + event substring in args
            tracker.RecordSyntheticCompletion("genexus_edit", 10, isError: false,
                new JObject { ["target"] = "InvoicePanel", ["body"] = "set OnClick = 'MyEvent'" });
            // matching tool but wrong target
            tracker.RecordSyntheticCompletion("genexus_edit", 10, isError: false,
                new JObject { ["target"] = "OtherPanel", ["body"] = "MyEvent" });
            // matching target but non-event tool
            tracker.RecordSyntheticCompletion("genexus_read", 10, isError: false,
                new JObject { ["target"] = "InvoicePanel", ["body"] = "MyEvent" });

            JObject result = tracker.BuildWatchEvent("InvoicePanel", "MyEvent", last: 50);
            var runs = (JArray)result["runs"]!;
            Assert.Single(runs);
            Assert.Equal("genexus_edit", runs[0]["tool"]?.ToString());
            Assert.Equal("MyEvent", result["event"]?.ToString());
        }

        [Fact]
        public void BuildWatchEvent_NoMatch_ReturnsEmptyRuns()
        {
            var tracker = new OperationTracker(TimeSpan.FromMinutes(5));
            tracker.RecordSyntheticCompletion("genexus_edit", 10, isError: false,
                new JObject { ["target"] = "Panel" });
            JObject result = tracker.BuildWatchEvent("Panel", "NotPresentEvent", last: 10);
            Assert.Empty((JArray)result["runs"]!);
            Assert.Equal(0, result["totalMatches"]?.ToObject<int>());
        }

        [Fact]
        public void BuildWatchEvent_ClampsLastTo50()
        {
            var tracker = new OperationTracker(TimeSpan.FromMinutes(5));
            for (int i = 0; i < 80; i++)
            {
                tracker.RecordSyntheticCompletion("genexus_edit", 5, isError: false,
                    new JObject { ["target"] = "Panel", ["body"] = "tick" });
            }
            JObject result = tracker.BuildWatchEvent("Panel", "tick", last: 100);
            Assert.Equal(50, ((JArray)result["runs"]!).Count);
            Assert.Equal(80, result["totalMatches"]?.ToObject<int>());
        }
    }
}

