using Dapper;
using FluentAssertions;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using Quartz;
using SIGCM2.Application.Audit;
using SIGCM2.Infrastructure.Audit.Jobs;
using SIGCM2.Infrastructure.Persistence;
using Xunit;

namespace SIGCM2.Application.Tests.Infrastructure.Audit;

/// UDT-010 Batch 11 — audit maintenance jobs integration tests.
/// Executes each IJob directly (no Quartz scheduler needed) against SIGCM2_Test.
[Collection("Database")]
public sealed class AuditJobsTests : IAsyncLifetime
{
    private const string ConnectionString = TestConnectionStrings.AppTestDb;

    private SqlConnection _connection = null!;
    private SqlConnectionFactory _factory = null!;

    public async Task InitializeAsync()
    {
        _connection = new SqlConnection(ConnectionString);
        await _connection.OpenAsync();
        await _connection.ExecuteAsync("DELETE FROM dbo.AuditEvent; DELETE FROM dbo.SecurityEvent;");
        _factory = new SqlConnectionFactory(ConnectionString);
    }

    public async Task DisposeAsync()
    {
        await _connection.ExecuteAsync("DELETE FROM dbo.AuditEvent; DELETE FROM dbo.SecurityEvent;");
        await _connection.CloseAsync();
        await _connection.DisposeAsync();
    }

    private static IJobExecutionContext MockContext()
    {
        var ctx = Substitute.For<IJobExecutionContext>();
        ctx.CancellationToken.Returns(CancellationToken.None);
        return ctx;
    }

    [Fact]
    public async Task PartitionManager_ExtendsFunctionForward_Idempotent()
    {
        var job = new AuditPartitionManagerJob(_factory, NullLogger<AuditPartitionManagerJob>.Instance, TimeProvider.System);

        // First run: ensure the target boundary exists
        await job.Execute(MockContext());

        // Compute target as the job does
        var now = DateTime.UtcNow;
        var target = new DateTime(now.Year, now.Month, 1, 0, 0, 0, DateTimeKind.Utc).AddMonths(2);

        foreach (var pf in new[] { "pf_AuditEvent_Monthly", "pf_SecurityEvent_Monthly" })
        {
            var count = await _connection.ExecuteScalarAsync<int>("""
                SELECT COUNT(*)
                FROM sys.partition_functions pf
                JOIN sys.partition_range_values prv ON prv.function_id = pf.function_id
                WHERE pf.name = @Name AND CAST(prv.value AS DATETIME2(3)) = @Target;
                """, new { Name = pf, Target = target });
            count.Should().Be(1, $"{pf} should have boundary {target:yyyy-MM-dd}");
        }

        // Second run must not throw (idempotent)
        await job.Execute(MockContext());
    }

    [Fact]
    public async Task RetentionEnforcer_PurgesAuditEventOlderThan10Years_AndSecurityOlderThan5Years()
    {
        await _connection.ExecuteAsync("""
            INSERT INTO dbo.AuditEvent (OccurredAt, ActorUserId, Action, TargetType, TargetId)
            VALUES
                (@Ancient, 1, 'x.y', 'T', '1'),       -- should be purged
                (@Recent,  1, 'x.y', 'T', '2');       -- should stay
            INSERT INTO dbo.SecurityEvent (OccurredAt, ActorUserId, Action, Result)
            VALUES
                (@Ancient5, 1, 'login', 'success'),    -- should be purged
                (@Recent,   1, 'login', 'success');    -- should stay
            """, new
        {
            Ancient = DateTime.UtcNow.AddYears(-11),
            Recent = DateTime.UtcNow.AddDays(-1),
            Ancient5 = DateTime.UtcNow.AddYears(-6),
        });

        var job = new AuditRetentionEnforcerJob(_factory, NullLogger<AuditRetentionEnforcerJob>.Instance, TimeProvider.System);
        await job.Execute(MockContext());

        var auditCount = await _connection.ExecuteScalarAsync<int>("SELECT COUNT(*) FROM dbo.AuditEvent;");
        var securityCount = await _connection.ExecuteScalarAsync<int>("SELECT COUNT(*) FROM dbo.SecurityEvent;");
        auditCount.Should().Be(1);
        securityCount.Should().Be(1);
    }

    [Fact]
    public async Task IntegrityCheck_AllOk_DoesNotEmitSecurityEvent()
    {
        var security = Substitute.For<ISecurityEventLogger>();
        var job = new AuditIntegrityCheckJob(_factory, security, NullLogger<AuditIntegrityCheckJob>.Instance, TimeProvider.System);

        // Ensure partition manager has run first so next-3-months exist
        await new AuditPartitionManagerJob(_factory, NullLogger<AuditPartitionManagerJob>.Instance, TimeProvider.System).Execute(MockContext());

        await job.Execute(MockContext());

        await security.DidNotReceive().LogAsync(
            action: "system.integrity_alert",
            result: Arg.Any<string>(),
            actorUserId: Arg.Any<int?>(),
            attemptedUsername: Arg.Any<string?>(),
            sessionId: Arg.Any<Guid?>(),
            failureReason: Arg.Any<string?>(),
            metadata: Arg.Any<object?>(),
            ct: Arg.Any<CancellationToken>());
    }
}
