using Baya.Application.Contracts.Common;
using Baya.Application.Contracts.Persistence;
using Baya.Application.Features.Identity.Commands.AddNurseBankAccount;
using Baya.Application.Features.Identity.Commands.SetPrimaryBankAccount;
using Baya.Application.Models.Identity;
using Baya.Domain.Entities.Identity;
using Baya.Domain.Entities.User;
using NSubstitute;
using NSubstitute.ReturnsExtensions;

namespace Baya.Test.Foundation.Identity;

public class NurseBankAccountHandlersTests
{
    private const string ValidIban = "IR062960000000100324200001";

    private readonly ICurrentUser _currentUser = Substitute.For<ICurrentUser>();
    private readonly IUnitOfWork _unitOfWork = Substitute.For<IUnitOfWork>();
    private readonly INurseProfileRepository _nurses = Substitute.For<INurseProfileRepository>();
    private readonly INurseBankAccountRepository _accounts = Substitute.For<INurseBankAccountRepository>();
    private readonly IFieldEncryptor _encryptor = Substitute.For<IFieldEncryptor>();
    private readonly IBankAccountOwnershipVerifier _verifier = Substitute.For<IBankAccountOwnershipVerifier>();

    public NurseBankAccountHandlersTests()
    {
        _currentUser.UserId.Returns(7);
        _currentUser.Roles.Returns([RoleNames.Nurse]);
        _unitOfWork.NurseProfileRepository.Returns(_nurses);
        _unitOfWork.NurseBankAccountRepository.Returns(_accounts);
        _nurses.GetIdentityContextByUserIdAsync(7, Arg.Any<CancellationToken>())
            .Returns(new NurseIdentityContext(42L, "0012345678"));
        _encryptor.Hash(Arg.Any<string>()).Returns(ci => "HASH-" + ci.Arg<string>());
    }

    private AddNurseBankAccountCommandHandler CreateAddHandler() =>
        new(_currentUser, _unitOfWork, _encryptor, _verifier);

    [Fact]
    public async Task Add_MatchingIban_RunsInquiryAndSetsMatchedTrueAndPrimary()
    {
        _accounts.IbanHashExistsAsync(Arg.Any<string>(), Arg.Any<CancellationToken>()).Returns(false);
        _accounts.HasAnyAsync(42L, Arg.Any<CancellationToken>()).Returns(false);
        _verifier.VerifyOwnershipAsync(Arg.Any<string>(), "0012345678", Arg.Any<CancellationToken>())
            .Returns(new OwnershipInquiryResult(true, "Verified Holder", "MOCK-SHEBA-ABC"));
        var handler = CreateAddHandler();

        var result = await handler.Handle(new AddNurseBankAccountCommand("Bank Melli", "Nurse Name", ValidIban), CancellationToken.None);

        Assert.True(result.IsSuccess);
        Assert.True(result.Result.MatchedNationalId);
        Assert.True(result.Result.IsPrimary);
        Assert.DoesNotContain(ValidIban, result.Result.IbanMasked);
        await _verifier.Received(1).VerifyOwnershipAsync(ValidIban, "0012345678", Arg.Any<CancellationToken>());
        await _accounts.Received(1).AddAsync(
            Arg.Is<NurseBankAccount>(a => a.NurseId == 42L && a.MatchedNationalId == true && a.OwnershipVendorRef == "MOCK-SHEBA-ABC" && a.IbanHash == "HASH-" + ValidIban),
            Arg.Any<CancellationToken>());
        await _unitOfWork.Received(1).CommitAsync();
    }

    [Fact]
    public async Task Add_MismatchIban_RecordsMatchedFalse()
    {
        _accounts.IbanHashExistsAsync(Arg.Any<string>(), Arg.Any<CancellationToken>()).Returns(false);
        _accounts.HasAnyAsync(42L, Arg.Any<CancellationToken>()).Returns(true);
        _verifier.VerifyOwnershipAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(new OwnershipInquiryResult(false, "Someone Else", "MOCK-SHEBA-XYZ"));
        var handler = CreateAddHandler();

        var result = await handler.Handle(new AddNurseBankAccountCommand("Bank Melli", "Nurse Name", ValidIban), CancellationToken.None);

        Assert.True(result.IsSuccess);
        Assert.False(result.Result.MatchedNationalId);
        Assert.False(result.Result.IsPrimary);
    }

    [Fact]
    public async Task Add_DuplicateIban_IsRejectedBeforeInsert()
    {
        _accounts.IbanHashExistsAsync("HASH-" + ValidIban, Arg.Any<CancellationToken>()).Returns(true);
        var handler = CreateAddHandler();

        var result = await handler.Handle(new AddNurseBankAccountCommand("Bank Melli", "Nurse Name", ValidIban), CancellationToken.None);

        Assert.False(result.IsSuccess);
        await _accounts.DidNotReceive().AddAsync(Arg.Any<NurseBankAccount>(), Arg.Any<CancellationToken>());
        await _verifier.DidNotReceive().VerifyOwnershipAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>());
        await _unitOfWork.DidNotReceive().CommitAsync();
    }

    [Fact]
    public async Task Add_NoNurseProfile_IsFailure()
    {
        _nurses.GetIdentityContextByUserIdAsync(7, Arg.Any<CancellationToken>()).ReturnsNull();
        var handler = CreateAddHandler();

        var result = await handler.Handle(new AddNurseBankAccountCommand("Bank Melli", "Nurse Name", ValidIban), CancellationToken.None);

        Assert.False(result.IsSuccess);
        await _accounts.DidNotReceive().AddAsync(Arg.Any<NurseBankAccount>(), Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task SetPrimary_OwnedNonPrimary_FlipsAtomically()
    {
        _nurses.GetProfileIdByUserIdAsync(7, Arg.Any<CancellationToken>()).Returns(42L);
        _accounts.GetOwnedAsync(5L, 42L, Arg.Any<CancellationToken>()).Returns(new NurseBankAccount { NurseId = 42L, IsPrimary = false });
        var handler = new SetPrimaryBankAccountCommandHandler(_currentUser, _unitOfWork);

        var result = await handler.Handle(new SetPrimaryBankAccountCommand(5L), CancellationToken.None);

        Assert.True(result.IsSuccess);
        await _accounts.Received(1).SetPrimaryAsync(42L, 5L, Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task SetPrimary_NotOwned_IsNotFound()
    {
        _nurses.GetProfileIdByUserIdAsync(7, Arg.Any<CancellationToken>()).Returns(42L);
        _accounts.GetOwnedAsync(5L, 42L, Arg.Any<CancellationToken>()).ReturnsNull();
        var handler = new SetPrimaryBankAccountCommandHandler(_currentUser, _unitOfWork);

        var result = await handler.Handle(new SetPrimaryBankAccountCommand(5L), CancellationToken.None);

        Assert.True(result.IsNotFound);
        await _accounts.DidNotReceive().SetPrimaryAsync(Arg.Any<long>(), Arg.Any<long>(), Arg.Any<CancellationToken>());
    }
}
