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.Capabilities.Permissions;
using AIRuntime.Runtime.Core;
using AIRuntime.Runtime.Filesystem;
using AIRuntime.Runtime.Models;
using AIRuntime.Runtime.Providers;
using AIRuntime.Runtime.Providers.Fallback;
using AIRuntime.Runtime.Providers.Health;
using AIRuntime.Runtime.Providers.Models;
using AIRuntime.Runtime.Providers.OpenAI;
using AIRuntime.Runtime.Providers.Ollama;
using AIRuntime.Runtime.Providers.Nvidia;
using AIRuntime.Runtime.Services.Agent;
using NUnit.Framework;
using UnityEngine.TestTools;

namespace AIRuntime.Tests
{
    public sealed class ProviderInputGuardTests
    {
        private sealed class Client : IOpenAIClient, IOllamaClient
        {
            public string Model => "text-model";
            public int Calls;
            public Task<string> ChatAsync(string jsonBody, CancellationToken cancellationToken = default)
            {
                Calls++;
                return Task.FromResult("{\"choices\":[{\"message\":{\"content\":\"OK\"}}]}");
            }
        }

        private sealed class VisionProvider : IProvider, IProviderInputCapabilities
        {
            public string Name => "Vision";
            public string Model => "configured-vision-model";
            public bool SupportsImageInput => true;
            public int Calls;
            public IReadOnlyList<ConversationMessage> Received;
            public Task<ChatCompletion> ChatAsync(IReadOnlyList<ConversationMessage> messages,
                IReadOnlyList<ProviderTool> tools, CancellationToken cancellationToken = default)
            {
                Calls++;
                Received = messages;
                return Task.FromResult(new ChatCompletion { Content = "Received image" });
            }
        }

        private static Conversation ConversationWithImage()
        {
            var conversation = new Conversation();
            conversation.AddCapabilityResult("capture-1", "capture_game_view",
                "{\"Artifacts\":[{\"Kind\":\"Image\",\"Path\":\"frame.png\",\"MediaType\":\"image/png\"}]}");
            return conversation;
        }

        [TestCase("OpenAI")]
        [TestCase("NVIDIA")]
        [TestCase("Ollama")]
        public void DirectAdapterBlocksImageBeforeTransport(string adapter)
        {
            var client = new Client();
            IProvider provider = adapter == "Ollama" ? (IProvider)new OllamaProvider(client) :
                adapter == "NVIDIA" ? new NvidiaDeveloperProvider(client) : new OpenAIProvider(client);
            var error = Assert.Throws<ProviderInputNotSupportedException>(() => provider.ChatAsync(
                ConversationWithImage().Messages, new List<ProviderTool>()).GetAwaiter().GetResult());
            Assert.AreEqual(0, client.Calls);
            StringAssert.Contains("text-model", error.Message);
            StringAssert.Contains("before sending", error.Message);
        }

        [Test]
        public void TextOnlyRequestsStillReachTransport()
        {
            var client = new Client();
            new OpenAIProvider(client).ChatAsync(new List<ConversationMessage>
                { new ConversationMessage { Role = "user", Content = "Hello" } },
                new List<ProviderTool>()).GetAwaiter().GetResult();
            Assert.AreEqual(1, client.Calls);
        }

        [TestCase(false)]
        [TestCase(true)]
        public void EditorFactoryAppliesExplicitImageSetting(bool enabled)
        {
            foreach (string id in new[] { "OpenAI", "NvidiaDeveloper", "Ollama", "OpenRouter", "NousPortal" })
            {
                var plugin = AIRuntimeEditorPluginCatalog.GetProviderPlugin(id);
                var provider = plugin.CreateProvider(new AIRuntimeEditorProviderSettings {
                    Model = "test-model", ApiKey = "unused", BaseUrl = plugin.DefaultBaseUrl,
                    SupportsImageInput = enabled });
                Assert.AreEqual(enabled, ((IProviderInputCapabilities)provider).SupportsImageInput, id);
            }
        }

        [Test]
        public void IncompatibleInputAllowsFallbackWithoutRetryOrCooldown()
        {
            var provider = new OpenAIProvider(new Client());
            var failure = new DefaultProviderFailureClassifier().Classify(provider,
                new ProviderInputNotSupportedException("Image input blocked"));
            Assert.AreEqual(ProviderFailureKind.UnsupportedInput, failure.Kind);
            Assert.IsTrue(failure.ShouldFallback);
            Assert.IsFalse(new ProviderRetryPolicy().ShouldRetry(failure.Kind));
            Assert.IsFalse(new ProviderCooldownPolicy().CountsTowardsCooldown(failure.Kind));
        }

        [UnityTest]
        public IEnumerator FallbackSkipsTextModelsAndPreservesImageEvidence()
        {
            var first = new Client();
            var second = new Client();
            var vision = new VisionProvider();
            var chain = new ProviderFallbackChain();
            chain.SetPrimaryProvider(new OpenAIProvider(first));
            chain.AddFallbackProvider(new NvidiaDeveloperProvider(second));
            chain.AddFallbackProvider(vision);
            Task<ChatCompletion> task = Request(chain);
            yield return Wait(task);
            Assert.IsFalse(task.IsFaulted, task.Exception?.ToString());
            Assert.AreEqual(0, first.Calls);
            Assert.AreEqual(0, second.Calls);
            Assert.AreEqual(1, vision.Calls);
            Assert.IsTrue(vision.Received.Any(message => message.Attachments?.Any(a => a.Path == "frame.png") == true));
        }

        [UnityTest]
        public IEnumerator AllTextModelsStopWithoutTransportCalls()
        {
            var first = new Client();
            var second = new Client();
            var chain = new ProviderFallbackChain();
            chain.SetPrimaryProvider(new OpenAIProvider(first));
            chain.AddFallbackProvider(new OllamaProvider(second));
            Task<ChatCompletion> task = Request(chain);
            yield return Wait(task);
            Assert.IsTrue(task.IsFaulted);
            Assert.IsInstanceOf<ProviderInputNotSupportedException>(task.Exception.GetBaseException());
            StringAssert.Contains("Visual verification blocked", task.Exception.GetBaseException().Message);
            Assert.AreEqual(0, first.Calls + second.Calls);
        }

        [Test]
        public void UnsupportedInputCategorySurvivesRunBoundary()
        {
            var method = typeof(ConversationEngine).GetMethod("ResolveProviderFailureKind",
                BindingFlags.Static | BindingFlags.NonPublic);
            Assert.AreEqual(ProviderFailureKind.UnsupportedInput,
                method.Invoke(null, new object[] { new ProviderInputNotSupportedException("Image input blocked") }));
        }

        [Test]
        public void ImageSettingChangesRuntimeSignature()
        {
            Type consoleType = typeof(AIRuntimeEditorPluginCatalog).Assembly.GetType("AIRuntime.Editor.AIRuntimeChatConsole");
            var window = UnityEngine.ScriptableObject.CreateInstance(consoleType);
            try
            {
                var flag = consoleType.GetField("_supportsImageInput", BindingFlags.Instance | BindingFlags.NonPublic);
                var signature = consoleType.GetMethod("BuildRuntimeSignature", BindingFlags.Instance | BindingFlags.NonPublic);
                flag.SetValue(window, false);
                object before = signature.Invoke(window, null);
                flag.SetValue(window, true);
                Assert.AreNotEqual(before, signature.Invoke(window, null));
            }
            finally { UnityEngine.Object.DestroyImmediate(window); }
        }
        private static Task<ChatCompletion> Request(ProviderFallbackChain chain)
        {
            var registry = new CapabilityRegistry();
            var engine = new ConversationEngine(chain, new ProviderRetryPolicy(),
                new ProviderHealthTracker(new ProviderCooldownPolicy()), registry,
                new CapabilityDispatcher(registry, new LocalFileSystem(new Workspace(Directory.GetCurrentDirectory())),
                    new CapabilityPermissionPolicy()), new AgentObserverCollection());
            return (Task<ChatCompletion>)typeof(ConversationEngine)
                .GetMethod("RequestProviderCompletionAsync", BindingFlags.Instance | BindingFlags.NonPublic)
                .Invoke(engine, new object[] { ConversationWithImage(), new List<ProviderTool>(),
                    ProviderRequestOptions.Default, CancellationToken.None });
        }

        private static IEnumerator Wait(Task task)
        {
            DateTime deadline = DateTime.UtcNow.AddSeconds(10);
            while (!task.IsCompleted && DateTime.UtcNow < deadline) yield return null;
            Assert.IsTrue(task.IsCompleted, "Provider request exceeded the bounded test timeout.");
        }
    }
}
