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

namespace SIGCM2.Application.Tests.Infrastructure.Pricing;

/// <summary>
/// PRC-001 — Integration tests for V020/V021/V022/V023/V024 migrations.
/// These tests verify the BD schema applied against SIGCM2_Test_App:
///   - V020: permission 'tasacion:caracteres_especiales:gestionar' exists.
///   - V021: dbo.ChargeableCharConfig table + SYSTEM_VERSIONING + SPs (initial MedioId shape).
///   - V022: 4 global seed rows ($, %, !, ¡) exist and are active.
///   - V023 (scope delta): MedioId → ProductTypeId refactor + ReactivateWithGuard SP.
///   - V024 (scope delta): global seed PricePerUnit reset to 0.0000 (opt-in billing).
///
/// After GREEN all tests pass. SqlTestFixture applies V023+V024 during initialization.
/// </summary>
[Collection("Database")]
public sealed class ChargeableCharConfigMigrationTests : IAsyncLifetime
{
    private SqlConnection _connection = null!;

    public async Task InitializeAsync()
    {
        _connection = new SqlConnection(TestConnectionStrings.AppTestDb);
        await _connection.OpenAsync();
    }

    public async Task DisposeAsync()
    {
        await _connection.CloseAsync();
        await _connection.DisposeAsync();
    }

    // ── V021: Table existence ─────────────────────────────────────────────

    [Fact]
    public async Task V021_Table_ChargeableCharConfig_Exists()
    {
        var exists = await _connection.ExecuteScalarAsync<int>(
            "SELECT COUNT(*) FROM sys.tables WHERE object_id = OBJECT_ID('dbo.ChargeableCharConfig')");

        exists.Should().Be(1, "V021 debe crear dbo.ChargeableCharConfig");
    }

    [Fact]
    public async Task V021_Table_ChargeableCharConfig_HasSystemVersioning()
    {
        var temporalType = await _connection.ExecuteScalarAsync<int?>(@"
            SELECT temporal_type
            FROM sys.tables
            WHERE object_id = OBJECT_ID('dbo.ChargeableCharConfig')");

        temporalType.Should().Be(2, "SYSTEM_VERSIONING = ON requiere temporal_type = 2");
    }

    [Fact]
    public async Task V021_HistoryTable_ChargeableCharConfigHistory_Exists()
    {
        var exists = await _connection.ExecuteScalarAsync<int>(
            "SELECT COUNT(*) FROM sys.tables WHERE object_id = OBJECT_ID('dbo.ChargeableCharConfig_History')");

        exists.Should().Be(1, "SYSTEM_VERSIONING debe crear dbo.ChargeableCharConfig_History");
    }

    [Fact]
    public async Task V021_FilteredUniqueIndex_UX_ChargeableCharConfig_Vigente_Exists()
    {
        var exists = await _connection.ExecuteScalarAsync<int>(@"
            SELECT COUNT(*)
            FROM sys.indexes
            WHERE name = 'UX_ChargeableCharConfig_Vigente'
              AND object_id = OBJECT_ID('dbo.ChargeableCharConfig')");

        exists.Should().Be(1, "UX_ChargeableCharConfig_Vigente filtered index debe existir");
    }

    [Fact]
    public async Task V021_CoverIndex_IX_ChargeableCharConfig_Query_Exists()
    {
        var exists = await _connection.ExecuteScalarAsync<int>(@"
            SELECT COUNT(*)
            FROM sys.indexes
            WHERE name = 'IX_ChargeableCharConfig_Query'
              AND object_id = OBJECT_ID('dbo.ChargeableCharConfig')");

        exists.Should().Be(1, "IX_ChargeableCharConfig_Query covering index debe existir");
    }

    // ── V021: SP — usp_ChargeableCharConfig_InsertWithClose ──────────────

    [Fact]
    public async Task V021_SP_InsertWithClose_Exists()
    {
        var exists = await _connection.ExecuteScalarAsync<int>(@"
            SELECT COUNT(*)
            FROM sys.objects
            WHERE object_id = OBJECT_ID('dbo.usp_ChargeableCharConfig_InsertWithClose')
              AND type = 'P'");

        exists.Should().Be(1, "usp_ChargeableCharConfig_InsertWithClose debe existir");
    }

    [Fact]
    public async Task usp_InsertWithClose_FirstPrice_ClosedIsNull()
    {
        // Cleanup (use ProductTypeId IS NULL after V023 refactor)
        await _connection.ExecuteAsync(
            "DELETE FROM dbo.ChargeableCharConfig WHERE ProductTypeId IS NULL AND Symbol = N'@'");

        var p = new DynamicParameters();
        p.Add("@ProductTypeId", null,   System.Data.DbType.Int32);  // V023: was @MedioId
        p.Add("@Symbol",        "@",    System.Data.DbType.String);
        p.Add("@Category",      "Other",System.Data.DbType.String);
        p.Add("@PricePerUnit",  1.5m,   System.Data.DbType.Decimal, precision: 18, scale: 4);
        p.Add("@ValidFrom",     new DateTime(2026, 1, 1), System.Data.DbType.Date);
        p.Add("@NewId",         dbType: System.Data.DbType.Int64,
               direction: System.Data.ParameterDirection.Output);
        p.Add("@ClosedId",      dbType: System.Data.DbType.Int64,
               direction: System.Data.ParameterDirection.Output);

        await _connection.ExecuteAsync(
            "dbo.usp_ChargeableCharConfig_InsertWithClose",
            p,
            commandType: System.Data.CommandType.StoredProcedure);

        var newId    = p.Get<long?>("@NewId");
        var closedId = p.Get<long?>("@ClosedId");

        newId.Should().BeGreaterThan(0, "primer insert debe devolver NewId > 0");
        closedId.Should().BeNull("primer insert no cierra nada — ClosedId debe ser NULL");

        // Cleanup
        await _connection.ExecuteAsync(
            "DELETE FROM dbo.ChargeableCharConfig WHERE Symbol = N'@' AND ProductTypeId IS NULL");
    }

    [Fact]
    public async Task usp_InsertWithClose_HappyPath_ClosesAndCreates()
    {
        // Seed primer activo
        await _connection.ExecuteAsync(
            "DELETE FROM dbo.ChargeableCharConfig WHERE ProductTypeId IS NULL AND Symbol = N'€'");

        var p1 = new DynamicParameters();
        p1.Add("@ProductTypeId", null,   System.Data.DbType.Int32);  // V023: was @MedioId
        p1.Add("@Symbol",        "€",    System.Data.DbType.String);
        p1.Add("@Category",      "Currency", System.Data.DbType.String);
        p1.Add("@PricePerUnit",  1.0m,   System.Data.DbType.Decimal, precision: 18, scale: 4);
        p1.Add("@ValidFrom",     new DateTime(2026, 1, 1), System.Data.DbType.Date);
        p1.Add("@NewId",         dbType: System.Data.DbType.Int64,
                direction: System.Data.ParameterDirection.Output);
        p1.Add("@ClosedId",      dbType: System.Data.DbType.Int64,
                direction: System.Data.ParameterDirection.Output);

        await _connection.ExecuteAsync("dbo.usp_ChargeableCharConfig_InsertWithClose", p1,
            commandType: System.Data.CommandType.StoredProcedure);
        var firstId = p1.Get<long?>("@NewId")!.Value;

        // Insertar segundo (debe cerrar el primero)
        var p2 = new DynamicParameters();
        p2.Add("@ProductTypeId", null,        System.Data.DbType.Int32);  // V023: was @MedioId
        p2.Add("@Symbol",        "€",         System.Data.DbType.String);
        p2.Add("@Category",      "Currency",  System.Data.DbType.String);
        p2.Add("@PricePerUnit",  2.0m,        System.Data.DbType.Decimal, precision: 18, scale: 4);
        p2.Add("@ValidFrom",     new DateTime(2026, 2, 1), System.Data.DbType.Date);
        p2.Add("@NewId",         dbType: System.Data.DbType.Int64,
                direction: System.Data.ParameterDirection.Output);
        p2.Add("@ClosedId",      dbType: System.Data.DbType.Int64,
                direction: System.Data.ParameterDirection.Output);

        await _connection.ExecuteAsync("dbo.usp_ChargeableCharConfig_InsertWithClose", p2,
            commandType: System.Data.CommandType.StoredProcedure);

        var newId    = p2.Get<long?>("@NewId")!.Value;
        var closedId = p2.Get<long?>("@ClosedId");

        newId.Should().BeGreaterThan(firstId);
        closedId.Should().Be(firstId, "el primer activo debe ser cerrado");

        // Verify el activo cerrado tiene ValidTo = 2026-01-31
        var closedValidTo = await _connection.ExecuteScalarAsync<DateTime?>(
            "SELECT ValidTo FROM dbo.ChargeableCharConfig WHERE Id = @Id",
            new { Id = firstId });
        closedValidTo.Should().Be(new DateTime(2026, 1, 31),
            "ValidTo del cerrado = ValidFrom(nuevo) - 1 día");

        // Cleanup
        await _connection.ExecuteAsync(
            "DELETE FROM dbo.ChargeableCharConfig WHERE Symbol = N'€' AND ProductTypeId IS NULL");
    }

    [Fact]
    public async Task usp_InsertWithClose_ForwardOnlyViolation_Throws50409()
    {
        await _connection.ExecuteAsync(
            "DELETE FROM dbo.ChargeableCharConfig WHERE ProductTypeId IS NULL AND Symbol = N'£'");

        var p1 = new DynamicParameters();
        p1.Add("@ProductTypeId", null,   System.Data.DbType.Int32);  // V023: was @MedioId
        p1.Add("@Symbol",        "£",    System.Data.DbType.String);
        p1.Add("@Category",      "Currency", System.Data.DbType.String);
        p1.Add("@PricePerUnit",  1.0m,   System.Data.DbType.Decimal, precision: 18, scale: 4);
        p1.Add("@ValidFrom",     new DateTime(2026, 3, 1), System.Data.DbType.Date);
        p1.Add("@NewId",         dbType: System.Data.DbType.Int64,
                direction: System.Data.ParameterDirection.Output);
        p1.Add("@ClosedId",      dbType: System.Data.DbType.Int64,
                direction: System.Data.ParameterDirection.Output);

        await _connection.ExecuteAsync("dbo.usp_ChargeableCharConfig_InsertWithClose", p1,
            commandType: System.Data.CommandType.StoredProcedure);

        // Intento con ValidFrom <= activo.ValidFrom → debe lanzar 50409
        var p2 = new DynamicParameters();
        p2.Add("@ProductTypeId", null,   System.Data.DbType.Int32);  // V023: was @MedioId
        p2.Add("@Symbol",        "£",    System.Data.DbType.String);
        p2.Add("@Category",      "Currency", System.Data.DbType.String);
        p2.Add("@PricePerUnit",  2.0m,   System.Data.DbType.Decimal, precision: 18, scale: 4);
        p2.Add("@ValidFrom",     new DateTime(2026, 3, 1), System.Data.DbType.Date); // igual → viola forward-only
        p2.Add("@NewId",         dbType: System.Data.DbType.Int64,
                direction: System.Data.ParameterDirection.Output);
        p2.Add("@ClosedId",      dbType: System.Data.DbType.Int64,
                direction: System.Data.ParameterDirection.Output);

        var act = async () => await _connection.ExecuteAsync(
            "dbo.usp_ChargeableCharConfig_InsertWithClose", p2,
            commandType: System.Data.CommandType.StoredProcedure);

        await act.Should().ThrowAsync<SqlException>()
            .Where(ex => ex.Number == 50409,
                "ValidFrom igual al activo debe generar THROW 50409 (forward-only)");

        // Cleanup
        await _connection.ExecuteAsync(
            "DELETE FROM dbo.ChargeableCharConfig WHERE Symbol = N'£' AND ProductTypeId IS NULL");
    }

    [Fact]
    public async Task usp_InsertWithClose_ProductTypeNull_GlobalFallback_Works()
    {
        // V023: global fallback is now ProductTypeId IS NULL (was MedioId IS NULL)
        await _connection.ExecuteAsync(
            "DELETE FROM dbo.ChargeableCharConfig WHERE ProductTypeId IS NULL AND Symbol = N'¥'");

        var p = new DynamicParameters();
        p.Add("@ProductTypeId", null,   System.Data.DbType.Int32);  // V023: was @MedioId
        p.Add("@Symbol",        "¥",    System.Data.DbType.String);
        p.Add("@Category",      "Currency", System.Data.DbType.String);
        p.Add("@PricePerUnit",  1.25m,  System.Data.DbType.Decimal, precision: 18, scale: 4);
        p.Add("@ValidFrom",     new DateTime(2026, 1, 1), System.Data.DbType.Date);
        p.Add("@NewId",         dbType: System.Data.DbType.Int64,
               direction: System.Data.ParameterDirection.Output);
        p.Add("@ClosedId",      dbType: System.Data.DbType.Int64,
               direction: System.Data.ParameterDirection.Output);

        await _connection.ExecuteAsync("dbo.usp_ChargeableCharConfig_InsertWithClose", p,
            commandType: System.Data.CommandType.StoredProcedure);

        var newId = p.Get<long?>("@NewId");
        newId.Should().BeGreaterThan(0, "global insert (ProductTypeId NULL) debe funcionar");

        // Cleanup
        await _connection.ExecuteAsync(
            "DELETE FROM dbo.ChargeableCharConfig WHERE Symbol = N'¥' AND ProductTypeId IS NULL");
    }

    [Fact]
    public async Task SystemVersioning_UpdateOnClose_ProducesHistoryRow()
    {
        await _connection.ExecuteAsync(
            "DELETE FROM dbo.ChargeableCharConfig WHERE ProductTypeId IS NULL AND Symbol = N'#'");

        var p1 = new DynamicParameters();
        p1.Add("@ProductTypeId", null,   System.Data.DbType.Int32);  // V023: was @MedioId
        p1.Add("@Symbol",        "#",    System.Data.DbType.String);
        p1.Add("@Category",      "Other", System.Data.DbType.String);
        p1.Add("@PricePerUnit",  1.0m,   System.Data.DbType.Decimal, precision: 18, scale: 4);
        p1.Add("@ValidFrom",     new DateTime(2026, 1, 1), System.Data.DbType.Date);
        p1.Add("@NewId",         dbType: System.Data.DbType.Int64,
                direction: System.Data.ParameterDirection.Output);
        p1.Add("@ClosedId",      dbType: System.Data.DbType.Int64,
                direction: System.Data.ParameterDirection.Output);

        await _connection.ExecuteAsync("dbo.usp_ChargeableCharConfig_InsertWithClose", p1,
            commandType: System.Data.CommandType.StoredProcedure);
        var firstId = p1.Get<long?>("@NewId")!.Value;

        // Close it by inserting a newer version
        var p2 = new DynamicParameters();
        p2.Add("@ProductTypeId", null,   System.Data.DbType.Int32);  // V023: was @MedioId
        p2.Add("@Symbol",        "#",    System.Data.DbType.String);
        p2.Add("@Category",      "Other", System.Data.DbType.String);
        p2.Add("@PricePerUnit",  2.0m,   System.Data.DbType.Decimal, precision: 18, scale: 4);
        p2.Add("@ValidFrom",     new DateTime(2026, 6, 1), System.Data.DbType.Date);
        p2.Add("@NewId",         dbType: System.Data.DbType.Int64,
                direction: System.Data.ParameterDirection.Output);
        p2.Add("@ClosedId",      dbType: System.Data.DbType.Int64,
                direction: System.Data.ParameterDirection.Output);

        await _connection.ExecuteAsync("dbo.usp_ChargeableCharConfig_InsertWithClose", p2,
            commandType: System.Data.CommandType.StoredProcedure);

        // The UPDATE on the closed row should produce a history row
        var historyCount = await _connection.ExecuteScalarAsync<int>(
            "SELECT COUNT(*) FROM dbo.ChargeableCharConfig_History WHERE Id = @Id",
            new { Id = firstId });

        historyCount.Should().BeGreaterThanOrEqualTo(1,
            "UPDATE en SYSTEM_VERSIONED table debe producir al menos 1 fila en history");

        // Cleanup
        await _connection.ExecuteAsync(
            "DELETE FROM dbo.ChargeableCharConfig WHERE Symbol = N'#' AND ProductTypeId IS NULL");
    }

    // ── V023 scope delta: SP — usp_ChargeableCharConfig_GetActiveForProductType ────────────

    [Fact]
    public async Task V023_SP_GetActiveForProductType_Exists()
    {
        var exists = await _connection.ExecuteScalarAsync<int>(@"
            SELECT COUNT(*)
            FROM sys.objects
            WHERE object_id = OBJECT_ID('dbo.usp_ChargeableCharConfig_GetActiveForProductType')
              AND type = 'P'");

        exists.Should().Be(1, "V023 debe crear usp_ChargeableCharConfig_GetActiveForProductType (renamed from GetActiveForMedio)");
    }

    [Fact]
    public async Task V023_SP_GetActiveForMedio_NoLongerExists()
    {
        var exists = await _connection.ExecuteScalarAsync<int>(@"
            SELECT COUNT(*)
            FROM sys.objects
            WHERE object_id = OBJECT_ID('dbo.usp_ChargeableCharConfig_GetActiveForMedio')
              AND type = 'P'");

        exists.Should().Be(0, "V023 debe eliminar usp_ChargeableCharConfig_GetActiveForMedio (renamed to GetActiveForProductType)");
    }

    [Fact]
    public async Task V023_SP_ReactivateWithGuard_Exists()
    {
        var exists = await _connection.ExecuteScalarAsync<int>(@"
            SELECT COUNT(*)
            FROM sys.objects
            WHERE object_id = OBJECT_ID('dbo.usp_ChargeableCharConfig_ReactivateWithGuard')
              AND type = 'P'");

        exists.Should().Be(1, "V023 debe crear usp_ChargeableCharConfig_ReactivateWithGuard (new SP for feature 3)");
    }

    // ── V023 scope delta: column ProductTypeId replaces MedioId ─────────

    [Fact]
    public async Task V023_Column_ProductTypeId_Exists()
    {
        var exists = await _connection.ExecuteScalarAsync<int>(@"
            SELECT COUNT(*)
            FROM sys.columns
            WHERE object_id = OBJECT_ID('dbo.ChargeableCharConfig')
              AND name = 'ProductTypeId'");

        exists.Should().Be(1, "V023 debe agregar columna ProductTypeId a dbo.ChargeableCharConfig");
    }

    [Fact]
    public async Task V023_Column_MedioId_NoLongerExists()
    {
        var exists = await _connection.ExecuteScalarAsync<int>(@"
            SELECT COUNT(*)
            FROM sys.columns
            WHERE object_id = OBJECT_ID('dbo.ChargeableCharConfig')
              AND name = 'MedioId'");

        exists.Should().Be(0, "V023 debe eliminar columna MedioId de dbo.ChargeableCharConfig");
    }

    [Fact]
    public async Task V023_FK_ProductType_Exists()
    {
        var exists = await _connection.ExecuteScalarAsync<int>(@"
            SELECT COUNT(*)
            FROM sys.foreign_keys
            WHERE parent_object_id = OBJECT_ID('dbo.ChargeableCharConfig')
              AND referenced_object_id = OBJECT_ID('dbo.ProductType')");

        exists.Should().Be(1, "V023 debe crear FK de ChargeableCharConfig a ProductType");
    }

    [Fact]
    public async Task V023_Check_Price_NonNegative_Exists()
    {
        var exists = await _connection.ExecuteScalarAsync<int>(@"
            SELECT COUNT(*)
            FROM sys.check_constraints
            WHERE name = 'CK_ChargeableCharConfig_Price_NonNegative'
              AND parent_object_id = OBJECT_ID('dbo.ChargeableCharConfig')");

        exists.Should().Be(1, "V023 debe crear CK_ChargeableCharConfig_Price_NonNegative (>= 0 para opt-in billing)");
    }

    [Fact]
    public async Task V023_Check_Price_Positive_NoLongerExists()
    {
        var exists = await _connection.ExecuteScalarAsync<int>(@"
            SELECT COUNT(*)
            FROM sys.check_constraints
            WHERE name = 'CK_ChargeableCharConfig_Price_Positive'
              AND parent_object_id = OBJECT_ID('dbo.ChargeableCharConfig')");

        exists.Should().Be(0, "V023 debe eliminar CK_ChargeableCharConfig_Price_Positive (reemplazado por NonNegative)");
    }

    // ── V020: Permission existence ────────────────────────────────────────

    [Fact]
    public async Task V020_Permission_tasacion_caracteres_especiales_gestionar_Exists()
    {
        var exists = await _connection.ExecuteScalarAsync<int>(@"
            SELECT COUNT(*) FROM dbo.Permiso
            WHERE Codigo = 'tasacion:caracteres_especiales:gestionar'");

        exists.Should().Be(1,
            "V020 debe insertar el permiso 'tasacion:caracteres_especiales:gestionar'");
    }

    // ── V022: Seed rows (after V023 refactor: use ProductTypeId IS NULL) ─────

    [Fact]
    public async Task V022_Seeds_AtLeastFourGlobalRows()
    {
        var count = await _connection.ExecuteScalarAsync<int>(@"
            SELECT COUNT(*) FROM dbo.ChargeableCharConfig
            WHERE ProductTypeId IS NULL AND Symbol IN (N'$', N'%', N'!', N'¡')
              AND ValidTo IS NULL AND IsActive = 1");

        count.Should().Be(4, "V022 debe sembrar exactamente 4 filas globales: $, %, !, ¡ (global = ProductTypeId IS NULL tras V023)");
    }

    [Fact]
    public async Task V022_AllSeedRowsHaveIsActive_True()
    {
        var inactiveCount = await _connection.ExecuteScalarAsync<int>(@"
            SELECT COUNT(*) FROM dbo.ChargeableCharConfig
            WHERE ProductTypeId IS NULL AND Symbol IN (N'$', N'%', N'!', N'¡')
              AND IsActive = 0");

        inactiveCount.Should().Be(0, "todas las filas de seed deben tener IsActive = 1");
    }

    // ── V024 scope delta: global seed prices = 0.0000 (opt-in billing) ───────

    [Fact]
    public async Task V024_GlobalSeedRows_HaveZeroPrice()
    {
        var nonZeroCount = await _connection.ExecuteScalarAsync<int>(@"
            SELECT COUNT(*) FROM dbo.ChargeableCharConfig
            WHERE ProductTypeId IS NULL
              AND Symbol IN (N'$', N'%', N'!', N'¡')
              AND ValidTo IS NULL
              AND PricePerUnit <> 0.0000");

        nonZeroCount.Should().Be(0,
            "V024 debe resetear todas las filas de seed global a PricePerUnit = 0.0000 (opt-in billing)");
    }

    [Fact]
    public async Task V024_GlobalSeedRows_AllHaveZeroPriceExact()
    {
        var rows = await _connection.QueryAsync<decimal>(@"
            SELECT PricePerUnit FROM dbo.ChargeableCharConfig
            WHERE ProductTypeId IS NULL
              AND Symbol IN (N'$', N'%', N'!', N'¡')
              AND ValidTo IS NULL");

        rows.Should().AllSatisfy(price =>
            price.Should().Be(0.0000m,
                "V024 seed: cada fila global debe tener PricePerUnit = 0.0000 exacto"));
    }
}
