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

using System;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using Moq;
using Xunit;

namespace Microsoft.AspNetCore.Antiforgery.Internal
{
    public class DefaultAntiforgeryTest
    {
        private const string ResponseCacheHeadersOverrideWarningMessage =
            "The 'Cache-Control' and 'Pragma' headers have been overridden and set to 'no-cache, no-store' and " +
             "'no-cache' respectively to prevent caching of this response. Any response that uses antiforgery " +
            "should not be cached.";

        [Fact]
        public async Task ChecksSSL_ValidateRequestAsync_Throws()
        {
            // Arrange
            var httpContext = GetHttpContext();
            var options = new AntiforgeryOptions
            {
                Cookie = new CookieBuilder
                {
                    SecurePolicy = CookieSecurePolicy.Always
                }
            };
            var antiforgery = GetAntiforgery(httpContext, options);

            // Act & Assert
            var exception = await Assert.ThrowsAsync<InvalidOperationException>(
                () => antiforgery.ValidateRequestAsync(httpContext));
            Assert.Equal(
                @"The antiforgery system has the configuration value AntiforgeryOptions.Cookie.SecurePolicy = Always, " +
                "but the current request is not an SSL request.",
                exception.Message);
        }

        [Fact]
        public async Task ChecksSSL_IsRequestValidAsync_Throws()
        {
            // Arrange
            var httpContext = GetHttpContext();
            var options = new AntiforgeryOptions()
            {
                Cookie = { SecurePolicy = CookieSecurePolicy.Always }
            };

            var antiforgery = GetAntiforgery(httpContext, options);

            // Act & Assert
            var exception = await Assert.ThrowsAsync<InvalidOperationException>(
                () => antiforgery.IsRequestValidAsync(httpContext));
            Assert.Equal(
                @"The antiforgery system has the configuration value AntiforgeryOptions.Cookie.SecurePolicy = Always, " +
                "but the current request is not an SSL request.",
                exception.Message);
        }

        [Fact]
        public void ChecksSSL_GetAndStoreTokens_Throws()
        {
            // Arrange
            var httpContext = GetHttpContext();
            var options = new AntiforgeryOptions()
            {
                Cookie = { SecurePolicy = CookieSecurePolicy.Always }
            };

            var antiforgery = GetAntiforgery(httpContext, options);

            // Act & Assert
            var exception = Assert.Throws<InvalidOperationException>(
                () => antiforgery.GetAndStoreTokens(httpContext));
            Assert.Equal(
                 @"The antiforgery system has the configuration value AntiforgeryOptions.Cookie.SecurePolicy = Always, " +
                 "but the current request is not an SSL request.",
                 exception.Message);
        }

        [Fact]
        public void ChecksSSL_GetTokens_Throws()
        {
            // Arrange
            var httpContext = GetHttpContext();
            var options = new AntiforgeryOptions()
            {
                Cookie = { SecurePolicy = CookieSecurePolicy.Always }
            };

            var antiforgery = GetAntiforgery(httpContext, options);

            // Act & Assert
            var exception = Assert.Throws<InvalidOperationException>(
                () => antiforgery.GetTokens(httpContext));
            Assert.Equal(
                 @"The antiforgery system has the configuration value AntiforgeryOptions.Cookie.SecurePolicy = Always, " +
                 "but the current request is not an SSL request.",
                 exception.Message);
        }

        [Fact]
        public void ChecksSSL_SetCookieTokenAndHeader_Throws()
        {
            // Arrange
            var httpContext = GetHttpContext();
            var options = new AntiforgeryOptions()
            {
                Cookie = { SecurePolicy = CookieSecurePolicy.Always }
            };

            var antiforgery = GetAntiforgery(httpContext, options);

            // Act & Assert
            var exception = Assert.Throws<InvalidOperationException>(
                () => antiforgery.SetCookieTokenAndHeader(httpContext));
            Assert.Equal(
                 @"The antiforgery system has the configuration value AntiforgeryOptions.Cookie.SecurePolicy = Always, " +
                 "but the current request is not an SSL request.",
                 exception.Message);
        }

        [Fact]
        public void GetTokens_ExistingInvalidCookieToken_GeneratesANewCookieTokenAndANewFormToken()
        {
            // Arrange
            var antiforgeryFeature = new AntiforgeryFeature();
            // Generate a new cookie.
            var context = CreateMockContext(
                new AntiforgeryOptions(),
                useOldCookie: false,
                isOldCookieValid: false,
                antiforgeryFeature: antiforgeryFeature);
            var antiforgery = GetAntiforgery(context);

            // Act
            var tokenset = antiforgery.GetTokens(context.HttpContext);

            // Assert
            Assert.Equal(context.TestTokenSet.NewCookieTokenString, tokenset.CookieToken);
            Assert.Equal(context.TestTokenSet.FormTokenString, tokenset.RequestToken);

            Assert.NotNull(antiforgeryFeature);
            Assert.True(antiforgeryFeature.HaveDeserializedCookieToken);
            Assert.Equal(context.TestTokenSet.OldCookieToken, antiforgeryFeature.CookieToken);
            Assert.True(antiforgeryFeature.HaveGeneratedNewCookieToken);
            Assert.Equal(context.TestTokenSet.NewCookieToken, antiforgeryFeature.NewCookieToken);
            Assert.Equal(context.TestTokenSet.NewCookieTokenString, antiforgeryFeature.NewCookieTokenString);
            Assert.Equal(context.TestTokenSet.RequestToken, antiforgeryFeature.NewRequestToken);
            Assert.Equal(context.TestTokenSet.FormTokenString, antiforgeryFeature.NewRequestTokenString);
        }

        [Fact]
        public void GetTokens_ExistingInvalidCookieToken_SwallowsExceptions()
        {
            // Arrange
            // Make sure the existing cookie is invalid.
            var context = CreateMockContext(
                new AntiforgeryOptions(),
                useOldCookie: false,
                isOldCookieValid: false);

            // Exception will cause the cookieToken to be null.
            context.TokenSerializer
                .Setup(o => o.Deserialize(context.TestTokenSet.OldCookieTokenString))
                .Throws(new Exception("should be swallowed"));
            context.TokenGenerator
                .Setup(o => o.IsCookieTokenValid(null))
                .Returns(false);

            var antiforgery = GetAntiforgery(context);

            // Act
            var tokenset = antiforgery.GetTokens(context.HttpContext);

            // Assert
            Assert.Equal(context.TestTokenSet.NewCookieTokenString, tokenset.CookieToken);
            Assert.Equal(context.TestTokenSet.FormTokenString, tokenset.RequestToken);
        }

        [Fact]
        public void GetTokens_ExistingValidCookieToken_GeneratesANewFormToken()
        {
            // Arrange
            var antiforgeryFeature = new AntiforgeryFeature();
            var context = CreateMockContext(
                new AntiforgeryOptions(),
                useOldCookie: true,
                isOldCookieValid: true,
                antiforgeryFeature: antiforgeryFeature);
            var antiforgery = GetAntiforgery(context);

            // Act
            var tokenset = antiforgery.GetTokens(context.HttpContext);

            // Assert
            Assert.Null(tokenset.CookieToken);
            Assert.Equal(context.TestTokenSet.FormTokenString, tokenset.RequestToken);

            Assert.NotNull(antiforgeryFeature);
            Assert.True(antiforgeryFeature.HaveDeserializedCookieToken);
            Assert.Equal(context.TestTokenSet.OldCookieToken, antiforgeryFeature.CookieToken);
            Assert.True(antiforgeryFeature.HaveGeneratedNewCookieToken);
            Assert.Null(antiforgeryFeature.NewCookieToken);
            Assert.Equal(context.TestTokenSet.RequestToken, antiforgeryFeature.NewRequestToken);
            Assert.Equal(context.TestTokenSet.FormTokenString, antiforgeryFeature.NewRequestTokenString);
        }

        [Fact]
        public void GetTokens_DoesNotSerializeTwice()
        {
            // Arrange
            var antiforgeryFeature = new AntiforgeryFeature
            {
                HaveDeserializedCookieToken = true,
                HaveGeneratedNewCookieToken = true,
                NewRequestToken = new AntiforgeryToken(),
                NewRequestTokenString = "serialized-form-token-from-context",
            };
            var context = CreateMockContext(
                new AntiforgeryOptions(),
                useOldCookie: true,
                isOldCookieValid: true,
                antiforgeryFeature: antiforgeryFeature);

            var antiforgery = GetAntiforgery(context);

            // Act
            var tokenset = antiforgery.GetTokens(context.HttpContext);

            // Assert
            Assert.Null(tokenset.CookieToken);
            Assert.Equal("serialized-form-token-from-context", tokenset.RequestToken);

            Assert.Null(antiforgeryFeature.NewCookieToken);

            // Token serializer not used.
            context.TokenSerializer.Verify(
                o => o.Deserialize(It.IsAny<string>()),
                Times.Never);
            context.TokenSerializer.Verify(
                o => o.Serialize(It.IsAny<AntiforgeryToken>()),
                Times.Never);
        }

        [Fact]
        public void GetAndStoreTokens_ExistingValidCookieToken_NotOverriden()
        {
            // Arrange
            var antiforgeryFeature = new AntiforgeryFeature();
            var context = CreateMockContext(
                new AntiforgeryOptions(),
                useOldCookie: true,
                isOldCookieValid: true,
                antiforgeryFeature: antiforgeryFeature);
            var antiforgery = GetAntiforgery(context);

            // Act
            var tokenSet = antiforgery.GetAndStoreTokens(context.HttpContext);

            // Assert
            // We shouldn't have saved the cookie because it already existed.
            context.TokenStore.Verify(
                t => t.SaveCookieToken(It.IsAny<HttpContext>(), It.IsAny<string>()),
                Times.Never);

            Assert.Null(tokenSet.CookieToken);
            Assert.Equal(context.TestTokenSet.FormTokenString, tokenSet.RequestToken);

            Assert.NotNull(antiforgeryFeature);
            Assert.True(antiforgeryFeature.HaveDeserializedCookieToken);
            Assert.Equal(context.TestTokenSet.OldCookieToken, antiforgeryFeature.CookieToken);
            Assert.True(antiforgeryFeature.HaveGeneratedNewCookieToken);
            Assert.Null(antiforgeryFeature.NewCookieToken);
            Assert.Equal(context.TestTokenSet.RequestToken, antiforgeryFeature.NewRequestToken);
            Assert.Equal(context.TestTokenSet.FormTokenString, antiforgeryFeature.NewRequestTokenString);
        }

        [Fact]
        public void GetAndStoreTokens_ExistingValidCookieToken_NotOverriden_AndSetsDoNotCacheHeaders()
        {
            // Arrange
            var antiforgeryFeature = new AntiforgeryFeature();
            var context = CreateMockContext(
                new AntiforgeryOptions(),
                useOldCookie: true,
                isOldCookieValid: true,
                antiforgeryFeature: antiforgeryFeature);
            var antiforgery = GetAntiforgery(context);

            // Act
            var tokenSet = antiforgery.GetAndStoreTokens(context.HttpContext);

            // Assert
            // We shouldn't have saved the cookie because it already existed.
            context.TokenStore.Verify(
                t => t.SaveCookieToken(It.IsAny<HttpContext>(), It.IsAny<string>()),
                Times.Never);

            Assert.Null(tokenSet.CookieToken);
            Assert.Equal(context.TestTokenSet.FormTokenString, tokenSet.RequestToken);

            Assert.NotNull(antiforgeryFeature);
            Assert.Equal(context.TestTokenSet.OldCookieToken, antiforgeryFeature.CookieToken);
            Assert.Equal("no-cache, no-store", context.HttpContext.Response.Headers.CacheControl);
            Assert.Equal("no-cache", context.HttpContext.Response.Headers.Pragma);
        }

        [Fact]
        public void GetAndStoreTokens_ExistingCachingHeaders_Overriden()
        {
            // Arrange
            var antiforgeryFeature = new AntiforgeryFeature();
            var context = CreateMockContext(
                new AntiforgeryOptions(),
                useOldCookie: true,
                isOldCookieValid: true,
                antiforgeryFeature: antiforgeryFeature);
            var antiforgery = GetAntiforgery(context);
            context.HttpContext.Response.Headers.CacheControl = "public";

            // Act
            var tokenSet = antiforgery.GetAndStoreTokens(context.HttpContext);

            // Assert
            // We shouldn't have saved the cookie because it already existed.
            context.TokenStore.Verify(
                t => t.SaveCookieToken(It.IsAny<HttpContext>(), It.IsAny<string>()),
                Times.Never);

            Assert.Null(tokenSet.CookieToken);
            Assert.Equal(context.TestTokenSet.FormTokenString, tokenSet.RequestToken);

            Assert.NotNull(antiforgeryFeature);
            Assert.Equal(context.TestTokenSet.OldCookieToken, antiforgeryFeature.CookieToken);
            Assert.Equal("no-cache, no-store", context.HttpContext.Response.Headers.CacheControl);
            Assert.Equal("no-cache", context.HttpContext.Response.Headers.Pragma);
        }

        private string GetAndStoreTokens_CacheHeadersArrangeAct(TestSink testSink, string headerName, string headerValue)
        {
            // Arrange
            var loggerFactory = new Mock<ILoggerFactory>();
            loggerFactory
                .Setup(lf => lf.CreateLogger(typeof(DefaultAntiforgery).FullName!))
                .Returns(new TestLogger("test logger", testSink, enabled: true));
            var services = new ServiceCollection();
            services.AddSingleton(loggerFactory.Object);
            var antiforgeryFeature = new AntiforgeryFeature();
            var context = CreateMockContext(
                new AntiforgeryOptions(),
                useOldCookie: true,
                isOldCookieValid: true,
                antiforgeryFeature: antiforgeryFeature);
            context.HttpContext.RequestServices = services.BuildServiceProvider();
            var antiforgery = GetAntiforgery(context);
            context.HttpContext.Response.Headers[headerName] = headerValue;

            // Act
            antiforgery.GetAndStoreTokens(context.HttpContext);
            return context.HttpContext.Response.Headers[headerName].ToString();
        }

        [Theory]
        [InlineData("Cache-Control", "no-cache, no-store")]
        [InlineData("Cache-Control", "NO-CACHE, NO-STORE")]
        [InlineData("Cache-Control", "no-cache, no-store, private")]
        [InlineData("Cache-Control", "NO-CACHE, NO-STORE, PRIVATE")]
        public void GetAndStoreTokens_DoesNotOverwriteCacheControlHeader(string headerName, string headerValue)
        {
            var testSink = new TestSink();
            var actualHeaderValue = GetAndStoreTokens_CacheHeadersArrangeAct(testSink, headerName, headerValue);

            // Assert
            Assert.Equal(headerValue, actualHeaderValue);

            var hasWarningMessage = testSink.Writes
                .Where(wc => wc.LogLevel == LogLevel.Warning)
                .Select(wc => wc.State?.ToString())
                .Contains(ResponseCacheHeadersOverrideWarningMessage);
            Assert.False(hasWarningMessage);
        }

        [Theory]
        [InlineData("Cache-Control", "no-cache, private")]
        [InlineData("Cache-Control", "NO-CACHE, PRIVATE")]
        public void GetAndStoreTokens_OverwritesCacheControlHeader_IfNoStoreIsNotSet(string headerName, string headerValue)
        {
            var testSink = new TestSink();
            var actualHeaderValue = GetAndStoreTokens_CacheHeadersArrangeAct(testSink, headerName, headerValue);

            // Assert
            Assert.NotEqual(headerValue, actualHeaderValue);

            var hasWarningMessage = testSink.Writes
                .Where(wc => wc.LogLevel == LogLevel.Warning)
                .Select(wc => wc.State?.ToString())
                .Contains(ResponseCacheHeadersOverrideWarningMessage);
            Assert.True(hasWarningMessage);
        }

        [Theory]
        [InlineData("Cache-Control", "no-store, private")]
        [InlineData("Cache-Control", "NO-STORE, PRIVATE")]
        public void GetAndStoreTokens_OverwritesCacheControlHeader_IfNoCacheIsNotSet(string headerName, string headerValue)
        {
            var testSink = new TestSink();
            var actualHeaderValue = GetAndStoreTokens_CacheHeadersArrangeAct(testSink, headerName, headerValue);

            // Assert
            Assert.NotEqual(headerValue, actualHeaderValue);

            var hasWarningMessage = testSink.Writes
                .Where(wc => wc.LogLevel == LogLevel.Warning)
                .Select(wc => wc.State?.ToString())
                .Contains(ResponseCacheHeadersOverrideWarningMessage);
            Assert.True(hasWarningMessage);
        }

        [Theory]
        [InlineData("Pragma", "no-cache")]
        [InlineData("Pragma", "NO-CACHE")]
        public void GetAndStoreTokens_DoesNotOverwritePragmaHeader(string headerName, string headerValue)
        {
            var testSink = new TestSink();
            var actualHeaderValue = GetAndStoreTokens_CacheHeadersArrangeAct(testSink, headerName, headerValue);

            // Assert
            Assert.Equal(headerValue, actualHeaderValue);

            var hasWarningMessage = testSink.Writes
                .Where(wc => wc.LogLevel == LogLevel.Warning)
                .Select(wc => wc.State?.ToString())
                .Contains(ResponseCacheHeadersOverrideWarningMessage);
            Assert.False(hasWarningMessage);
        }

        [Fact]
        public void GetAndStoreTokens_NoExistingCookieToken_Saved()
        {
            // Arrange
            var antiforgeryFeature = new AntiforgeryFeature();
            var context = CreateMockContext(
                new AntiforgeryOptions(),
                useOldCookie: false,
                isOldCookieValid: false,
                antiforgeryFeature: antiforgeryFeature);
            var antiforgery = GetAntiforgery(context);

            // Act
            var tokenSet = antiforgery.GetAndStoreTokens(context.HttpContext);

            // Assert
            context.TokenStore.Verify(
                t => t.SaveCookieToken(It.IsAny<HttpContext>(), context.TestTokenSet.NewCookieTokenString),
                Times.Once);

            Assert.Equal(context.TestTokenSet.NewCookieTokenString, tokenSet.CookieToken);
            Assert.Equal(context.TestTokenSet.FormTokenString, tokenSet.RequestToken);

            Assert.NotNull(antiforgeryFeature);
            Assert.True(antiforgeryFeature.HaveDeserializedCookieToken);
            Assert.Equal(context.TestTokenSet.OldCookieToken, antiforgeryFeature.CookieToken);
            Assert.True(antiforgeryFeature.HaveGeneratedNewCookieToken);
            Assert.Equal(context.TestTokenSet.NewCookieToken, antiforgeryFeature.NewCookieToken);
            Assert.Equal(context.TestTokenSet.NewCookieTokenString, antiforgeryFeature.NewCookieTokenString);
            Assert.Equal(context.TestTokenSet.RequestToken, antiforgeryFeature.NewRequestToken);
            Assert.Equal(context.TestTokenSet.FormTokenString, antiforgeryFeature.NewRequestTokenString);
            Assert.True(antiforgeryFeature.HaveStoredNewCookieToken);
        }

        [Fact]
        public void GetAndStoreTokens_NoExistingCookieToken_Saved_AndSetsDoNotCacheHeaders()
        {
            // Arrange
            var antiforgeryFeature = new AntiforgeryFeature();
            var context = CreateMockContext(
                new AntiforgeryOptions(),
                useOldCookie: false,
                isOldCookieValid: false,
                antiforgeryFeature: antiforgeryFeature);
            var antiforgery = GetAntiforgery(context);

            // Act
            var tokenSet = antiforgery.GetAndStoreTokens(context.HttpContext);

            // Assert
            context.TokenStore.Verify(
                t => t.SaveCookieToken(It.IsAny<HttpContext>(), context.TestTokenSet.NewCookieTokenString),
                Times.Once);

            Assert.Equal(context.TestTokenSet.NewCookieTokenString, tokenSet.CookieToken);
            Assert.Equal(context.TestTokenSet.FormTokenString, tokenSet.RequestToken);

            Assert.NotNull(antiforgeryFeature);
            Assert.True(antiforgeryFeature.HaveDeserializedCookieToken);
            Assert.Equal(context.TestTokenSet.OldCookieToken, antiforgeryFeature.CookieToken);
            Assert.Equal("no-cache, no-store", context.HttpContext.Response.Headers.CacheControl);
            Assert.Equal("no-cache", context.HttpContext.Response.Headers.Pragma);
        }

        [Fact]
        public void GetAndStoreTokens_DoesNotSerializeTwice()
        {
            // Arrange
            var antiforgeryFeature = new AntiforgeryFeature
            {
                HaveDeserializedCookieToken = true,
                HaveGeneratedNewCookieToken = true,
                NewCookieToken = new AntiforgeryToken(),
                NewCookieTokenString = "serialized-cookie-token-from-context",
                NewRequestToken = new AntiforgeryToken(),
                NewRequestTokenString = "serialized-form-token-from-context",
            };
            var context = CreateMockContext(
                new AntiforgeryOptions(),
                useOldCookie: true,
                isOldCookieValid: true,
                antiforgeryFeature: antiforgeryFeature);
            var antiforgery = GetAntiforgery(context);

            context.TokenStore
                .Setup(t => t.SaveCookieToken(context.HttpContext, "serialized-cookie-token-from-context"))
                .Verifiable();

            // Act
            var tokenset = antiforgery.GetAndStoreTokens(context.HttpContext);

            // Assert
            // Token store used once, with expected arguments.
            // Passed context's cookie token though request's cookie token was valid.
            context.TokenStore.Verify(
                t => t.SaveCookieToken(context.HttpContext, "serialized-cookie-token-from-context"),
                Times.Once);

            // Token serializer not used.
            context.TokenSerializer.Verify(
                o => o.Deserialize(It.IsAny<string>()),
                Times.Never);
            context.TokenSerializer.Verify(
                o => o.Serialize(It.IsAny<AntiforgeryToken>()),
                Times.Never);

            Assert.Equal("serialized-cookie-token-from-context", tokenset.CookieToken);
            Assert.Equal("serialized-form-token-from-context", tokenset.RequestToken);

            Assert.True(antiforgeryFeature.HaveStoredNewCookieToken);
        }

        [Fact]
        public void GetAndStoreTokens_DoesNotStoreTwice()
        {
            // Arrange
            var antiforgeryFeature = new AntiforgeryFeature
            {
                HaveDeserializedCookieToken = true,
                HaveGeneratedNewCookieToken = true,
                HaveStoredNewCookieToken = true,
                NewCookieToken = new AntiforgeryToken(),
                NewCookieTokenString = "serialized-cookie-token-from-context",
                NewRequestToken = new AntiforgeryToken(),
                NewRequestTokenString = "serialized-form-token-from-context",
            };
            var context = CreateMockContext(
                new AntiforgeryOptions(),
                useOldCookie: true,
                isOldCookieValid: true,
                antiforgeryFeature: antiforgeryFeature);
            var antiforgery = GetAntiforgery(context);

            // Act
            var tokenset = antiforgery.GetAndStoreTokens(context.HttpContext);

            // Assert
            // Token store not used.
            context.TokenStore.Verify(
                t => t.SaveCookieToken(It.IsAny<HttpContext>(), It.IsAny<string>()),
                Times.Never);

            // Token serializer not used.
            context.TokenSerializer.Verify(
                o => o.Deserialize(It.IsAny<string>()),
                Times.Never);
            context.TokenSerializer.Verify(
                o => o.Serialize(It.IsAny<AntiforgeryToken>()),
                Times.Never);

            Assert.Equal("serialized-cookie-token-from-context", tokenset.CookieToken);
            Assert.Equal("serialized-form-token-from-context", tokenset.RequestToken);
        }

        [Fact]
        public async Task IsRequestValidAsync_FromStore_Failure()
        {
            // Arrange
            var antiforgeryFeature = new AntiforgeryFeature();
            var context = CreateMockContext(new AntiforgeryOptions(), antiforgeryFeature: antiforgeryFeature);

            string? message;
            context.TokenGenerator
                .Setup(o => o.TryValidateTokenSet(
                    context.HttpContext,
                    context.TestTokenSet.OldCookieToken,
                    context.TestTokenSet.RequestToken,
                    out message))
                .Returns(false);

            var antiforgery = GetAntiforgery(context);

            // Act
            var result = await antiforgery.IsRequestValidAsync(context.HttpContext);

            // Assert
            Assert.False(result);
            context.TokenGenerator.Verify();

            // Failed _after_ updating the AntiforgeryContext.
            Assert.NotNull(antiforgeryFeature);
            Assert.True(antiforgeryFeature.HaveDeserializedCookieToken);
            Assert.Equal(context.TestTokenSet.OldCookieToken, antiforgeryFeature.CookieToken);
            Assert.True(antiforgeryFeature.HaveDeserializedRequestToken);
            Assert.Equal(context.TestTokenSet.RequestToken, antiforgeryFeature.RequestToken);
        }

        [Fact]
        public async Task IsRequestValidAsync_FromStore_Success()
        {
            // Arrange
            var antiforgeryFeature = new AntiforgeryFeature();
            var context = CreateMockContext(new AntiforgeryOptions(), antiforgeryFeature: antiforgeryFeature);
            context.HttpContext.Request.Method = "POST";

            string? message;
            context.TokenGenerator
                .Setup(o => o.TryValidateTokenSet(
                    context.HttpContext,
                    context.TestTokenSet.OldCookieToken,
                    context.TestTokenSet.RequestToken,
                    out message))
                .Returns(true)
                .Verifiable();

            var antiforgery = GetAntiforgery(context);

            // Act
            var result = await antiforgery.IsRequestValidAsync(context.HttpContext);

            // Assert
            Assert.True(result);
            context.TokenGenerator.Verify();

            Assert.NotNull(antiforgeryFeature);
            Assert.True(antiforgeryFeature.HaveDeserializedCookieToken);
            Assert.Equal(context.TestTokenSet.OldCookieToken, antiforgeryFeature.CookieToken);
            Assert.True(antiforgeryFeature.HaveDeserializedRequestToken);
            Assert.Equal(context.TestTokenSet.RequestToken, antiforgeryFeature.RequestToken);
        }

        [Fact]
        public async Task IsRequestValidAsync_DoesNotDeserializeTwice()
        {
            // Arrange
            var antiforgeryFeature = new AntiforgeryFeature
            {
                HaveDeserializedCookieToken = true,
                CookieToken = new AntiforgeryToken(),
                HaveDeserializedRequestToken = true,
                RequestToken = new AntiforgeryToken(),
            };
            var context = CreateMockContext(new AntiforgeryOptions(), antiforgeryFeature: antiforgeryFeature);
            context.HttpContext.Request.Method = "POST";

            string? message;
            context.TokenGenerator
                .Setup(o => o.TryValidateTokenSet(
                    context.HttpContext,
                    antiforgeryFeature.CookieToken,
                    antiforgeryFeature.RequestToken,
                    out message))
                .Returns(true)
                .Verifiable();

            var antiforgery = GetAntiforgery(context);

            // Act
            var result = await antiforgery.IsRequestValidAsync(context.HttpContext);

            // Assert
            Assert.True(result);
            context.TokenGenerator.Verify();

            // Token serializer not used.
            context.TokenSerializer.Verify(
                o => o.Deserialize(It.IsAny<string>()),
                Times.Never);
            context.TokenSerializer.Verify(
                o => o.Serialize(It.IsAny<AntiforgeryToken>()),
                Times.Never);
        }

        [Theory]
        [InlineData("GeT")]
        [InlineData("HEAD")]
        [InlineData("options")]
        [InlineData("TrAcE")]
        public async Task IsRequestValidAsync_SkipsAntiforgery_ForSafeHttpMethods(string httpMethod)
        {
            // Arrange
            var context = CreateMockContext(new AntiforgeryOptions());
            context.HttpContext.Request.Method = httpMethod;

            string? message;
            context.TokenGenerator
                .Setup(o => o.TryValidateTokenSet(
                    context.HttpContext,
                    It.IsAny<AntiforgeryToken>(),
                    It.IsAny<AntiforgeryToken>(),
                    out message))
                .Returns(false)
                .Verifiable();

            var antiforgery = GetAntiforgery(context);

            // Act
            var result = await antiforgery.IsRequestValidAsync(context.HttpContext);

            // Assert
            Assert.True(result);
            context.TokenGenerator
                .Verify(o => o.TryValidateTokenSet(
                    context.HttpContext,
                    It.IsAny<AntiforgeryToken>(),
                    It.IsAny<AntiforgeryToken>(),
                    out message),
                    Times.Never);
        }

        [Theory]
        [InlineData("PUT")]
        [InlineData("post")]
        [InlineData("Delete")]
        [InlineData("Custom")]
        public async Task IsRequestValidAsync_ValidatesAntiforgery_ForNonSafeHttpMethods(string httpMethod)
        {
            // Arrange
            var context = CreateMockContext(new AntiforgeryOptions());
            context.HttpContext.Request.Method = httpMethod;

            string? message;
            context.TokenGenerator
                .Setup(o => o.TryValidateTokenSet(
                    context.HttpContext,
                    It.IsAny<AntiforgeryToken>(),
                    It.IsAny<AntiforgeryToken>(),
                    out message))
                .Returns(true)
                .Verifiable();

            var antiforgery = GetAntiforgery(context);

            // Act
            var result = await antiforgery.IsRequestValidAsync(context.HttpContext);

            // Assert
            Assert.True(result);
            context.TokenGenerator.Verify();
        }

        [Fact]
        public async Task ValidateRequestAsync_FromStore_Failure()
        {
            // Arrange
            var antiforgeryFeature = new AntiforgeryFeature();
            var context = CreateMockContext(new AntiforgeryOptions(), antiforgeryFeature: antiforgeryFeature);

            var message = "my-message";
            context.TokenGenerator
                .Setup(o => o.TryValidateTokenSet(
                    context.HttpContext,
                    context.TestTokenSet.OldCookieToken,
                    context.TestTokenSet.RequestToken,
                    out message))
                .Returns(false)
                .Verifiable();
            var antiforgery = GetAntiforgery(context);

            // Act & assert
            var exception = await Assert.ThrowsAsync<AntiforgeryValidationException>(
                () => antiforgery.ValidateRequestAsync(context.HttpContext));
            Assert.Equal("my-message", exception.Message);
            context.TokenGenerator.Verify();

            // Failed _after_ updating the AntiforgeryContext.
            Assert.NotNull(antiforgeryFeature);
            Assert.True(antiforgeryFeature.HaveDeserializedCookieToken);
            Assert.Equal(context.TestTokenSet.OldCookieToken, antiforgeryFeature.CookieToken);
            Assert.True(antiforgeryFeature.HaveDeserializedRequestToken);
            Assert.Equal(context.TestTokenSet.RequestToken, antiforgeryFeature.RequestToken);
        }

        [Fact]
        public async Task ValidateRequestAsync_FromStore_Success()
        {
            // Arrange
            var antiforgeryFeature = new AntiforgeryFeature();
            var context = CreateMockContext(new AntiforgeryOptions(), antiforgeryFeature: antiforgeryFeature);

            string? message;
            context.TokenGenerator
                .Setup(o => o.TryValidateTokenSet(
                    context.HttpContext,
                    context.TestTokenSet.OldCookieToken,
                    context.TestTokenSet.RequestToken,
                    out message))
                .Returns(true)
                .Verifiable();

            var antiforgery = GetAntiforgery(context);

            // Act
            await antiforgery.ValidateRequestAsync(context.HttpContext);

            // Assert
            context.TokenGenerator.Verify();

            Assert.NotNull(antiforgeryFeature);
            Assert.True(antiforgeryFeature.HaveDeserializedCookieToken);
            Assert.Equal(context.TestTokenSet.OldCookieToken, antiforgeryFeature.CookieToken);
            Assert.True(antiforgeryFeature.HaveDeserializedRequestToken);
            Assert.Equal(context.TestTokenSet.RequestToken, antiforgeryFeature.RequestToken);
        }

        [Fact]
        public async Task ValidateRequestAsync_NoCookieToken_Throws()
        {
            // Arrange
            var context = CreateMockContext(new AntiforgeryOptions()
            {
                Cookie = { Name = "cookie-name" },
                FormFieldName = "form-field-name",
                HeaderName = null,
            });

            var tokenSet = new AntiforgeryTokenSet(null, null, "form-field-name", null);
            context.TokenStore
                .Setup(s => s.GetRequestTokensAsync(context.HttpContext))
                .Returns(Task.FromResult(tokenSet));

            var antiforgery = GetAntiforgery(context);

            // Act & Assert
            var exception = await Assert.ThrowsAsync<AntiforgeryValidationException>(
                () => antiforgery.ValidateRequestAsync(context.HttpContext));
            Assert.Equal("The required antiforgery cookie \"cookie-name\" is not present.", exception.Message);
        }

        [Fact]
        public async Task ValidateRequestAsync_NonFormRequest_HeaderDisabled_Throws()
        {
            // Arrange
            var context = CreateMockContext(new AntiforgeryOptions()
            {
                Cookie = { Name = "cookie-name" },
                FormFieldName = "form-field-name",
                HeaderName = null,
            });

            var tokenSet = new AntiforgeryTokenSet(null, "cookie-token", "form-field-name", null);
            context.TokenStore
                .Setup(s => s.GetRequestTokensAsync(context.HttpContext))
                .Returns(Task.FromResult(tokenSet));

            var antiforgery = GetAntiforgery(context);

            // Act & Assert
            var exception = await Assert.ThrowsAsync<AntiforgeryValidationException>(
                () => antiforgery.ValidateRequestAsync(context.HttpContext));
            Assert.Equal("The required antiforgery form field \"form-field-name\" is not present.", exception.Message);
        }

        [Fact]
        public async Task ValidateRequestAsync_NonFormRequest_NoHeaderValue_Throws()
        {
            // Arrange
            var context = CreateMockContext(new AntiforgeryOptions()
            {
                Cookie = { Name = "cookie-name" },
                FormFieldName = "form-field-name",
                HeaderName = "header-name",
            });

            context.HttpContext.Request.ContentType = "application/json";

            var tokenSet = new AntiforgeryTokenSet(null, "cookie-token", "form-field-name", "header-name");
            context.TokenStore
                .Setup(s => s.GetRequestTokensAsync(context.HttpContext))
                .Returns(Task.FromResult(tokenSet));

            var antiforgery = GetAntiforgery(context);

            // Act & Assert
            var exception = await Assert.ThrowsAsync<AntiforgeryValidationException>(
                () => antiforgery.ValidateRequestAsync(context.HttpContext));
            Assert.Equal("The required antiforgery header value \"header-name\" is not present.", exception.Message);
        }

        [Fact]
        public async Task ValidateRequestAsync_FormRequest_NoRequestTokenValue_Throws()
        {
            // Arrange
            var context = CreateMockContext(new AntiforgeryOptions()
            {
                Cookie = { Name = "cookie-name" },
                FormFieldName = "form-field-name",
                HeaderName = "header-name",
            });

            context.HttpContext.Request.ContentType = "application/x-www-form-urlencoded";

            var tokenSet = new AntiforgeryTokenSet(null, "cookie-token", "form-field-name", "header-name");
            context.TokenStore
                .Setup(s => s.GetRequestTokensAsync(context.HttpContext))
                .Returns(Task.FromResult(tokenSet));

            var antiforgery = GetAntiforgery(context);

            // Act & Assert
            var exception = await Assert.ThrowsAsync<AntiforgeryValidationException>(
                () => antiforgery.ValidateRequestAsync(context.HttpContext));
            Assert.Equal(
                "The required antiforgery request token was not provided in either form field \"form-field-name\" " +
                "or header value \"header-name\".",
                exception.Message);
        }

        [Fact]
        public async Task ValidateRequestAsync_DoesNotDeserializeTwice()
        {
            // Arrange
            var antiforgeryFeature = new AntiforgeryFeature
            {
                HaveDeserializedCookieToken = true,
                CookieToken = new AntiforgeryToken(),
                HaveDeserializedRequestToken = true,
                RequestToken = new AntiforgeryToken(),
            };
            var context = CreateMockContext(new AntiforgeryOptions(), antiforgeryFeature: antiforgeryFeature);

            string? message;
            context.TokenGenerator
                .Setup(o => o.TryValidateTokenSet(
                    context.HttpContext,
                    antiforgeryFeature.CookieToken,
                    antiforgeryFeature.RequestToken,
                    out message))
                .Returns(true)
                .Verifiable();

            var antiforgery = GetAntiforgery(context);

            // Act
            await antiforgery.ValidateRequestAsync(context.HttpContext);

            // Assert (does not throw)
            context.TokenGenerator.Verify();

            // Token serializer not used.
            context.TokenSerializer.Verify(
                o => o.Deserialize(It.IsAny<string>()),
                Times.Never);
            context.TokenSerializer.Verify(
                o => o.Serialize(It.IsAny<AntiforgeryToken>()),
                Times.Never);
        }

        [Fact]
        public void SetCookieTokenAndHeader_PreserveXFrameOptionsHeader()
        {
            // Arrange
            var options = new AntiforgeryOptions();
            var antiforgeryFeature = new AntiforgeryFeature();
            var expectedHeaderValue = "DIFFERENTORIGIN";

            // Generate a new cookie.
            var context = CreateMockContext(
                options,
                useOldCookie: false,
                isOldCookieValid: false,
                antiforgeryFeature: antiforgeryFeature);
            var antiforgery = GetAntiforgery(context);
            context.HttpContext.Response.Headers["X-Frame-Options"] = expectedHeaderValue;

            // Act
            antiforgery.SetCookieTokenAndHeader(context.HttpContext);

            // Assert
            var xFrameOptions = context.HttpContext.Response.Headers["X-Frame-Options"];
            Assert.Equal(expectedHeaderValue, xFrameOptions);
        }

        [Fact]
        public void SetCookieTokenAndHeader_NewCookieToken_SetsDoNotCacheHeaders()
        {
            // Arrange
            var options = new AntiforgeryOptions();
            var antiforgeryFeature = new AntiforgeryFeature();

            // Generate a new cookie.
            var context = CreateMockContext(
                options,
                useOldCookie: false,
                isOldCookieValid: false,
                antiforgeryFeature: antiforgeryFeature);
            var antiforgery = GetAntiforgery(context);

            // Act
            antiforgery.SetCookieTokenAndHeader(context.HttpContext);

            // Assert
            Assert.Equal("no-cache, no-store", context.HttpContext.Response.Headers["Cache-Control"]);
            Assert.Equal("no-cache", context.HttpContext.Response.Headers["Pragma"]);
        }

        [Fact]
        public void SetCookieTokenAndHeader_ValidOldCookieToken_SetsDoNotCacheHeaders()
        {
            // Arrange
            var options = new AntiforgeryOptions();
            var antiforgeryFeature = new AntiforgeryFeature();

            // Generate a new cookie.
            var context = CreateMockContext(
                options,
                useOldCookie: true,
                isOldCookieValid: true,
                antiforgeryFeature: antiforgeryFeature);
            var antiforgery = GetAntiforgery(context);

            // Act
            antiforgery.SetCookieTokenAndHeader(context.HttpContext);

            // Assert
            Assert.Equal("no-cache, no-store", context.HttpContext.Response.Headers["Cache-Control"]);
            Assert.Equal("no-cache", context.HttpContext.Response.Headers["Pragma"]);
        }

        [Fact]
        public void SetCookieTokenAndHeader_OverridesExistingCachingHeaders()
        {
            // Arrange
            var options = new AntiforgeryOptions();
            var antiforgeryFeature = new AntiforgeryFeature();

            // Generate a new cookie.
            var context = CreateMockContext(
                options,
                useOldCookie: true,
                isOldCookieValid: true,
                antiforgeryFeature: antiforgeryFeature);
            var antiforgery = GetAntiforgery(context);
            context.HttpContext.Response.Headers["Cache-Control"] = "public";

            // Act
            antiforgery.SetCookieTokenAndHeader(context.HttpContext);

            // Assert
            Assert.Equal("no-cache, no-store", context.HttpContext.Response.Headers["Cache-Control"]);
            Assert.Equal("no-cache", context.HttpContext.Response.Headers["Pragma"]);
        }

        [Theory]
        [InlineData(false, "SAMEORIGIN")]
        [InlineData(true, null)]
        public void SetCookieTokenAndHeader_AddsXFrameOptionsHeader(
            bool suppressXFrameOptions,
            string expectedHeaderValue)
        {
            // Arrange
            var options = new AntiforgeryOptions()
            {
                SuppressXFrameOptionsHeader = suppressXFrameOptions
            };
            var antiforgeryFeature = new AntiforgeryFeature();

            // Generate a new cookie.
            var context = CreateMockContext(
                options,
                useOldCookie: false,
                isOldCookieValid: false,
                antiforgeryFeature: antiforgeryFeature);
            var antiforgery = GetAntiforgery(context);

            // Act
            antiforgery.SetCookieTokenAndHeader(context.HttpContext);

            // Assert
            var xFrameOptions = context.HttpContext.Response.Headers["X-Frame-Options"];
            Assert.Equal(expectedHeaderValue, xFrameOptions);

            Assert.NotNull(antiforgeryFeature);
            Assert.True(antiforgeryFeature.HaveDeserializedCookieToken);
            Assert.Equal(context.TestTokenSet.OldCookieToken, antiforgeryFeature.CookieToken);
            Assert.True(antiforgeryFeature.HaveGeneratedNewCookieToken);
            Assert.Equal(context.TestTokenSet.NewCookieToken, antiforgeryFeature.NewCookieToken);
            Assert.Equal(context.TestTokenSet.NewCookieTokenString, antiforgeryFeature.NewCookieTokenString);
            Assert.True(antiforgeryFeature.HaveStoredNewCookieToken);
        }

        [Fact]
        public void SetCookieTokenAndHeader_DoesNotDeserializeTwice()
        {
            // Arrange
            var antiforgeryFeature = new AntiforgeryFeature
            {
                HaveDeserializedCookieToken = true,
                HaveGeneratedNewCookieToken = true,
                NewCookieToken = new AntiforgeryToken(),
                NewCookieTokenString = "serialized-cookie-token-from-context",
                NewRequestToken = new AntiforgeryToken(),
                NewRequestTokenString = "serialized-form-token-from-context",
            };
            var context = CreateMockContext(
                new AntiforgeryOptions(),
                useOldCookie: true,
                isOldCookieValid: true,
                antiforgeryFeature: antiforgeryFeature);
            var antiforgery = GetAntiforgery(context);

            context.TokenStore
                .Setup(t => t.SaveCookieToken(context.HttpContext, "serialized-cookie-token-from-context"))
                .Verifiable();

            // Act
            antiforgery.SetCookieTokenAndHeader(context.HttpContext);

            // Assert
            // Token store used once, with expected arguments.
            // Passed context's cookie token though request's cookie token was valid.
            context.TokenStore.Verify(
                t => t.SaveCookieToken(context.HttpContext, "serialized-cookie-token-from-context"),
                Times.Once);

            // Token serializer not used.
            context.TokenSerializer.Verify(
                o => o.Deserialize(It.IsAny<string>()),
                Times.Never);
            context.TokenSerializer.Verify(
                o => o.Serialize(It.IsAny<AntiforgeryToken>()),
                Times.Never);
        }

        [Fact]
        public void SetCookieTokenAndHeader_DoesNotStoreTwice()
        {
            // Arrange
            var antiforgeryFeature = new AntiforgeryFeature
            {
                HaveDeserializedCookieToken = true,
                HaveGeneratedNewCookieToken = true,
                HaveStoredNewCookieToken = true,
                NewCookieToken = new AntiforgeryToken(),
                NewCookieTokenString = "serialized-cookie-token-from-context",
                NewRequestToken = new AntiforgeryToken(),
                NewRequestTokenString = "serialized-form-token-from-context",
            };
            var context = CreateMockContext(
                new AntiforgeryOptions(),
                useOldCookie: true,
                isOldCookieValid: true,
                antiforgeryFeature: antiforgeryFeature);
            var antiforgery = GetAntiforgery(context);

            // Act
            antiforgery.SetCookieTokenAndHeader(context.HttpContext);

            // Assert
            // Token serializer not used.
            context.TokenSerializer.Verify(
                o => o.Deserialize(It.IsAny<string>()),
                Times.Never);
            context.TokenSerializer.Verify(
                o => o.Serialize(It.IsAny<AntiforgeryToken>()),
                Times.Never);

            // Token store not used.
            context.TokenStore.Verify(
                t => t.SaveCookieToken(It.IsAny<HttpContext>(), It.IsAny<string>()),
                Times.Never);
        }

        [Fact]
        public void SetCookieTokenAndHeader_NullCookieToken()
        {
            // Arrange
            var antiforgeryFeature = new AntiforgeryFeature
            {
                HaveDeserializedCookieToken = false,
                HaveGeneratedNewCookieToken = false,
                HaveStoredNewCookieToken = true,
                NewCookieToken = new AntiforgeryToken(),
                NewCookieTokenString = "serialized-cookie-token-from-context",
                NewRequestToken = new AntiforgeryToken(),
                NewRequestTokenString = "serialized-form-token-from-context",
            };
            var context = CreateMockContext(
                new AntiforgeryOptions(),
                useOldCookie: false,
                isOldCookieValid: false,
                antiforgeryFeature: antiforgeryFeature);
            var testTokenSet = new TestTokenSet
            {
                OldCookieTokenString = null!
            };

            var nullTokenStore = GetTokenStore(context.HttpContext, testTokenSet, false);
            var antiforgery = GetAntiforgery(
                context.HttpContext,
                tokenGenerator: context.TokenGenerator.Object,
                tokenStore: nullTokenStore.Object);

            // Act
            antiforgery.SetCookieTokenAndHeader(context.HttpContext);

            // Assert
            context.TokenSerializer.Verify(s => s.Deserialize(null!), Times.Never);
        }

        [Fact]
        public void SetCookieTokenAndHeader_DoesNotModifyHeadersAfterResponseHasStarted()
        {
            // Arrange
            var antiforgeryFeature = new AntiforgeryFeature
            {
                HaveDeserializedCookieToken = false,
                HaveGeneratedNewCookieToken = false,
                HaveStoredNewCookieToken = true,
                NewCookieToken = new AntiforgeryToken(),
                NewCookieTokenString = "serialized-cookie-token-from-context",
                NewRequestToken = new AntiforgeryToken(),
                NewRequestTokenString = "serialized-form-token-from-context",
            };
            var context = CreateMockContext(
                new AntiforgeryOptions(),
                useOldCookie: false,
                isOldCookieValid: false,
                antiforgeryFeature: antiforgeryFeature);
            var testTokenSet = new TestTokenSet
            {
                OldCookieTokenString = null!
            };

            var nullTokenStore = GetTokenStore(context.HttpContext, testTokenSet, false);
            var antiforgery = GetAntiforgery(
                context.HttpContext,
                tokenGenerator: context.TokenGenerator.Object,
                tokenStore: nullTokenStore.Object);

            TestResponseFeature testResponse = new TestResponseFeature();
            context.HttpContext.Features.Set<IHttpResponseFeature>(testResponse);
            context.HttpContext.Response.Headers["Cache-Control"] = "public";
            testResponse.StartResponse();

            // Act
            antiforgery.SetCookieTokenAndHeader(context.HttpContext);

            Assert.Equal("public", context.HttpContext.Response.Headers["Cache-Control"]);
        }

        [Fact]
        public void GetAndStoreTokens_DoesNotLogWarning_IfNoExistingCacheHeadersPresent()
        {
            // Arrange
            var testSink = new TestSink();
            var loggerFactory = new Mock<ILoggerFactory>();
            loggerFactory
                .Setup(lf => lf.CreateLogger(typeof(DefaultAntiforgery).FullName!))
                .Returns(new TestLogger("test logger", testSink, enabled: true));
            var services = new ServiceCollection();
            services.AddSingleton(loggerFactory.Object);
            var antiforgeryFeature = new AntiforgeryFeature();
            var context = CreateMockContext(
                new AntiforgeryOptions(),
                useOldCookie: false,
                isOldCookieValid: false,
                antiforgeryFeature: antiforgeryFeature);
            context.HttpContext.RequestServices = services.BuildServiceProvider();
            var antiforgery = GetAntiforgery(context);

            // Act
            var tokenSet = antiforgery.GetAndStoreTokens(context.HttpContext);

            // Assert
            var hasWarningMessage = testSink.Writes
                .Where(wc => wc.LogLevel == LogLevel.Warning)
                .Select(wc => wc.State?.ToString())
                .Contains(ResponseCacheHeadersOverrideWarningMessage);
            Assert.False(hasWarningMessage);
        }

        [Theory]
        [InlineData("Cache-Control", "Public")]
        [InlineData("Cache-Control", "PuBlic")]
        [InlineData("Cache-Control", "Private")]
        [InlineData("Cache-Control", "PriVate")]
        [InlineData("Cache-Control", "No-Store")]
        [InlineData("Cache-Control", "No-store")]
        [InlineData("Pragma", "Foo")]
        public void GetAndStoreTokens_LogsWarning_NonNoCacheHeadersAlreadyPresent(string headerName, string headerValue)
        {
            // Arrange
            var testSink = new TestSink();
            var loggerFactory = new Mock<ILoggerFactory>();
            loggerFactory
                .Setup(lf => lf.CreateLogger(typeof(DefaultAntiforgery).FullName!))
                .Returns(new TestLogger("test logger", testSink, enabled: true));
            var services = new ServiceCollection();
            services.AddSingleton(loggerFactory.Object);
            var antiforgeryFeature = new AntiforgeryFeature();
            var context = CreateMockContext(
                new AntiforgeryOptions(),
                useOldCookie: false,
                isOldCookieValid: false,
                antiforgeryFeature: antiforgeryFeature);
            context.HttpContext.RequestServices = services.BuildServiceProvider();
            var antiforgery = GetAntiforgery(context);
            context.HttpContext.Response.Headers[headerName] = headerValue;

            // Act
            var tokenSet = antiforgery.GetAndStoreTokens(context.HttpContext);

            // Assert
            var hasWarningMessage = testSink.Writes
                .Where(wc => wc.LogLevel == LogLevel.Warning)
                .Select(wc => wc.State?.ToString())
                .Contains(ResponseCacheHeadersOverrideWarningMessage);
            Assert.True(hasWarningMessage);
        }

        [Theory]
        [InlineData("Cache-Control", "no-cache, no-store")]
        [InlineData("Pragma", "no-cache")]
        public void GetAndStoreTokens_DoesNotLogsWarning_ForNoCacheHeaders_AlreadyPresent(string headerName, string headerValue)
        {
            // Arrange
            var testSink = new TestSink();
            var loggerFactory = new Mock<ILoggerFactory>();
            loggerFactory
                .Setup(lf => lf.CreateLogger(typeof(DefaultAntiforgery).FullName!))
                .Returns(new TestLogger("test logger", testSink, enabled: true));
            var services = new ServiceCollection();
            services.AddSingleton(loggerFactory.Object);
            var antiforgeryFeature = new AntiforgeryFeature();
            var context = CreateMockContext(
                new AntiforgeryOptions(),
                useOldCookie: false,
                isOldCookieValid: false,
                antiforgeryFeature: antiforgeryFeature);
            context.HttpContext.RequestServices = services.BuildServiceProvider();
            var antiforgery = GetAntiforgery(context);
            context.HttpContext.Response.Headers[headerName] = headerValue;

            // Act
            var tokenSet = antiforgery.GetAndStoreTokens(context.HttpContext);

            // Assert
            var hasWarningMessage = testSink.Writes
                .Where(wc => wc.LogLevel == LogLevel.Warning)
                .Select(wc => wc.State?.ToString())
                .Contains(ResponseCacheHeadersOverrideWarningMessage);
            Assert.False(hasWarningMessage);
        }

        private DefaultAntiforgery GetAntiforgery(
            HttpContext httpContext,
            AntiforgeryOptions? options = null,
            IAntiforgeryTokenGenerator? tokenGenerator = null,
            IAntiforgeryTokenSerializer? tokenSerializer = null,
            IAntiforgeryTokenStore? tokenStore = null)
        {
            var optionsManager = new TestOptionsManager();
            if (options != null)
            {
                optionsManager.Value = options;
            }

            var loggerFactory = httpContext.RequestServices.GetRequiredService<ILoggerFactory>();
            return new DefaultAntiforgery(
                antiforgeryOptionsAccessor: optionsManager,
                tokenGenerator: tokenGenerator!,
                tokenSerializer: tokenSerializer!,
                tokenStore: tokenStore!,
                loggerFactory: loggerFactory);
        }

        private IServiceProvider GetServices()
        {
            var builder = new ServiceCollection();
            builder.AddSingleton<ILoggerFactory>(new LoggerFactory());

            return builder.BuildServiceProvider();
        }

        private HttpContext GetHttpContext(IAntiforgeryFeature? antiforgeryFeature = null)
        {
            var httpContext = new DefaultHttpContext();
            antiforgeryFeature = antiforgeryFeature ?? new AntiforgeryFeature();
            httpContext.Features.Set(antiforgeryFeature);
            httpContext.RequestServices = GetServices();
            httpContext.User = new ClaimsPrincipal(new ClaimsIdentity("some-auth"));

            return httpContext;
        }

        private DefaultAntiforgery GetAntiforgery(AntiforgeryMockContext context)
        {
            return GetAntiforgery(
                context.HttpContext,
                context.Options,
                context.TokenGenerator?.Object,
                context.TokenSerializer?.Object,
                context.TokenStore?.Object);
        }

        private Mock<IAntiforgeryTokenStore> GetTokenStore(
            HttpContext context,
            TestTokenSet testTokenSet,
            bool saveNewCookie = true)
        {
            var oldCookieToken = testTokenSet.OldCookieTokenString;
            var formToken = testTokenSet.FormTokenString;
            var mockTokenStore = new Mock<IAntiforgeryTokenStore>(MockBehavior.Strict);
            mockTokenStore
                .Setup(o => o.GetCookieToken(context))
                .Returns(oldCookieToken);

            mockTokenStore
                .Setup(o => o.GetRequestTokensAsync(context))
                .Returns(() => Task.FromResult(new AntiforgeryTokenSet(
                    formToken,
                    oldCookieToken,
                    "form",
                    "header")));

            if (saveNewCookie)
            {
                var newCookieToken = testTokenSet.NewCookieTokenString;
                mockTokenStore
                    .Setup(o => o.SaveCookieToken(context, newCookieToken))
                    .Verifiable();
            }

            return mockTokenStore;
        }

        private Mock<IAntiforgeryTokenSerializer> GetTokenSerializer(TestTokenSet testTokenSet)
        {
            var oldCookieToken = testTokenSet.OldCookieToken;
            var newCookieToken = testTokenSet.NewCookieToken;
            var formToken = testTokenSet.RequestToken;
            var mockSerializer = new Mock<IAntiforgeryTokenSerializer>(MockBehavior.Strict);
            mockSerializer.Setup(o => o.Serialize(formToken))
                          .Returns(testTokenSet.FormTokenString);
            mockSerializer.Setup(o => o.Deserialize(testTokenSet.FormTokenString))
                          .Returns(formToken);
            mockSerializer.Setup(o => o.Deserialize(testTokenSet.OldCookieTokenString))
                          .Returns(oldCookieToken);
            mockSerializer.Setup(o => o.Serialize(oldCookieToken))
                          .Returns(testTokenSet.OldCookieTokenString);
            mockSerializer.Setup(o => o.Serialize(newCookieToken))
                          .Returns(testTokenSet.NewCookieTokenString);
            return mockSerializer;
        }

        private AntiforgeryMockContext CreateMockContext(
            AntiforgeryOptions options,
            bool useOldCookie = false,
            bool isOldCookieValid = true,
            IAntiforgeryFeature? antiforgeryFeature = null)
        {
            // Arrange
            var httpContext = GetHttpContext(antiforgeryFeature);
            var testTokenSet = GetTokenSet();

            var mockSerializer = GetTokenSerializer(testTokenSet);

            var mockTokenStore = GetTokenStore(httpContext, testTokenSet, !useOldCookie);

            var mockGenerator = new Mock<IAntiforgeryTokenGenerator>(MockBehavior.Strict);
            mockGenerator
                .Setup(o => o.GenerateRequestToken(
                    httpContext,
                    useOldCookie ? testTokenSet.OldCookieToken : testTokenSet.NewCookieToken))
                .Returns(testTokenSet.RequestToken);

            mockGenerator
                .Setup(o => o.GenerateCookieToken())
                .Returns(useOldCookie ? testTokenSet.OldCookieToken : testTokenSet.NewCookieToken);
            mockGenerator
                .Setup(o => o.IsCookieTokenValid(null))
                .Returns(false);
            mockGenerator
                .Setup(o => o.IsCookieTokenValid(testTokenSet.OldCookieToken))
                .Returns(isOldCookieValid);

            mockGenerator
                .Setup(o => o.IsCookieTokenValid(testTokenSet.NewCookieToken))
                .Returns(!isOldCookieValid);

            return new AntiforgeryMockContext()
            {
                Options = options,
                HttpContext = httpContext,
                TokenGenerator = mockGenerator,
                TokenSerializer = mockSerializer,
                TokenStore = mockTokenStore,
                TestTokenSet = testTokenSet
            };
        }

        private TestTokenSet GetTokenSet()
        {
            return new TestTokenSet()
            {
                RequestToken = new AntiforgeryToken() { IsCookieToken = false },
                FormTokenString = "serialized-form-token",
                OldCookieToken = new AntiforgeryToken() { IsCookieToken = true },
                OldCookieTokenString = "serialized-old-cookie-token",
                NewCookieToken = new AntiforgeryToken() { IsCookieToken = true },
                NewCookieTokenString = "serialized-new-cookie-token",
            };
        }

        private class TestTokenSet
        {
            public AntiforgeryToken RequestToken { get; set; } = default!;

            public string FormTokenString { get; set; } = default!;

            public AntiforgeryToken OldCookieToken { get; set; } = default!;

            public string OldCookieTokenString { get; set; } = default!;

            public AntiforgeryToken NewCookieToken { get; set; } = default!;

            public string NewCookieTokenString { get; set; } = default!;
        }

        private class AntiforgeryMockContext
        {
            public AntiforgeryOptions Options { get; set; } = default!;

            public TestTokenSet TestTokenSet { get; set; } = default!;

            public HttpContext HttpContext { get; set; } = default!;

            public Mock<IAntiforgeryTokenGenerator> TokenGenerator { get; set; } = default!;

            public Mock<IAntiforgeryTokenStore> TokenStore { get; set; } = default!;

            public Mock<IAntiforgeryTokenSerializer> TokenSerializer { get; set; } = default!;
        }

        private class TestOptionsManager : IOptions<AntiforgeryOptions>
        {
            public AntiforgeryOptions Value { get; set; } = new AntiforgeryOptions();
        }

        private class TestResponseFeature : HttpResponseFeature
        {
            private bool _hasStarted = false;

            public override bool HasStarted { get => _hasStarted; }

            public TestResponseFeature()
            {
            }

            public void StartResponse()
            {
                _hasStarted = true;
            }
        }
    }
}
