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

using System.Globalization;
using System.Linq.Expressions;
using System.Security.Claims;
using Microsoft.AspNetCore.Identity.Test;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test;

public class UserStoreWithGenericsTest : IdentitySpecificationTestBase<IdentityUserWithGenerics, MyIdentityRole, string>, IClassFixture<ScratchDatabaseFixture>
{
    private readonly ScratchDatabaseFixture _fixture;

    public UserStoreWithGenericsTest(ScratchDatabaseFixture fixture)
    {
        _fixture = fixture;
    }

    private ContextWithGenerics CreateContext()
    {
        var db = DbUtil.Create<ContextWithGenerics>(_fixture.Connection);
        db.Database.EnsureCreated();
        return db;
    }

    protected override object CreateTestContext()
    {
        return CreateContext();
    }

    protected override void AddUserStore(IServiceCollection services, object context = null)
    {
        services.AddSingleton<IUserStore<IdentityUserWithGenerics>>(new UserStoreWithGenerics((ContextWithGenerics)context, "TestContext"));
    }

    protected override void AddRoleStore(IServiceCollection services, object context = null)
    {
        services.AddSingleton<IRoleStore<MyIdentityRole>>(new RoleStoreWithGenerics((ContextWithGenerics)context, "TestContext"));
    }

    protected override IdentityUserWithGenerics CreateTestUser(string namePrefix = "", string email = "", string phoneNumber = "",
        bool lockoutEnabled = false, DateTimeOffset? lockoutEnd = default(DateTimeOffset?), bool useNamePrefixAsUserName = false)
    {
        return new IdentityUserWithGenerics
        {
            UserName = useNamePrefixAsUserName ? namePrefix : string.Format(CultureInfo.InvariantCulture, "{0}{1}", namePrefix, Guid.NewGuid()),
            Email = email,
            PhoneNumber = phoneNumber,
            LockoutEnabled = lockoutEnabled,
            LockoutEnd = lockoutEnd
        };
    }

    protected override MyIdentityRole CreateTestRole(string roleNamePrefix = "", bool useRoleNamePrefixAsRoleName = false)
    {
        var roleName = useRoleNamePrefixAsRoleName ? roleNamePrefix : string.Format(CultureInfo.InvariantCulture, "{0}{1}", roleNamePrefix, Guid.NewGuid());
        return new MyIdentityRole(roleName);
    }

    protected override void SetUserPasswordHash(IdentityUserWithGenerics user, string hashedPassword)
    {
        user.PasswordHash = hashedPassword;
    }

    protected override Expression<Func<IdentityUserWithGenerics, bool>> UserNameEqualsPredicate(string userName) => u => u.UserName == userName;

    protected override Expression<Func<MyIdentityRole, bool>> RoleNameEqualsPredicate(string roleName) => r => r.Name == roleName;

#pragma warning disable CA1310 // Specify StringComparison for correctness
    protected override Expression<Func<IdentityUserWithGenerics, bool>> UserNameStartsWithPredicate(string userName) => u => u.UserName.StartsWith(userName);

    protected override Expression<Func<MyIdentityRole, bool>> RoleNameStartsWithPredicate(string roleName) => r => r.Name.StartsWith(roleName);
#pragma warning restore CA1310 // Specify StringComparison for correctness

    [Fact]
    public void AddEntityFrameworkStoresWithInvalidUserThrows()
    {
        var services = new ServiceCollection();
        var builder = services.AddIdentity<object, IdentityRole>();
        var e = Assert.Throws<InvalidOperationException>(() => builder.AddEntityFrameworkStores<ContextWithGenerics>());
        Assert.Contains("AddEntityFrameworkStores", e.Message);
    }

    [Fact]
    public void AddEntityFrameworkStoresWithInvalidRoleThrows()
    {
        var services = new ServiceCollection();
        var builder = services.AddIdentity<IdentityUser, object>();
        var e = Assert.Throws<InvalidOperationException>(() => builder.AddEntityFrameworkStores<ContextWithGenerics>());
        Assert.Contains("AddEntityFrameworkStores", e.Message);
    }

    [Fact]
    public async Task CanAddRemoveUserClaimWithIssuer()
    {
        var manager = CreateManager();
        var user = CreateTestUser();
        IdentityResultAssert.IsSuccess(await manager.CreateAsync(user));
        Claim[] claims = { new Claim("c1", "v1", null, "i1"), new Claim("c2", "v2", null, "i2"), new Claim("c2", "v3", null, "i3") };
        foreach (Claim c in claims)
        {
            IdentityResultAssert.IsSuccess(await manager.AddClaimAsync(user, c));
        }

        var userId = await manager.GetUserIdAsync(user);
        var userClaims = await manager.GetClaimsAsync(user);
        Assert.Equal(3, userClaims.Count);
        Assert.Equal(3, userClaims.Intersect(claims, ClaimEqualityComparer.Default).Count());

        IdentityResultAssert.IsSuccess(await manager.RemoveClaimAsync(user, claims[0]));
        userClaims = await manager.GetClaimsAsync(user);
        Assert.Equal(2, userClaims.Count);
        IdentityResultAssert.IsSuccess(await manager.RemoveClaimAsync(user, claims[1]));
        userClaims = await manager.GetClaimsAsync(user);
        Assert.Single(userClaims);
        IdentityResultAssert.IsSuccess(await manager.RemoveClaimAsync(user, claims[2]));
        userClaims = await manager.GetClaimsAsync(user);
        Assert.Empty(userClaims);
    }

    [Fact]
    public async Task RemoveClaimWithIssuerOnlyAffectsUser()
    {
        var manager = CreateManager();
        var user = CreateTestUser();
        var user2 = CreateTestUser();
        IdentityResultAssert.IsSuccess(await manager.CreateAsync(user));
        IdentityResultAssert.IsSuccess(await manager.CreateAsync(user2));
        Claim[] claims = { new Claim("c", "v", null, "i1"), new Claim("c2", "v2", null, "i2"), new Claim("c2", "v3", null, "i3") };
        foreach (Claim c in claims)
        {
            IdentityResultAssert.IsSuccess(await manager.AddClaimAsync(user, c));
            IdentityResultAssert.IsSuccess(await manager.AddClaimAsync(user2, c));
        }
        var userClaims = await manager.GetClaimsAsync(user);
        Assert.Equal(3, userClaims.Count);
        IdentityResultAssert.IsSuccess(await manager.RemoveClaimAsync(user, claims[0]));
        userClaims = await manager.GetClaimsAsync(user);
        Assert.Equal(2, userClaims.Count);
        IdentityResultAssert.IsSuccess(await manager.RemoveClaimAsync(user, claims[1]));
        userClaims = await manager.GetClaimsAsync(user);
        Assert.Single(userClaims);
        IdentityResultAssert.IsSuccess(await manager.RemoveClaimAsync(user, claims[2]));
        userClaims = await manager.GetClaimsAsync(user);
        Assert.Empty(userClaims);
        var userClaims2 = await manager.GetClaimsAsync(user2);
        Assert.Equal(3, userClaims2.Count);
    }

    [Fact]
    public async Task CanReplaceUserClaimWithIssuer()
    {
        var manager = CreateManager();
        var user = CreateTestUser();
        IdentityResultAssert.IsSuccess(await manager.CreateAsync(user));
        IdentityResultAssert.IsSuccess(await manager.AddClaimAsync(user, new Claim("c", "a", "i")));
        var userClaims = await manager.GetClaimsAsync(user);
        Assert.Single(userClaims);
        Claim claim = new Claim("c", "b", "i");
        Claim oldClaim = userClaims.FirstOrDefault();
        IdentityResultAssert.IsSuccess(await manager.ReplaceClaimAsync(user, oldClaim, claim));
        var newUserClaims = await manager.GetClaimsAsync(user);
        Assert.Single(newUserClaims);
        Claim newClaim = newUserClaims.FirstOrDefault();
        Assert.Equal(claim.Type, newClaim.Type);
        Assert.Equal(claim.Value, newClaim.Value);
        Assert.Equal(claim.Issuer, newClaim.Issuer);
    }
}

public class ClaimEqualityComparer : IEqualityComparer<Claim>
{
    public static IEqualityComparer<Claim> Default = new ClaimEqualityComparer();

    public bool Equals(Claim x, Claim y)
    {
        return x.Value == y.Value && x.Type == y.Type && x.Issuer == y.Issuer;
    }

    public int GetHashCode(Claim obj)
    {
        return 1;
    }
}

#region Generic Type defintions

public class IdentityUserWithGenerics : IdentityUser<string>
{
    public IdentityUserWithGenerics()
    {
        Id = Guid.NewGuid().ToString();
    }
}

public class UserStoreWithGenerics : UserStore<IdentityUserWithGenerics, MyIdentityRole, ContextWithGenerics, string, IdentityUserClaimWithIssuer, IdentityUserRoleWithDate, IdentityUserLoginWithContext, IdentityUserTokenWithStuff, IdentityRoleClaimWithIssuer>
{
    public string LoginContext { get; set; }

    public UserStoreWithGenerics(ContextWithGenerics context, string loginContext) : base(context)
    {
        LoginContext = loginContext;
    }

    protected override IdentityUserRoleWithDate CreateUserRole(IdentityUserWithGenerics user, MyIdentityRole role)
    {
        return new IdentityUserRoleWithDate()
        {
            RoleId = role.Id,
            UserId = user.Id,
            Created = DateTime.UtcNow
        };
    }

    protected override IdentityUserClaimWithIssuer CreateUserClaim(IdentityUserWithGenerics user, Claim claim)
    {
        return new IdentityUserClaimWithIssuer { UserId = user.Id, ClaimType = claim.Type, ClaimValue = claim.Value, Issuer = claim.Issuer };
    }

    protected override IdentityUserLoginWithContext CreateUserLogin(IdentityUserWithGenerics user, UserLoginInfo login)
    {
        return new IdentityUserLoginWithContext
        {
            UserId = user.Id,
            ProviderKey = login.ProviderKey,
            LoginProvider = login.LoginProvider,
            ProviderDisplayName = login.ProviderDisplayName,
            Context = LoginContext
        };
    }

    protected override IdentityUserTokenWithStuff CreateUserToken(IdentityUserWithGenerics user, string loginProvider, string name, string value)
    {
        return new IdentityUserTokenWithStuff
        {
            UserId = user.Id,
            LoginProvider = loginProvider,
            Name = name,
            Value = value,
            Stuff = "stuff"
        };
    }
}

public class RoleStoreWithGenerics : RoleStore<MyIdentityRole, ContextWithGenerics, string, IdentityUserRoleWithDate, IdentityRoleClaimWithIssuer>
{
    private string _loginContext;
    public RoleStoreWithGenerics(ContextWithGenerics context, string loginContext) : base(context)
    {
        _loginContext = loginContext;
    }

    protected override IdentityRoleClaimWithIssuer CreateRoleClaim(MyIdentityRole role, Claim claim)
    {
        return new IdentityRoleClaimWithIssuer { RoleId = role.Id, ClaimType = claim.Type, ClaimValue = claim.Value, Issuer = claim.Issuer };
    }
}

public class IdentityUserClaimWithIssuer : IdentityUserClaim<string>
{
    public string Issuer { get; set; }

    public override Claim ToClaim()
    {
        return new Claim(ClaimType, ClaimValue, null, Issuer);
    }

    public override void InitializeFromClaim(Claim other)
    {
        ClaimValue = other.Value;
        ClaimType = other.Type;
        Issuer = other.Issuer;
    }
}

public class IdentityRoleClaimWithIssuer : IdentityRoleClaim<string>
{
    public string Issuer { get; set; }

    public override Claim ToClaim()
    {
        return new Claim(ClaimType, ClaimValue, null, Issuer);
    }

    public override void InitializeFromClaim(Claim other)
    {
        ClaimValue = other.Value;
        ClaimType = other.Type;
        Issuer = other.Issuer;
    }
}

public class IdentityUserRoleWithDate : IdentityUserRole<string>
{
    public DateTime Created { get; set; }
}

public class MyIdentityRole : IdentityRole<string>
{
    public MyIdentityRole() : base()
    {
        Id = Guid.NewGuid().ToString();
    }

    public MyIdentityRole(string roleName) : this()
    {
        Name = roleName;
    }
}

public class IdentityUserTokenWithStuff : IdentityUserToken<string>
{
    public string Stuff { get; set; }
}

public class IdentityUserLoginWithContext : IdentityUserLogin<string>
{
    public string Context { get; set; }
}

public class ContextWithGenerics : IdentityDbContext<IdentityUserWithGenerics, MyIdentityRole, string, IdentityUserClaimWithIssuer, IdentityUserRoleWithDate, IdentityUserLoginWithContext, IdentityRoleClaimWithIssuer, IdentityUserTokenWithStuff>
{
    public ContextWithGenerics(DbContextOptions options) : base(options) { }
}

#endregion
