using FluentAssertions;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using SolutionErp.Application.Auth.Commands.ChangePassword;
using SolutionErp.Application.Common.Interfaces;
using SolutionErp.Domain.Identity;
using SolutionErp.Infrastructure.Tests.Common;
using AppValidationException = SolutionErp.Application.Common.Exceptions.ValidationException;
using AppUnauthorizedException = SolutionErp.Application.Common.Exceptions.UnauthorizedException;

namespace SolutionErp.Infrastructure.Tests.Application;

// ===== NEW (go-live 2026-06-26) — Self-service đổi mật khẩu =====
// ChangePasswordCommand.cs (Application/Auth/Commands/ChangePassword).
// Security-sensitive (xử lý mật khẩu) → test-before-merge per docs/rules.md §7.
// Test theo CODE đã land (S34 rule — KHÔNG touch production).
//
// Spec handler:
//   - !IsAuthenticated || UserId is null → UnauthorizedException.
//   - FindByIdAsync(UserId) null → UnauthorizedException.
//   - userManager.ChangePasswordAsync fail → ValidationException (CUSTOM app
//     exception, Errors là Dictionary<string,string[]> key=PropertyName):
//       code "PasswordMismatch" → key "CurrentPassword" ("Mật khẩu hiện tại không đúng.")
//       lỗi khác              → key "NewPassword".
//   - Success → user.RefreshToken=null + RefreshTokenExpiresAt=null + UpdateAsync.
//
// Validator (test trực tiếp .Validate() — mirror PeSuggestedPriceSetterAuthzTests):
//   CurrentPassword NotEmpty; NewPassword NotEmpty + MinimumLength(12);
//   NewPassword NotEqual CurrentPassword.
//
// ⚠️ GOTCHA harness: IdentityFixture cấu hình Password.RequiredLength=4 + tắt
//   mọi complexity rule (NV codegen tests dùng "Test@123"). Nên ràng-buộc-độ-dài-12
//   là DO VALIDATOR enforce, KHÔNG phải Identity ở tầng test → case "<12" test ở
//   validator (Identity tầng này sẽ pass length=4). "PasswordMismatch" thì Identity
//   VẪN verify mật khẩu cũ bất kể RequiredLength → test #2 đi qua handler thật.
//
// ⚠️ Tạo user bằng UserManager.CreateAsync(user, "<pass cụ thể>") trực tiếp (KHÔNG
//   dùng fix.CreateUserAsync — helper đó hardcode "Test@123", không kiểm soát được
//   pass cũ cần cho đổi-mật-khẩu).
public class ChangePasswordCommandTests
{
    private const string OldPassword = "OldPass@12345";   // ≥12 ký tự
    private const string NewPassword = "NewPass@12345";   // ≥12 ký tự, khác pass cũ

    // ICurrentUser stub — UserId settable + IsAuthenticated cấu hình riêng để
    // mô phỏng cả nhánh "chưa đăng nhập" (IsAuthenticated=false) lẫn "UserId null".
    private sealed class FakeCurrentUser : ICurrentUser
    {
        public Guid? UserId { get; init; }
        public string? Email { get; init; } = "actor@test.local";
        public string? FullName { get; init; } = "Actor Test";
        public IReadOnlyList<string> Roles { get; init; } = Array.Empty<string>();
        public bool IsAuthenticated { get; init; } = true;
    }

    // Tạo user với mật khẩu cụ thể (faithful cho password-change test).
    private static async Task<User> CreateUserWithPasswordAsync(
        IdentityFixture fix, string password, string email = "changepass@test.local")
    {
        var um = fix.Services.GetRequiredService<UserManager<User>>();
        var user = new User
        {
            Id = Guid.NewGuid(),
            UserName = email,
            Email = email,
            EmailConfirmed = true,
            FullName = "Đổi Mật Khẩu",
            IsActive = true,
            // Pre-seed refresh token để chứng minh handler XÓA nó khi đổi pass thành công.
            RefreshToken = "stale-refresh-token",
            RefreshTokenExpiresAt = DateTime.UtcNow.AddDays(7),
        };
        var created = await um.CreateAsync(user, password);
        created.Succeeded.Should().BeTrue(
            "seed user phải tạo được: " + string.Join(",", created.Errors.Select(e => e.Description)));
        return user;
    }

    // ====================================================================
    // ===== Handler =====
    // ====================================================================

    // 1. Đổi mật khẩu thành công → KHÔNG throw; pass mới hiệu lực + refresh token xóa.
    [Fact]
    public async Task Handle_CurrentCorrect_NewValid_ChangesPasswordAndClearsRefreshToken()
    {
        using var fix = new IdentityFixture();
        var um = fix.Services.GetRequiredService<UserManager<User>>();
        var user = await CreateUserWithPasswordAsync(fix, OldPassword);

        var handler = new ChangePasswordCommandHandler(
            um, new FakeCurrentUser { UserId = user.Id });

        var act = async () => await handler.Handle(
            new ChangePasswordCommand(OldPassword, NewPassword), CancellationToken.None);

        await act.Should().NotThrowAsync();

        // Re-load sạch để verify state đã persist.
        var reloaded = await um.FindByIdAsync(user.Id.ToString());
        reloaded.Should().NotBeNull();
        (await um.CheckPasswordAsync(reloaded!, NewPassword)).Should().BeTrue("pass mới phải hiệu lực");
        (await um.CheckPasswordAsync(reloaded!, OldPassword)).Should().BeFalse("pass cũ phải hết hiệu lực");
        reloaded!.RefreshToken.Should().BeNull("đổi pass phải vô hiệu refresh token");
        reloaded.RefreshTokenExpiresAt.Should().BeNull();
    }

    // 2. Mật khẩu hiện tại SAI → ValidationException (custom), failure key chứa "CurrentPassword".
    [Fact]
    public async Task Handle_WrongCurrentPassword_ThrowsValidationOnCurrentPassword()
    {
        using var fix = new IdentityFixture();
        var um = fix.Services.GetRequiredService<UserManager<User>>();
        var user = await CreateUserWithPasswordAsync(fix, OldPassword);

        var handler = new ChangePasswordCommandHandler(
            um, new FakeCurrentUser { UserId = user.Id });

        var act = async () => await handler.Handle(
            new ChangePasswordCommand("WrongCurrent@999", NewPassword), CancellationToken.None);

        var ex = (await act.Should().ThrowAsync<AppValidationException>()).Which;
        ex.Errors.Keys.Should().Contain(nameof(ChangePasswordCommand.CurrentPassword));
        ex.Errors[nameof(ChangePasswordCommand.CurrentPassword)]
            .Should().Contain("Mật khẩu hiện tại không đúng.");

        // Pass cũ KHÔNG bị đổi (fail-safe) + refresh token còn nguyên (handler thoát trước khi clear).
        var reloaded = await um.FindByIdAsync(user.Id.ToString());
        (await um.CheckPasswordAsync(reloaded!, OldPassword)).Should().BeTrue();
        reloaded!.RefreshToken.Should().Be("stale-refresh-token");
    }

    // 5. Chưa đăng nhập (IsAuthenticated=false) → UnauthorizedException trước mọi side-effect.
    [Fact]
    public async Task Handle_NotAuthenticated_ThrowsUnauthorized()
    {
        using var fix = new IdentityFixture();
        var um = fix.Services.GetRequiredService<UserManager<User>>();
        var user = await CreateUserWithPasswordAsync(fix, OldPassword);

        // IsAuthenticated=false dù UserId có giá trị → guard đầu tiên phải bắt.
        var handler = new ChangePasswordCommandHandler(
            um, new FakeCurrentUser { UserId = user.Id, IsAuthenticated = false });

        var act = async () => await handler.Handle(
            new ChangePasswordCommand(OldPassword, NewPassword), CancellationToken.None);

        await act.Should().ThrowAsync<AppUnauthorizedException>();

        // Không có side-effect: pass cũ giữ nguyên.
        var reloaded = await um.FindByIdAsync(user.Id.ToString());
        (await um.CheckPasswordAsync(reloaded!, OldPassword)).Should().BeTrue();
    }

    // 5-bis. UserId null (kể cả IsAuthenticated=true) → UnauthorizedException.
    [Fact]
    public async Task Handle_NullUserId_ThrowsUnauthorized()
    {
        using var fix = new IdentityFixture();
        var um = fix.Services.GetRequiredService<UserManager<User>>();

        var handler = new ChangePasswordCommandHandler(
            um, new FakeCurrentUser { UserId = null, IsAuthenticated = true });

        var act = async () => await handler.Handle(
            new ChangePasswordCommand(OldPassword, NewPassword), CancellationToken.None);

        await act.Should().ThrowAsync<AppUnauthorizedException>();
    }

    // 5-ter. Authenticated nhưng user không tồn tại (UserId lạ) → UnauthorizedException (FindByIdAsync null).
    [Fact]
    public async Task Handle_UserNotFound_ThrowsUnauthorized()
    {
        using var fix = new IdentityFixture();
        var um = fix.Services.GetRequiredService<UserManager<User>>();

        var handler = new ChangePasswordCommandHandler(
            um, new FakeCurrentUser { UserId = Guid.NewGuid(), IsAuthenticated = true });

        var act = async () => await handler.Handle(
            new ChangePasswordCommand(OldPassword, NewPassword), CancellationToken.None);

        await act.Should().ThrowAsync<AppUnauthorizedException>();
    }

    // ====================================================================
    // ===== Validator =====
    // ====================================================================

    // 3. Mật khẩu mới <12 ký tự → validator reject (Identity tầng test pass length=4,
    //    nên độ-dài-12 là do VALIDATOR enforce).
    [Fact]
    public void Validator_NewPasswordTooShort_Fails()
    {
        var validator = new ChangePasswordCommandValidator();

        var result = validator.Validate(
            new ChangePasswordCommand(OldPassword, NewPassword: "Short@12345"));   // 11 ký tự

        result.IsValid.Should().BeFalse();
        result.Errors.Should().Contain(e => e.PropertyName == nameof(ChangePasswordCommand.NewPassword));
    }

    // 3-bis. Đúng 12 ký tự (biên) → hợp lệ về độ dài.
    [Fact]
    public void Validator_NewPasswordExactly12_PassesLengthRule()
    {
        var validator = new ChangePasswordCommandValidator();

        // "Abcdef@12345" = 12 ký tự, khác CurrentPassword → toàn bộ rule pass.
        validator.Validate(new ChangePasswordCommand("Current@9999", "Abcdef@12345"))
            .IsValid.Should().BeTrue();
    }

    // 4. Mật khẩu mới == mật khẩu cũ → validator reject (NotEqual), trên field NewPassword.
    [Fact]
    public void Validator_NewEqualsCurrent_Fails()
    {
        var validator = new ChangePasswordCommandValidator();

        var result = validator.Validate(
            new ChangePasswordCommand(OldPassword, NewPassword: OldPassword));

        result.IsValid.Should().BeFalse();
        result.Errors.Should().Contain(e =>
            e.PropertyName == nameof(ChangePasswordCommand.NewPassword)
            && e.ErrorMessage == "Mật khẩu mới phải khác mật khẩu hiện tại.");
    }

    // 4-bis. CurrentPassword rỗng → validator reject (NotEmpty).
    [Fact]
    public void Validator_EmptyCurrentPassword_Fails()
    {
        var validator = new ChangePasswordCommandValidator();

        var result = validator.Validate(
            new ChangePasswordCommand(CurrentPassword: "", NewPassword: NewPassword));

        result.IsValid.Should().BeFalse();
        result.Errors.Should().Contain(e => e.PropertyName == nameof(ChangePasswordCommand.CurrentPassword));
    }
}
