using Baya.Application.Contracts.Audit;
using Baya.Application.Contracts.Common;
using Baya.Application.Contracts.Persistence;
using Baya.Application.Contracts.Search;
using Baya.Application.Contracts.SupportAlerts;
using Baya.Application.Features.Verification.Commands.ReviewStep;
using Baya.Application.Features.Verification.Commands.ScanExpiringCredentials;
using Baya.Application.Features.Verification.Commands.SuspendVerification;
using Baya.Domain.Entities.Identity;
using Baya.Domain.Entities.SupportAlerts;
using Baya.Domain.Entities.Verification;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using static Baya.Test.Foundation.Verification.VerificationTestSupport;

namespace Baya.Test.Foundation.Verification;

public class AdminVerificationHandlersTests
{
    private static readonly DateTimeOffset Now = new(2026, 7, 1, 12, 0, 0, TimeSpan.Zero);

    private readonly ICurrentUser _currentUser = Substitute.For<ICurrentUser>();
    private readonly IUnitOfWork _unitOfWork = Substitute.For<IUnitOfWork>();
    private readonly IVerificationRepository _verif = Substitute.For<IVerificationRepository>();
    private readonly INurseProfileRepository _nurses = Substitute.For<INurseProfileRepository>();
    private readonly ICacheService _cache = Substitute.For<ICacheService>();
    private readonly IAuditLogger _audit = Substitute.For<IAuditLogger>();
    private readonly IDateTimeProvider _clock = Substitute.For<IDateTimeProvider>();
    private readonly ISearchIndexMaintainer _searchIndex = Substitute.For<ISearchIndexMaintainer>();

    public AdminVerificationHandlersTests()
    {
        _currentUser.UserId.Returns(99);
        _unitOfWork.VerificationRepository.Returns(_verif);
        _unitOfWork.NurseProfileRepository.Returns(_nurses);
        _clock.UtcNow.Returns(Now);
    }

    private static (VerificationStep Step, NurseVerification Verification) MohStepInReview()
    {
        var step = Step(5, StepType(3, VerificationStepTypeCodes.MohCompetencyLicense, automated: false), VerificationStepStatus.InReview);
        var verification = new NurseVerification { NurseId = 42, Status = VerificationStatus.InReview };
        verification.Steps.Add(step);
        step.NurseVerification = verification;
        return (step, verification);
    }

    private AdminReviewStepCommandHandler ReviewHandler(ICredentialVerifier credentialVerifier)
        => new(_currentUser, _unitOfWork, credentialVerifier, _audit, _cache, _clock, _searchIndex);

    [Fact]
    public async Task Review_ApproveCredentialStep_RecordsCredentialAndFlipsVerified()
    {
        var (step, _) = MohStepInReview();
        _verif.GetTrackedStepWithVerificationAsync(5, Arg.Any<CancellationToken>()).Returns(step);
        _verif.GetNurseIdentityNameAsync(42, Arg.Any<CancellationToken>()).Returns("Ali Ahmadi");
        var profile = new NurseProfile();
        _nurses.GetTrackedByIdAsync(42, Arg.Any<CancellationToken>()).Returns(profile);
        var credVerifier = Substitute.For<ICredentialVerifier>();
        credVerifier.VerifyAsync(VerificationStepTypeCodes.MohCompetencyLicense, "LIC-1", Arg.Any<CancellationToken>())
            .Returns(new CredentialVerificationResult(CredentialVerificationStatus.RequiresManualReview, "manual", null));

        var result = await ReviewHandler(credVerifier).Handle(
            new AdminReviewStepCommand(5, true, null, "LIC-1", "Ali Ahmadi", "Ministry of Health", null, null, null),
            CancellationToken.None);

        Assert.True(result.IsSuccess);
        Assert.Equal(VerificationStepStatus.Passed, step.Status);
        Assert.True(profile.IsVerified);
        await _verif.Received(1).AddCredentialAsync(
            Arg.Is<NurseCredential>(c =>
                c.NurseId == 42
                && c.CredentialType == VerificationStepTypeCodes.MohCompetencyLicense
                && c.HolderNameSnapshot == "Ali Ahmadi"
                && c.VerificationMethod == "manual"
                && c.VerifiedByAdminId == 99),
            Arg.Any<CancellationToken>());
        await _audit.Received(1).WriteAsync("verification_step", "5", "approve", Arg.Any<IReadOnlyDictionary<string, object?>>(), Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task Review_HolderNameMismatch_IsRejectedAndRecordsNoCredential()
    {
        var (step, _) = MohStepInReview();
        _verif.GetTrackedStepWithVerificationAsync(5, Arg.Any<CancellationToken>()).Returns(step);
        _verif.GetNurseIdentityNameAsync(42, Arg.Any<CancellationToken>()).Returns("Ali Ahmadi");
        var credVerifier = Substitute.For<ICredentialVerifier>();

        var result = await ReviewHandler(credVerifier).Handle(
            new AdminReviewStepCommand(5, true, null, "LIC-1", "Someone Else", "Ministry of Health", null, null, null),
            CancellationToken.None);

        Assert.False(result.IsSuccess);
        Assert.Equal(VerificationStepStatus.InReview, step.Status);
        await _verif.DidNotReceive().AddCredentialAsync(Arg.Any<NurseCredential>(), Arg.Any<CancellationToken>());
        await _unitOfWork.DidNotReceive().CommitAsync();
    }

    [Fact]
    public async Task Review_Reject_FailsStepWithReason()
    {
        var (step, verification) = MohStepInReview();
        _verif.GetTrackedStepWithVerificationAsync(5, Arg.Any<CancellationToken>()).Returns(step);
        _nurses.GetTrackedByIdAsync(42, Arg.Any<CancellationToken>()).Returns(new NurseProfile());
        var credVerifier = Substitute.For<ICredentialVerifier>();

        var result = await ReviewHandler(credVerifier).Handle(
            new AdminReviewStepCommand(5, false, "Document is illegible", null, null, null, null, null, null),
            CancellationToken.None);

        Assert.True(result.IsSuccess);
        Assert.Equal(VerificationStepStatus.Failed, step.Status);
        Assert.Equal("Document is illegible", step.FailureReason);
        Assert.Equal("Document is illegible", verification.RejectionReason);
    }

    [Fact]
    public async Task Suspend_ReversesVerifiedInSameTransaction()
    {
        var verification = new NurseVerification { NurseId = 42, Status = VerificationStatus.Approved };
        verification.Steps.Add(Step(1, StepType(1, VerificationStepTypeCodes.IdentityKyc, automated: true), VerificationStepStatus.Passed));
        _verif.GetTrackedByIdAsync(10, Arg.Any<CancellationToken>()).Returns(verification);
        var profile = new NurseProfile();
        profile.MarkVerified();
        _nurses.GetTrackedByIdAsync(42, Arg.Any<CancellationToken>()).Returns(profile);
        var handler = new AdminSuspendVerificationCommandHandler(_currentUser, _unitOfWork, _audit, _cache, _clock, _searchIndex);

        var result = await handler.Handle(new AdminSuspendVerificationCommand(10, "Fraud reported"), CancellationToken.None);

        Assert.True(result.IsSuccess);
        Assert.Equal(VerificationStatus.Suspended, verification.Status);
        Assert.Equal(Now, verification.SuspendedAt);
        Assert.False(profile.IsVerified);
        await _cache.Received(1).RemoveAsync(Arg.Any<string>(), Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task Scan_ExpiredCriminalRecord_RevertsStepRaisesAlertAndNotifies()
    {
        var bank = Step(4, StepType(4, VerificationStepTypeCodes.BankAccountVerification, automated: true), VerificationStepStatus.Passed);
        var criminal = Step(5, StepType(5, VerificationStepTypeCodes.CriminalRecord, automated: false), VerificationStepStatus.Passed);
        criminal.ExpiresAt = Now.AddDays(-1);
        var verification = new NurseVerification { NurseId = 42, Status = VerificationStatus.Approved };
        verification.Steps.Add(bank);
        verification.Steps.Add(criminal);

        _verif.GetExpiredPassedStepsAsync(Now, 1, 50, Arg.Any<CancellationToken>())
            .Returns(new List<ExpiringStepRow> { new(5, 10, 42) });
        _verif.GetTrackedByIdAsync(10, Arg.Any<CancellationToken>()).Returns(verification);
        var profile = new NurseProfile { UserId = 7 };
        profile.MarkVerified();
        _nurses.GetTrackedByIdAsync(42, Arg.Any<CancellationToken>()).Returns(profile);

        var alerts = Substitute.For<ISupportAlertService>();
        var notifications = Substitute.For<INotificationDispatcher>();
        var handler = new ScanExpiringCredentialsCommandHandler(_unitOfWork, alerts, notifications, _cache, _clock, _searchIndex);

        var result = await handler.Handle(new ScanExpiringCredentialsCommand(), CancellationToken.None);

        Assert.True(result.IsSuccess);
        Assert.Equal(1, result.Result.RevertedNurses);
        Assert.Equal(VerificationStepStatus.Expired, criminal.Status);
        Assert.False(profile.IsVerified);
        await alerts.Received(1).RaiseAsync(
            SupportAlertType.VerificationExpired, "nurse_profile", "42", SupportAlertSeverity.Medium,
            Arg.Any<long?>(), Arg.Any<long?>(), Arg.Any<CancellationToken>());
        await notifications.Received(1).DispatchAsync(
            Arg.Is<Notification>(n => n.RecipientUserId == 7 && n.Type == "verification_expiry_prompt"),
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task Scan_NullProfileNurse_LeavesNoDirtyStepForNextNursesCommit()
    {
        // Nurse A's profile is missing (e.g. soft-deleted) while their verification still has an expired
        // step; nurse B is valid. The guard must run before mutating A's step, so A's step is never left
        // dirty for B's commit to flush without the atomic re-gate.
        var stepA = Step(5, StepType(5, VerificationStepTypeCodes.CriminalRecord, automated: false), VerificationStepStatus.Passed);
        stepA.ExpiresAt = Now.AddDays(-1);
        var verificationA = new NurseVerification { NurseId = 42, Status = VerificationStatus.Approved };
        verificationA.Steps.Add(stepA);

        var stepB = Step(6, StepType(5, VerificationStepTypeCodes.CriminalRecord, automated: false), VerificationStepStatus.Passed);
        stepB.ExpiresAt = Now.AddDays(-1);
        var verificationB = new NurseVerification { NurseId = 99, Status = VerificationStatus.Approved };
        verificationB.Steps.Add(stepB);

        _verif.GetExpiredPassedStepsAsync(Now, 1, 50, Arg.Any<CancellationToken>())
            .Returns(new List<ExpiringStepRow> { new(5, 10, 42), new(6, 20, 99) });
        _verif.GetTrackedByIdAsync(10, Arg.Any<CancellationToken>()).Returns(verificationA);
        _verif.GetTrackedByIdAsync(20, Arg.Any<CancellationToken>()).Returns(verificationB);
        _nurses.GetTrackedByIdAsync(42, Arg.Any<CancellationToken>()).ReturnsNull();
        var profileB = new NurseProfile { UserId = 8 };
        profileB.MarkVerified();
        _nurses.GetTrackedByIdAsync(99, Arg.Any<CancellationToken>()).Returns(profileB);

        var handler = new ScanExpiringCredentialsCommandHandler(
            _unitOfWork, Substitute.For<ISupportAlertService>(), Substitute.For<INotificationDispatcher>(), _cache, _clock, _searchIndex);

        var result = await handler.Handle(new ScanExpiringCredentialsCommand(), CancellationToken.None);

        Assert.Equal(1, result.Result.RevertedNurses);
        Assert.Equal(VerificationStepStatus.Passed, stepA.Status); // never mutated
        Assert.Equal(VerificationStepStatus.Expired, stepB.Status);
        Assert.False(profileB.IsVerified);
        await _unitOfWork.Received(1).CommitAsync();
    }
}
