// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.TestHost;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Net.Http.Headers;

namespace Microsoft.AspNetCore.CookiePolicy.Test;

public class CookieConsentTests
{
    [Fact]
    public async Task ConsentChecksOffByDefault()
    {
        var httpContext = await RunTestAsync(options => { }, requestContext => { }, context =>
        {
            var feature = context.Features.Get<ITrackingConsentFeature>();
            Assert.False(feature.IsConsentNeeded);
            Assert.False(feature.HasConsent);
            Assert.True(feature.CanTrack);
            context.Response.Cookies.Append("Test", "Value");
            return Task.CompletedTask;
        });
        Assert.Equal("Test=Value; path=/", httpContext.Response.Headers.SetCookie);
    }

    [Fact]
    public async Task ConsentEnabledForTemplateScenario()
    {
        var httpContext = await RunTestAsync(options =>
        {
            options.CheckConsentNeeded = context => true;
        },
        requestContext => { }, context =>
        {
            var feature = context.Features.Get<ITrackingConsentFeature>();
            Assert.True(feature.IsConsentNeeded);
            Assert.False(feature.HasConsent);
            Assert.False(feature.CanTrack);
            context.Response.Cookies.Append("Test", "Value");
            return Task.CompletedTask;
        });
        Assert.Equal(0, httpContext.Response.Headers.SetCookie.Count);
    }

    [Fact]
    public async Task NonEssentialCookiesWithOptionsExcluded()
    {
        var httpContext = await RunTestAsync(options =>
        {
            options.CheckConsentNeeded = context => true;
        },
        requestContext => { }, context =>
        {
            var feature = context.Features.Get<ITrackingConsentFeature>();
            Assert.True(feature.IsConsentNeeded);
            Assert.False(feature.HasConsent);
            Assert.False(feature.CanTrack);
            context.Response.Cookies.Append("Test", "Value", new CookieOptions() { IsEssential = false });
            return Task.CompletedTask;
        });
        Assert.Equal(0, httpContext.Response.Headers.SetCookie.Count);
    }

    [Fact]
    public async Task NonEssentialCookiesCanBeAllowedViaOnAppendCookie()
    {
        var httpContext = await RunTestAsync(options =>
        {
            options.CheckConsentNeeded = context => true;
            options.OnAppendCookie = context =>
            {
                Assert.True(context.IsConsentNeeded);
                Assert.False(context.HasConsent);
                Assert.False(context.IssueCookie);
                context.IssueCookie = true;
            };
        },
        requestContext => { }, context =>
        {
            var feature = context.Features.Get<ITrackingConsentFeature>();
            Assert.True(feature.IsConsentNeeded);
            Assert.False(feature.HasConsent);
            Assert.False(feature.CanTrack);
            context.Response.Cookies.Append("Test", "Value", new CookieOptions() { IsEssential = false });
            return Task.CompletedTask;
        });
        Assert.Equal("Test=Value; path=/", httpContext.Response.Headers.SetCookie);
    }

    [Fact]
    public async Task NeedsConsentDoesNotPreventEssentialCookies()
    {
        var httpContext = await RunTestAsync(options =>
        {
            options.CheckConsentNeeded = context => true;
        },
        requestContext => { }, context =>
        {
            var feature = context.Features.Get<ITrackingConsentFeature>();
            Assert.True(feature.IsConsentNeeded);
            Assert.False(feature.HasConsent);
            Assert.False(feature.CanTrack);
            context.Response.Cookies.Append("Test", "Value", new CookieOptions() { IsEssential = true });
            return Task.CompletedTask;
        });
        Assert.Equal("Test=Value; path=/", httpContext.Response.Headers.SetCookie);
    }

    [Fact]
    public async Task EssentialCookiesCanBeExcludedByOnAppendCookie()
    {
        var httpContext = await RunTestAsync(options =>
        {
            options.CheckConsentNeeded = context => true;
            options.OnAppendCookie = context =>
            {
                Assert.True(context.IsConsentNeeded);
                Assert.True(context.HasConsent);
                Assert.True(context.IssueCookie);
                context.IssueCookie = false;
            };
        },
        requestContext =>
        {
            requestContext.Request.Headers.Cookie = ".AspNet.Consent=yes";
        },
        context =>
        {
            var feature = context.Features.Get<ITrackingConsentFeature>();
            Assert.True(feature.IsConsentNeeded);
            Assert.True(feature.HasConsent);
            Assert.True(feature.CanTrack);
            context.Response.Cookies.Append("Test", "Value", new CookieOptions() { IsEssential = true });
            return Task.CompletedTask;
        });
        Assert.Equal(0, httpContext.Response.Headers.SetCookie.Count);
    }

    [Fact]
    public async Task HasConsentReadsRequestCookie()
    {
        var httpContext = await RunTestAsync(options =>
        {
            options.CheckConsentNeeded = context => true;
        },
        requestContext =>
        {
            requestContext.Request.Headers.Cookie = ".AspNet.Consent=yes";
        },
        context =>
        {
            var feature = context.Features.Get<ITrackingConsentFeature>();
            Assert.True(feature.IsConsentNeeded);
            Assert.True(feature.HasConsent);
            Assert.True(feature.CanTrack);
            context.Response.Cookies.Append("Test", "Value");
            return Task.CompletedTask;
        });
        Assert.Equal("Test=Value; path=/", httpContext.Response.Headers.SetCookie);
    }

    [Fact]
    public async Task HasConsentIgnoresInvalidRequestCookie()
    {
        var httpContext = await RunTestAsync(options =>
        {
            options.CheckConsentNeeded = context => true;
        },
        requestContext =>
        {
            requestContext.Request.Headers.Cookie = ".AspNet.Consent=IAmATeapot";
        },
        context =>
        {
            var feature = context.Features.Get<ITrackingConsentFeature>();
            Assert.True(feature.IsConsentNeeded);
            Assert.False(feature.HasConsent);
            Assert.False(feature.CanTrack);
            context.Response.Cookies.Append("Test", "Value");
            return Task.CompletedTask;
        });
        Assert.Equal(0, httpContext.Response.Headers.SetCookie.Count);
    }

    [Fact]
    public async Task GrantConsentSetsCookie()
    {
        var httpContext = await RunTestAsync(options =>
        {
            options.CheckConsentNeeded = context => true;
        },
        requestContext => { },
        context =>
        {
            var feature = context.Features.Get<ITrackingConsentFeature>();
            Assert.True(feature.IsConsentNeeded);
            Assert.False(feature.HasConsent);
            Assert.False(feature.CanTrack);

            feature.GrantConsent();

            Assert.True(feature.IsConsentNeeded);
            Assert.True(feature.HasConsent);
            Assert.True(feature.CanTrack);

            context.Response.Cookies.Append("Test", "Value");
            return Task.CompletedTask;
        });

        var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers.SetCookie);
        Assert.Equal(2, cookies.Count);
        var consentCookie = cookies[0];
        Assert.Equal(".AspNet.Consent", consentCookie.Name.AsSpan());
        Assert.Equal("yes", consentCookie.Value.AsSpan());
        Assert.True(consentCookie.Expires.HasValue);
        Assert.True(consentCookie.Expires.Value > DateTimeOffset.Now + TimeSpan.FromDays(364));
        Assert.Equal(Net.Http.Headers.SameSiteMode.Unspecified, consentCookie.SameSite);
        Assert.NotNull(consentCookie.Expires);
        var testCookie = cookies[1];
        Assert.Equal("Test", testCookie.Name.AsSpan());
        Assert.Equal("Value", testCookie.Value.AsSpan());
        Assert.Equal(Net.Http.Headers.SameSiteMode.Unspecified, testCookie.SameSite);
        Assert.Null(testCookie.Expires);
    }

    [Fact]
    public async Task GrantConsentAppliesPolicyToConsentCookie()
    {
        var httpContext = await RunTestAsync(options =>
        {
            options.CheckConsentNeeded = context => true;
            options.MinimumSameSitePolicy = Http.SameSiteMode.Strict;
            options.OnAppendCookie = context =>
            {
                Assert.Equal(".AspNet.Consent", context.CookieName.AsSpan());
                Assert.Equal("yes", context.CookieValue.AsSpan());
                Assert.Equal(Http.SameSiteMode.Strict, context.CookieOptions.SameSite);
                context.CookieName += "1";
                context.CookieValue += "1";
                context.CookieOptions.Extensions.Add("extension");
            };
        },
        requestContext => { },
        context =>
        {
            var feature = context.Features.Get<ITrackingConsentFeature>();
            Assert.True(feature.IsConsentNeeded);
            Assert.False(feature.HasConsent);
            Assert.False(feature.CanTrack);

            feature.GrantConsent();

            Assert.True(feature.IsConsentNeeded);
            Assert.True(feature.HasConsent);
            Assert.True(feature.CanTrack);

            return Task.CompletedTask;
        });

        var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers.SetCookie);
        Assert.Single(cookies);
        var consentCookie = cookies[0];
        Assert.Equal(".AspNet.Consent1", consentCookie.Name.AsSpan());
        Assert.Equal("yes1", consentCookie.Value.AsSpan());
        Assert.Equal(Net.Http.Headers.SameSiteMode.Strict, consentCookie.SameSite);
        Assert.NotNull(consentCookie.Expires);
        Assert.Contains("extension", consentCookie.Extensions);
    }

    [Fact]
    public async Task GrantConsentWhenAlreadyHasItDoesNotSetCookie()
    {
        var httpContext = await RunTestAsync(options =>
        {
            options.CheckConsentNeeded = context => true;
        },
        requestContext =>
        {
            requestContext.Request.Headers.Cookie = ".AspNet.Consent=yes";
        },
        context =>
        {
            var feature = context.Features.Get<ITrackingConsentFeature>();
            Assert.True(feature.IsConsentNeeded);
            Assert.True(feature.HasConsent);
            Assert.True(feature.CanTrack);

            feature.GrantConsent();

            Assert.True(feature.IsConsentNeeded);
            Assert.True(feature.HasConsent);
            Assert.True(feature.CanTrack);

            context.Response.Cookies.Append("Test", "Value");
            return Task.CompletedTask;
        });

        Assert.Equal("Test=Value; path=/", httpContext.Response.Headers.SetCookie);
    }

    [Fact]
    public async Task GrantConsentAfterResponseStartsSetsHasConsentButDoesNotSetCookie()
    {
        var httpContext = await RunTestAsync(options =>
        {
            options.CheckConsentNeeded = context => true;
        },
        requestContext => { },
        async context =>
        {
            var feature = context.Features.Get<ITrackingConsentFeature>();
            Assert.True(feature.IsConsentNeeded);
            Assert.False(feature.HasConsent);
            Assert.False(feature.CanTrack);

            await context.Response.WriteAsync("Started.");

            feature.GrantConsent();

            Assert.True(feature.IsConsentNeeded);
            Assert.True(feature.HasConsent);
            Assert.True(feature.CanTrack);

            Assert.Throws<InvalidOperationException>(() => context.Response.Cookies.Append("Test", "Value"));

            await context.Response.WriteAsync("Granted.");
        });

        var reader = new StreamReader(httpContext.Response.Body);
        Assert.Equal("Started.Granted.", await reader.ReadToEndAsync());
        Assert.Equal(0, httpContext.Response.Headers.SetCookie.Count);
    }

    [Fact]
    public async Task WithdrawConsentWhenNotHasConsentNoOps()
    {
        var httpContext = await RunTestAsync(options =>
        {
            options.CheckConsentNeeded = context => true;
        },
        requestContext => { },
        context =>
        {
            var feature = context.Features.Get<ITrackingConsentFeature>();
            Assert.True(feature.IsConsentNeeded);
            Assert.False(feature.HasConsent);
            Assert.False(feature.CanTrack);

            feature.WithdrawConsent();

            Assert.True(feature.IsConsentNeeded);
            Assert.False(feature.HasConsent);
            Assert.False(feature.CanTrack);

            context.Response.Cookies.Append("Test", "Value");
            return Task.CompletedTask;
        });

        Assert.Equal(0, httpContext.Response.Headers.SetCookie.Count);
    }

    [Fact]
    public async Task WithdrawConsentDeletesCookie()
    {
        var httpContext = await RunTestAsync(options =>
        {
            options.CheckConsentNeeded = context => true;
        },
        requestContext =>
        {
            requestContext.Request.Headers.Cookie = ".AspNet.Consent=yes";
        },
        context =>
        {
            var feature = context.Features.Get<ITrackingConsentFeature>();
            Assert.True(feature.IsConsentNeeded);
            Assert.True(feature.HasConsent);
            Assert.True(feature.CanTrack);
            context.Response.Cookies.Append("Test", "Value1");

            feature.WithdrawConsent();

            Assert.True(feature.IsConsentNeeded);
            Assert.False(feature.HasConsent);
            Assert.False(feature.CanTrack);

            context.Response.Cookies.Append("Test", "Value2");
            return Task.CompletedTask;
        });

        var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers.SetCookie);
        Assert.Equal(2, cookies.Count);
        var testCookie = cookies[0];
        Assert.Equal("Test", testCookie.Name.AsSpan());
        Assert.Equal("Value1", testCookie.Value.AsSpan());
        Assert.Equal(Net.Http.Headers.SameSiteMode.Unspecified, testCookie.SameSite);
        Assert.Null(testCookie.Expires);
        var consentCookie = cookies[1];
        Assert.Equal(".AspNet.Consent", consentCookie.Name.AsSpan());
        Assert.Equal("", consentCookie.Value.AsSpan());
        Assert.Equal(Net.Http.Headers.SameSiteMode.Unspecified, consentCookie.SameSite);
        Assert.NotNull(consentCookie.Expires);
    }

    [Fact]
    public async Task WithdrawConsentAppliesPolicyToDeleteCookie()
    {
        var httpContext = await RunTestAsync(options =>
        {
            options.CheckConsentNeeded = context => true;
            options.MinimumSameSitePolicy = Http.SameSiteMode.Strict;
            options.OnDeleteCookie = context =>
            {
                Assert.Equal(".AspNet.Consent", context.CookieName.AsSpan());
                context.CookieName += "1";
            };
        },
        requestContext =>
        {
            requestContext.Request.Headers.Cookie = ".AspNet.Consent=yes";
        },
        context =>
        {
            var feature = context.Features.Get<ITrackingConsentFeature>();
            Assert.True(feature.IsConsentNeeded);
            Assert.True(feature.HasConsent);
            Assert.True(feature.CanTrack);

            feature.WithdrawConsent();

            Assert.True(feature.IsConsentNeeded);
            Assert.False(feature.HasConsent);
            Assert.False(feature.CanTrack);

            return Task.CompletedTask;
        });

        var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers.SetCookie);
        Assert.Single(cookies);
        var consentCookie = cookies[0];
        Assert.Equal(".AspNet.Consent1", consentCookie.Name.AsSpan());
        Assert.Equal("", consentCookie.Value.AsSpan());
        Assert.Equal(Net.Http.Headers.SameSiteMode.Strict, consentCookie.SameSite);
        Assert.NotNull(consentCookie.Expires);
    }

    [Fact]
    public async Task WithdrawConsentAfterResponseHasStartedDoesNotDeleteCookie()
    {
        var httpContext = await RunTestAsync(options =>
        {
            options.CheckConsentNeeded = context => true;
        },
        requestContext =>
        {
            requestContext.Request.Headers.Cookie = ".AspNet.Consent=yes";
        },
        async context =>
        {
            var feature = context.Features.Get<ITrackingConsentFeature>();
            Assert.True(feature.IsConsentNeeded);
            Assert.True(feature.HasConsent);
            Assert.True(feature.CanTrack);
            context.Response.Cookies.Append("Test", "Value1");

            await context.Response.WriteAsync("Started.");

            feature.WithdrawConsent();

            Assert.True(feature.IsConsentNeeded);
            Assert.False(feature.HasConsent);
            Assert.False(feature.CanTrack);

            // Doesn't throw the normal InvalidOperationException because the cookie is never written
            context.Response.Cookies.Append("Test", "Value2");

            await context.Response.WriteAsync("Withdrawn.");
        });

        var reader = new StreamReader(httpContext.Response.Body);
        Assert.Equal("Started.Withdrawn.", await reader.ReadToEndAsync());
        Assert.Equal("Test=Value1; path=/", httpContext.Response.Headers.SetCookie);
    }

    [Fact]
    public async Task DeleteCookieDoesNotRequireConsent()
    {
        var httpContext = await RunTestAsync(options =>
        {
            options.CheckConsentNeeded = context => true;
        },
        requestContext => { },
        context =>
        {
            var feature = context.Features.Get<ITrackingConsentFeature>();
            Assert.True(feature.IsConsentNeeded);
            Assert.False(feature.HasConsent);
            Assert.False(feature.CanTrack);
            context.Response.Cookies.Delete("Test");
            return Task.CompletedTask;
        });

        var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers.SetCookie);
        Assert.Single(cookies);
        var testCookie = cookies[0];
        Assert.Equal("Test", testCookie.Name.AsSpan());
        Assert.Equal("", testCookie.Value.AsSpan());
        Assert.Equal(Net.Http.Headers.SameSiteMode.Unspecified, testCookie.SameSite);
        Assert.NotNull(testCookie.Expires);
    }

    [Fact]
    public async Task OnDeleteCookieCanSuppressCookie()
    {
        var httpContext = await RunTestAsync(options =>
        {
            options.CheckConsentNeeded = context => true;
            options.OnDeleteCookie = context =>
            {
                Assert.True(context.IsConsentNeeded);
                Assert.False(context.HasConsent);
                Assert.True(context.IssueCookie);
                context.IssueCookie = false;
            };
        },
        requestContext => { },
        context =>
        {
            var feature = context.Features.Get<ITrackingConsentFeature>();
            Assert.True(feature.IsConsentNeeded);
            Assert.False(feature.HasConsent);
            Assert.False(feature.CanTrack);
            context.Response.Cookies.Delete("Test");
            return Task.CompletedTask;
        });

        Assert.Equal(0, httpContext.Response.Headers.SetCookie.Count);
    }

    [Fact]
    public async Task CreateConsentCookieMatchesGrantConsentCookie()
    {
        var httpContext = await RunTestAsync(options =>
        {
            options.CheckConsentNeeded = context => true;
        },
        requestContext => { },
        context =>
        {
            var feature = context.Features.Get<ITrackingConsentFeature>();
            Assert.True(feature.IsConsentNeeded);
            Assert.False(feature.HasConsent);
            Assert.False(feature.CanTrack);

            feature.GrantConsent();

            Assert.True(feature.IsConsentNeeded);
            Assert.True(feature.HasConsent);
            Assert.True(feature.CanTrack);

            var cookie = feature.CreateConsentCookie();
            context.Response.Headers["ManualCookie"] = cookie;

            return Task.CompletedTask;
        });

        var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers.SetCookie);
        Assert.Single(cookies);
        var consentCookie = cookies[0];
        Assert.Equal(".AspNet.Consent", consentCookie.Name.AsSpan());
        Assert.Equal("yes", consentCookie.Value.AsSpan());
        Assert.Equal(Net.Http.Headers.SameSiteMode.Unspecified, consentCookie.SameSite);
        Assert.NotNull(consentCookie.Expires);

        cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers["ManualCookie"]);
        Assert.Single(cookies);
        var manualCookie = cookies[0];
        Assert.Equal(consentCookie.Name, manualCookie.Name);
        Assert.Equal(consentCookie.Value, manualCookie.Value);
        Assert.Equal(consentCookie.SameSite, manualCookie.SameSite);
        Assert.NotNull(manualCookie.Expires); // Expires may not exactly match to the second.
    }

    [Fact]
    public async Task CreateConsentCookieAppliesPolicy()
    {
        var httpContext = await RunTestAsync(options =>
        {
            options.CheckConsentNeeded = context => true;
            options.MinimumSameSitePolicy = Http.SameSiteMode.Strict;
            options.OnAppendCookie = context =>
            {
                Assert.Equal(".AspNet.Consent", context.CookieName);
                Assert.Equal("yes", context.CookieValue);
                Assert.Equal(Http.SameSiteMode.Strict, context.CookieOptions.SameSite);
                context.CookieName += "1";
                context.CookieValue += "1";
            };
        },
        requestContext => { },
        context =>
        {
            var feature = context.Features.Get<ITrackingConsentFeature>();
            Assert.True(feature.IsConsentNeeded);
            Assert.False(feature.HasConsent);
            Assert.False(feature.CanTrack);

            feature.GrantConsent();

            Assert.True(feature.IsConsentNeeded);
            Assert.True(feature.HasConsent);
            Assert.True(feature.CanTrack);

            var cookie = feature.CreateConsentCookie();
            context.Response.Headers["ManualCookie"] = cookie;

            return Task.CompletedTask;
        });

        var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers.SetCookie);
        Assert.Single(cookies);
        var consentCookie = cookies[0];
        Assert.Equal(".AspNet.Consent1", consentCookie.Name.AsSpan());
        Assert.Equal("yes1", consentCookie.Value.AsSpan());
        Assert.Equal(Net.Http.Headers.SameSiteMode.Strict, consentCookie.SameSite);
        Assert.NotNull(consentCookie.Expires);

        cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers["ManualCookie"]);
        Assert.Single(cookies);
        var manualCookie = cookies[0];
        Assert.Equal(consentCookie.Name, manualCookie.Name);
        Assert.Equal(consentCookie.Value, manualCookie.Value);
        Assert.Equal(consentCookie.SameSite, manualCookie.SameSite);
        Assert.NotNull(manualCookie.Expires); // Expires may not exactly match to the second.
    }

    [Fact]
    public async Task CreateConsentCookieMatchesGrantConsentCookieWhenCookieValueIsCustom()
    {
        var httpContext = await RunTestAsync(options =>
        {
            options.CheckConsentNeeded = context => true;
            options.ConsentCookieValue = "true";
        },
        requestContext => { },
        context =>
        {
            var feature = context.Features.Get<ITrackingConsentFeature>();
            Assert.True(feature.IsConsentNeeded);
            Assert.False(feature.HasConsent);
            Assert.False(feature.CanTrack);

            feature.GrantConsent();

            Assert.True(feature.IsConsentNeeded);
            Assert.True(feature.HasConsent);
            Assert.True(feature.CanTrack);

            var cookie = feature.CreateConsentCookie();
            context.Response.Headers["ManualCookie"] = cookie;

            return Task.CompletedTask;
        });

        var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers.SetCookie);
        Assert.Single(cookies);
        var consentCookie = cookies[0];
        Assert.Equal(".AspNet.Consent", consentCookie.Name.AsSpan());
        Assert.Equal("true", consentCookie.Value.AsSpan());
        Assert.Equal(Net.Http.Headers.SameSiteMode.Unspecified, consentCookie.SameSite);
        Assert.NotNull(consentCookie.Expires);

        cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers["ManualCookie"]);
        Assert.Single(cookies);
        var manualCookie = cookies[0];
        Assert.Equal(consentCookie.Name, manualCookie.Name);
        Assert.Equal(consentCookie.Value, manualCookie.Value);
        Assert.Equal(consentCookie.SameSite, manualCookie.SameSite);
        Assert.NotNull(manualCookie.Expires); // Expires may not exactly match to the second.
    }

    [Theory]
    [InlineData(null, "Value cannot be null.")]
    [InlineData("", "The value cannot be an empty string.")]
    public void CreateCookiePolicyOptionsWithEmptyConsentCookieValueThrows(string value, string expectedMessage)
    {
        var options = new CookiePolicyOptions();

        ExceptionAssert.ThrowsArgument(
            () => options.ConsentCookieValue = value,
            "value",
            expectedMessage);
    }

    private async Task<HttpContext> RunTestAsync(Action<CookiePolicyOptions> configureOptions, Action<HttpContext> configureRequest, RequestDelegate handleRequest)
    {
        var host = new HostBuilder()
            .ConfigureWebHost(webHostBuilder =>
            {
                webHostBuilder
                    .Configure(app =>
                    {
                        app.UseCookiePolicy();
                        app.Run(handleRequest);
                    })
                    .UseTestServer();
            })
            .ConfigureServices(services =>
            {
                services.Configure(configureOptions);
            })
            .Build();

        var server = host.GetTestServer();

        await host.StartAsync();

        return await server.SendAsync(configureRequest);
    }
}
