using Dapper;
using FluentAssertions;
using Microsoft.Data.SqlClient;
using Xunit;

namespace SIGCM2.Api.Tests.Audit;

/// UDT-010 Batch 1 — V010 migration integration smoke tests.
/// Validates:
///   REQ-AUD-1: SYSTEM_VERSIONING active on catalog entities (smoke: Usuario + Usuario_History query).
///   REQ-AUD-2: dbo.AuditEvent exists, accepts valid INSERT, CHECK constraints reject invalid data.
///   REQ-SEC-1: dbo.SecurityEvent exists, CK_SecurityEvent_Result rejects invalid Result.
[Collection("ApiIntegration")]
public sealed class V010MigrationTests : IClassFixture<SIGCM2.TestSupport.TestWebAppFactory>
{
    private const string ConnectionString = TestConnectionStrings.ApiTestDb;

    public V010MigrationTests(SIGCM2.TestSupport.TestWebAppFactory _)
    {
        // Depend on the factory so SqlTestFixture.InitializeAsync runs (validates V010 applied + resets DB).
    }

    [Fact]
    public async Task AuditEvent_Insert_WithValidData_Persists()
    {
        await using var conn = new SqlConnection(ConnectionString);
        await conn.OpenAsync();

        var correlationId = Guid.NewGuid();
        var insertedId = await conn.ExecuteScalarAsync<long>("""
            INSERT INTO dbo.AuditEvent (ActorUserId, Action, TargetType, TargetId, CorrelationId, Metadata)
            VALUES (@ActorUserId, @Action, @TargetType, @TargetId, @CorrelationId, @Metadata);
            SELECT CAST(SCOPE_IDENTITY() AS BIGINT);
            """,
            new
            {
                ActorUserId = 1,
                Action = "usuario.create",
                TargetType = "Usuario",
                TargetId = "42",
                CorrelationId = correlationId,
                Metadata = """{"after":{"username":"juan"}}"""
            });

        insertedId.Should().BeGreaterThan(0);

        var roundtrip = await conn.QuerySingleAsync<(string Action, string TargetType, string TargetId, Guid? CorrelationId)>(
            "SELECT Action, TargetType, TargetId, CorrelationId FROM dbo.AuditEvent WHERE Id = @Id",
            new { Id = insertedId });
        roundtrip.Action.Should().Be("usuario.create");
        roundtrip.TargetType.Should().Be("Usuario");
        roundtrip.TargetId.Should().Be("42");
        roundtrip.CorrelationId.Should().Be(correlationId);
    }

    [Fact]
    public async Task AuditEvent_Insert_WithInvalidActionFormat_FailsCheckConstraint()
    {
        await using var conn = new SqlConnection(ConnectionString);
        await conn.OpenAsync();

        var act = async () => await conn.ExecuteAsync("""
            INSERT INTO dbo.AuditEvent (ActorUserId, Action, TargetType, TargetId)
            VALUES (1, 'invalid_no_dot', 'Usuario', '1');
            """);

        await act.Should().ThrowAsync<SqlException>()
            .Where(e => e.Message.Contains("CK_AuditEvent_Action"));
    }

    [Fact]
    public async Task AuditEvent_Insert_WithNonJsonMetadata_FailsCheckConstraint()
    {
        await using var conn = new SqlConnection(ConnectionString);
        await conn.OpenAsync();

        var act = async () => await conn.ExecuteAsync("""
            INSERT INTO dbo.AuditEvent (ActorUserId, Action, TargetType, TargetId, Metadata)
            VALUES (1, 'usuario.create', 'Usuario', '1', 'not-json');
            """);

        await act.Should().ThrowAsync<SqlException>()
            .Where(e => e.Message.Contains("CK_AuditEvent_Metadata"));
    }

    [Fact]
    public async Task SecurityEvent_Insert_WithInvalidResult_FailsCheckConstraint()
    {
        await using var conn = new SqlConnection(ConnectionString);
        await conn.OpenAsync();

        var act = async () => await conn.ExecuteAsync("""
            INSERT INTO dbo.SecurityEvent (ActorUserId, Action, Result)
            VALUES (1, 'login', 'neutral');
            """);

        await act.Should().ThrowAsync<SqlException>()
            .Where(e => e.Message.Contains("CK_SecurityEvent_Result"));
    }

    [Fact]
    public async Task Usuario_SystemVersioning_IsActive_And_TemporalQueryReturnsHistory()
    {
        await using var conn = new SqlConnection(ConnectionString);
        await conn.OpenAsync();

        // Seed: insert a scratch user and wait long enough for ValidFrom to be in the past
        var username = "b1spike_" + Guid.NewGuid().ToString("N")[..8];
        var newId = await conn.ExecuteScalarAsync<int>("""
            INSERT INTO dbo.Usuario (Username, PasswordHash, Nombre, Apellido, Rol, PermisosJson)
            VALUES (@u, 'hash', 'Temp', 'User', 'admin', '{"grant":[],"deny":[]}');
            SELECT CAST(SCOPE_IDENTITY() AS INT);
            """, new { u = username });

        // Capture a timestamp *after* creation but *before* update
        await Task.Delay(50);
        var snapshotTime = DateTime.UtcNow;
        await Task.Delay(50);

        // Update the email
        await conn.ExecuteAsync(
            "UPDATE dbo.Usuario SET Email = @e WHERE Id = @id",
            new { e = "new@example.com", id = newId });

        // Temporal query: state at snapshotTime should NOT yet have the new email
        var historicalEmail = await conn.ExecuteScalarAsync<string?>("""
            SELECT Email FROM dbo.Usuario
            FOR SYSTEM_TIME AS OF @ts
            WHERE Id = @id
            """, new { ts = snapshotTime, id = newId });

        historicalEmail.Should().BeNull("the historical snapshot predates the email update");

        // Current state HAS the new email
        var currentEmail = await conn.ExecuteScalarAsync<string?>(
            "SELECT Email FROM dbo.Usuario WHERE Id = @id",
            new { id = newId });
        currentEmail.Should().Be("new@example.com");

        // Usuario_History must have at least one row for this user
        var historyCount = await conn.ExecuteScalarAsync<int>(
            "SELECT COUNT(*) FROM dbo.Usuario_History WHERE Id = @id",
            new { id = newId });
        historyCount.Should().BeGreaterThan(0);

        // Cleanup: Respawn will reset between test runs, but this test could run alone — best-effort delete.
        await conn.ExecuteAsync("DELETE FROM dbo.Usuario WHERE Id = @id", new { id = newId });
    }
}
