using Dapper;
using Microsoft.Data.SqlClient;
using SIGCM2.Infrastructure.Persistence;

namespace SIGCM2.Application.Tests.Integration;

/// <summary>
/// Integration tests for RolPermisoRepository against SIGCM2_Test.
/// RED: written before the repository implementation exists.
/// </summary>
[Collection("Database")]
public class RolPermisoRepositoryTests : IAsyncLifetime
{
    private const string ConnectionString = TestConnectionStrings.AppTestDb;

    private SqlConnection _connection = null!;
    private RolPermisoRepository _repository = null!;

    public async Task InitializeAsync()
    {
        _connection = new SqlConnection(ConnectionString);
        await _connection.OpenAsync();

        // Ensure the 18 canonical permisos exist — idempotent MERGE.
        // Other test classes call Respawn.ResetAsync which may have cleared Permiso.
        await SeedPermisosCanonicalAsync();

        // Ensure canonical RolPermiso seeds are present.
        await SeedRolPermisosCanonicalAsync();

        // Restore RolPermiso seeds for 'cajero' (4 permisos) in case prior test modified them.
        await RestoreCajeroPermisosAsync();

        var factory = new SqlConnectionFactory(ConnectionString);
        _repository = new RolPermisoRepository(factory);
    }

    private async Task SeedPermisosCanonicalAsync()
    {
        const string sql = """
            SET QUOTED_IDENTIFIER ON;
            MERGE dbo.Permiso AS t
            USING (VALUES
                ('ventas:contado:crear',                N'Cargar orden contado',                NULL,                                           'ventas'),
                ('ventas:contado:modificar',            N'Modificar orden contado',             NULL,                                           'ventas'),
                ('ventas:contado:cobrar',               N'Cobrar orden contado',                NULL,                                           'ventas'),
                ('ventas:contado:facturar',             N'Facturar orden contado',              NULL,                                           'ventas'),
                ('ventas:ctacte:crear',                 N'Cargar orden cuenta corriente',       NULL,                                           'ventas'),
                ('ventas:ctacte:facturar',              N'Facturar lote cuenta corriente',      NULL,                                           'ventas'),
                ('textos:editar',                       N'Editar textos',                       NULL,                                           'textos'),
                ('textos:reclamos:ver',                 N'Ver reclamos de textos',              NULL,                                           'textos'),
                ('pauta:azanu:ver',                     N'Ver AZANU en pauta',                  NULL,                                           'pauta'),
                ('pauta:limpiar',                       N'Limpieza de pauta',                   NULL,                                           'pauta'),
                ('pauta:recursos:fueradehora',          N'Recursos fuera de hora',              NULL,                                           'pauta'),
                ('productores:deuda:ver',               N'Ver deuda propia de productores',     NULL,                                           'productores'),
                ('productores:pendientes:crear',        N'Cargar pendientes de productores',    NULL,                                           'productores'),
                ('productores:deuda:bypass',            N'Bypass de deuda de productores',      NULL,                                           'productores'),
                ('administracion:usuarios:gestionar',   N'Gestionar usuarios del sistema',      N'Crear, editar y desactivar usuarios',         'administracion'),
                ('administracion:tarifarios:gestionar', N'Gestionar tarifarios',                N'Crear y modificar tarifarios de publicidad',  'administracion'),
                ('administracion:medios:gestionar',     N'Gestionar medios publicitarios',      N'Alta y configuracion de medios',              'administracion'),
                ('administracion:auditoria:ver',        N'Ver logs de auditoria',               N'Acceso al dashboard de auditoria',            'administracion')
            ) AS s (Codigo, Nombre, Descripcion, Modulo)
            ON t.Codigo = s.Codigo
            WHEN NOT MATCHED BY TARGET THEN
                INSERT (Codigo, Nombre, Descripcion, Modulo)
                VALUES (s.Codigo, s.Nombre, s.Descripcion, s.Modulo);
            """;
        await _connection.ExecuteAsync(sql);
    }

    private async Task SeedRolPermisosCanonicalAsync()
    {
        const string sql = """
            SET QUOTED_IDENTIFIER ON;
            MERGE dbo.RolPermiso AS t
            USING (
                SELECT r.Id AS RolId, p.Id AS PermisoId
                FROM (VALUES
                    ('admin', 'ventas:contado:crear'),
                    ('admin', 'ventas:contado:modificar'),
                    ('admin', 'ventas:contado:cobrar'),
                    ('admin', 'ventas:contado:facturar'),
                    ('admin', 'ventas:ctacte:crear'),
                    ('admin', 'ventas:ctacte:facturar'),
                    ('admin', 'textos:editar'),
                    ('admin', 'textos:reclamos:ver'),
                    ('admin', 'pauta:azanu:ver'),
                    ('admin', 'pauta:limpiar'),
                    ('admin', 'pauta:recursos:fueradehora'),
                    ('admin', 'productores:deuda:ver'),
                    ('admin', 'productores:pendientes:crear'),
                    ('admin', 'productores:deuda:bypass'),
                    ('admin', 'administracion:usuarios:gestionar'),
                    ('admin', 'administracion:tarifarios:gestionar'),
                    ('admin', 'administracion:medios:gestionar'),
                    ('admin', 'administracion:auditoria:ver'),
                    ('cajero', 'ventas:contado:crear'),
                    ('cajero', 'ventas:contado:modificar'),
                    ('cajero', 'ventas:contado:cobrar'),
                    ('cajero', 'ventas:contado:facturar'),
                    ('operador_ctacte', 'ventas:ctacte:crear'),
                    ('operador_ctacte', 'ventas:ctacte:facturar'),
                    ('picadora', 'textos:editar'),
                    ('picadora', 'textos:reclamos:ver'),
                    ('jefe_publicidad', 'textos:editar'),
                    ('jefe_publicidad', 'textos:reclamos:ver'),
                    ('jefe_publicidad', 'pauta:azanu:ver'),
                    ('jefe_publicidad', 'pauta:limpiar'),
                    ('jefe_publicidad', 'pauta:recursos:fueradehora'),
                    ('jefe_publicidad', 'productores:deuda:ver'),
                    ('jefe_publicidad', 'productores:deuda:bypass'),
                    ('productor', 'productores:deuda:ver'),
                    ('productor', 'productores:pendientes:crear'),
                    ('diagramacion', 'pauta:azanu:ver')
                ) AS x (RolCodigo, PermisoCodigo)
                JOIN dbo.Rol     r ON r.Codigo = x.RolCodigo
                JOIN dbo.Permiso p ON p.Codigo = x.PermisoCodigo
            ) AS s ON t.RolId = s.RolId AND t.PermisoId = s.PermisoId
            WHEN NOT MATCHED BY TARGET THEN
                INSERT (RolId, PermisoId) VALUES (s.RolId, s.PermisoId);
            """;
        await _connection.ExecuteAsync(sql);
    }

    public async Task DisposeAsync()
    {
        // Restore cajero permisos so TablesToIgnore still reflects clean seed state.
        await RestoreCajeroPermisosAsync();
        await _connection.CloseAsync();
        await _connection.DisposeAsync();
    }

    /// <summary>
    /// Restores the 4 canonical cajero permisos to match V006 seed state.
    /// Uses MERGE so it's idempotent.
    /// </summary>
    private async Task RestoreCajeroPermisosAsync()
    {
        // Delete any extra permisos assigned to cajero by tests
        await _connection.ExecuteAsync("""
            DELETE rp FROM dbo.RolPermiso rp
            JOIN dbo.Rol r ON r.Id = rp.RolId
            JOIN dbo.Permiso p ON p.Id = rp.PermisoId
            WHERE r.Codigo = 'cajero'
              AND p.Codigo NOT IN (
                'ventas:contado:crear',
                'ventas:contado:modificar',
                'ventas:contado:cobrar',
                'ventas:contado:facturar'
              );
            """);

        // Re-add the 4 canonical cajero permisos if missing
        await _connection.ExecuteAsync("""
            SET QUOTED_IDENTIFIER ON;
            MERGE dbo.RolPermiso AS t
            USING (
                SELECT r.Id AS RolId, p.Id AS PermisoId
                FROM (VALUES
                    ('cajero', 'ventas:contado:crear'),
                    ('cajero', 'ventas:contado:modificar'),
                    ('cajero', 'ventas:contado:cobrar'),
                    ('cajero', 'ventas:contado:facturar')
                ) AS x (RolCodigo, PermisoCodigo)
                JOIN dbo.Rol     r ON r.Codigo = x.RolCodigo
                JOIN dbo.Permiso p ON p.Codigo = x.PermisoCodigo
            ) AS s ON t.RolId = s.RolId AND t.PermisoId = s.PermisoId
            WHEN NOT MATCHED BY TARGET THEN
                INSERT (RolId, PermisoId) VALUES (s.RolId, s.PermisoId);
            """);
    }

    // ── GetByRolCodigoAsync ──────────────────────────────────────────────────

    [Fact]
    public async Task GetByRolCodigoAsync_Admin_Returns27Permisos()
    {
        // admin has 18 permisos from V006 + 3 new admin permisos from V007 (UDT-006)
        // + 1 from V011 (ADM-001): 'administracion:secciones:gestionar'
        // + 1 from V013 (ADM-008): 'administracion:puntos_de_venta:gestionar'
        // + 1 from V014 (ADM-009): 'administracion:fiscal:gestionar'
        // + 1 from V016 (CAT-001): 'catalogo:rubros:gestionar'
        // + 1 from V017 (PRD-001): 'catalogo:tipos:gestionar'
        // + 1 from V018 (PRD-002): 'catalogo:productos:gestionar' = 27 total
        var permisos = await _repository.GetByRolCodigoAsync("admin");

        Assert.Equal(27, permisos.Count);
    }

    [Fact]
    public async Task GetByRolCodigoAsync_Admin_ContainsAllModules()
    {
        var permisos = await _repository.GetByRolCodigoAsync("admin");
        var codigos = permisos.Select(p => p.Codigo).ToHashSet();

        Assert.Contains("ventas:contado:crear", codigos);
        Assert.Contains("administracion:auditoria:ver", codigos);
        Assert.Contains("pauta:limpiar", codigos);
        Assert.Contains("productores:deuda:bypass", codigos);
    }

    [Fact]
    public async Task GetByRolCodigoAsync_Cajero_Returns4Permisos()
    {
        // cajero: ventas:contado:crear, :modificar, :cobrar, :facturar
        var permisos = await _repository.GetByRolCodigoAsync("cajero");

        Assert.Equal(4, permisos.Count);
    }

    [Fact]
    public async Task GetByRolCodigoAsync_Cajero_OnlyVentasContadoPermisos()
    {
        var permisos = await _repository.GetByRolCodigoAsync("cajero");
        var codigos = permisos.Select(p => p.Codigo).ToHashSet();

        Assert.Contains("ventas:contado:crear", codigos);
        Assert.Contains("ventas:contado:modificar", codigos);
        Assert.Contains("ventas:contado:cobrar", codigos);
        Assert.Contains("ventas:contado:facturar", codigos);
    }

    [Fact]
    public async Task GetByRolCodigoAsync_Reportes_ReturnsEmpty()
    {
        // 'reportes' rol has 0 permisos in V006 seed
        var permisos = await _repository.GetByRolCodigoAsync("reportes");

        Assert.Empty(permisos);
    }

    [Fact]
    public async Task GetByRolCodigoAsync_NonExistentRol_ReturnsEmpty()
    {
        // Unknown rol código — returns empty list, not an exception
        var permisos = await _repository.GetByRolCodigoAsync("rol_inexistente_xyz");

        Assert.Empty(permisos);
    }

    [Fact]
    public async Task GetByRolCodigoAsync_ReturnsFullPermisoEntities()
    {
        var permisos = await _repository.GetByRolCodigoAsync("cajero");

        var primero = permisos.First();
        // All entity fields must be populated
        Assert.True(primero.Id > 0);
        Assert.False(string.IsNullOrWhiteSpace(primero.Codigo));
        Assert.False(string.IsNullOrWhiteSpace(primero.Nombre));
    }

    // ── ReplaceForRolAsync ───────────────────────────────────────────────────

    [Fact]
    public async Task ReplaceForRolAsync_ReplacesExistingSetWithNewSet()
    {
        // Get cajero's rol ID and a different permiso ID
        var cajeroId = await _connection.ExecuteScalarAsync<int>(
            "SELECT Id FROM dbo.Rol WHERE Codigo = 'cajero'");
        var textoPermisoId = await _connection.ExecuteScalarAsync<int>(
            "SELECT Id FROM dbo.Permiso WHERE Codigo = 'textos:editar'");

        // Replace cajero's 4 permisos with just 1
        await _repository.ReplaceForRolAsync(cajeroId, new[] { textoPermisoId });

        var permisos = await _repository.GetByRolCodigoAsync("cajero");
        Assert.Single(permisos);
        Assert.Equal("textos:editar", permisos[0].Codigo);
    }

    [Fact]
    public async Task ReplaceForRolAsync_Idempotent_SameCallTwiceProducesSameResult()
    {
        var cajeroId = await _connection.ExecuteScalarAsync<int>(
            "SELECT Id FROM dbo.Rol WHERE Codigo = 'cajero'");
        var permisoId = await _connection.ExecuteScalarAsync<int>(
            "SELECT Id FROM dbo.Permiso WHERE Codigo = 'ventas:contado:crear'");

        await _repository.ReplaceForRolAsync(cajeroId, new[] { permisoId });
        await _repository.ReplaceForRolAsync(cajeroId, new[] { permisoId });

        var permisos = await _repository.GetByRolCodigoAsync("cajero");
        Assert.Single(permisos);
        Assert.Equal("ventas:contado:crear", permisos[0].Codigo);
    }

    [Fact]
    public async Task ReplaceForRolAsync_WithEmptyList_DeletesAllPermisos()
    {
        var cajeroId = await _connection.ExecuteScalarAsync<int>(
            "SELECT Id FROM dbo.Rol WHERE Codigo = 'cajero'");

        await _repository.ReplaceForRolAsync(cajeroId, Array.Empty<int>());

        var permisos = await _repository.GetByRolCodigoAsync("cajero");
        Assert.Empty(permisos);
    }

    [Fact]
    public async Task ReplaceForRolAsync_PostReplace_GetReflectsNewSet()
    {
        var cajeroId = await _connection.ExecuteScalarAsync<int>(
            "SELECT Id FROM dbo.Rol WHERE Codigo = 'cajero'");

        // Get IDs of 2 specific permisos
        var rows = await _connection.QueryAsync<(int Id, string Codigo)>(
            "SELECT Id, Codigo FROM dbo.Permiso WHERE Codigo IN ('pauta:azanu:ver', 'pauta:limpiar')");
        var ids = rows.Select(r => r.Id).ToArray();

        await _repository.ReplaceForRolAsync(cajeroId, ids);

        var permisos = await _repository.GetByRolCodigoAsync("cajero");
        var codigos = permisos.Select(p => p.Codigo).ToHashSet();

        Assert.Equal(2, permisos.Count);
        Assert.Contains("pauta:azanu:ver", codigos);
        Assert.Contains("pauta:limpiar", codigos);
    }
}
