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

#nullable disable
using System;
using System.Security.Claims;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Testing;
using Moq;
using Xunit;

namespace Microsoft.AspNetCore.Antiforgery.Internal
{
    public class DefaultAntiforgeryTokenGeneratorProviderTest
    {
        [Fact]
        public void GenerateCookieToken()
        {
            // Arrange
            var tokenProvider = new DefaultAntiforgeryTokenGenerator(
                claimUidExtractor: null,
                additionalDataProvider: null);

            // Act
            var token = tokenProvider.GenerateCookieToken();

            // Assert
            Assert.NotNull(token);
        }

        [Fact]
        public void GenerateRequestToken_InvalidCookieToken()
        {
            // Arrange
            var cookieToken = new AntiforgeryToken() { IsCookieToken = false };
            var httpContext = new DefaultHttpContext();
            httpContext.User = new ClaimsPrincipal(new ClaimsIdentity());
            Assert.False(httpContext.User.Identity.IsAuthenticated);

            var tokenProvider = new DefaultAntiforgeryTokenGenerator(
                claimUidExtractor: null,
                additionalDataProvider: null);

            // Act & Assert
            ExceptionAssert.ThrowsArgument(
                () => tokenProvider.GenerateRequestToken(httpContext, cookieToken),
                "cookieToken",
                "The antiforgery cookie token is invalid.");
        }

        [Fact]
        public void GenerateRequestToken_AnonymousUser()
        {
            // Arrange
            var cookieToken = new AntiforgeryToken() { IsCookieToken = true };
            var httpContext = new DefaultHttpContext();
            httpContext.User = new ClaimsPrincipal(new ClaimsIdentity());
            Assert.False(httpContext.User.Identity.IsAuthenticated);

            var tokenProvider = new DefaultAntiforgeryTokenGenerator(
                claimUidExtractor: null,
                additionalDataProvider: null);

            // Act
            var fieldToken = tokenProvider.GenerateRequestToken(httpContext, cookieToken);

            // Assert
            Assert.NotNull(fieldToken);
            Assert.Equal(cookieToken.SecurityToken, fieldToken.SecurityToken);
            Assert.False(fieldToken.IsCookieToken);
            Assert.Empty(fieldToken.Username);
            Assert.Null(fieldToken.ClaimUid);
            Assert.Empty(fieldToken.AdditionalData);
        }

        [Fact]
        public void GenerateRequestToken_AuthenticatedWithoutUsernameAndNoAdditionalData_NoAdditionalData()
        {
            // Arrange
            var cookieToken = new AntiforgeryToken()
            {
                IsCookieToken = true
            };

            var httpContext = new DefaultHttpContext();
            httpContext.User = new ClaimsPrincipal(new MyAuthenticatedIdentityWithoutUsername());

            var options = new AntiforgeryOptions();
            var claimUidExtractor = new Mock<IClaimUidExtractor>().Object;

            var tokenProvider = new DefaultAntiforgeryTokenGenerator(
                claimUidExtractor: claimUidExtractor,
                additionalDataProvider: null);

            // Act & assert
            var exception = Assert.Throws<InvalidOperationException>(
                    () => tokenProvider.GenerateRequestToken(httpContext, cookieToken));
            Assert.Equal(
                "The provided identity of type " +
                $"'{typeof(MyAuthenticatedIdentityWithoutUsername).FullName}' " +
                "is marked IsAuthenticated = true but does not have a value for Name. " +
                "By default, the antiforgery system requires that all authenticated identities have a unique Name. " +
                "If it is not possible to provide a unique Name for this identity, " +
                "consider extending IAntiforgeryAdditionalDataProvider by overriding the " +
                "DefaultAntiforgeryAdditionalDataProvider " +
                "or a custom type that can provide some form of unique identifier for the current user.",
                exception.Message);
        }

        [Fact]
        public void GenerateRequestToken_AuthenticatedWithoutUsername_WithAdditionalData()
        {
            // Arrange
            var cookieToken = new AntiforgeryToken() { IsCookieToken = true };

            var httpContext = new DefaultHttpContext();
            httpContext.User = new ClaimsPrincipal(new MyAuthenticatedIdentityWithoutUsername());

            var mockAdditionalDataProvider = new Mock<IAntiforgeryAdditionalDataProvider>();
            mockAdditionalDataProvider.Setup(o => o.GetAdditionalData(httpContext))
                                      .Returns("additional-data");

            var claimUidExtractor = new Mock<IClaimUidExtractor>().Object;

            var tokenProvider = new DefaultAntiforgeryTokenGenerator(
                claimUidExtractor: claimUidExtractor,
                additionalDataProvider: mockAdditionalDataProvider.Object);

            // Act
            var fieldToken = tokenProvider.GenerateRequestToken(httpContext, cookieToken);

            // Assert
            Assert.NotNull(fieldToken);
            Assert.Equal(cookieToken.SecurityToken, fieldToken.SecurityToken);
            Assert.False(fieldToken.IsCookieToken);
            Assert.Empty(fieldToken.Username);
            Assert.Null(fieldToken.ClaimUid);
            Assert.Equal("additional-data", fieldToken.AdditionalData);
        }

        [Fact]
        public void GenerateRequestToken_ClaimsBasedIdentity()
        {
            // Arrange
            var cookieToken = new AntiforgeryToken() { IsCookieToken = true };

            var identity = GetAuthenticatedIdentity("some-identity");
            var httpContext = new DefaultHttpContext();
            httpContext.User = new ClaimsPrincipal(identity);

            byte[] data = new byte[256 / 8];
            RandomNumberGenerator.Fill(data);
            var base64ClaimUId = Convert.ToBase64String(data);
            var expectedClaimUid = new BinaryBlob(256, data);

            var mockClaimUidExtractor = new Mock<IClaimUidExtractor>();
            mockClaimUidExtractor.Setup(o => o.ExtractClaimUid(It.Is<ClaimsPrincipal>(c => c.Identity == identity)))
                                 .Returns(base64ClaimUId);

            var tokenProvider = new DefaultAntiforgeryTokenGenerator(
                claimUidExtractor: mockClaimUidExtractor.Object,
                additionalDataProvider: null);

            // Act
            var fieldToken = tokenProvider.GenerateRequestToken(httpContext, cookieToken);

            // Assert
            Assert.NotNull(fieldToken);
            Assert.Equal(cookieToken.SecurityToken, fieldToken.SecurityToken);
            Assert.False(fieldToken.IsCookieToken);
            Assert.Equal("", fieldToken.Username);
            Assert.Equal(expectedClaimUid, fieldToken.ClaimUid);
            Assert.Equal("", fieldToken.AdditionalData);
        }

        [Fact]
        public void GenerateRequestToken_RegularUserWithUsername()
        {
            // Arrange
            var cookieToken = new AntiforgeryToken() { IsCookieToken = true };

            var httpContext = new DefaultHttpContext();
            var mockIdentity = new Mock<ClaimsIdentity>();
            mockIdentity.Setup(o => o.IsAuthenticated)
                        .Returns(true);
            mockIdentity.Setup(o => o.Name)
                        .Returns("my-username");

            httpContext.User = new ClaimsPrincipal(mockIdentity.Object);

            var claimUidExtractor = new Mock<IClaimUidExtractor>().Object;

            var tokenProvider = new DefaultAntiforgeryTokenGenerator(
                claimUidExtractor: claimUidExtractor,
                additionalDataProvider: null);

            // Act
            var fieldToken = tokenProvider.GenerateRequestToken(httpContext, cookieToken);

            // Assert
            Assert.NotNull(fieldToken);
            Assert.Equal(cookieToken.SecurityToken, fieldToken.SecurityToken);
            Assert.False(fieldToken.IsCookieToken);
            Assert.Equal("my-username", fieldToken.Username);
            Assert.Null(fieldToken.ClaimUid);
            Assert.Empty(fieldToken.AdditionalData);
        }

        [Fact]
        public void IsCookieTokenValid_FieldToken_ReturnsFalse()
        {
            // Arrange
            var cookieToken = new AntiforgeryToken()
            {
                IsCookieToken = false
            };

            var tokenProvider = new DefaultAntiforgeryTokenGenerator(
                claimUidExtractor: null,
                additionalDataProvider: null);

            // Act
            var isValid = tokenProvider.IsCookieTokenValid(cookieToken);

            // Assert
            Assert.False(isValid);
        }

        [Fact]
        public void IsCookieTokenValid_NullToken_ReturnsFalse()
        {
            // Arrange
            AntiforgeryToken cookieToken = null;
            var tokenProvider = new DefaultAntiforgeryTokenGenerator(
                claimUidExtractor: null,
                additionalDataProvider: null);

            // Act
            var isValid = tokenProvider.IsCookieTokenValid(cookieToken);

            // Assert
            Assert.False(isValid);
        }

        [Fact]
        public void IsCookieTokenValid_ValidToken_ReturnsTrue()
        {
            // Arrange
            var cookieToken = new AntiforgeryToken()
            {
                IsCookieToken = true
            };

            var tokenProvider = new DefaultAntiforgeryTokenGenerator(
                claimUidExtractor: null,
                additionalDataProvider: null);

            // Act
            var isValid = tokenProvider.IsCookieTokenValid(cookieToken);

            // Assert
            Assert.True(isValid);
        }


        [Fact]
        public void TryValidateTokenSet_CookieTokenMissing()
        {
            // Arrange
            var httpContext = new DefaultHttpContext();
            httpContext.User = new ClaimsPrincipal(new ClaimsIdentity());

            var fieldtoken = new AntiforgeryToken() { IsCookieToken = false };

            var tokenProvider = new DefaultAntiforgeryTokenGenerator(
                claimUidExtractor: null,
                additionalDataProvider: null);

            // Act & Assert
            string message;
            var ex = Assert.Throws<ArgumentNullException>(
                () => tokenProvider.TryValidateTokenSet(httpContext, null, fieldtoken, out message));

            Assert.StartsWith(@"The required antiforgery cookie token must be provided.", ex.Message);
        }

        [Fact]
        public void TryValidateTokenSet_FieldTokenMissing()
        {
            // Arrange
            var httpContext = new DefaultHttpContext();
            httpContext.User = new ClaimsPrincipal(new ClaimsIdentity());

            var cookieToken = new AntiforgeryToken() { IsCookieToken = true };

            var tokenProvider = new DefaultAntiforgeryTokenGenerator(
                claimUidExtractor: null,
                additionalDataProvider: null);


            // Act & Assert
            string message;
            var ex = Assert.Throws<ArgumentNullException>(
                () => tokenProvider.TryValidateTokenSet(httpContext, cookieToken, null, out message));

            Assert.StartsWith("The required antiforgery request token must be provided.", ex.Message);
        }

        [Fact]
        public void TryValidateTokenSet_FieldAndCookieTokensSwapped_FieldTokenDuplicated()
        {
            // Arrange
            var httpContext = new DefaultHttpContext();
            httpContext.User = new ClaimsPrincipal(new ClaimsIdentity());

            var cookieToken = new AntiforgeryToken() { IsCookieToken = true };
            var fieldtoken = new AntiforgeryToken() { IsCookieToken = false };

            var tokenProvider = new DefaultAntiforgeryTokenGenerator(
                claimUidExtractor: null,
                additionalDataProvider: null);

            string expectedMessage =
                "Validation of the provided antiforgery token failed. " +
                "The cookie token and the request token were swapped.";

            // Act
            string message;
            var result = tokenProvider.TryValidateTokenSet(httpContext, fieldtoken, fieldtoken, out message);

            // Assert
            Assert.False(result);
            Assert.Equal(expectedMessage, message);
        }

        [Fact]
        public void TryValidateTokenSet_FieldAndCookieTokensSwapped_CookieDuplicated()
        {
            // Arrange
            var httpContext = new DefaultHttpContext();
            httpContext.User = new ClaimsPrincipal(new ClaimsIdentity());

            var cookieToken = new AntiforgeryToken() { IsCookieToken = true };
            var fieldtoken = new AntiforgeryToken() { IsCookieToken = false };

            var tokenProvider = new DefaultAntiforgeryTokenGenerator(
                claimUidExtractor: null,
                additionalDataProvider: null);

            string expectedMessage =
                "Validation of the provided antiforgery token failed. " +
                "The cookie token and the request token were swapped.";

            // Act
            string message;
            var result = tokenProvider.TryValidateTokenSet(httpContext, cookieToken, cookieToken, out message);

            // Assert
            Assert.False(result);
            Assert.Equal(expectedMessage, message);
        }

        [Fact]
        public void TryValidateTokenSet_FieldAndCookieTokensHaveDifferentSecurityKeys()
        {
            // Arrange
            var httpContext = new DefaultHttpContext();
            httpContext.User = new ClaimsPrincipal(new ClaimsIdentity());

            var cookieToken = new AntiforgeryToken() { IsCookieToken = true };
            var fieldtoken = new AntiforgeryToken() { IsCookieToken = false };

            var tokenProvider = new DefaultAntiforgeryTokenGenerator(
                claimUidExtractor: null,
                additionalDataProvider: null);

            string expectedMessage = "The antiforgery cookie token and request token do not match.";

            // Act
            string message;
            var result = tokenProvider.TryValidateTokenSet(httpContext, cookieToken, fieldtoken, out message);

            // Assert
            Assert.False(result);
            Assert.Equal(expectedMessage, message);
        }

        [Theory]
        [InlineData("the-user", "the-other-user")]
        [InlineData("http://example.com/uri-casing", "http://example.com/URI-casing")]
        [InlineData("https://example.com/secure-uri-casing", "https://example.com/secure-URI-casing")]
        public void TryValidateTokenSet_UsernameMismatch(string identityUsername, string embeddedUsername)
        {
            // Arrange
            var httpContext = new DefaultHttpContext();
            var identity = GetAuthenticatedIdentity(identityUsername);
            httpContext.User = new ClaimsPrincipal(identity);

            var cookieToken = new AntiforgeryToken() { IsCookieToken = true };
            var fieldtoken = new AntiforgeryToken()
            {
                SecurityToken = cookieToken.SecurityToken,
                Username = embeddedUsername,
                IsCookieToken = false
            };

            var mockClaimUidExtractor = new Mock<IClaimUidExtractor>();
            mockClaimUidExtractor.Setup(o => o.ExtractClaimUid(It.Is<ClaimsPrincipal>(c => c.Identity == identity)))
                                 .Returns((string)null);

            var tokenProvider = new DefaultAntiforgeryTokenGenerator(
                claimUidExtractor: mockClaimUidExtractor.Object,
                additionalDataProvider: null);

            string expectedMessage =
                $"The provided antiforgery token was meant for user \"{embeddedUsername}\", " +
                $"but the current user is \"{identityUsername}\".";

            // Act
            string message;
            var result = tokenProvider.TryValidateTokenSet(httpContext, cookieToken, fieldtoken, out message);

            // Assert
            Assert.False(result);
            Assert.Equal(expectedMessage, message);
        }

        [Fact]
        public void TryValidateTokenSet_ClaimUidMismatch()
        {
            // Arrange
            var httpContext = new DefaultHttpContext();
            var identity = GetAuthenticatedIdentity("the-user");
            httpContext.User = new ClaimsPrincipal(identity);

            var cookieToken = new AntiforgeryToken() { IsCookieToken = true };
            var fieldtoken = new AntiforgeryToken()
            {
                SecurityToken = cookieToken.SecurityToken,
                IsCookieToken = false,
                ClaimUid = new BinaryBlob(256)
            };

            var differentToken = new BinaryBlob(256);
            var mockClaimUidExtractor = new Mock<IClaimUidExtractor>();
            mockClaimUidExtractor.Setup(o => o.ExtractClaimUid(It.Is<ClaimsPrincipal>(c => c.Identity == identity)))
                                 .Returns(Convert.ToBase64String(differentToken.GetData()));

            var tokenProvider = new DefaultAntiforgeryTokenGenerator(
                claimUidExtractor: mockClaimUidExtractor.Object,
                additionalDataProvider: null);

            string expectedMessage =
                "The provided antiforgery token was meant for a different " +
                "claims-based user than the current user.";

            // Act
            string message;
            var result = tokenProvider.TryValidateTokenSet(httpContext, cookieToken, fieldtoken, out message);

            // Assert
            Assert.False(result);
            Assert.Equal(expectedMessage, message);
        }

        [Fact]
        public void TryValidateTokenSet_AdditionalDataRejected()
        {
            // Arrange
            var httpContext = new DefaultHttpContext();
            var identity = new ClaimsIdentity();
            httpContext.User = new ClaimsPrincipal(identity);

            var cookieToken = new AntiforgeryToken() { IsCookieToken = true };
            var fieldtoken = new AntiforgeryToken()
            {
                SecurityToken = cookieToken.SecurityToken,
                Username = String.Empty,
                IsCookieToken = false,
                AdditionalData = "some-additional-data"
            };

            var mockAdditionalDataProvider = new Mock<IAntiforgeryAdditionalDataProvider>();
            mockAdditionalDataProvider
                .Setup(o => o.ValidateAdditionalData(httpContext, "some-additional-data"))
                .Returns(false);

            var tokenProvider = new DefaultAntiforgeryTokenGenerator(
                claimUidExtractor: null,
                additionalDataProvider: mockAdditionalDataProvider.Object);

            string expectedMessage = "The provided antiforgery token failed a custom data check.";

            // Act
            string message;
            var result = tokenProvider.TryValidateTokenSet(httpContext, cookieToken, fieldtoken, out message);

            // Assert
            Assert.False(result);
            Assert.Equal(expectedMessage, message);
        }

        [Fact]
        public void TryValidateTokenSet_Success_AnonymousUser()
        {
            // Arrange
            var httpContext = new DefaultHttpContext();
            var identity = new ClaimsIdentity();
            httpContext.User = new ClaimsPrincipal(identity);

            var cookieToken = new AntiforgeryToken() { IsCookieToken = true };
            var fieldtoken = new AntiforgeryToken()
            {
                SecurityToken = cookieToken.SecurityToken,
                Username = String.Empty,
                IsCookieToken = false,
                AdditionalData = "some-additional-data"
            };

            var mockAdditionalDataProvider = new Mock<IAntiforgeryAdditionalDataProvider>();
            mockAdditionalDataProvider.Setup(o => o.ValidateAdditionalData(httpContext, "some-additional-data"))
                                      .Returns(true);

            var tokenProvider = new DefaultAntiforgeryTokenGenerator(
                claimUidExtractor: null,
                additionalDataProvider: mockAdditionalDataProvider.Object);

            // Act
            string message;
            var result = tokenProvider.TryValidateTokenSet(httpContext, cookieToken, fieldtoken, out message);

            // Assert
            Assert.True(result);
            Assert.Null(message);
        }

        [Fact]
        public void TryValidateTokenSet_Success_AuthenticatedUserWithUsername()
        {
            // Arrange
            var httpContext = new DefaultHttpContext();
            var identity = GetAuthenticatedIdentity("the-user");
            httpContext.User = new ClaimsPrincipal(identity);

            var cookieToken = new AntiforgeryToken() { IsCookieToken = true };
            var fieldtoken = new AntiforgeryToken()
            {
                SecurityToken = cookieToken.SecurityToken,
                Username = "THE-USER",
                IsCookieToken = false,
                AdditionalData = "some-additional-data"
            };

            var mockAdditionalDataProvider = new Mock<IAntiforgeryAdditionalDataProvider>();
            mockAdditionalDataProvider.Setup(o => o.ValidateAdditionalData(httpContext, "some-additional-data"))
                                      .Returns(true);

            var tokenProvider = new DefaultAntiforgeryTokenGenerator(
                claimUidExtractor: new Mock<IClaimUidExtractor>().Object,
                additionalDataProvider: mockAdditionalDataProvider.Object);

            // Act
            string message;
            var result = tokenProvider.TryValidateTokenSet(httpContext, cookieToken, fieldtoken, out message);

            // Assert
            Assert.True(result);
            Assert.Null(message);
        }

        [Fact]
        public void TryValidateTokenSet_Success_ClaimsBasedUser()
        {
            // Arrange
            var httpContext = new DefaultHttpContext();
            var identity = GetAuthenticatedIdentity("the-user");
            httpContext.User = new ClaimsPrincipal(identity);

            var cookieToken = new AntiforgeryToken() { IsCookieToken = true };
            var fieldtoken = new AntiforgeryToken()
            {
                SecurityToken = cookieToken.SecurityToken,
                IsCookieToken = false,
                ClaimUid = new BinaryBlob(256)
            };

            var mockClaimUidExtractor = new Mock<IClaimUidExtractor>();
            mockClaimUidExtractor.Setup(o => o.ExtractClaimUid(It.Is<ClaimsPrincipal>(c => c.Identity == identity)))
                                 .Returns(Convert.ToBase64String(fieldtoken.ClaimUid.GetData()));

            var tokenProvider = new DefaultAntiforgeryTokenGenerator(
                claimUidExtractor: mockClaimUidExtractor.Object,
                additionalDataProvider: null);

            // Act
            string message;
            var result = tokenProvider.TryValidateTokenSet(httpContext, cookieToken, fieldtoken, out message);

            // Assert
            Assert.True(result);
            Assert.Null(message);
        }

        private static ClaimsIdentity GetAuthenticatedIdentity(string identityUsername)
        {
            var claim = new Claim(ClaimsIdentity.DefaultNameClaimType, identityUsername);
            return new ClaimsIdentity(new[] { claim }, "Some-Authentication");
        }

        private sealed class MyAuthenticatedIdentityWithoutUsername : ClaimsIdentity
        {
            public override bool IsAuthenticated
            {
                get { return true; }
            }

            public override string Name
            {
                get { return String.Empty; }
            }
        }
    }
}
#nullable restore
