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

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
#if NET
using Microsoft.Agents.AI.Tools.Shell;
#endif
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using Moq;

namespace Microsoft.Agents.AI.UnitTests;

public class HarnessAgentTests
{
    private const int TestMaxContextWindowTokens = 100_000;
    private const int TestMaxOutputTokens = 10_000;

    /// <summary>
    /// Creates a HarnessAgent with all default features disabled to isolate tests for specific behaviors.
    /// </summary>
    private static HarnessAgentOptions CreateAllDisabledOptions() => new()
    {
        DisableToolApproval = true,
        DisableOpenTelemetry = true,
        DisableFileMemory = true,
        DisableFileAccess = true,
        DisableWebSearch = true,
        DisableTodoProvider = true,
        DisableAgentModeProvider = true,
        DisableAgentSkillsProvider = true,
    };

    #region Constructor Validation

    /// <summary>
    /// Verify that the constructor throws when chatClient is null.
    /// </summary>
    [Fact]
    public void Constructor_ThrowsWhenChatClientIsNull()
    {
        // Act & Assert
        Assert.Throws<ArgumentNullException>(() => new HarnessAgent(null!, TestMaxContextWindowTokens, TestMaxOutputTokens));
    }

    /// <summary>
    /// Verify that the constructor throws when MaxContextWindowTokens is invalid (zero).
    /// </summary>
    [Fact]
    public void Constructor_ThrowsWhenMaxContextWindowTokensIsZero()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;

        // Act & Assert
        Assert.Throws<ArgumentOutOfRangeException>(() => new HarnessAgent(chatClient, 0, TestMaxOutputTokens));
    }

    /// <summary>
    /// Verify that the constructor throws when MaxOutputTokens equals MaxContextWindowTokens.
    /// </summary>
    [Fact]
    public void Constructor_ThrowsWhenMaxOutputTokensEqualsContextWindow()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;

        // Act & Assert
        Assert.Throws<ArgumentOutOfRangeException>(() => new HarnessAgent(chatClient, 100_000, 100_000));
    }

    /// <summary>
    /// Verify that the constructor succeeds when options is null.
    /// </summary>
    [Fact]
    public void Constructor_SucceedsWhenOptionsIsNull()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens);

        // Assert
        Assert.NotNull(agent);
    }

    #endregion

    #region Agent Identity

    /// <summary>
    /// Verify that Name and Description are passed through to the inner agent.
    /// </summary>
    [Fact]
    public void NameAndDescription_ArePassedThrough()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;
        var options = CreateAllDisabledOptions();
        options.Name = "TestAgent";
        options.Description = "A test agent";

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);

        // Assert
        Assert.Equal("TestAgent", agent.Name);
        Assert.Equal("A test agent", agent.Description);
    }

    /// <summary>
    /// Verify that Id is passed through to the inner agent.
    /// </summary>
    [Fact]
    public void Id_IsPassedThrough()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;
        var options = CreateAllDisabledOptions();
        options.Id = "my-agent-id";

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);

        // Assert
        Assert.Equal("my-agent-id", agent.Id);
    }

    #endregion

    #region Instructions

    /// <summary>
    /// Verify that default instructions are used when none are provided.
    /// </summary>
    [Fact]
    public void Instructions_DefaultsToBuiltInInstructions()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
        var innerAgent = agent.GetService<ChatClientAgent>();

        // Assert
        Assert.NotNull(innerAgent);
        Assert.Equal(HarnessAgent.DefaultInstructions, innerAgent!.Instructions);
    }

    /// <summary>
    /// Verify that default instructions are used when options is provided but neither HarnessInstructions nor ChatOptions.Instructions is set.
    /// </summary>
    [Fact]
    public void Instructions_DefaultsWhenBothNull()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;
        var options = CreateAllDisabledOptions();
        options.ChatOptions = new ChatOptions { Temperature = 0.5f };

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
        var innerAgent = agent.GetService<ChatClientAgent>();

        // Assert
        Assert.NotNull(innerAgent);
        Assert.Equal(HarnessAgent.DefaultInstructions, innerAgent!.Instructions);
    }

    /// <summary>
    /// Verify that ChatOptions.Instructions is appended to the default HarnessInstructions.
    /// </summary>
    [Fact]
    public void Instructions_CombinesDefaultHarnessWithAgentInstructions()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;
        var options = CreateAllDisabledOptions();
        options.ChatOptions = new ChatOptions { Instructions = "You are a custom assistant." };

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
        var innerAgent = agent.GetService<ChatClientAgent>();

        // Assert
        Assert.NotNull(innerAgent);
        var expected = $"{HarnessAgent.DefaultInstructions}\n\nYou are a custom assistant.";
        Assert.Equal(expected, innerAgent!.Instructions);
    }

    /// <summary>
    /// Verify that custom HarnessInstructions replaces the default.
    /// </summary>
    [Fact]
    public void Instructions_CustomHarnessInstructionsReplacesDefault()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;
        var options = CreateAllDisabledOptions();
        options.HarnessInstructions = "Custom harness rules.";

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
        var innerAgent = agent.GetService<ChatClientAgent>();

        // Assert
        Assert.NotNull(innerAgent);
        Assert.Equal("Custom harness rules.", innerAgent!.Instructions);
    }

    /// <summary>
    /// Verify that custom HarnessInstructions and ChatOptions.Instructions are combined.
    /// </summary>
    [Fact]
    public void Instructions_CombinesCustomHarnessWithAgentInstructions()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;
        var options = CreateAllDisabledOptions();
        options.HarnessInstructions = "Custom harness rules.";
        options.ChatOptions = new ChatOptions { Instructions = "You are a research agent." };

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
        var innerAgent = agent.GetService<ChatClientAgent>();

        // Assert
        Assert.NotNull(innerAgent);
        Assert.Equal("Custom harness rules.\n\nYou are a research agent.", innerAgent!.Instructions);
    }

    /// <summary>
    /// Verify that empty HarnessInstructions omits harness portion, using only agent instructions.
    /// </summary>
    [Fact]
    public void Instructions_EmptyHarnessInstructionsUsesOnlyAgentInstructions()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;
        var options = CreateAllDisabledOptions();
        options.HarnessInstructions = string.Empty;
        options.ChatOptions = new ChatOptions { Instructions = "Agent only instructions." };

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
        var innerAgent = agent.GetService<ChatClientAgent>();

        // Assert
        Assert.NotNull(innerAgent);
        Assert.Equal("Agent only instructions.", innerAgent!.Instructions);
    }

    /// <summary>
    /// Verify that empty HarnessInstructions with no agent instructions results in empty string.
    /// </summary>
    [Fact]
    public void Instructions_EmptyHarnessInstructionsWithNoAgentInstructions()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;
        var options = CreateAllDisabledOptions();
        options.HarnessInstructions = string.Empty;

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
        var innerAgent = agent.GetService<ChatClientAgent>();

        // Assert
        Assert.NotNull(innerAgent);
        Assert.Equal(string.Empty, innerAgent!.Instructions);
    }

    #endregion

    #region ChatHistoryProvider

    /// <summary>
    /// Verify that the default ChatHistoryProvider is InMemoryChatHistoryProvider when none is specified.
    /// </summary>
    [Fact]
    public void ChatHistoryProvider_DefaultsToInMemory()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
        var innerAgent = agent.GetService<ChatClientAgent>();

        // Assert
        Assert.NotNull(innerAgent);
        Assert.IsType<InMemoryChatHistoryProvider>(innerAgent!.ChatHistoryProvider);
    }

    /// <summary>
    /// Verify that a custom ChatHistoryProvider is used when provided.
    /// </summary>
    [Fact]
    public void ChatHistoryProvider_UsesCustomProviderWhenSpecified()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;
        var customProvider = new InMemoryChatHistoryProvider();
        var options = CreateAllDisabledOptions();
        options.ChatHistoryProvider = customProvider;

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
        var innerAgent = agent.GetService<ChatClientAgent>();

        // Assert
        Assert.NotNull(innerAgent);
        Assert.Same(customProvider, innerAgent!.ChatHistoryProvider);
    }

    #endregion

    #region ChatClient Pipeline

    /// <summary>
    /// Verify that the inner agent's ChatClient includes FunctionInvokingChatClient in the pipeline.
    /// </summary>
    [Fact]
    public void Pipeline_IncludesFunctionInvokingChatClient()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
        var innerAgent = agent.GetService<ChatClientAgent>();

        // Assert
        Assert.NotNull(innerAgent);
        var ficc = innerAgent!.ChatClient.GetService<FunctionInvokingChatClient>();
        Assert.NotNull(ficc);
    }

    /// <summary>
    /// Verify that the inner agent's ChatClient pipeline includes more than just the raw chat client,
    /// confirming that per-service-call persistence and other decorators have been applied.
    /// </summary>
    [Fact]
    public void Pipeline_HasDecoratedChatClient()
    {
        // Arrange
        var mockClient = new Mock<IChatClient>();
        var rawClient = mockClient.Object;

        // Act
        var agent = new HarnessAgent(rawClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
        var innerAgent = agent.GetService<ChatClientAgent>();

        // Assert — the pipeline wraps the raw client, so the outer client is not the same object.
        Assert.NotNull(innerAgent);
        Assert.NotSame(rawClient, innerAgent!.ChatClient);
    }

    #endregion

    #region AIContextProviders

    /// <summary>
    /// Verify that additional AIContextProviders from options are passed to the inner ChatClientAgent.
    /// </summary>
    [Fact]
    public void AIContextProviders_ArePassedToInnerAgent()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;
        var customProvider = new TodoProvider();
        var options = CreateAllDisabledOptions();
        options.AIContextProviders = [customProvider];

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
        var innerAgent = agent.GetService<ChatClientAgent>();

        // Assert — the custom provider should appear in the inner agent's AIContextProviders.
        Assert.NotNull(innerAgent);
        Assert.NotNull(innerAgent!.AIContextProviders);
        Assert.Contains(customProvider, innerAgent.AIContextProviders!);
    }

    /// <summary>
    /// Verify that when all default providers are disabled and no user AIContextProviders are specified,
    /// the inner agent has an empty providers list.
    /// </summary>
    [Fact]
    public void AIContextProviders_IsEmptyWhenAllDisabledAndNoneSpecified()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
        var innerAgent = agent.GetService<ChatClientAgent>();

        // Assert
        Assert.NotNull(innerAgent);
        Assert.NotNull(innerAgent!.AIContextProviders);
        Assert.Empty(innerAgent.AIContextProviders!);
    }

    #endregion

    #region ChatOptions and Tools

    /// <summary>
    /// Verify that tools from ChatOptions are passed to the model during invocation.
    /// </summary>
    [Fact]
    public async Task ChatOptions_ToolsArePreservedAsync()
    {
        // Arrange
        var tool = AIFunctionFactory.Create(() => "test", "TestTool");
        var mockClient = new Mock<IChatClient>();
        ChatOptions? capturedOptions = null;
        mockClient
            .Setup(c => c.GetResponseAsync(
                It.IsAny<IEnumerable<ChatMessage>>(),
                It.IsAny<ChatOptions>(),
                It.IsAny<CancellationToken>()))
            .Callback<IEnumerable<ChatMessage>, ChatOptions?, CancellationToken>((_, opts, _) => capturedOptions = opts)
            .ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Done")));

        var options = CreateAllDisabledOptions();
        options.ChatOptions = new ChatOptions { Tools = [tool] };

        var agent = new HarnessAgent(mockClient.Object, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
        var session = await agent.CreateSessionAsync();

        // Act
        await agent.RunAsync([new ChatMessage(ChatRole.User, "Hi")], session);

        // Assert — verify the tool was included in the ChatOptions passed to the model.
        Assert.NotNull(capturedOptions);
        Assert.NotNull(capturedOptions!.Tools);
        Assert.Contains(capturedOptions.Tools, t => t == tool);
    }

    /// <summary>
    /// Verify that the source ChatOptions are cloned and not modified.
    /// </summary>
    [Fact]
    public void ChatOptions_SourceIsNotModified()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;
        var sourceChatOptions = new ChatOptions
        {
            Instructions = "original instructions",
            Temperature = 0.7f,
        };

        // Act
        _ = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, new HarnessAgentOptions
        {
            ChatOptions = sourceChatOptions,
        });

        // Assert — source ChatOptions should not be mutated.
        Assert.Equal("original instructions", sourceChatOptions.Instructions);
        Assert.Equal(0.7f, sourceChatOptions.Temperature);
    }

    #endregion

    #region GetService

    /// <summary>
    /// Verify that GetService returns the HarnessAgent for its own type.
    /// </summary>
    [Fact]
    public void GetService_ReturnsSelfForHarnessAgentType()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());

        // Assert
        Assert.Same(agent, agent.GetService<HarnessAgent>());
    }

    /// <summary>
    /// Verify that GetService returns the inner ChatClientAgent.
    /// </summary>
    [Fact]
    public void GetService_ReturnsInnerChatClientAgent()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());

        // Assert
        Assert.NotNull(agent.GetService<ChatClientAgent>());
    }

    #endregion

    #region RunAsync Delegation

    /// <summary>
    /// Verify that RunAsync delegates to the inner ChatClientAgent.
    /// </summary>
    [Fact]
    public async Task RunAsync_DelegatesToInnerAgentAsync()
    {
        // Arrange
        var mockClient = new Mock<IChatClient>();
        mockClient
            .Setup(c => c.GetResponseAsync(
                It.IsAny<IEnumerable<ChatMessage>>(),
                It.IsAny<ChatOptions>(),
                It.IsAny<CancellationToken>()))
            .ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Hello!")));

        var agent = new HarnessAgent(mockClient.Object, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
        var session = await agent.CreateSessionAsync();

        // Act
        var response = await agent.RunAsync(
            [new ChatMessage(ChatRole.User, "Hi")],
            session);

        // Assert
        Assert.NotNull(response);
        Assert.True(response.Messages.Any());
    }

    #endregion

    #region DefaultInstructions

    /// <summary>
    /// Verify that DefaultInstructions is a non-empty public constant.
    /// </summary>
    [Fact]
    public void DefaultInstructions_IsNonEmpty()
    {
        // Assert
        Assert.False(string.IsNullOrWhiteSpace(HarnessAgent.DefaultInstructions));
    }

    #endregion

    #region AsHarnessAgent Extension Method

    /// <summary>
    /// Verify that AsHarnessAgent creates a HarnessAgent with default options.
    /// </summary>
    [Fact]
    public void AsHarnessAgent_CreatesAgentWithDefaults()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;

        // Act
        var agent = chatClient.AsHarnessAgent(TestMaxContextWindowTokens, TestMaxOutputTokens);

        // Assert
        Assert.NotNull(agent);
        Assert.IsType<HarnessAgent>(agent);
        Assert.Equal(HarnessAgent.DefaultInstructions, agent.GetService<ChatClientAgent>()!.Instructions);
    }

    /// <summary>
    /// Verify that AsHarnessAgent passes options through to the HarnessAgent.
    /// </summary>
    [Fact]
    public void AsHarnessAgent_PassesOptionsThrough()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;
        var options = CreateAllDisabledOptions();
        options.Name = "ExtensionAgent";
        options.ChatOptions = new ChatOptions { Instructions = "Custom instructions" };

        // Act
        var agent = chatClient.AsHarnessAgent(TestMaxContextWindowTokens, TestMaxOutputTokens, options);
        var innerAgent = agent.GetService<ChatClientAgent>();

        // Assert
        Assert.Equal("ExtensionAgent", agent.Name);
        Assert.NotNull(innerAgent);
        var expected = $"{HarnessAgent.DefaultInstructions}\n\nCustom instructions";
        Assert.Equal(expected, innerAgent!.Instructions);
    }

    /// <summary>
    /// Verify that AsHarnessAgent throws when chatClient is null.
    /// </summary>
    [Fact]
    public void AsHarnessAgent_ThrowsWhenChatClientIsNull()
    {
        // Act & Assert
        Assert.Throws<ArgumentNullException>(() => ((IChatClient)null!).AsHarnessAgent(TestMaxContextWindowTokens, TestMaxOutputTokens));
    }

    #endregion

    #region Feature: ToolApproval

    /// <summary>
    /// Verify that ToolApprovalAgent is included in the pipeline by default.
    /// </summary>
    [Fact]
    public void ToolApproval_IncludedByDefault()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;
        var options = CreateAllDisabledOptions();
        options.DisableToolApproval = false;

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);

        // Assert
        Assert.NotNull(agent.GetService<ToolApprovalAgent>());
    }

    /// <summary>
    /// Verify that ToolApprovalAgent is excluded when disabled.
    /// </summary>
    [Fact]
    public void ToolApproval_ExcludedWhenDisabled()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());

        // Assert
        Assert.Null(agent.GetService<ToolApprovalAgent>());
    }

    #endregion

    #region Feature: OpenTelemetry

    /// <summary>
    /// Verify that OpenTelemetryAgent is included in the pipeline by default.
    /// </summary>
    [Fact]
    public void OpenTelemetry_IncludedByDefault()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;
        var options = CreateAllDisabledOptions();
        options.DisableOpenTelemetry = false;

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);

        // Assert
        Assert.NotNull(agent.GetService<OpenTelemetryAgent>());
    }

    /// <summary>
    /// Verify that OpenTelemetryAgent is excluded when disabled.
    /// </summary>
    [Fact]
    public void OpenTelemetry_ExcludedWhenDisabled()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());

        // Assert
        Assert.Null(agent.GetService<OpenTelemetryAgent>());
    }

    /// <summary>
    /// Verify that a custom OpenTelemetrySourceName is accepted without error.
    /// </summary>
    [Fact]
    public void OpenTelemetry_CustomSourceNameIsAccepted()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;
        var options = CreateAllDisabledOptions();
        options.DisableOpenTelemetry = false;
        options.OpenTelemetrySourceName = "MyApp.AgentTracing";

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);

        // Assert
        Assert.NotNull(agent.GetService<OpenTelemetryAgent>());
    }

    #endregion

    #region Feature: WebSearch

    /// <summary>
    /// Verify that HostedWebSearchTool is added to ChatOptions.Tools by default.
    /// </summary>
    [Fact]
    public async Task WebSearch_IncludedByDefaultAsync()
    {
        // Arrange
        var mockClient = new Mock<IChatClient>();
        ChatOptions? capturedOptions = null;
        mockClient
            .Setup(c => c.GetResponseAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), It.IsAny<CancellationToken>()))
            .Callback<IEnumerable<ChatMessage>, ChatOptions?, CancellationToken>((_, opts, _) => capturedOptions = opts)
            .ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Done")));

        var options = CreateAllDisabledOptions();
        options.DisableWebSearch = false;

        var agent = new HarnessAgent(mockClient.Object, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
        var session = await agent.CreateSessionAsync();

        // Act
        await agent.RunAsync([new ChatMessage(ChatRole.User, "Hi")], session);

        // Assert
        Assert.NotNull(capturedOptions?.Tools);
        Assert.Contains(capturedOptions!.Tools!, t => t is HostedWebSearchTool);
    }

    /// <summary>
    /// Verify that HostedWebSearchTool is not added when disabled.
    /// </summary>
    [Fact]
    public async Task WebSearch_ExcludedWhenDisabledAsync()
    {
        // Arrange
        var mockClient = new Mock<IChatClient>();
        ChatOptions? capturedOptions = null;
        mockClient
            .Setup(c => c.GetResponseAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), It.IsAny<CancellationToken>()))
            .Callback<IEnumerable<ChatMessage>, ChatOptions?, CancellationToken>((_, opts, _) => capturedOptions = opts)
            .ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Done")));

        var agent = new HarnessAgent(mockClient.Object, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
        var session = await agent.CreateSessionAsync();

        // Act
        await agent.RunAsync([new ChatMessage(ChatRole.User, "Hi")], session);

        // Assert
        Assert.NotNull(capturedOptions);
        if (capturedOptions!.Tools != null)
        {
            Assert.DoesNotContain(capturedOptions.Tools, t => t is HostedWebSearchTool);
        }
    }

    /// <summary>
    /// Verify that user-provided tools are preserved alongside the default HostedWebSearchTool.
    /// </summary>
    [Fact]
    public async Task WebSearch_CoexistsWithUserToolsAsync()
    {
        // Arrange
        var mockClient = new Mock<IChatClient>();
        ChatOptions? capturedOptions = null;
        mockClient
            .Setup(c => c.GetResponseAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), It.IsAny<CancellationToken>()))
            .Callback<IEnumerable<ChatMessage>, ChatOptions?, CancellationToken>((_, opts, _) => capturedOptions = opts)
            .ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Done")));

        var userTool = AIFunctionFactory.Create(() => "test", "UserTool");
        var options = CreateAllDisabledOptions();
        options.DisableWebSearch = false;
        options.ChatOptions = new ChatOptions { Tools = [userTool] };

        var agent = new HarnessAgent(mockClient.Object, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
        var session = await agent.CreateSessionAsync();

        // Act
        await agent.RunAsync([new ChatMessage(ChatRole.User, "Hi")], session);

        // Assert
        Assert.NotNull(capturedOptions?.Tools);
        Assert.Contains(capturedOptions!.Tools!, t => t is HostedWebSearchTool);
        Assert.Contains(capturedOptions.Tools!, t => t == userTool);
    }

    #endregion

    #region Feature: TodoProvider

    /// <summary>
    /// Verify that TodoProvider is included in AIContextProviders by default.
    /// </summary>
    [Fact]
    public void TodoProvider_IncludedByDefault()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;
        var options = CreateAllDisabledOptions();
        options.DisableTodoProvider = false;

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
        var innerAgent = agent.GetService<ChatClientAgent>();

        // Assert
        Assert.NotNull(innerAgent?.AIContextProviders);
        Assert.Contains(innerAgent!.AIContextProviders!, p => p is TodoProvider);
    }

    /// <summary>
    /// Verify that TodoProvider is excluded when disabled.
    /// </summary>
    [Fact]
    public void TodoProvider_ExcludedWhenDisabled()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
        var innerAgent = agent.GetService<ChatClientAgent>();

        // Assert
        Assert.NotNull(innerAgent);
        if (innerAgent!.AIContextProviders != null)
        {
            Assert.DoesNotContain(innerAgent.AIContextProviders, p => p is TodoProvider);
        }
    }

    #endregion

    #region Feature: AgentModeProvider

    /// <summary>
    /// Verify that AgentModeProvider is included in AIContextProviders by default.
    /// </summary>
    [Fact]
    public void AgentModeProvider_IncludedByDefault()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;
        var options = CreateAllDisabledOptions();
        options.DisableAgentModeProvider = false;

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
        var innerAgent = agent.GetService<ChatClientAgent>();

        // Assert
        Assert.NotNull(innerAgent?.AIContextProviders);
        Assert.Contains(innerAgent!.AIContextProviders!, p => p is AgentModeProvider);
    }

    /// <summary>
    /// Verify that AgentModeProvider is excluded when disabled.
    /// </summary>
    [Fact]
    public void AgentModeProvider_ExcludedWhenDisabled()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
        var innerAgent = agent.GetService<ChatClientAgent>();

        // Assert
        Assert.NotNull(innerAgent);
        if (innerAgent!.AIContextProviders != null)
        {
            Assert.DoesNotContain(innerAgent.AIContextProviders, p => p is AgentModeProvider);
        }
    }

    /// <summary>
    /// Verify that custom AgentModeProviderOptions are passed through.
    /// </summary>
    [Fact]
    public void AgentModeProvider_UsesCustomOptions()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;
        var options = CreateAllDisabledOptions();
        options.DisableAgentModeProvider = false;
        options.AgentModeProviderOptions = new AgentModeProviderOptions
        {
            Modes =
            [
                new AgentModeProviderOptions.AgentMode("custom-mode", "A custom mode for testing"),
            ],
            DefaultMode = "custom-mode",
        };

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
        var innerAgent = agent.GetService<ChatClientAgent>();

        // Assert — AgentModeProvider should be present (we can't easily inspect its internal options,
        // but we verify it is created and present).
        Assert.NotNull(innerAgent?.AIContextProviders);
        Assert.Contains(innerAgent!.AIContextProviders!, p => p is AgentModeProvider);
    }

    #endregion

    #region Feature: FileMemoryProvider

    /// <summary>
    /// Verify that FileMemoryProvider is included in AIContextProviders by default.
    /// </summary>
    [Fact]
    public void FileMemoryProvider_IncludedByDefault()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;
        var options = CreateAllDisabledOptions();
        options.DisableFileMemory = false;

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
        var innerAgent = agent.GetService<ChatClientAgent>();

        // Assert
        Assert.NotNull(innerAgent?.AIContextProviders);
        Assert.Contains(innerAgent!.AIContextProviders!, p => p is FileMemoryProvider);
    }

    /// <summary>
    /// Verify that FileMemoryProvider is excluded when disabled.
    /// </summary>
    [Fact]
    public void FileMemoryProvider_ExcludedWhenDisabled()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
        var innerAgent = agent.GetService<ChatClientAgent>();

        // Assert
        Assert.NotNull(innerAgent);
        if (innerAgent!.AIContextProviders != null)
        {
            Assert.DoesNotContain(innerAgent.AIContextProviders, p => p is FileMemoryProvider);
        }
    }

    /// <summary>
    /// Verify that a custom FileMemoryStore is used when provided.
    /// </summary>
    [Fact]
    public void FileMemoryProvider_UsesCustomStore()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;
        var customStore = new Mock<AgentFileStore>().Object;
        var options = CreateAllDisabledOptions();
        options.DisableFileMemory = false;
        options.FileMemoryStore = customStore;

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
        var innerAgent = agent.GetService<ChatClientAgent>();

        // Assert — FileMemoryProvider should be present with the custom store.
        Assert.NotNull(innerAgent?.AIContextProviders);
        Assert.Contains(innerAgent!.AIContextProviders!, p => p is FileMemoryProvider);
    }

    #endregion

    #region Feature: FileAccessProvider

    /// <summary>
    /// Verify that FileAccessProvider is included in AIContextProviders by default.
    /// </summary>
    [Fact]
    public void FileAccessProvider_IncludedByDefault()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;
        var options = CreateAllDisabledOptions();
        options.DisableFileAccess = false;

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
        var innerAgent = agent.GetService<ChatClientAgent>();

        // Assert
        Assert.NotNull(innerAgent?.AIContextProviders);
        Assert.Contains(innerAgent!.AIContextProviders!, p => p is FileAccessProvider);
    }

    /// <summary>
    /// Verify that FileAccessProvider is excluded when disabled.
    /// </summary>
    [Fact]
    public void FileAccessProvider_ExcludedWhenDisabled()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
        var innerAgent = agent.GetService<ChatClientAgent>();

        // Assert
        Assert.NotNull(innerAgent);
        if (innerAgent!.AIContextProviders != null)
        {
            Assert.DoesNotContain(innerAgent.AIContextProviders, p => p is FileAccessProvider);
        }
    }

    /// <summary>
    /// Verify that a custom FileAccessStore is used when provided.
    /// </summary>
    [Fact]
    public void FileAccessProvider_UsesCustomStore()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;
        var customStore = new Mock<AgentFileStore>().Object;
        var options = CreateAllDisabledOptions();
        options.DisableFileAccess = false;
        options.FileAccessStore = customStore;

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
        var innerAgent = agent.GetService<ChatClientAgent>();

        // Assert — FileAccessProvider should be present with the custom store.
        Assert.NotNull(innerAgent?.AIContextProviders);
        Assert.Contains(innerAgent!.AIContextProviders!, p => p is FileAccessProvider);
    }

    #endregion

    #region Feature: AgentSkillsProvider

    /// <summary>
    /// Verify that AgentSkillsProvider is included in AIContextProviders by default.
    /// </summary>
    [Fact]
    public void AgentSkillsProvider_IncludedByDefault()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;
        var options = CreateAllDisabledOptions();
        options.DisableAgentSkillsProvider = false;

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
        var innerAgent = agent.GetService<ChatClientAgent>();

        // Assert
        Assert.NotNull(innerAgent?.AIContextProviders);
        Assert.Contains(innerAgent!.AIContextProviders!, p => p is AgentSkillsProvider);
    }

    /// <summary>
    /// Verify that AgentSkillsProvider is excluded when disabled.
    /// </summary>
    [Fact]
    public void AgentSkillsProvider_ExcludedWhenDisabled()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
        var innerAgent = agent.GetService<ChatClientAgent>();

        // Assert
        Assert.NotNull(innerAgent);
        if (innerAgent!.AIContextProviders != null)
        {
            Assert.DoesNotContain(innerAgent.AIContextProviders, p => p is AgentSkillsProvider);
        }
    }

    /// <summary>
    /// Verify that a custom AgentSkillsSource is used when provided.
    /// </summary>
    [Fact]
    public void AgentSkillsProvider_UsesCustomSource()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;
        var customSource = new Mock<AgentSkillsSource>().Object;
        var options = CreateAllDisabledOptions();
        options.DisableAgentSkillsProvider = false;
        options.AgentSkillsSource = customSource;

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
        var innerAgent = agent.GetService<ChatClientAgent>();

        // Assert — AgentSkillsProvider should be present.
        Assert.NotNull(innerAgent?.AIContextProviders);
        Assert.Contains(innerAgent!.AIContextProviders!, p => p is AgentSkillsProvider);
    }

    #endregion

    #region Feature: MaximumIterationsPerRequest

    /// <summary>
    /// Verify that MaximumIterationsPerRequest configures the FunctionInvokingChatClient.
    /// </summary>
    [Fact]
    public void MaximumIterationsPerRequest_ConfiguresFunctionInvokingChatClient()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;
        var options = CreateAllDisabledOptions();
        options.MaximumIterationsPerRequest = 42;

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
        var innerAgent = agent.GetService<ChatClientAgent>();
        var ficc = innerAgent!.ChatClient.GetService<FunctionInvokingChatClient>();

        // Assert
        Assert.NotNull(ficc);
        Assert.Equal(42, ficc!.MaximumIterationsPerRequest);
    }

    /// <summary>
    /// Verify that the default MaximumIterationsPerRequest is used when not set.
    /// </summary>
    [Fact]
    public void MaximumIterationsPerRequest_UsesDefaultWhenNotSet()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
        var innerAgent = agent.GetService<ChatClientAgent>();
        var ficc = innerAgent!.ChatClient.GetService<FunctionInvokingChatClient>();

        // Assert — default is not 0 and not our custom value.
        Assert.NotNull(ficc);
        Assert.NotEqual(0, ficc!.MaximumIterationsPerRequest);
    }

    #endregion

    #region Feature: All Defaults Enabled

    /// <summary>
    /// Verify that when no options are provided, all default features are enabled.
    /// </summary>
    [Fact]
    public async Task AllDefaults_AllFeaturesEnabledAsync()
    {
        // Arrange
        var mockClient = new Mock<IChatClient>();
        ChatOptions? capturedOptions = null;
        mockClient
            .Setup(c => c.GetResponseAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), It.IsAny<CancellationToken>()))
            .Callback<IEnumerable<ChatMessage>, ChatOptions?, CancellationToken>((_, opts, _) => capturedOptions = opts)
            .ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Done")));

        // Act
        var agent = new HarnessAgent(mockClient.Object, TestMaxContextWindowTokens, TestMaxOutputTokens);
        var innerAgent = agent.GetService<ChatClientAgent>();

        // Assert — agent wrappers
        Assert.NotNull(agent.GetService<ToolApprovalAgent>());
        Assert.NotNull(agent.GetService<OpenTelemetryAgent>());

        // Assert — default context providers
        Assert.NotNull(innerAgent);
        Assert.NotNull(innerAgent!.AIContextProviders);

        var providers = innerAgent.AIContextProviders!.ToList();
        Assert.Contains(providers, p => p is TodoProvider);
        Assert.Contains(providers, p => p is AgentModeProvider);
        Assert.Contains(providers, p => p is FileMemoryProvider);
        Assert.Contains(providers, p => p is FileAccessProvider);
        Assert.Contains(providers, p => p is AgentSkillsProvider);

        // Assert — HostedWebSearchTool is present in the tools sent to the model
        var session = await agent.CreateSessionAsync();
        await agent.RunAsync([new ChatMessage(ChatRole.User, "Hi")], session);
        Assert.NotNull(capturedOptions?.Tools);
        Assert.Contains(capturedOptions!.Tools!, t => t is HostedWebSearchTool);
    }

    #endregion

    #region Feature: BackgroundAgentsProvider

    /// <summary>
    /// Verify that BackgroundAgentsProvider is included when BackgroundAgents are specified.
    /// </summary>
    [Fact]
    public void BackgroundAgentsProvider_IncludedWhenAgentsSpecified()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;
        var bgAgentMock = new Mock<AIAgent>();
        bgAgentMock.Setup(a => a.Name).Returns("TestBackgroundAgent");
        var options = CreateAllDisabledOptions();
        options.BackgroundAgents = [bgAgentMock.Object];

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
        var innerAgent = agent.GetService<ChatClientAgent>();

        // Assert
        Assert.NotNull(innerAgent?.AIContextProviders);
        Assert.Contains(innerAgent!.AIContextProviders!, p => p is BackgroundAgentsProvider);
    }

    /// <summary>
    /// Verify that BackgroundAgentsProvider is not included when BackgroundAgents is null.
    /// </summary>
    [Fact]
    public void BackgroundAgentsProvider_ExcludedWhenAgentsNull()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;
        var options = CreateAllDisabledOptions();
        options.BackgroundAgents = null;

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
        var innerAgent = agent.GetService<ChatClientAgent>();

        // Assert
        Assert.NotNull(innerAgent);
        if (innerAgent!.AIContextProviders != null)
        {
            Assert.DoesNotContain(innerAgent.AIContextProviders, p => p is BackgroundAgentsProvider);
        }
    }

    /// <summary>
    /// Verify that BackgroundAgentsProvider is not included when BackgroundAgents is an empty collection.
    /// </summary>
    [Fact]
    public void BackgroundAgentsProvider_ExcludedWhenAgentsEmpty()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;
        var options = CreateAllDisabledOptions();
        options.BackgroundAgents = Array.Empty<AIAgent>();

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
        var innerAgent = agent.GetService<ChatClientAgent>();

        // Assert
        Assert.NotNull(innerAgent);
        if (innerAgent!.AIContextProviders != null)
        {
            Assert.DoesNotContain(innerAgent.AIContextProviders, p => p is BackgroundAgentsProvider);
        }
    }

    /// <summary>
    /// Verify that BackgroundAgentsProviderOptions is passed through when specified.
    /// </summary>
    [Fact]
    public async Task BackgroundAgentsProvider_UsesProvidedOptionsAsync()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;
        var bgAgentMock = new Mock<AIAgent>();
        bgAgentMock.Setup(a => a.Name).Returns("TestBackgroundAgent");
        bgAgentMock.Setup(a => a.Description).Returns("A test background agent");
        var providerOptions = new BackgroundAgentsProviderOptions
        {
            Instructions = "Custom instructions with {background_agents} list.",
        };
        var options = CreateAllDisabledOptions();
        options.BackgroundAgents = [bgAgentMock.Object];
        options.BackgroundAgentsProviderOptions = providerOptions;

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
        var innerAgent = agent.GetService<ChatClientAgent>();
        var bgProvider = innerAgent!.AIContextProviders!.OfType<BackgroundAgentsProvider>().Single();

#pragma warning disable MAAI001
        var invokingContext = new AIContextProvider.InvokingContext(
            new Mock<AIAgent>().Object,
            new Mock<AgentSession>().Object,
            new AIContext());
#pragma warning restore MAAI001

        AIContext result = await bgProvider.InvokingAsync(invokingContext);

        // Assert — custom instructions template is used and agent info is included
        Assert.NotNull(result.Instructions);
        Assert.Contains("Custom instructions with", result.Instructions);
        Assert.Contains("TestBackgroundAgent", result.Instructions);
    }

    /// <summary>
    /// Verify that multiple background agents are all passed to the provider.
    /// </summary>
    [Fact]
    public async Task BackgroundAgentsProvider_IncludesMultipleAgentsAsync()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;
        var agent1Mock = new Mock<AIAgent>();
        agent1Mock.Setup(a => a.Name).Returns("Agent1");
        agent1Mock.Setup(a => a.Description).Returns("First agent");
        var agent2Mock = new Mock<AIAgent>();
        agent2Mock.Setup(a => a.Name).Returns("Agent2");
        agent2Mock.Setup(a => a.Description).Returns("Second agent");
        var options = CreateAllDisabledOptions();
        options.BackgroundAgents = [agent1Mock.Object, agent2Mock.Object];

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
        var innerAgent = agent.GetService<ChatClientAgent>();
        var bgProvider = innerAgent!.AIContextProviders!.OfType<BackgroundAgentsProvider>().Single();

#pragma warning disable MAAI001
        var invokingContext = new AIContextProvider.InvokingContext(
            new Mock<AIAgent>().Object,
            new Mock<AgentSession>().Object,
            new AIContext());
#pragma warning restore MAAI001

        AIContext result = await bgProvider.InvokingAsync(invokingContext);

        // Assert — both agents appear in the provider's instructions
        Assert.NotNull(result.Instructions);
        Assert.Contains("Agent1", result.Instructions);
        Assert.Contains("First agent", result.Instructions);
        Assert.Contains("Agent2", result.Instructions);
        Assert.Contains("Second agent", result.Instructions);
    }

    #endregion

#if NET
    #region Feature: ShellEnvironmentProvider

    /// <summary>
    /// Verify that ShellEnvironmentProvider is included when ShellExecutor is provided.
    /// </summary>
    [Fact]
    public void ShellEnvironmentProvider_IncludedWhenExecutorProvided()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;
        var executorMock = new Mock<ShellExecutor>();
        executorMock.Setup(e => e.AsAIFunction(It.IsAny<string>(), It.IsAny<string?>(), It.IsAny<bool>()))
            .Returns(AIFunctionFactory.Create(() => "test", "run_shell"));
        var options = CreateAllDisabledOptions();
        options.ShellExecutor = executorMock.Object;

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
        var innerAgent = agent.GetService<ChatClientAgent>();

        // Assert
        Assert.NotNull(innerAgent?.AIContextProviders);
        Assert.Contains(innerAgent!.AIContextProviders!, p => p is ShellEnvironmentProvider);
    }

    /// <summary>
    /// Verify that ShellEnvironmentProvider is not included when ShellExecutor is null.
    /// </summary>
    [Fact]
    public void ShellEnvironmentProvider_ExcludedWhenExecutorNull()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;
        var options = CreateAllDisabledOptions();
        options.ShellExecutor = null;

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
        var innerAgent = agent.GetService<ChatClientAgent>();

        // Assert
        Assert.NotNull(innerAgent);
        Assert.NotNull(innerAgent!.AIContextProviders);
        Assert.DoesNotContain(innerAgent.AIContextProviders!, p => p is ShellEnvironmentProvider);
    }

    /// <summary>
    /// Verify that the shell tool AIFunction is added to ChatOptions.Tools when ShellExecutor is provided.
    /// </summary>
    [Fact]
    public async Task ShellExecutor_ToolAddedToChatOptionsAsync()
    {
        // Arrange
        ChatOptions? capturedOptions = null;
        var chatClientMock = new Mock<IChatClient>();
        chatClientMock
            .Setup(c => c.GetResponseAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), It.IsAny<CancellationToken>()))
            .Callback<IEnumerable<ChatMessage>, ChatOptions?, CancellationToken>((_, opts, _) => capturedOptions = opts)
            .ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "done")));

        var executorMock = new Mock<ShellExecutor>();
        executorMock.Setup(e => e.AsAIFunction(It.IsAny<string>(), It.IsAny<string?>(), It.IsAny<bool>()))
            .Returns(AIFunctionFactory.Create(() => "shell output", "run_shell"));

        var options = CreateAllDisabledOptions();
        options.DisableWebSearch = true;
        options.ShellExecutor = executorMock.Object;

        // Act
        var agent = new HarnessAgent(chatClientMock.Object, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
        var session = await agent.CreateSessionAsync();
        await agent.RunAsync([new ChatMessage(ChatRole.User, "Hi")], session);

        // Assert — the shell tool should be present
        Assert.NotNull(capturedOptions?.Tools);
        Assert.Contains(capturedOptions!.Tools!, t => t is AIFunction f && f.Name == "run_shell");
    }

    /// <summary>
    /// Verify that ShellEnvironmentProvider is present when ShellEnvironmentProviderOptions is also specified.
    /// </summary>
    [Fact]
    public void ShellEnvironmentProvider_PresentWhenOptionsProvided()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;
        var executorMock = new Mock<ShellExecutor>();
        executorMock.Setup(e => e.AsAIFunction(It.IsAny<string>(), It.IsAny<string?>(), It.IsAny<bool>()))
            .Returns(AIFunctionFactory.Create(() => "test", "run_shell"));
        var envOptions = new ShellEnvironmentProviderOptions
        {
            ProbeTools = ["git", "python"],
        };
        var options = CreateAllDisabledOptions();
        options.ShellExecutor = executorMock.Object;
        options.ShellEnvironmentProviderOptions = envOptions;

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
        var innerAgent = agent.GetService<ChatClientAgent>();

        // Assert — provider should exist (options wiring is validated by the provider's behavior)
        Assert.NotNull(innerAgent?.AIContextProviders);
        Assert.Contains(innerAgent!.AIContextProviders!, p => p is ShellEnvironmentProvider);
    }

    #endregion
#endif

    #region LoggerFactory and ServiceProvider

    /// <summary>
    /// Verify that the constructor succeeds when loggerFactory is provided.
    /// </summary>
    [Fact]
    public void Constructor_SucceedsWithLoggerFactory()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;
        var loggerFactory = new Mock<ILoggerFactory>().Object;

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions(), loggerFactory);

        // Assert
        Assert.NotNull(agent);
    }

    /// <summary>
    /// Verify that the constructor succeeds when serviceProvider is provided.
    /// </summary>
    [Fact]
    public void Constructor_SucceedsWithServiceProvider()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;
        var services = new Mock<IServiceProvider>().Object;

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions(), services: services);

        // Assert
        Assert.NotNull(agent);
    }

    /// <summary>
    /// Verify that the constructor succeeds when both loggerFactory and serviceProvider are provided.
    /// </summary>
    [Fact]
    public void Constructor_SucceedsWithLoggerFactoryAndServiceProvider()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;
        var loggerFactory = new Mock<ILoggerFactory>().Object;
        var services = new Mock<IServiceProvider>().Object;

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions(), loggerFactory, services);

        // Assert
        Assert.NotNull(agent);
    }

    /// <summary>
    /// Verify that AsHarnessAgent extension method accepts loggerFactory and serviceProvider.
    /// </summary>
    [Fact]
    public void AsHarnessAgent_SucceedsWithLoggerFactoryAndServiceProvider()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;
        var loggerFactory = new Mock<ILoggerFactory>().Object;
        var services = new Mock<IServiceProvider>().Object;

        // Act
        var agent = chatClient.AsHarnessAgent(TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions(), loggerFactory, services);

        // Assert
        Assert.NotNull(agent);
    }

    /// <summary>
    /// Verify that ILoggerFactory is threaded to downstream components by confirming CreateLogger is called.
    /// </summary>
    [Fact]
    public void Constructor_LoggerFactoryIsUsedByDownstreamComponents()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;
        var mockLoggerFactory = new Mock<ILoggerFactory>();
        mockLoggerFactory
            .Setup(lf => lf.CreateLogger(It.IsAny<string>()))
            .Returns(new Mock<ILogger>().Object);

        // Act — use options that leave CompactionProvider and AgentSkillsProvider enabled
        var options = new HarnessAgentOptions
        {
            DisableToolApproval = true,
            DisableOpenTelemetry = true,
            DisableFileMemory = true,
            DisableFileAccess = true,
            DisableWebSearch = true,
            DisableTodoProvider = true,
            DisableAgentModeProvider = true,
        };
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options, mockLoggerFactory.Object);

        // Assert — CreateLogger should have been called by one or more downstream components
        Assert.NotNull(agent);
        mockLoggerFactory.Verify(lf => lf.CreateLogger(It.IsAny<string>()), Times.AtLeastOnce());
    }

    /// <summary>
    /// Verify that IServiceProvider is propagated through the agent pipeline by confirming
    /// it is queried during agent construction.
    /// </summary>
    [Fact]
    public void Constructor_ServiceProviderIsQueriedDuringBuild()
    {
        // Arrange
        var chatClient = new Mock<IChatClient>().Object;
        var mockServices = new Mock<IServiceProvider>();
        mockServices
            .Setup(sp => sp.GetService(It.IsAny<Type>()))
            .Returns(null!);

        // Act
        var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions(), services: mockServices.Object);

        // Assert — the service provider should have been queried during pipeline construction
        Assert.NotNull(agent);
        mockServices.Verify(sp => sp.GetService(It.IsAny<Type>()), Times.AtLeastOnce());
    }

    #endregion
}
