using Microsoft.Extensions.Caching.Memory;
using OmsorgCore.Application.Services;
using OmsorgCore.Domain.Entities;
using OmsorgCore.Domain.Enums;
using OmsorgCore.Tests.TestDoubles;

namespace OmsorgCore.Tests.Services;

public class UserServiceTests
{
    private static Employee CreateEmployee(string? email = "mitarbeiter@example.com") => new()
    {
        FirstName = "Test",
        LastName = "Mitarbeiter",
        Email = email
    };

    private static UserService CreateSut(
        FakeUserRepository users,
        FakeEmployeeRepository employees,
        FakeRoleRepository roles,
        FakePasswordResetCodeRepository? codes = null,
        FakeRefreshTokenRepository? refreshTokens = null)
    {
        var actualRefreshTokens = refreshTokens ?? new FakeRefreshTokenRepository();
        var passwordPolicy = new FakePasswordPolicy();
        var passwordResetService = new PasswordResetService(
            users, codes ?? new FakePasswordResetCodeRepository(),
            new FakePasswordResetCodeGenerator(), new FakePasswordHasher(),
            actualRefreshTokens, passwordPolicy);

        return new UserService(
            users, employees, roles, new FakePasswordHasher(),
            passwordResetService, actualRefreshTokens, passwordPolicy,
            new PermissionService(users, new MemoryCache(new MemoryCacheOptions())));
    }

    [Fact]
    public async Task CreateForEmployeeAsync_InviteMode_CreatesUserWithoutMustChangePassword_AndIssuesPin()
    {
        var employee = CreateEmployee();
        var employees = new FakeEmployeeRepository();
        await employees.AddAsync(employee);
        var role = new Role { Name = "Aussendienst" };
        var roles = new FakeRoleRepository(role);
        var users = new FakeUserRepository();

        var sut = CreateSut(users, employees, roles);

        var result = await sut.CreateForEmployeeAsync(
            employee.Id, "neuer.user", role.Id, UserCreationMode.Invite,
            initialPassword: null, invitePinValidity: TimeSpan.FromDays(7));

        Assert.True(result.Success);
        Assert.Equal(employee.Email, result.Email);
        Assert.False(string.IsNullOrEmpty(result.RawInvitePin));

        var created = await users.GetByIdAsync(result.UserId!.Value);
        Assert.NotNull(created);
        Assert.False(created!.MustChangePassword);
    }

    [Fact]
    public async Task CreateForEmployeeAsync_DirectMode_SetsPasswordAndMustChangePassword()
    {
        var employee = CreateEmployee();
        var employees = new FakeEmployeeRepository();
        await employees.AddAsync(employee);
        var role = new Role { Name = "Disposition" };
        var roles = new FakeRoleRepository(role);
        var users = new FakeUserRepository();

        var sut = CreateSut(users, employees, roles);

        var result = await sut.CreateForEmployeeAsync(
            employee.Id, "direkt.user", role.Id, UserCreationMode.Direct,
            initialPassword: "initialesPasswort", invitePinValidity: null);

        Assert.True(result.Success);
        Assert.Null(result.RawInvitePin);

        var created = await users.GetByIdAsync(result.UserId!.Value);
        Assert.NotNull(created);
        Assert.True(created!.MustChangePassword);
        Assert.Equal("hashed:initialesPasswort", created.PasswordHash);
    }

    [Fact]
    public async Task CreateForEmployeeAsync_DirectMode_WithoutPassword_Fails()
    {
        var employee = CreateEmployee();
        var employees = new FakeEmployeeRepository();
        await employees.AddAsync(employee);
        var role = new Role { Name = "Disposition" };
        var roles = new FakeRoleRepository(role);
        var users = new FakeUserRepository();

        var sut = CreateSut(users, employees, roles);

        var result = await sut.CreateForEmployeeAsync(
            employee.Id, "direkt.user", role.Id, UserCreationMode.Direct,
            initialPassword: null, invitePinValidity: null);

        Assert.False(result.Success);
        Assert.Equal(CreateUserFailureReason.InitialPasswordRequired, result.FailureReason);
    }

    [Fact]
    public async Task CreateForEmployeeAsync_DirectMode_PasswordTooShort_Fails()
    {
        var employee = CreateEmployee();
        var employees = new FakeEmployeeRepository();
        await employees.AddAsync(employee);
        var role = new Role { Name = "Disposition" };
        var roles = new FakeRoleRepository(role);
        var users = new FakeUserRepository();

        var sut = CreateSut(users, employees, roles);

        var result = await sut.CreateForEmployeeAsync(
            employee.Id, "direkt.user", role.Id, UserCreationMode.Direct,
            initialPassword: "kurz", invitePinValidity: null);

        Assert.False(result.Success);
        Assert.Equal(CreateUserFailureReason.PasswordTooShort, result.FailureReason);
    }

    [Fact]
    public async Task CreateForEmployeeAsync_InviteMode_WithoutEmployeeEmail_Fails()
    {
        var employee = CreateEmployee(email: null);
        var employees = new FakeEmployeeRepository();
        await employees.AddAsync(employee);
        var role = new Role { Name = "Aussendienst" };
        var roles = new FakeRoleRepository(role);
        var users = new FakeUserRepository();

        var sut = CreateSut(users, employees, roles);

        var result = await sut.CreateForEmployeeAsync(
            employee.Id, "neuer.user", role.Id, UserCreationMode.Invite,
            initialPassword: null, invitePinValidity: TimeSpan.FromDays(7));

        Assert.False(result.Success);
        Assert.Equal(CreateUserFailureReason.EmployeeEmailMissing, result.FailureReason);
    }

    [Fact]
    public async Task CreateForEmployeeAsync_EmployeeAlreadyHasAccount_Fails()
    {
        var employee = CreateEmployee();
        var employees = new FakeEmployeeRepository();
        await employees.AddAsync(employee);
        var role = new Role { Name = "Aussendienst" };
        var roles = new FakeRoleRepository(role);
        var existingUser = new User
        {
            Username = "schon.da",
            PasswordHash = "hashed:x",
            RoleId = role.Id,
            Role = role,
            EmployeeId = employee.Id
        };
        var users = new FakeUserRepository(existingUser);

        var sut = CreateSut(users, employees, roles);

        var result = await sut.CreateForEmployeeAsync(
            employee.Id, "zweiter.user", role.Id, UserCreationMode.Direct,
            initialPassword: "irgendeinPasswort", invitePinValidity: null);

        Assert.False(result.Success);
        Assert.Equal(CreateUserFailureReason.EmployeeAlreadyHasAccount, result.FailureReason);
    }

    [Fact]
    public async Task CreateForEmployeeAsync_UsernameTaken_Fails()
    {
        var employee = CreateEmployee();
        var employees = new FakeEmployeeRepository();
        await employees.AddAsync(employee);
        var role = new Role { Name = "Aussendienst" };
        var roles = new FakeRoleRepository(role);
        var existingUser = new User
        {
            Username = "belegt",
            PasswordHash = "hashed:x",
            RoleId = role.Id,
            Role = role
        };
        var users = new FakeUserRepository(existingUser);

        var sut = CreateSut(users, employees, roles);

        var result = await sut.CreateForEmployeeAsync(
            employee.Id, "belegt", role.Id, UserCreationMode.Direct,
            initialPassword: "irgendeinPasswort", invitePinValidity: null);

        Assert.False(result.Success);
        Assert.Equal(CreateUserFailureReason.UsernameTaken, result.FailureReason);
    }

    [Fact]
    public async Task ChangeOwnPasswordAsync_WithCorrectCurrentPassword_UpdatesPasswordAndClearsFlag()
    {
        var role = new Role { Name = "Disposition" };
        var user = new User
        {
            Username = "muss.wechseln",
            PasswordHash = "hashed:altesPasswort",
            RoleId = role.Id,
            Role = role,
            MustChangePassword = true
        };
        var users = new FakeUserRepository(user);
        var refreshTokens = new FakeRefreshTokenRepository();
        await refreshTokens.AddAsync(new RefreshToken
        {
            UserId = user.Id,
            User = user,
            TokenHash = "session-hash",
            ExpiresAt = DateTime.UtcNow.AddDays(1)
        });

        var sut = CreateSut(users, new FakeEmployeeRepository(), new FakeRoleRepository(), refreshTokens: refreshTokens);
        var oldStamp = user.SecurityStamp;

        var result = await sut.ChangeOwnPasswordAsync(user.Id, "altesPasswort", "neuesPasswort");

        Assert.Equal(ChangeOwnPasswordResult.Success, result);
        Assert.False(user.MustChangePassword);
        Assert.Equal("hashed:neuesPasswort", user.PasswordHash);
        Assert.NotEqual(oldStamp, user.SecurityStamp);

        var session = await refreshTokens.GetByTokenHashAsync("session-hash");
        Assert.NotNull(session!.RevokedAt);
    }

    [Fact]
    public async Task AdminResetPasswordAsync_DirectMode_SetsPasswordAndMustChangePassword()
    {
        var role = new Role { Name = "Disposition" };
        var employee = CreateEmployee();
        var user = new User
        {
            Username = "bestehender.user",
            PasswordHash = "hashed:altesPasswort",
            RoleId = role.Id,
            Role = role,
            Employee = employee,
            EmployeeId = employee.Id
        };
        var users = new FakeUserRepository(user);
        var sut = CreateSut(users, new FakeEmployeeRepository(), new FakeRoleRepository(role));

        var result = await sut.AdminResetPasswordAsync(user.Id, UserCreationMode.Direct, "neuesInitialesPw", null);

        Assert.True(result.Success);
        Assert.Equal("hashed:neuesInitialesPw", user.PasswordHash);
        Assert.True(user.MustChangePassword);
    }

    [Fact]
    public async Task AdminResetPasswordAsync_InviteMode_IssuesPin()
    {
        var role = new Role { Name = "Disposition" };
        var employee = CreateEmployee();
        var user = new User
        {
            Username = "bestehender.user",
            PasswordHash = "hashed:altesPasswort",
            RoleId = role.Id,
            Role = role,
            Employee = employee,
            EmployeeId = employee.Id
        };
        var users = new FakeUserRepository(user);
        var sut = CreateSut(users, new FakeEmployeeRepository(), new FakeRoleRepository(role));

        var result = await sut.AdminResetPasswordAsync(user.Id, UserCreationMode.Invite, null, TimeSpan.FromDays(3));

        Assert.True(result.Success);
        Assert.Equal(employee.Email, result.Email);
        Assert.False(string.IsNullOrEmpty(result.RawInvitePin));
    }

    [Fact]
    public async Task AdminResetPasswordAsync_UnknownUser_Fails()
    {
        var sut = CreateSut(new FakeUserRepository(), new FakeEmployeeRepository(), new FakeRoleRepository());

        var result = await sut.AdminResetPasswordAsync(Guid.NewGuid(), UserCreationMode.Direct, "irgendeinPasswort", null);

        Assert.False(result.Success);
        Assert.Equal(AdminResetPasswordFailureReason.UserNotFound, result.FailureReason);
    }

    [Fact]
    public async Task AdminResetPasswordAsync_DirectMode_PasswordTooShort_Fails()
    {
        var role = new Role { Name = "Disposition" };
        var employee = CreateEmployee();
        var user = new User
        {
            Username = "bestehender.user",
            PasswordHash = "hashed:altesPasswort",
            RoleId = role.Id,
            Role = role,
            Employee = employee,
            EmployeeId = employee.Id
        };
        var users = new FakeUserRepository(user);
        var sut = CreateSut(users, new FakeEmployeeRepository(), new FakeRoleRepository(role));

        var result = await sut.AdminResetPasswordAsync(user.Id, UserCreationMode.Direct, "kurz", null);

        Assert.False(result.Success);
        Assert.Equal(AdminResetPasswordFailureReason.PasswordTooShort, result.FailureReason);
        Assert.Equal("hashed:altesPasswort", user.PasswordHash);
    }

    [Fact]
    public async Task UpdateAsync_DeactivatingUser_RevokesSessionsImmediately()
    {
        var role = new Role { Name = "Disposition" };
        var user = new User
        {
            Username = "aktiver.user",
            PasswordHash = "hashed:x",
            RoleId = role.Id,
            Role = role,
            IsActive = true
        };
        var users = new FakeUserRepository(user);
        var refreshTokens = new FakeRefreshTokenRepository();
        await refreshTokens.AddAsync(new RefreshToken
        {
            UserId = user.Id,
            User = user,
            TokenHash = "session-hash",
            ExpiresAt = DateTime.UtcNow.AddDays(1)
        });
        var sut = CreateSut(users, new FakeEmployeeRepository(), new FakeRoleRepository(role), refreshTokens: refreshTokens);
        var oldStamp = user.SecurityStamp;

        var result = await sut.UpdateAsync(user.Id, role.Id, isActive: false);

        Assert.True(result.Success);
        Assert.False(user.IsActive);
        Assert.NotEqual(oldStamp, user.SecurityStamp);
        var session = await refreshTokens.GetByTokenHashAsync("session-hash");
        Assert.NotNull(session!.RevokedAt);
    }

    [Fact]
    public async Task UpdateAsync_UnknownRole_Fails()
    {
        var role = new Role { Name = "Disposition" };
        var user = new User { Username = "u", PasswordHash = "hashed:x", RoleId = role.Id, Role = role };
        var users = new FakeUserRepository(user);
        var sut = CreateSut(users, new FakeEmployeeRepository(), new FakeRoleRepository(role));

        var result = await sut.UpdateAsync(user.Id, Guid.NewGuid(), isActive: true);

        Assert.False(result.Success);
        Assert.Equal(UpdateUserFailureReason.RoleNotFound, result.FailureReason);
    }

    [Fact]
    public async Task ChangeOwnPasswordAsync_WithWrongCurrentPassword_Fails()
    {
        var role = new Role { Name = "Disposition" };
        var user = new User
        {
            Username = "muss.wechseln",
            PasswordHash = "hashed:altesPasswort",
            RoleId = role.Id,
            Role = role,
            MustChangePassword = true
        };
        var users = new FakeUserRepository(user);

        var sut = CreateSut(users, new FakeEmployeeRepository(), new FakeRoleRepository());

        var result = await sut.ChangeOwnPasswordAsync(user.Id, "falschesPasswort", "neuesPasswort");

        Assert.Equal(ChangeOwnPasswordResult.InvalidCurrentPassword, result);
        Assert.True(user.MustChangePassword);
    }

    [Fact]
    public async Task ChangeOwnPasswordAsync_NewPasswordTooShort_Fails()
    {
        var role = new Role { Name = "Disposition" };
        var user = new User
        {
            Username = "muss.wechseln",
            PasswordHash = "hashed:altesPasswort",
            RoleId = role.Id,
            Role = role,
            MustChangePassword = true
        };
        var users = new FakeUserRepository(user);

        var sut = CreateSut(users, new FakeEmployeeRepository(), new FakeRoleRepository());

        var result = await sut.ChangeOwnPasswordAsync(user.Id, "altesPasswort", "kurz");

        Assert.Equal(ChangeOwnPasswordResult.PasswordTooShort, result);
        Assert.Equal("hashed:altesPasswort", user.PasswordHash);
    }

    [Fact]
    public async Task AddPermissionOverrideAsync_NewOverride_AddsIt()
    {
        var role = new Role { Name = "Disposition" };
        var user = new User { Username = "u", PasswordHash = "hashed:x", RoleId = role.Id, Role = role };
        var users = new FakeUserRepository(user);
        var sut = CreateSut(users, new FakeEmployeeRepository(), new FakeRoleRepository(role));

        var result = await sut.AddPermissionOverrideAsync(user.Id, ModuleType.Invoices, PermissionAction.View, PermissionEffect.Grant, PermissionScope.All);

        Assert.True(result.Success);
        Assert.Single(user.PermissionOverrides);
        Assert.Equal(PermissionEffect.Grant, result.Override!.Effect);
    }

    [Fact]
    public async Task AddPermissionOverrideAsync_ExistingOverrideForSamePair_UpdatesEffectInsteadOfDuplicating()
    {
        var role = new Role { Name = "Disposition" };
        var user = new User { Username = "u", PasswordHash = "hashed:x", RoleId = role.Id, Role = role };
        user.PermissionOverrides.Add(new UserPermissionOverride
        {
            UserId = user.Id, User = user, Module = ModuleType.Invoices, Action = PermissionAction.View, Effect = PermissionEffect.Grant
        });
        var users = new FakeUserRepository(user);
        var sut = CreateSut(users, new FakeEmployeeRepository(), new FakeRoleRepository(role));

        var result = await sut.AddPermissionOverrideAsync(user.Id, ModuleType.Invoices, PermissionAction.View, PermissionEffect.Revoke, PermissionScope.All);

        Assert.True(result.Success);
        var single = Assert.Single(user.PermissionOverrides);
        Assert.Equal(PermissionEffect.Revoke, single.Effect);
    }

    [Fact]
    public async Task AddPermissionOverrideAsync_UnknownUser_Fails()
    {
        var sut = CreateSut(new FakeUserRepository(), new FakeEmployeeRepository(), new FakeRoleRepository());

        var result = await sut.AddPermissionOverrideAsync(Guid.NewGuid(), ModuleType.Invoices, PermissionAction.View, PermissionEffect.Grant, PermissionScope.All);

        Assert.False(result.Success);
        Assert.Equal(AddPermissionOverrideFailureReason.UserNotFound, result.FailureReason);
    }

    [Fact]
    public async Task RemovePermissionOverrideAsync_ExistingOverride_RemovesIt()
    {
        var role = new Role { Name = "Disposition" };
        var user = new User { Username = "u", PasswordHash = "hashed:x", RoleId = role.Id, Role = role };
        var toRemove = new UserPermissionOverride
        {
            UserId = user.Id, User = user, Module = ModuleType.Invoices, Action = PermissionAction.View, Effect = PermissionEffect.Grant
        };
        user.PermissionOverrides.Add(toRemove);
        var users = new FakeUserRepository(user);
        var sut = CreateSut(users, new FakeEmployeeRepository(), new FakeRoleRepository(role));

        var result = await sut.RemovePermissionOverrideAsync(user.Id, toRemove.Id);

        Assert.True(result.Success);
        Assert.Empty(user.PermissionOverrides);
    }

    [Fact]
    public async Task RemovePermissionOverrideAsync_UnknownOverride_Fails()
    {
        var role = new Role { Name = "Disposition" };
        var user = new User { Username = "u", PasswordHash = "hashed:x", RoleId = role.Id, Role = role };
        var users = new FakeUserRepository(user);
        var sut = CreateSut(users, new FakeEmployeeRepository(), new FakeRoleRepository(role));

        var result = await sut.RemovePermissionOverrideAsync(user.Id, Guid.NewGuid());

        Assert.False(result.Success);
        Assert.Equal(RemovePermissionOverrideFailureReason.OverrideNotFound, result.FailureReason);
    }

    [Fact]
    public async Task GetPermissionOverridesAsync_UnknownUser_ReturnsNull()
    {
        var sut = CreateSut(new FakeUserRepository(), new FakeEmployeeRepository(), new FakeRoleRepository());

        var result = await sut.GetPermissionOverridesAsync(Guid.NewGuid());

        Assert.Null(result);
    }
}
