// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using OpenAI.Responses;

namespace Microsoft.Agents.AI.OpenAI.UnitTests.Extensions;

/// <summary>
/// Unit tests for the <see cref="OpenAIResponseClientExtensions"/> class.
/// </summary>
public sealed class OpenAIResponseClientExtensionsTests
{
    /// <summary>
    /// Test custom chat client that can be used to verify clientFactory functionality.
    /// </summary>
    private sealed class TestChatClient : IChatClient
    {
        private readonly IChatClient _innerClient;

        public TestChatClient(IChatClient innerClient)
        {
            this._innerClient = innerClient;
        }

        public Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
            => this._innerClient.GetResponseAsync(messages, options, cancellationToken);

        public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
            IEnumerable<ChatMessage> messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
        {
            await foreach (var update in this._innerClient.GetStreamingResponseAsync(messages, options, cancellationToken))
            {
                yield return update;
            }
        }

        public object? GetService(Type serviceType, object? serviceKey = null)
        {
            // Return this instance when requested
            if (serviceType == typeof(TestChatClient))
            {
                return this;
            }

            return this._innerClient.GetService(serviceType, serviceKey);
        }

        public void Dispose() => this._innerClient.Dispose();
    }

    /// <summary>
    /// Creates a test ResponsesClient implementation for testing.
    /// </summary>
    private sealed class TestOpenAIResponseClient : ResponsesClient
    {
        public TestOpenAIResponseClient()
        {
        }
    }

    /// <summary>
    /// Verify that CreateAIAgent with clientFactory parameter correctly applies the factory.
    /// </summary>
    [Fact]
    public void CreateAIAgent_WithClientFactory_AppliesFactoryCorrectly()
    {
        // Arrange
        var responseClient = new TestOpenAIResponseClient();
        var testChatClient = new TestChatClient(responseClient.AsIChatClient());

        // Act
        var agent = responseClient.AsAIAgent(
            instructions: "Test instructions",
            name: "Test Agent",
            description: "Test description",
            clientFactory: (innerClient) => testChatClient);

        // Assert
        Assert.NotNull(agent);
        Assert.Equal("Test Agent", agent.Name);
        Assert.Equal("Test description", agent.Description);

        // Verify that the custom chat client can be retrieved from the agent's service collection
        var retrievedTestClient = agent.GetService<TestChatClient>();
        Assert.NotNull(retrievedTestClient);
        Assert.Same(testChatClient, retrievedTestClient);
    }

    /// <summary>
    /// Verify that CreateAIAgent without clientFactory works normally.
    /// </summary>
    [Fact]
    public void CreateAIAgent_WithoutClientFactory_WorksNormally()
    {
        // Arrange
        var responseClient = new TestOpenAIResponseClient();

        // Act
        var agent = responseClient.AsAIAgent(
            instructions: "Test instructions",
            name: "Test Agent");

        // Assert
        Assert.NotNull(agent);
        Assert.Equal("Test Agent", agent.Name);

        // Verify that no TestChatClient is available since no factory was provided
        var retrievedTestClient = agent.GetService<TestChatClient>();
        Assert.Null(retrievedTestClient);
    }

    /// <summary>
    /// Verify that CreateAIAgent with null clientFactory works normally.
    /// </summary>
    [Fact]
    public void CreateAIAgent_WithNullClientFactory_WorksNormally()
    {
        // Arrange
        var responseClient = new TestOpenAIResponseClient();

        // Act
        var agent = responseClient.AsAIAgent(
            instructions: "Test instructions",
            name: "Test Agent",
            clientFactory: null);

        // Assert
        Assert.NotNull(agent);
        Assert.Equal("Test Agent", agent.Name);

        // Verify that no TestChatClient is available since no factory was provided
        var retrievedTestClient = agent.GetService<TestChatClient>();
        Assert.Null(retrievedTestClient);
    }

    /// <summary>
    /// Verify that CreateAIAgent throws ArgumentNullException when client is null.
    /// </summary>
    [Fact]
    public void CreateAIAgent_WithNullClient_ThrowsArgumentNullException()
    {
        // Act & Assert
        var exception = Assert.Throws<ArgumentNullException>(() =>
            ((ResponsesClient)null!).AsAIAgent());

        Assert.Equal("client", exception.ParamName);
    }

    /// <summary>
    /// Verify that CreateAIAgent with options throws ArgumentNullException when options is null.
    /// </summary>
    [Fact]
    public void CreateAIAgent_WithNullOptions_ThrowsArgumentNullException()
    {
        // Arrange
        var responseClient = new TestOpenAIResponseClient();

        // Act & Assert
        var exception = Assert.Throws<ArgumentNullException>(() =>
            responseClient.AsAIAgent((ChatClientAgentOptions)null!));

        Assert.Equal("options", exception.ParamName);
    }

    /// <summary>
    /// Verify that CreateAIAgent with services parameter correctly passes it through to the ChatClientAgent.
    /// </summary>
    [Fact]
    public void CreateAIAgent_WithServices_PassesServicesToAgent()
    {
        // Arrange
        var responseClient = new TestOpenAIResponseClient();
        var serviceProvider = new TestServiceProvider();

        // Act
        var agent = responseClient.AsAIAgent(
            instructions: "Test instructions",
            name: "Test Agent",
            services: serviceProvider);

        // Assert
        Assert.NotNull(agent);

        // Verify the IServiceProvider was passed through to the FunctionInvokingChatClient
        var chatClient = agent.GetService<IChatClient>();
        Assert.NotNull(chatClient);
        var functionInvokingClient = chatClient.GetService<FunctionInvokingChatClient>();
        Assert.NotNull(functionInvokingClient);
        Assert.Same(serviceProvider, GetFunctionInvocationServices(functionInvokingClient));
    }

    /// <summary>
    /// Verify that CreateAIAgent with options and services parameter correctly passes it through to the ChatClientAgent.
    /// </summary>
    [Fact]
    public void CreateAIAgent_WithOptionsAndServices_PassesServicesToAgent()
    {
        // Arrange
        var responseClient = new TestOpenAIResponseClient();
        var serviceProvider = new TestServiceProvider();
        var options = new ChatClientAgentOptions
        {
            Name = "Test Agent",
            ChatOptions = new() { Instructions = "Test instructions" }
        };

        // Act
        var agent = responseClient.AsAIAgent(options, services: serviceProvider);

        // Assert
        Assert.NotNull(agent);
        Assert.Equal("Test Agent", agent.Name);

        // Verify the IServiceProvider was passed through to the FunctionInvokingChatClient
        var chatClient = agent.GetService<IChatClient>();
        Assert.NotNull(chatClient);
        var functionInvokingClient = chatClient.GetService<FunctionInvokingChatClient>();
        Assert.NotNull(functionInvokingClient);
        Assert.Same(serviceProvider, GetFunctionInvocationServices(functionInvokingClient));
    }

    /// <summary>
    /// Verify that CreateAIAgent with both clientFactory and services works correctly.
    /// </summary>
    [Fact]
    public void CreateAIAgent_WithClientFactoryAndServices_AppliesBothCorrectly()
    {
        // Arrange
        var responseClient = new TestOpenAIResponseClient();
        var serviceProvider = new TestServiceProvider();
        var testChatClient = new TestChatClient(responseClient.AsIChatClient());

        // Act
        var agent = responseClient.AsAIAgent(
            instructions: "Test instructions",
            name: "Test Agent",
            clientFactory: (innerClient) => testChatClient,
            services: serviceProvider);

        // Assert
        Assert.NotNull(agent);

        // Verify the custom chat client was applied
        var retrievedTestClient = agent.GetService<TestChatClient>();
        Assert.NotNull(retrievedTestClient);
        Assert.Same(testChatClient, retrievedTestClient);

        // Verify the IServiceProvider was passed through
        var chatClient = agent.GetService<IChatClient>();
        Assert.NotNull(chatClient);
        var functionInvokingClient = chatClient.GetService<FunctionInvokingChatClient>();
        Assert.NotNull(functionInvokingClient);
        Assert.Same(serviceProvider, GetFunctionInvocationServices(functionInvokingClient));
    }

    /// <summary>
    /// Verify that AsIChatClientWithStoredOutputDisabled throws ArgumentNullException when client is null.
    /// </summary>
    [Fact]
    public void AsIChatClientWithStoredOutputDisabled_WithNullClient_ThrowsArgumentNullException()
    {
        // Act & Assert
        var exception = Assert.Throws<ArgumentNullException>(() =>
            ((ResponsesClient)null!).AsIChatClientWithStoredOutputDisabled());

        Assert.Equal("responseClient", exception.ParamName);
    }

    /// <summary>
    /// Verify that AsIChatClientWithStoredOutputDisabled wraps the original ResponsesClient,
    /// which remains accessible via the service chain.
    /// </summary>
    [Fact]
    public void AsIChatClientWithStoredOutputDisabled_InnerResponsesClientIsAccessible()
    {
        // Arrange
        var responseClient = new TestOpenAIResponseClient();

        // Act
        var chatClient = responseClient.AsIChatClientWithStoredOutputDisabled();

        // Assert - the inner ResponsesClient should be accessible via GetService
        var innerClient = chatClient.GetService<ResponsesClient>();
        Assert.NotNull(innerClient);
        Assert.Same(responseClient, innerClient);
    }

    /// <summary>
    /// Verify that AsIChatClientWithStoredOutputDisabled with includeReasoningEncryptedContent false
    /// wraps the original ResponsesClient, which remains accessible via the service chain.
    /// </summary>
    [Fact]
    public void AsIChatClientWithStoredOutputDisabled_WithIncludeReasoningFalse_InnerResponsesClientIsAccessible()
    {
        // Arrange
        var responseClient = new TestOpenAIResponseClient();

        // Act
        var chatClient = responseClient.AsIChatClientWithStoredOutputDisabled(includeReasoningEncryptedContent: false);

        // Assert - the inner ResponsesClient should be accessible via GetService
        var innerClient = chatClient.GetService<ResponsesClient>();
        Assert.NotNull(innerClient);
        Assert.Same(responseClient, innerClient);
    }

    /// <summary>
    /// Verify that AsIChatClientWithStoredOutputDisabled with default parameter (includeReasoningEncryptedContent = true)
    /// configures StoredOutputEnabled to false and includes ReasoningEncryptedContent in IncludedProperties.
    /// </summary>
    [Fact]
    public void AsIChatClientWithStoredOutputDisabled_Default_ConfiguresStoredOutputDisabledWithReasoningEncryptedContent()
    {
        // Arrange
        var responseClient = new TestOpenAIResponseClient();

        // Act
        var chatClient = responseClient.AsIChatClientWithStoredOutputDisabled();

        // Assert
        var createResponseOptions = GetCreateResponseOptionsFromPipeline(chatClient);
        Assert.NotNull(createResponseOptions);
        Assert.False(createResponseOptions.StoredOutputEnabled);
        Assert.Contains(IncludedResponseProperty.ReasoningEncryptedContent, createResponseOptions.IncludedProperties);
    }

    /// <summary>
    /// Verify that AsIChatClientWithStoredOutputDisabled with includeReasoningEncryptedContent explicitly set to true
    /// configures StoredOutputEnabled to false and includes ReasoningEncryptedContent in IncludedProperties.
    /// </summary>
    [Fact]
    public void AsIChatClientWithStoredOutputDisabled_WithIncludeReasoningTrue_ConfiguresStoredOutputDisabledWithReasoningEncryptedContent()
    {
        // Arrange
        var responseClient = new TestOpenAIResponseClient();

        // Act
        var chatClient = responseClient.AsIChatClientWithStoredOutputDisabled(includeReasoningEncryptedContent: true);

        // Assert
        var createResponseOptions = GetCreateResponseOptionsFromPipeline(chatClient);
        Assert.NotNull(createResponseOptions);
        Assert.False(createResponseOptions.StoredOutputEnabled);
        Assert.Contains(IncludedResponseProperty.ReasoningEncryptedContent, createResponseOptions.IncludedProperties);
    }

    /// <summary>
    /// Verify that AsIChatClientWithStoredOutputDisabled with includeReasoningEncryptedContent set to false
    /// configures StoredOutputEnabled to false and does not include ReasoningEncryptedContent in IncludedProperties.
    /// </summary>
    [Fact]
    public void AsIChatClientWithStoredOutputDisabled_WithIncludeReasoningFalse_ConfiguresStoredOutputDisabledWithoutReasoningEncryptedContent()
    {
        // Arrange
        var responseClient = new TestOpenAIResponseClient();

        // Act
        var chatClient = responseClient.AsIChatClientWithStoredOutputDisabled(includeReasoningEncryptedContent: false);

        // Assert
        var createResponseOptions = GetCreateResponseOptionsFromPipeline(chatClient);
        Assert.NotNull(createResponseOptions);
        Assert.False(createResponseOptions.StoredOutputEnabled);
        Assert.DoesNotContain(IncludedResponseProperty.ReasoningEncryptedContent, createResponseOptions.IncludedProperties);
    }

    /// <summary>
    /// Verify that AsIChatClientWithStoredOutputDisabled preserves an existing RawRepresentationFactory
    /// set on ChatOptions, augmenting it with StoredOutputEnabled and ReasoningEncryptedContent
    /// rather than replacing it.
    /// </summary>
    [Fact]
    public void AsIChatClientWithStoredOutputDisabled_PreservesExistingRawRepresentationFactory()
    {
        // Arrange
        var responseClient = new TestOpenAIResponseClient();
        var chatClient = responseClient.AsIChatClientWithStoredOutputDisabled();

        // Simulate a caller setting their own RawRepresentationFactory on ChatOptions
        // (e.g., to add WebSearchCallActionSources).
        var options = new ChatOptions
        {
            RawRepresentationFactory = _ => new CreateResponseOptions
            {
                IncludedProperties = { IncludedResponseProperty.WebSearchCallActionSources },
            },
        };

        // Act
        var createResponseOptions = GetCreateResponseOptionsFromPipeline(chatClient, options);

        // Assert
        Assert.NotNull(createResponseOptions);
        Assert.False(createResponseOptions.StoredOutputEnabled);
        Assert.Contains(IncludedResponseProperty.ReasoningEncryptedContent, createResponseOptions.IncludedProperties);
        Assert.Contains(IncludedResponseProperty.WebSearchCallActionSources, createResponseOptions.IncludedProperties);
    }

    /// <summary>
    /// Verify that AsIChatClientWithStoredOutputDisabled does not duplicate ReasoningEncryptedContent
    /// when the existing factory already includes it.
    /// </summary>
    [Fact]
    public void AsIChatClientWithStoredOutputDisabled_DoesNotDuplicateReasoningEncryptedContent()
    {
        // Arrange
        var responseClient = new TestOpenAIResponseClient();
        var chatClient = responseClient.AsIChatClientWithStoredOutputDisabled();

        // Simulate a caller that already includes ReasoningEncryptedContent
        var options = new ChatOptions
        {
            RawRepresentationFactory = _ => new CreateResponseOptions
            {
                IncludedProperties = { IncludedResponseProperty.ReasoningEncryptedContent },
            },
        };

        // Act
        var createResponseOptions = GetCreateResponseOptionsFromPipeline(chatClient, options);

        // Assert - ReasoningEncryptedContent should appear exactly once
        Assert.NotNull(createResponseOptions);
        int count = 0;
        foreach (var prop in createResponseOptions.IncludedProperties)
        {
            if (prop == IncludedResponseProperty.ReasoningEncryptedContent)
            {
                count++;
            }
        }

        Assert.Equal(1, count);
    }

    /// <summary>
    /// A simple test IServiceProvider implementation for testing.
    /// </summary>
    private sealed class TestServiceProvider : IServiceProvider
    {
        public object? GetService(Type serviceType) => null;
    }

    /// <summary>
    /// Uses reflection to access the FunctionInvocationServices property which is not public.
    /// </summary>
    private static IServiceProvider? GetFunctionInvocationServices(FunctionInvokingChatClient client)
    {
        var property = typeof(FunctionInvokingChatClient).GetProperty(
            "FunctionInvocationServices",
            BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
        return property?.GetValue(client) as IServiceProvider;
    }

    /// <summary>
    /// Extracts the <see cref="CreateResponseOptions"/> produced by the ConfigureOptions pipeline
    /// by using reflection to access the configure action and invoking it on a test <see cref="ChatOptions"/>.
    /// </summary>
    private static CreateResponseOptions? GetCreateResponseOptionsFromPipeline(IChatClient chatClient)
    {
        return GetCreateResponseOptionsFromPipeline(chatClient, new ChatOptions());
    }

    /// <summary>
    /// Overload that runs the configure action on caller-supplied <see cref="ChatOptions"/>,
    /// useful for testing that existing factories are preserved.
    /// </summary>
    private static CreateResponseOptions? GetCreateResponseOptionsFromPipeline(IChatClient chatClient, ChatOptions options)
    {
        // The ConfigureOptionsChatClient stores the configure action in a private field.
        var configureField = chatClient.GetType().GetField("_configureOptions", BindingFlags.NonPublic | BindingFlags.Instance);
        Assert.NotNull(configureField);

        var configureAction = configureField.GetValue(chatClient) as Action<ChatOptions>;
        Assert.NotNull(configureAction);

        configureAction(options);

        Assert.NotNull(options.RawRepresentationFactory);
        return options.RawRepresentationFactory(chatClient) as CreateResponseOptions;
    }
}
