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

using AIRuntime.Runtime.Models;
using AIRuntime.Runtime.Providers;
using AIRuntime.Runtime.Providers.Models;
using AIRuntime.Runtime.Providers.NousPortal;
using AIRuntime.Runtime.Providers.Nvidia;
using AIRuntime.Runtime.Providers.OpenAI;
using AIRuntime.Runtime.Providers.OpenRouter;

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

using NUnit.Framework;


namespace AIRuntime.Tests
{
    public sealed class OpenAICompatibleProviderTests
    {
        private const string ResponseJson =
            "{\"choices\":[{\"message\":{\"role\":\"assistant\",\"content\":\"Done.\"}}]}";


        [Test]
        public void NamedProvidersExposeStableIdentity()
        {
            FakeClient client =
                new FakeClient(
                    ResponseJson);


            Assert.AreEqual(
                "Nous Portal",
                new NousPortalProvider(
                    client).Name);

            Assert.AreEqual(
                "OpenRouter",
                new OpenRouterProvider(
                    client).Name);

            Assert.AreEqual(
                "NVIDIA Developer",
                new NvidiaDeveloperProvider(
                    client).Name);
        }


        [Test]
        public void NamedProvidersExposeOfficialBaseUrls()
        {
            Assert.AreEqual(
                "https://inference-api.nousresearch.com/v1",
                NousPortalProvider.DefaultBaseUrl);

            Assert.AreEqual(
                "https://openrouter.ai/api/v1",
                OpenRouterProvider.DefaultBaseUrl);

            Assert.AreEqual(
                "https://integrate.api.nvidia.com/v1",
                NvidiaDeveloperProvider.DefaultBaseUrl);
        }


        [Test]
        public void CompatibleProviderUsesMaxTokensField()
        {
            FakeClient client =
                new FakeClient(
                    ResponseJson);

            OpenAICompatibleProvider provider =
                new OpenAICompatibleProvider(
                    "Compatible API",
                    client);


            ChatCompletion completion =
                provider.ChatAsync(
                        new List<ConversationMessage>
                        {
                            new ConversationMessage
                            {
                                Role =
                                    "user",

                                Content =
                                    "Hello"
                            }
                        },
                        new List<ProviderTool>(),
                        new ProviderRequestOptions(
                            0.4,
                            2048))
                    .GetAwaiter()
                    .GetResult();


            JObject request =
                JObject.Parse(
                    client.LastRequestJson);


            Assert.AreEqual(
                2048,
                (int)request["max_tokens"]);

            Assert.IsNull(
                request["max_completion_tokens"]);

            Assert.AreEqual(
                "Done.",
                completion.Content);
        }


        [Test]
        public void NousProviderAddsPortalAttributionTags()
        {
            FakeClient client =
                new FakeClient(
                    ResponseJson);

            NousPortalProvider provider =
                new NousPortalProvider(
                    client);


            provider.ChatAsync(
                    new List<ConversationMessage>
                    {
                        new ConversationMessage
                        {
                            Role = "user",
                            Content = "Hello"
                        }
                    },
                    new List<ProviderTool>())
                .GetAwaiter()
                .GetResult();


            JObject request =
                JObject.Parse(
                    client.LastRequestJson);

            CollectionAssert.AreEqual(
                new[]
                {
                    NousPortalProvider.ProductTag,
                    NousPortalProvider.ClientTag,
                    NousPortalProvider.UserTag
                },
                request["tags"].ToObject<string[]>());
        }


        [Test]
        public void NousProviderDeclaresPortalStreamTerminationPolicy()
        {
            Assert.AreSame(
                ProviderStreamTerminationPolicy.NousPortal,
                NousPortalProvider.StreamTerminationPolicy);
        }


        [Test]
        public void GenericCompatibleProviderOmitsPortalTags()
        {
            FakeClient client =
                new FakeClient(
                    ResponseJson);

            OpenAICompatibleProvider provider =
                new OpenAICompatibleProvider(
                    "Compatible API",
                    client);


            provider.ChatAsync(
                    new List<ConversationMessage>(),
                    new List<ProviderTool>())
                .GetAwaiter()
                .GetResult();


            JObject request =
                JObject.Parse(
                    client.LastRequestJson);

            Assert.IsNull(
                request["tags"]);
        }


        [Test]
        public void NvidiaProviderRejectsTemperatureAboveApiLimit()
        {
            NvidiaDeveloperProvider provider =
                new NvidiaDeveloperProvider(
                    new FakeClient(
                        ResponseJson));


            Assert.Throws<System.ArgumentOutOfRangeException>(
                () => provider.ChatAsync(
                        new List<ConversationMessage>(),
                        new List<ProviderTool>(),
                        new ProviderRequestOptions(
                            1.1,
                            2048))
                    .GetAwaiter()
                    .GetResult());
        }


        [Test]
        public void NvidiaProviderDisablesThinkingForToolCalls()
        {
            FakeClient client =
                new FakeClient(
                    ResponseJson);

            NvidiaDeveloperProvider provider =
                new NvidiaDeveloperProvider(
                    client);

            provider.ChatAsync(
                    new List<ConversationMessage>(),
                    new List<ProviderTool>
                    {
                        new ProviderTool
                        {
                            Type =
                                "function",

                            Function =
                                new ProviderToolFunction
                                {
                                    Name =
                                        "list_directory",

                                    Parameters =
                                        new JObject()
                                }
                        }
                    })
                .GetAwaiter()
                .GetResult();

            JObject request =
                JObject.Parse(
                    client.LastRequestJson);

            Assert.AreEqual(
                "auto",
                (string)request["tool_choice"]);

            Assert.IsFalse(
                (bool)request["chat_template_kwargs"]["enable_thinking"]);
        }


        [Test]
        public void NvidiaProviderLeavesThinkingSettingsUnsetWithoutTools()
        {
            FakeClient client =
                new FakeClient(
                    ResponseJson);

            NvidiaDeveloperProvider provider =
                new NvidiaDeveloperProvider(
                    client);

            provider.ChatAsync(
                    new List<ConversationMessage>(),
                    new List<ProviderTool>())
                .GetAwaiter()
                .GetResult();

            JObject request =
                JObject.Parse(
                    client.LastRequestJson);

            Assert.IsNull(
                request["tool_choice"]);

            Assert.IsNull(
                request["chat_template_kwargs"]);
        }


        [Test]
        public void NvidiaProviderConvertsContentInvocationToToolCall()
        {
            const string response =
                "{\"choices\":[{\"message\":{\"role\":\"assistant\",\"content\":\"{\\\"invocation\\\":{\\\"name\\\":\\\"get_file_info\\\",\\\"arguments\\\":{\\\"path\\\":\\\"Assets/Runtime/Docs\\\"}}}\"}}]}";

            NvidiaDeveloperProvider provider =
                new NvidiaDeveloperProvider(
                    new FakeClient(
                        response));

            ChatCompletion completion =
                provider.ChatAsync(
                        new List<ConversationMessage>(),
                        new List<ProviderTool>
                        {
                            new ProviderTool
                            {
                                Type =
                                    "function",

                                Function =
                                    new ProviderToolFunction
                                    {
                                        Name =
                                            "get_file_info",

                                        Description =
                                            "Get file information",

                                        Parameters =
                                            new JObject()
                                    }
                            }
                        })
                    .GetAwaiter()
                    .GetResult();

            Assert.AreEqual(
                string.Empty,
                completion.Content);

            Assert.AreEqual(
                1,
                completion.ToolCalls.Count);

            Assert.AreEqual(
                "get_file_info",
                completion.ToolCalls[0].Name);

            Assert.AreEqual(
                "Assets/Runtime/Docs",
                (string)JObject.Parse(
                    completion.ToolCalls[0].Arguments)["path"]);
        }


        [Test]
        public void NousProviderConvertsTaggedContentInvocationsToToolCalls()
        {
            const string content =
                "<tool_calls:batch>" +
                "<tool_call:batch>create_child</arg_value:batch>" +
                "<arg_key:batch>title</arg_key:batch>" +
                "<arg_value:batch>Repair spatial layout</arg_value:batch>" +
                "</tool_call:batch>" +
                "<tool_call:batch>create_child<tool_sep:batch>" +
                "<arg_key:batch>title</arg_key:batch>" +
                "<arg_value:batch>Verify integrated behavior</arg_value:batch>" +
                "</tool_call:batch>" +
                "</tool_calls:batch>";

            string response =
                JsonConvert.SerializeObject(
                    new
                    {
                        choices = new[]
                        {
                            new
                            {
                                message = new
                                {
                                    role = "assistant",
                                    content
                                }
                            }
                        }
                    });

            NousPortalProvider provider =
                new NousPortalProvider(
                    new FakeClient(response));

            ChatCompletion completion =
                provider.ChatAsync(
                        new List<ConversationMessage>(),
                        new List<ProviderTool>
                        {
                            CreateTool("create_child")
                        })
                    .GetAwaiter()
                    .GetResult();

            Assert.AreEqual(2, completion.ToolCalls.Count);
            Assert.AreEqual("create_child", completion.ToolCalls[0].Name);
            Assert.AreEqual(
                "Repair spatial layout",
                (string)JObject.Parse(
                    completion.ToolCalls[0].Arguments)["title"]);
            Assert.AreEqual(
                "Verify integrated behavior",
                (string)JObject.Parse(
                    completion.ToolCalls[1].Arguments)["title"]);
            Assert.AreEqual(string.Empty, completion.Content);
        }


        [Test]
        public void TaggedContentCannotCallUnadvertisedTool()
        {
            const string content =
                "<tool_call:batch>delete_file<tool_sep:batch>" +
                "<arg_key:batch>path</arg_key:batch>" +
                "<arg_value:batch>Assets/file.txt</arg_value:batch>" +
                "</tool_call:batch>";

            string response =
                JsonConvert.SerializeObject(
                    new
                    {
                        choices = new[]
                        {
                            new
                            {
                                message = new
                                {
                                    role = "assistant",
                                    content
                                }
                            }
                        }
                    });

            NousPortalProvider provider =
                new NousPortalProvider(
                    new FakeClient(response));

            System.InvalidOperationException exception =
                Assert.Throws<System.InvalidOperationException>(
                    () => provider.ChatAsync(
                            new List<ConversationMessage>(),
                            new List<ProviderTool>
                            {
                                CreateTool("read_file")
                            })
                        .GetAwaiter()
                        .GetResult());

            StringAssert.Contains(
                "unavailable or unidentifiable tool",
                exception.Message);
        }


        [Test]
        public void TaggedContentWithNoOfferedTools_IsRejectedInsteadOfBecomingFinalText()
        {
            const string content =
                "I still need to inspect the directory." +
                "<tool_calls:batch>" +
                "<tool_call:batch>list_directory<tool_sep:batch>" +
                "<arg_key:batch>path</arg_key:batch>" +
                "<arg_value:batch>Assets/BallJumpGame/Scripts</arg_value:batch>" +
                "</tool_call:batch>" +
                "</tool_calls:batch>";

            string response =
                JsonConvert.SerializeObject(
                    new
                    {
                        choices = new[]
                        {
                            new
                            {
                                message = new
                                {
                                    role = "assistant",
                                    content
                                }
                            }
                        }
                    });

            NousPortalProvider provider =
                new NousPortalProvider(
                    new FakeClient(response));

            System.InvalidOperationException exception =
                Assert.Throws<System.InvalidOperationException>(
                    () => provider.ChatAsync(
                            new List<ConversationMessage>(),
                            new List<ProviderTool>())
                        .GetAwaiter()
                        .GetResult());

            StringAssert.Contains(
                "tagged tool calls when no tools were offered",
                exception.Message);
        }


        [Test]
        public void ContentInvocationCannotCallUnadvertisedTool()
        {
            const string response =
                "{\"choices\":[{\"message\":{\"role\":\"assistant\",\"content\":\"{\\\"invocation\\\":{\\\"name\\\":\\\"delete_file\\\",\\\"arguments\\\":{\\\"path\\\":\\\"Assets/file.txt\\\"}}}\"}}]}";

            NvidiaDeveloperProvider provider =
                new NvidiaDeveloperProvider(
                    new FakeClient(
                        response));

            System.InvalidOperationException exception =
                Assert.Throws<System.InvalidOperationException>(
                    () => provider.ChatAsync(
                            new List<ConversationMessage>(),
                            new List<ProviderTool>
                            {
                                new ProviderTool
                                {
                                    Type =
                                        "function",

                                    Function =
                                        new ProviderToolFunction
                                        {
                                            Name =
                                                "get_file_info",

                                            Parameters =
                                                new JObject()
                                        }
                                }
                            })
                        .GetAwaiter()
                        .GetResult());

            StringAssert.Contains(
                "unavailable tool 'delete_file'",
                exception.Message);
        }


        private static ProviderTool CreateTool(
            string name)
        {
            return new ProviderTool
            {
                Type = "function",
                Function = new ProviderToolFunction
                {
                    Name = name,
                    Parameters = new JObject()
                }
            };
        }


        private sealed class FakeClient :
            IOpenAIClient
        {
            private readonly string _responseJson;


            public FakeClient(
                string responseJson)
            {
                _responseJson =
                    responseJson;
            }


            public string Model =>
                "test-model";

            public string LastRequestJson
            {
                get;
                private set;
            }


            public Task<string> ChatAsync(
                string jsonBody,
                CancellationToken cancellationToken = default)
            {
                cancellationToken
                    .ThrowIfCancellationRequested();


                LastRequestJson =
                    jsonBody;


                return Task.FromResult(
                    _responseJson);
            }
        }
    }
}
