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

using AIRuntime.Runtime.Models;
using AIRuntime.Runtime.Providers;
using AIRuntime.Runtime.Providers.Fallback;
using AIRuntime.Runtime.Providers.Models;

using NUnit.Framework;
using Newtonsoft.Json.Linq;


namespace AIRuntime.Tests
{
    public sealed class ProviderJournalTests
    {
        private string _directory;
        private string _previousDirectory;

        [SetUp]
        public void SetUp()
        {
            _previousDirectory = ProviderTransportDebug.JournalDirectory;
            _directory = Path.Combine(Path.GetTempPath(), "AIRuntimeProviderJournalTests", Guid.NewGuid().ToString("N"));
            ProviderTransportDebug.ConfigureFileLogging(_directory);
            ProviderTransportDebug.Clear();
        }

        [TearDown]
        public void TearDown()
        {
            ProviderTransportDebug.ConfigureFileLogging(_previousDirectory);
            ProviderTransportDebug.Clear();
            if (Directory.Exists(_directory)) Directory.Delete(_directory, true);
            if (File.Exists(_directory)) File.Delete(_directory);
        }

        private static object Invoke(string name, params object[] arguments) =>
            typeof(ProviderTransportDebug).GetMethod(name, BindingFlags.Static | BindingFlags.NonPublic)
                .Invoke(null, arguments);

        private JObject[] ReadJournal() => Directory.GetFiles(_directory, "*.jsonl")
            .SelectMany(File.ReadAllLines).Select(JObject.Parse).ToArray();

        [Test]
        public void JournalRetainsFullPayloadAndStatusWhileRedactingCredentials()
        {
            const string credential = "journal-test-secret-72804";
            Invoke("RegisterCredential", credential);
            string body = new string('x', 120000) + " full payload tail";
            JObject headers = (JObject)Invoke("RedactHeaders", new Dictionary<string, string> {
                ["Authorization"] = "Bearer " + credential, ["Set-Cookie"] = "session=private",
                ["x-request-id"] = "upstream-123"
            });
            using (ProviderTransportDebug.BeginAttempt("NVIDIA Developer", "session-camera", 2))
                Invoke("Publish", "NVIDIA Developer", "HTTP RESPONSE", "https://provider.test/chat",
                    body + credential, false, new JObject { ["StatusCode"] = 401, ["Headers"] = headers });

            JObject[] records = ReadJournal();
            JObject response = records.Single(record => (string)record["Direction"] == "HTTP RESPONSE");
            Assert.AreEqual(body + "[REDACTED CREDENTIAL]", (string)response["Payload"]);
            Assert.AreEqual(401, (int)response["Metadata"]["StatusCode"]);
            Assert.AreEqual("upstream-123", (string)response["Metadata"]["Headers"]["x-request-id"]);
            Assert.AreEqual("session-camera", (string)response["SessionId"]);
            Assert.AreEqual(2, (int)response["Attempt"]);
            Assert.AreEqual((string)records[0]["RequestId"], (string)response["RequestId"]);
            string persisted = string.Join("\n", records.Select(record => record.ToString()));
            StringAssert.DoesNotContain(credential, persisted);
            StringAssert.DoesNotContain("session=private", persisted);
            StringAssert.DoesNotContain(credential, ProviderTransportDebug.Snapshot().Last().Payload);
        }

        [Test]
        public void JournalSurvivesClearAndReconfigurationAndSeparatesRetryAttempts()
        {
            using (ProviderTransportDebug.BeginAttempt("Nous Portal", "same-session", 1))
                ProviderTransportDebug.WriteEvent("Nous Portal", "ATTEMPT FAILED", new { Kind = "EmptyResponse" });
            ProviderTransportDebug.Clear();
            ProviderTransportDebug.ConfigureFileLogging(_directory);
            using (ProviderTransportDebug.BeginAttempt("Nous Portal", "same-session", 2))
                ProviderTransportDebug.WriteEvent("Nous Portal", "ATTEMPT SUCCEEDED", new { Content = "Done" });
            JObject[] records = ReadJournal();
            Assert.AreEqual(4, records.Length);
            Assert.AreEqual(2, records.Select(record => (string)record["RequestId"]).Distinct().Count());
            Assert.AreEqual(1, records.Select(record => (string)record["SessionId"]).Distinct().Count());
            Assert.AreEqual("ATTEMPT FAILED", (string)records[1]["Direction"]);
            Assert.AreEqual(2, ProviderTransportDebug.Snapshot().Count);
        }

        [Test]
        public void StreamingJournalStoresEachDeltaOnceAndRecordsTermination()
        {
            string[] chunks = { "data: {\"choices\":[", "{\"delta\":{\"content\":\"hello\"}}]}\n\n", "data: [DONE]\n\n" };
            using (ProviderTransportDebug.BeginAttempt("NVIDIA Developer", "stream-session", 1))
            {
                object stream = Invoke("BeginStreamingResponse", "NVIDIA Developer", "https://provider.test/chat");
                foreach (string chunk in chunks) Invoke("AppendStreamingResponse", stream, chunk);
                Invoke("MarkStreamingResponseCompleted", stream, "[DONE]");
            }
            JObject[] records = ReadJournal();
            string[] persistedChunks = records.Where(record => (string)record["Direction"] == "STREAM CHUNK")
                .Select(record => (string)record["Payload"]).ToArray();
            CollectionAssert.AreEqual(chunks, persistedChunks);
            Assert.AreEqual("[DONE]", (string)records.Last()["StreamTerminationSignal"]);
            Assert.AreEqual(1, records.Select(record => (string)record["RequestId"]).Distinct().Count());
        }

        [Test]
        public void JournalWriteFailureDoesNotInterruptProviderAndIsVisible()
        {
            Directory.CreateDirectory(Path.GetDirectoryName(_directory));
            File.WriteAllText(_directory, "This file deliberately blocks directory creation.");
            UnityEngine.TestTools.LogAssert.Expect(UnityEngine.LogType.Warning,
                new System.Text.RegularExpressions.Regex("\\[ProviderTrace\\] Could not write diagnostics:"));
            Assert.DoesNotThrow(() => ProviderTransportDebug.WriteEvent("Provider", "TEST", new { Content = "kept" }));
            Assert.IsNotEmpty(ProviderTransportDebug.LastJournalError);
            Assert.AreEqual(1, ProviderTransportDebug.Snapshot().Count);
            Assert.DoesNotThrow(() => ProviderTransportDebug.WriteEvent("Provider", "TEST", new { Content = "still runs" }));
        }

        [Test]
        public void NestedAttemptsRestoreTheirParentCorrelation()
        {
            using (ProviderTransportDebug.BeginAttempt("outer", "session-a", 1))
            {
                using (ProviderTransportDebug.BeginAttempt("inner", "session-b", 1))
                    ProviderTransportDebug.WriteEvent("inner", "INNER", new { });
                ProviderTransportDebug.WriteEvent("outer", "OUTER", new { });
            }
            JObject[] records = ReadJournal();
            Assert.AreEqual((string)records[0]["RequestId"], (string)records.Last()["RequestId"]);
            Assert.AreNotEqual((string)records[0]["RequestId"], (string)records[1]["RequestId"]);
            Assert.AreEqual("session-a", (string)records.Last()["SessionId"]);
        }

        [Test]
        public void AgentEventsRetainFullToolResultsAndCardContextInSessionJournal()
        {
            string result = "compiler diagnostics\n" + new string('x', 20000) + "\nCS1061: missing TriggerHazardRespawn";
            var context = new AIRuntime.Editor.AIRuntimeEditorRuntimePluginContext(
                "Kanban", "DeveloperAgent", "Side-View Camera Controller", "Implementation", "card-camera");
            using (var observer = new AIRuntime.Editor.AIRuntimeAgentActivityObserver(
                new AIRuntime.Editor.AIRuntimeAgentActivityStore(), context))
                observer.OnAgentEvent(new AIRuntime.Runtime.Services.Agent.AgentEvent(
                    AIRuntime.Runtime.Services.Agent.AgentEventType.ToolCompleted, "session-camera",
                    "get_compilation_errors", result));
            JObject record = ReadJournal().Single();
            JObject payload = JObject.Parse((string)record["Payload"]);
            Assert.AreEqual("session-camera", (string)record["SessionId"]);
            Assert.AreEqual("card-camera", (string)payload["CorrelationId"]);
            Assert.AreEqual("DeveloperAgent", (string)payload["Agent"]);
            Assert.AreEqual(result, (string)payload["Message"]);
        }

        [UnityEngine.TestTools.UnityTest]
        public System.Collections.IEnumerator Http401IsJournaledBeforeThrowingWithAuthenticationPresenceOnly()
        {
            const string secret = "journal-local-http-test-credential";
            const string body = "{\"error\":{\"message\":\"Missing Authentication header\",\"code\":401}}";
            var listener = new System.Net.Sockets.TcpListener(System.Net.IPAddress.Loopback, 0);
            listener.Start();
            int port = ((System.Net.IPEndPoint)listener.LocalEndpoint).Port;
            Task server = Task.Run(async () => {
                using (var peer = await listener.AcceptTcpClientAsync())
                using (var stream = peer.GetStream())
                {
                    var received = new System.Text.StringBuilder();
                    var buffer = new byte[4096];
                    while (!received.ToString().Contains("\r\n\r\n{}"))
                    {
                        int count = await stream.ReadAsync(buffer, 0, buffer.Length);
                        if (count == 0) throw new IOException("Request ended before its body.");
                        received.Append(System.Text.Encoding.UTF8.GetString(buffer, 0, count));
                    }
                    byte[] response = System.Text.Encoding.UTF8.GetBytes(
                        "HTTP/1.1 401 Unauthorized\r\nContent-Type: application/json\r\nConnection: close\r\n" +
                        "x-request-id: local-401\r\nContent-Length: " + System.Text.Encoding.UTF8.GetByteCount(body) +
                        "\r\n\r\n" + body);
                    await stream.WriteAsync(response, 0, response.Length);
                }
            });
            using (var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
            {
                try
                {
                    var client = new AIRuntime.Runtime.Providers.OpenAI.OpenAIClient(secret,
                        baseUrl: "http://127.0.0.1:" + port, timeoutSeconds: 5);
                    Task<string> call = client.ChatAsync("{}", cancellation.Token);
                    while (!call.IsCompleted) yield return null;
                    Assert.Throws<ProviderHttpException>(() => call.GetAwaiter().GetResult());
                    while (!server.IsCompleted && !cancellation.IsCancellationRequested) yield return null;
                    Assert.IsTrue(server.IsCompleted, "Local HTTP fixture should finish.");
                    server.GetAwaiter().GetResult();
                    JObject[] records = ReadJournal();
                    JObject response = records.Single(record => (string)record["Direction"] == "HTTP RESPONSE");
                    Assert.AreEqual(body, (string)response["Payload"]);
                    Assert.AreEqual(401, (int)response["Metadata"]["StatusCode"]);
                    JObject request = records.Single(record => (string)record["Direction"] == "REQUEST");
                    Assert.AreEqual("[REDACTED; present]", (string)request["Metadata"]["Headers"]["Authorization"]);
                    Assert.AreEqual((string)request["RequestId"], (string)response["RequestId"]);
                    Assert.AreEqual("TRANSPORT END", (string)records.Last()["Direction"]);
                    StringAssert.DoesNotContain(secret, string.Join("\n", records.Select(record => record.ToString())));
                }
                finally { cancellation.Cancel(); listener.Stop(); }
            }
        }
    }

    public sealed class ProviderHttpExceptionTests
    {
        private readonly IProvider _provider =
            new TestProvider();


        [Test]
        public void ConstructorCapturesStructuredFailureDetails()
        {
            ProviderHttpException exception =
                new ProviderHttpException(
                    " OpenAI ",
                    429,
                    " Too Many Requests ",
                    " {\"error\":\"rate limited\"} ");


            Assert.AreEqual(
                "OpenAI",
                exception.ProviderName);

            Assert.AreEqual(
                429,
                exception.StatusCode);

            Assert.IsTrue(
                exception.HasStatusCode);

            Assert.AreEqual(
                " Too Many Requests ",
                exception.TransportError);

            Assert.AreEqual(
                " {\"error\":\"rate limited\"} ",
                exception.ResponseBody);

            StringAssert.Contains(
                "HTTP 429",
                exception.Message);
        }


        [Test]
        public void ConstructorCapturesRetryAfterWithoutLosingStructuredFailure()
        {
            ProviderHttpException exception =
                new ProviderHttpException(
                    "Nous Portal",
                    429,
                    "Too Many Requests",
                    "{\"status\":429}",
                    TimeSpan.FromSeconds(17.5));


            Assert.AreEqual(
                TimeSpan.FromSeconds(17.5),
                exception.RetryAfter);

            StringAssert.Contains(
                "Retry after 17.5 seconds.",
                exception.Message);
        }


        [TestCase(
            "data: [DONE]",
            ProviderStreamTerminationPolicy.DoneSentinelSignal)]
        [TestCase(
            "data: {\"done\":true}",
            "done")]
        public void StandardStreamPolicyRecognizesOpenAITerminalFrames(
            string frame,
            string expectedSignal)
        {
            bool terminal =
                ProviderStreamTerminationPolicy
                    .OpenAICompatible
                    .TryGetTerminalSignal(
                        frame,
                        out string signal);


            Assert.IsTrue(
                terminal);

            Assert.AreEqual(
                expectedSignal,
                signal);
        }


        [Test]
        public void NousStreamPolicyRecognizesPortalLastOneMarker()
        {
            bool terminal =
                ProviderStreamTerminationPolicy
                    .NousPortal
                    .TryGetTerminalSignal(
                        "data: {\"choices\":[],\"lastOne\":true,\"usage\":{\"total_tokens\":42}}",
                        out string signal);


            Assert.IsTrue(
                terminal);

            Assert.AreEqual(
                "lastOne",
                signal);
        }


        [Test]
        public void StandardStreamPolicyDoesNotAcceptNousLastOneMarker()
        {
            bool terminal =
                ProviderStreamTerminationPolicy
                    .OpenAICompatible
                    .TryGetTerminalSignal(
                        "data: {\"choices\":[],\"lastOne\":true}",
                        out string signal);


            Assert.IsFalse(
                terminal);

            Assert.IsNull(
                signal);
        }


        [Test]
        public void NousStreamPolicyKeepsFinishReasonNonTerminalUntilMetadataArrives()
        {
            bool terminal =
                ProviderStreamTerminationPolicy
                    .NousPortal
                    .TryGetTerminalSignal(
                        "data: {\"choices\":[{\"delta\":{},\"finish_reason\":\"stop\"}]}",
                        out string signal);


            Assert.IsFalse(
                terminal);

            Assert.IsNull(
                signal);
        }


        [TestCase(null)]
        [TestCase("")]
        [TestCase("   ")]
        public void ConstructorRejectsMissingProviderName(
            string providerName)
        {
            Assert.Throws<ArgumentException>(
                () => new ProviderHttpException(
                    providerName,
                    500,
                    string.Empty,
                    string.Empty));
        }


        [Test]
        public void ConstructorRejectsNegativeStatusCode()
        {
            Assert.Throws<ArgumentOutOfRangeException>(
                () => new ProviderHttpException(
                    "Provider",
                    -1,
                    string.Empty,
                    string.Empty));
        }


        [TestCase(
            400,
            ProviderFailureKind.InvalidRequest,
            false)]
        [TestCase(
            401,
            ProviderFailureKind.Authentication,
            true)]
        [TestCase(
            403,
            ProviderFailureKind.Authentication,
            true)]
        [TestCase(
            404,
            ProviderFailureKind.ModelUnavailable,
            true)]
        [TestCase(
            408,
            ProviderFailureKind.Timeout,
            true)]
        [TestCase(
            422,
            ProviderFailureKind.InvalidRequest,
            false)]
        [TestCase(
            429,
            ProviderFailureKind.RateLimited,
            true)]
        [TestCase(
            500,
            ProviderFailureKind.Unavailable,
            true)]
        [TestCase(
            503,
            ProviderFailureKind.Unavailable,
            true)]
        [TestCase(
            504,
            ProviderFailureKind.Timeout,
            true)]
        public void ClassifierUsesStructuredHttpStatus(
            long statusCode,
            ProviderFailureKind expectedKind,
            bool expectedFallback)
        {
            ProviderFailure failure =
                Classify(
                    statusCode,
                    "A deliberately unhelpful transport message.");


            Assert.AreEqual(
                expectedKind,
                failure.Kind);

            Assert.AreEqual(
                expectedFallback,
                failure.ShouldFallback);
        }


        [Test]
        public void ClassifierTreatsMissingHttpResponseAsUnavailable()
        {
            ProviderFailure failure =
                Classify(
                    0,
                    "Cannot connect to destination host");


            Assert.AreEqual(
                ProviderFailureKind.Unavailable,
                failure.Kind);

            Assert.IsTrue(
                failure.ShouldFallback);
        }


        [Test]
        public void ClassifierTreatsFailedSuccessfulStatusAsInvalidResponse()
        {
            ProviderFailure failure =
                Classify(
                    200,
                    "Response could not be processed.");


            Assert.AreEqual(
                ProviderFailureKind.InvalidResponse,
                failure.Kind);

            Assert.IsTrue(
                failure.ShouldFallback);
        }


        [Test]
        public void ClassifierTreatsEmptyAssistantPayloadAsRecoverableEmptyResponse()
        {
            var classifier =
                new DefaultProviderFailureClassifier();

            ProviderFailure failure =
                classifier.Classify(
                    _provider,
                    new ProviderEmptyResponseException(
                        "Provider"));

            Assert.AreEqual(
                ProviderFailureKind.EmptyResponse,
                failure.Kind);
            Assert.IsTrue(
                failure.ShouldFallback);
        }


        [Test]
        public void ClassifierUnwrapsStructuredAggregateFailure()
        {
            var classifier =
                new DefaultProviderFailureClassifier();

            var exception =
                new AggregateException(
                    new ProviderHttpException(
                        "Provider",
                        503,
                        "Unavailable",
                        string.Empty));


            ProviderFailure failure =
                classifier.Classify(
                    _provider,
                    exception);


            Assert.AreEqual(
                ProviderFailureKind.Unavailable,
                failure.Kind);

            Assert.IsInstanceOf<ProviderHttpException>(
                failure.Exception);
        }


        private ProviderFailure Classify(
            long statusCode,
            string transportError)
        {
            var classifier =
                new DefaultProviderFailureClassifier();


            return classifier.Classify(
                _provider,
                new ProviderHttpException(
                    "Provider",
                    statusCode,
                    transportError,
                    "{\"error\":\"request failed\"}"));
        }


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


            public Task<ChatCompletion> ChatAsync(
                IReadOnlyList<ConversationMessage> messages,
                IReadOnlyList<ProviderTool> tools,
                CancellationToken cancellationToken = default)
            {
                throw new NotSupportedException();
            }
        }
    }
}
