// 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.Security.Claims;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Testing;
using Moq;

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
