using FluentAssertions;
using NSubstitute;
using SIGCM2.Application.Audit;
using SIGCM2.Domain.Exceptions;
using Xunit;

namespace SIGCM2.Application.Tests.Audit;

/// UDT-010 Batch 2 — shape contract tests for audit abstractions.
/// These tests fail to compile if the interface/DTO/exception shape drifts from #D-8.
public sealed class AuditAbstractionsTests
{
    [Fact]
    public void IAuditContext_ExposesExpectedReadOnlyProperties()
    {
        var ctx = Substitute.For<IAuditContext>();
        ctx.ActorUserId.Returns(42);
        ctx.ActorRoleId.Returns(7);
        ctx.Ip.Returns("127.0.0.1");
        ctx.UserAgent.Returns("test-agent/1.0");
        var corrId = Guid.NewGuid();
        ctx.CorrelationId.Returns(corrId);

        ctx.ActorUserId.Should().Be(42);
        ctx.ActorRoleId.Should().Be(7);
        ctx.Ip.Should().Be("127.0.0.1");
        ctx.UserAgent.Should().Be("test-agent/1.0");
        ctx.CorrelationId.Should().Be(corrId);
    }

    [Fact]
    public void IAuditContext_AllowsNullActorAndConnectionMetadata()
    {
        var ctx = Substitute.For<IAuditContext>();
        ctx.ActorUserId.Returns((int?)null);
        ctx.ActorRoleId.Returns((int?)null);
        ctx.Ip.Returns((string?)null);
        ctx.UserAgent.Returns((string?)null);

        ctx.ActorUserId.Should().BeNull();
        ctx.ActorRoleId.Should().BeNull();
        ctx.Ip.Should().BeNull();
        ctx.UserAgent.Should().BeNull();
    }

    [Fact]
    public async Task IAuditLogger_LogAsync_ExposesExpectedSignature()
    {
        var logger = Substitute.For<IAuditLogger>();

        await logger.LogAsync(
            action: "usuario.create",
            targetType: "Usuario",
            targetId: "42",
            metadata: new { username = "juan" },
            ct: CancellationToken.None);

        await logger.Received(1).LogAsync(
            "usuario.create",
            "Usuario",
            "42",
            Arg.Any<object>(),
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task IAuditLogger_LogAsync_AllowsNullMetadata()
    {
        var logger = Substitute.For<IAuditLogger>();
        await logger.LogAsync("usuario.deactivate", "Usuario", "42");

        await logger.Received(1).LogAsync(
            "usuario.deactivate",
            "Usuario",
            "42",
            null,
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task ISecurityEventLogger_LogAsync_ExposesFullSignatureForLoginFailure()
    {
        var logger = Substitute.For<ISecurityEventLogger>();
        var sessionId = Guid.NewGuid();

        await logger.LogAsync(
            action: "login",
            result: "failure",
            actorUserId: null,
            attemptedUsername: "juan",
            sessionId: sessionId,
            failureReason: "invalid_password",
            metadata: new { ip = "1.2.3.4" },
            ct: CancellationToken.None);

        await logger.Received(1).LogAsync(
            "login", "failure", null, "juan", sessionId, "invalid_password",
            Arg.Any<object>(), Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task ISecurityEventLogger_LogAsync_MinimalSuccessCall()
    {
        var logger = Substitute.For<ISecurityEventLogger>();
        await logger.LogAsync("logout", "success", actorUserId: 42);

        await logger.Received(1).LogAsync(
            "logout", "success", 42, null, null, null, null, Arg.Any<CancellationToken>());
    }

    [Fact]
    public void AuditContextMissingException_IsDomainException_WithFixedMessage()
    {
        var ex = new AuditContextMissingException();

        ex.Should().BeAssignableTo<DomainException>();
        ex.Message.Should().NotBeNullOrWhiteSpace();
        ex.Message.Should().Contain("ActorUserId");
    }

    [Fact]
    public void AuditOptions_HasSanitizedKeysDefault()
    {
        var opts = new AuditOptions();

        opts.SanitizedKeys.Should().NotBeNull();
        opts.SanitizedKeys.Should().Contain(new[] { "password", "passwordHash", "token", "refreshToken" });
    }

    [Fact]
    public void AuditEventDto_IsReadonlyRecord()
    {
        var dto = new AuditEventDto(
            Id: 1,
            OccurredAt: DateTime.UtcNow,
            ActorUserId: 42,
            ActorUsername: "admin",
            Action: "usuario.create",
            TargetType: "Usuario",
            TargetId: "99",
            CorrelationId: Guid.NewGuid(),
            IpAddress: "1.2.3.4",
            Metadata: """{"after":{"username":"juan"}}""");

        dto.Id.Should().Be(1);
        dto.Action.Should().Be("usuario.create");
        dto.TargetType.Should().Be("Usuario");
    }

    [Fact]
    public void AuditEventFilter_DefaultLimitIsReasonable()
    {
        var f = new AuditEventFilter(
            ActorUserId: null,
            TargetType: null,
            TargetId: null,
            From: null,
            To: null,
            Cursor: null);

        f.Limit.Should().BeInRange(1, 100);
    }

    [Fact]
    public void AuditEventFilter_AcceptsExplicitLimitWithinBounds()
    {
        var f = new AuditEventFilter(
            ActorUserId: 42,
            TargetType: "Usuario",
            TargetId: "99",
            From: DateTime.UtcNow.AddDays(-7),
            To: DateTime.UtcNow,
            Cursor: "opaque-cursor",
            Limit: 100);

        f.Limit.Should().Be(100);
        f.ActorUserId.Should().Be(42);
    }
}
