using FluentAssertions;
using Microsoft.Extensions.Options;
using NSubstitute;
using SIGCM2.Application.Audit;
using SIGCM2.Domain.Exceptions;
using SIGCM2.Infrastructure.Audit;
using Xunit;

namespace SIGCM2.Application.Tests.Infrastructure.Audit;

/// UDT-010 Batch 6 — AuditLogger unit tests (Strict TDD).
/// Covers #REQ-AUD-3/4/5: enriches from IAuditContext, fail-closed on missing actor,
/// sanitizes metadata via JsonSanitizer + AuditOptions.
public sealed class AuditLoggerTests
{
    private static AuditLogger Build(
        IAuditContext? context = null,
        IAuditEventRepository? repo = null,
        AuditOptions? options = null)
    {
        context ??= Substitute.For<IAuditContext>();
        repo ??= Substitute.For<IAuditEventRepository>();
        options ??= new AuditOptions();
        var optsWrapper = Options.Create(options);
        return new AuditLogger(context, repo, optsWrapper, TimeProvider.System);
    }

    [Fact]
    public async Task LogAsync_WithAllContext_PassesEnrichedValuesToRepo()
    {
        var context = Substitute.For<IAuditContext>();
        var correlationId = Guid.NewGuid();
        context.ActorUserId.Returns(42);
        context.ActorRoleId.Returns(7);
        context.Ip.Returns("10.0.0.5");
        context.UserAgent.Returns("ua/1.0");
        context.CorrelationId.Returns(correlationId);

        var repo = Substitute.For<IAuditEventRepository>();
        var logger = Build(context, repo);

        await logger.LogAsync("usuario.create", "Usuario", "99",
            metadata: new { username = "juan" });

        await repo.Received(1).InsertAsync(
            Arg.Any<DateTime>(),
            42,
            7,
            "usuario.create",
            "Usuario",
            "99",
            correlationId,
            "10.0.0.5",
            "ua/1.0",
            Arg.Is<string?>(m => m != null && m.Contains("\"username\"") && m.Contains("juan")),
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task LogAsync_WithoutActorUserId_ThrowsAuditContextMissingException()
    {
        var context = Substitute.For<IAuditContext>();
        context.ActorUserId.Returns((int?)null);
        context.CorrelationId.Returns(Guid.NewGuid());
        var repo = Substitute.For<IAuditEventRepository>();
        var logger = Build(context, repo);

        var act = async () => await logger.LogAsync("usuario.create", "Usuario", "1");

        await act.Should().ThrowAsync<AuditContextMissingException>();
        await repo.DidNotReceive().InsertAsync(
            Arg.Any<DateTime>(), Arg.Any<int?>(), Arg.Any<int?>(),
            Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
            Arg.Any<Guid?>(), Arg.Any<string?>(), Arg.Any<string?>(),
            Arg.Any<string?>(), Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task LogAsync_SanitizesMetadata_StripsBlacklistedKeys()
    {
        var context = Substitute.For<IAuditContext>();
        context.ActorUserId.Returns(1);
        var repo = Substitute.For<IAuditEventRepository>();
        var logger = Build(context, repo);

        await logger.LogAsync("usuario.update", "Usuario", "1",
            metadata: new { password = "secret", email = "e@x" });

        await repo.Received(1).InsertAsync(
            Arg.Any<DateTime>(), Arg.Any<int?>(), Arg.Any<int?>(),
            Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
            Arg.Any<Guid?>(), Arg.Any<string?>(), Arg.Any<string?>(),
            Arg.Is<string?>(m => m != null && !m.Contains("\"password\"") && m.Contains("\"email\"")),
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task LogAsync_NullMetadata_PassesNullToRepo()
    {
        var context = Substitute.For<IAuditContext>();
        context.ActorUserId.Returns(1);
        var repo = Substitute.For<IAuditEventRepository>();
        var logger = Build(context, repo);

        await logger.LogAsync("usuario.deactivate", "Usuario", "1");

        await repo.Received(1).InsertAsync(
            Arg.Any<DateTime>(), Arg.Any<int?>(), Arg.Any<int?>(),
            "usuario.deactivate", "Usuario", "1",
            Arg.Any<Guid?>(), Arg.Any<string?>(), Arg.Any<string?>(),
            Arg.Is<string?>(m => m == null),
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task LogAsync_RepositoryThrows_ExceptionBubblesUp()
    {
        var context = Substitute.For<IAuditContext>();
        context.ActorUserId.Returns(1);
        var repo = Substitute.For<IAuditEventRepository>();
        repo.InsertAsync(
                Arg.Any<DateTime>(), Arg.Any<int?>(), Arg.Any<int?>(),
                Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
                Arg.Any<Guid?>(), Arg.Any<string?>(), Arg.Any<string?>(),
                Arg.Any<string?>(), Arg.Any<CancellationToken>())
            .Returns<long>(_ => throw new InvalidOperationException("simulated db failure"));

        var logger = Build(context, repo);

        var act = async () => await logger.LogAsync("usuario.create", "Usuario", "1");

        // Fail-closed: exception MUST bubble to caller (caller's TransactionScope will roll back).
        await act.Should().ThrowAsync<InvalidOperationException>()
            .WithMessage("simulated db failure");
    }

    [Fact]
    public async Task LogAsync_UsesCustomSanitizedKeys_FromOptions()
    {
        var context = Substitute.For<IAuditContext>();
        context.ActorUserId.Returns(1);
        var repo = Substitute.For<IAuditEventRepository>();
        var logger = Build(context, repo, new AuditOptions { SanitizedKeys = new[] { "internalId" } });

        await logger.LogAsync("x.y", "T", "1", metadata: new { internalId = "secret", visible = "ok" });

        await repo.Received(1).InsertAsync(
            Arg.Any<DateTime>(), Arg.Any<int?>(), Arg.Any<int?>(),
            Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
            Arg.Any<Guid?>(), Arg.Any<string?>(), Arg.Any<string?>(),
            Arg.Is<string?>(m => m != null && !m.Contains("\"internalId\"") && m.Contains("\"visible\"")),
            Arg.Any<CancellationToken>());
    }
}
