using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using Dapper;
using Microsoft.Data.SqlClient;
using SIGCM2.TestSupport;

namespace SIGCM2.Api.Tests.Permisos;

/// <summary>
/// Integration tests for /api/v1/permisos and /api/v1/roles/{codigo}/permisos (UDT-005).
/// RED: written before PermisosController exists.
/// </summary>
[Collection("ApiIntegration")]
public sealed class PermisosEndpointTests : IAsyncLifetime
{
    private const string TestConnectionString = TestConnectionStrings.ApiTestDb;

    private const string AdminUsername = "admin";
    private const string AdminPassword = "@Diego550@";

    private readonly HttpClient _client;

    public PermisosEndpointTests(TestWebAppFactory factory)
    {
        _client = factory.CreateClient();
    }

    public Task InitializeAsync() => Task.CompletedTask;
    public Task DisposeAsync() => Task.CompletedTask;

    // ── Helpers ──────────────────────────────────────────────────────────────

    private async Task<string> GetBearerTokenAsync(string username, string password)
    {
        var response = await _client.PostAsJsonAsync("/api/v1/auth/login", new { username, password });
        if (!response.IsSuccessStatusCode)
        {
            var body = await response.Content.ReadAsStringAsync();
            throw new InvalidOperationException($"Login failed ({(int)response.StatusCode}): {body}");
        }
        var json = await response.Content.ReadFromJsonAsync<JsonElement>();
        return json.GetProperty("accessToken").GetString()!;
    }

    private HttpRequestMessage BuildRequest(HttpMethod method, string url, object? body = null, string? bearerToken = null)
    {
        var request = new HttpRequestMessage(method, url);
        if (bearerToken is not null)
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken);
        if (body is not null)
            request.Content = JsonContent.Create(body);
        return request;
    }

    private static async Task RestoreCajeroPermisosAsync()
    {
        await using var conn = new SqlConnection(TestConnectionString);
        await conn.OpenAsync();

        // Remove any test-added permisos from cajero
        await conn.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 missing canonical cajero permisos
        await conn.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);
            """);
    }

    private async Task<string> CreateNonAdminUserAndGetTokenAsync(string username, string rol = "cajero")
    {
        var adminToken = await GetBearerTokenAsync(AdminUsername, AdminPassword);

        // Create non-admin user via API
        using var mkUser = BuildRequest(HttpMethod.Post, "/api/v1/users", new
        {
            username,
            password = "Secure1234!",
            nombre = "Non",
            apellido = "Admin",
            email = (string?)null,
            rol
        }, adminToken);
        var mkUserResp = await _client.SendAsync(mkUser);
        if (mkUserResp.StatusCode != HttpStatusCode.Created && mkUserResp.StatusCode != HttpStatusCode.Conflict)
            Assert.Fail($"Seed non-admin user failed: {mkUserResp.StatusCode}");

        return await GetBearerTokenAsync(username, "Secure1234!");
    }

    private static async Task DeleteUsuarioIfExistsAsync(string username)
    {
        await using var conn = new SqlConnection(TestConnectionString);
        await conn.OpenAsync();
        await conn.ExecuteAsync("""
            DELETE rt FROM dbo.RefreshToken rt
            INNER JOIN dbo.Usuario u ON u.Id = rt.UsuarioId
            WHERE u.Username = @Username
            """, new { Username = username });
        await conn.ExecuteAsync(
            "DELETE FROM dbo.Usuario WHERE Username = @Username",
            new { Username = username });
    }

    // ── GET /api/v1/permisos — catalog ───────────────────────────────────────

    [Fact]
    public async Task GetPermisos_WithAdmin_Returns200With27Items()
    {
        var token = await GetBearerTokenAsync(AdminUsername, AdminPassword);
        using var req = BuildRequest(HttpMethod.Get, "/api/v1/permisos", bearerToken: token);
        var resp = await _client.SendAsync(req);

        Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
        var list = await resp.Content.ReadFromJsonAsync<JsonElement>();
        // V011 (ADM-001) adds 'administracion:secciones:gestionar' → 22
        // V013 (ADM-008) adds 'administracion:puntos_de_venta:gestionar' → 23
        // V014 (ADM-009) adds 'administracion:fiscal:gestionar' → 24
        // V016 (CAT-001) adds 'catalogo:rubros:gestionar' → 25
        // V017 (PRD-001) adds 'catalogo:tipos:gestionar' → 26
        // V018 (PRD-002) adds 'catalogo:productos:gestionar' → 27 total
        Assert.Equal(27, list.GetArrayLength());
    }

    [Fact]
    public async Task GetPermisos_WithoutToken_Returns401()
    {
        var resp = await _client.SendAsync(BuildRequest(HttpMethod.Get, "/api/v1/permisos"));
        Assert.Equal(HttpStatusCode.Unauthorized, resp.StatusCode);
    }

    [Fact]
    public async Task GetPermisos_WithNonAdminToken_Returns403()
    {
        const string username = "perm_nonadmin_list";
        try
        {
            var token = await CreateNonAdminUserAndGetTokenAsync(username);
            using var req = BuildRequest(HttpMethod.Get, "/api/v1/permisos", bearerToken: token);
            var resp = await _client.SendAsync(req);
            Assert.Equal(HttpStatusCode.Forbidden, resp.StatusCode);
        }
        finally
        {
            await DeleteUsuarioIfExistsAsync(username);
        }
    }

    [Fact]
    public async Task GetPermisos_ResponseContainsCodigoNombreFields()
    {
        var token = await GetBearerTokenAsync(AdminUsername, AdminPassword);
        using var req = BuildRequest(HttpMethod.Get, "/api/v1/permisos", bearerToken: token);
        var resp = await _client.SendAsync(req);

        var list = await resp.Content.ReadFromJsonAsync<JsonElement>();
        var first = list.EnumerateArray().First();
        Assert.True(first.TryGetProperty("codigo", out _), "Response item missing 'codigo' field");
        Assert.True(first.TryGetProperty("nombre", out _), "Response item missing 'nombre' field");
    }

    // ── GET /api/v1/roles/{codigo}/permisos ──────────────────────────────────

    [Fact]
    public async Task GetRolPermisos_AdminRol_Returns200With27Items()
    {
        var token = await GetBearerTokenAsync(AdminUsername, AdminPassword);
        using var req = BuildRequest(HttpMethod.Get, "/api/v1/roles/admin/permisos", bearerToken: token);
        var resp = await _client.SendAsync(req);

        Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
        var list = await resp.Content.ReadFromJsonAsync<JsonElement>();
        // V011 (ADM-001) adds 'administracion:secciones:gestionar' → 22
        // V013 (ADM-008) adds 'administracion:puntos_de_venta:gestionar' → 23
        // V014 (ADM-009) adds 'administracion:fiscal:gestionar' → 24
        // V016 (CAT-001) adds 'catalogo:rubros:gestionar' → 25
        // V017 (PRD-001) adds 'catalogo:tipos:gestionar' → 26
        // V018 (PRD-002) adds 'catalogo:productos:gestionar' → 27 total
        Assert.Equal(27, list.GetArrayLength());
    }

    [Fact]
    public async Task GetRolPermisos_CajeroRol_Returns200With4Items()
    {
        var token = await GetBearerTokenAsync(AdminUsername, AdminPassword);
        using var req = BuildRequest(HttpMethod.Get, "/api/v1/roles/cajero/permisos", bearerToken: token);
        var resp = await _client.SendAsync(req);

        Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
        var list = await resp.Content.ReadFromJsonAsync<JsonElement>();
        Assert.Equal(4, list.GetArrayLength());
    }

    [Fact]
    public async Task GetRolPermisos_ReportesRol_Returns200WithEmptyArray()
    {
        var token = await GetBearerTokenAsync(AdminUsername, AdminPassword);
        using var req = BuildRequest(HttpMethod.Get, "/api/v1/roles/reportes/permisos", bearerToken: token);
        var resp = await _client.SendAsync(req);

        Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
        var list = await resp.Content.ReadFromJsonAsync<JsonElement>();
        Assert.Equal(0, list.GetArrayLength());
    }

    [Fact]
    public async Task GetRolPermisos_InexistentRol_Returns404()
    {
        var token = await GetBearerTokenAsync(AdminUsername, AdminPassword);
        using var req = BuildRequest(HttpMethod.Get, "/api/v1/roles/rol_inexistente_xyz/permisos", bearerToken: token);
        var resp = await _client.SendAsync(req);

        Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode);
    }

    [Fact]
    public async Task GetRolPermisos_WithoutToken_Returns401()
    {
        var resp = await _client.SendAsync(BuildRequest(HttpMethod.Get, "/api/v1/roles/admin/permisos"));
        Assert.Equal(HttpStatusCode.Unauthorized, resp.StatusCode);
    }

    [Fact]
    public async Task GetRolPermisos_WithNonAdminToken_Returns403()
    {
        const string username = "perm_nonadmin_getRol";
        try
        {
            var token = await CreateNonAdminUserAndGetTokenAsync(username);
            using var req = BuildRequest(HttpMethod.Get, "/api/v1/roles/admin/permisos", bearerToken: token);
            var resp = await _client.SendAsync(req);
            Assert.Equal(HttpStatusCode.Forbidden, resp.StatusCode);
        }
        finally
        {
            await DeleteUsuarioIfExistsAsync(username);
        }
    }

    [Fact]
    public async Task GetRolPermisos_InvalidCodigoFormat_Returns400()
    {
        var token = await GetBearerTokenAsync(AdminUsername, AdminPassword);
        // "ROL-INVALIDO" no matchea ^[a-z][a-z0-9_]*$ (tiene guion y mayúsculas)
        using var req = BuildRequest(HttpMethod.Get, "/api/v1/roles/ROL-INVALIDO/permisos", bearerToken: token);
        var resp = await _client.SendAsync(req);

        Assert.Equal(HttpStatusCode.BadRequest, resp.StatusCode);
    }

    // ── PUT /api/v1/roles/{codigo}/permisos ──────────────────────────────────

    [Fact]
    public async Task PutRolPermisos_ValidAssignment_Returns200WithUpdatedSet()
    {
        var token = await GetBearerTokenAsync(AdminUsername, AdminPassword);

        try
        {
            using var req = BuildRequest(
                HttpMethod.Put,
                "/api/v1/roles/cajero/permisos",
                new { codigos = new[] { "ventas:contado:crear" } },
                token);
            var resp = await _client.SendAsync(req);

            Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
            var list = await resp.Content.ReadFromJsonAsync<JsonElement>();
            Assert.Equal(1, list.GetArrayLength());
            Assert.Equal("ventas:contado:crear", list[0].GetProperty("codigo").GetString());
        }
        finally
        {
            await RestoreCajeroPermisosAsync();
        }
    }

    [Fact]
    public async Task PutRolPermisos_ThenGet_ReturnsUpdatedSet()
    {
        var token = await GetBearerTokenAsync(AdminUsername, AdminPassword);

        try
        {
            // Assign 1 permiso to cajero
            using var putReq = BuildRequest(
                HttpMethod.Put,
                "/api/v1/roles/cajero/permisos",
                new { codigos = new[] { "textos:editar" } },
                token);
            var putResp = await _client.SendAsync(putReq);
            Assert.Equal(HttpStatusCode.OK, putResp.StatusCode);

            // GET should now return 1 item
            using var getReq = BuildRequest(HttpMethod.Get, "/api/v1/roles/cajero/permisos", bearerToken: token);
            var getResp = await _client.SendAsync(getReq);

            Assert.Equal(HttpStatusCode.OK, getResp.StatusCode);
            var list = await getResp.Content.ReadFromJsonAsync<JsonElement>();
            Assert.Equal(1, list.GetArrayLength());
            Assert.Equal("textos:editar", list[0].GetProperty("codigo").GetString());
        }
        finally
        {
            await RestoreCajeroPermisosAsync();
        }
    }

    [Fact]
    public async Task PutRolPermisos_Idempotent_TwoCallsSameResult()
    {
        var token = await GetBearerTokenAsync(AdminUsername, AdminPassword);

        try
        {
            var body = new { codigos = new[] { "ventas:contado:crear", "textos:editar" } };

            using var req1 = BuildRequest(HttpMethod.Put, "/api/v1/roles/cajero/permisos", body, token);
            var resp1 = await _client.SendAsync(req1);
            Assert.Equal(HttpStatusCode.OK, resp1.StatusCode);

            using var req2 = BuildRequest(HttpMethod.Put, "/api/v1/roles/cajero/permisos", body, token);
            var resp2 = await _client.SendAsync(req2);
            Assert.Equal(HttpStatusCode.OK, resp2.StatusCode);

            var list2 = await resp2.Content.ReadFromJsonAsync<JsonElement>();
            Assert.Equal(2, list2.GetArrayLength());
        }
        finally
        {
            await RestoreCajeroPermisosAsync();
        }
    }

    [Fact]
    public async Task PutRolPermisos_AdminWithEmptyList_Returns400()
    {
        var token = await GetBearerTokenAsync(AdminUsername, AdminPassword);

        using var req = BuildRequest(
            HttpMethod.Put,
            "/api/v1/roles/admin/permisos",
            new { codigos = Array.Empty<string>() },
            token);
        var resp = await _client.SendAsync(req);

        Assert.Equal(HttpStatusCode.BadRequest, resp.StatusCode);
    }

    [Fact]
    public async Task PutRolPermisos_NonExistentPermiso_Returns404()
    {
        var token = await GetBearerTokenAsync(AdminUsername, AdminPassword);

        using var req = BuildRequest(
            HttpMethod.Put,
            "/api/v1/roles/cajero/permisos",
            new { codigos = new[] { "permiso:no:existe" } },
            token);
        var resp = await _client.SendAsync(req);

        // Validator rejects unknown codes with 400 (not in catalog) before handler can 404
        // The validator checks Permiso.Todos — if code not in static catalog → 400
        Assert.True(
            resp.StatusCode == HttpStatusCode.BadRequest || resp.StatusCode == HttpStatusCode.NotFound,
            $"Expected 400 or 404 but got {resp.StatusCode}");
    }

    [Fact]
    public async Task PutRolPermisos_InexistentRol_Returns404()
    {
        var token = await GetBearerTokenAsync(AdminUsername, AdminPassword);

        using var req = BuildRequest(
            HttpMethod.Put,
            "/api/v1/roles/rol_inexistente_xyz/permisos",
            new { codigos = new[] { "ventas:contado:crear" } },
            token);
        var resp = await _client.SendAsync(req);

        Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode);
    }

    [Fact]
    public async Task PutRolPermisos_WithoutToken_Returns401()
    {
        var resp = await _client.SendAsync(BuildRequest(
            HttpMethod.Put,
            "/api/v1/roles/cajero/permisos",
            new { codigos = new[] { "ventas:contado:crear" } }));
        Assert.Equal(HttpStatusCode.Unauthorized, resp.StatusCode);
    }

    [Fact]
    public async Task PutRolPermisos_WithNonAdminToken_Returns403()
    {
        const string username = "perm_nonadmin_put";
        try
        {
            var token = await CreateNonAdminUserAndGetTokenAsync(username);
            using var req = BuildRequest(
                HttpMethod.Put,
                "/api/v1/roles/cajero/permisos",
                new { codigos = new[] { "ventas:contado:crear" } },
                token);
            var resp = await _client.SendAsync(req);
            Assert.Equal(HttpStatusCode.Forbidden, resp.StatusCode);
        }
        finally
        {
            await DeleteUsuarioIfExistsAsync(username);
        }
    }

    // ── UDT-006: 403 ProblemDetails shape ─────────────────────────────────────

    [Fact]
    public async Task GetPermisos_WithCajeroToken_Returns403WithProblemDetailsShape()
    {
        const string username = "udt006_permisos_403_cajero";
        try
        {
            var token = await CreateNonAdminUserAndGetTokenAsync(username);
            using var req = BuildRequest(HttpMethod.Get, "/api/v1/permisos", bearerToken: token);
            var resp = await _client.SendAsync(req);

            Assert.Equal(HttpStatusCode.Forbidden, resp.StatusCode);
            Assert.Contains("problem+json", resp.Content.Headers.ContentType?.MediaType ?? "");

            var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
            Assert.Equal(403, json.GetProperty("status").GetInt32());
            Assert.Equal("Acceso denegado", json.GetProperty("title").GetString());
            Assert.True(json.TryGetProperty("permisoRequerido", out var perm),
                "Response must contain 'permisoRequerido'");
            // GET /permisos migra a administracion:permisos:ver
            Assert.Equal("administracion:permisos:ver", perm.GetString());
        }
        finally
        {
            await DeleteUsuarioIfExistsAsync(username);
        }
    }

    [Fact]
    public async Task PutRolPermisos_WithCajeroToken_Returns403WithProblemDetailsShape()
    {
        const string username = "udt006_put_permisos_403";
        try
        {
            var token = await CreateNonAdminUserAndGetTokenAsync(username);
            using var req = BuildRequest(
                HttpMethod.Put,
                "/api/v1/roles/cajero/permisos",
                new { codigos = new[] { "ventas:contado:crear" } },
                token);
            var resp = await _client.SendAsync(req);

            Assert.Equal(HttpStatusCode.Forbidden, resp.StatusCode);
            Assert.Contains("problem+json", resp.Content.Headers.ContentType?.MediaType ?? "");

            var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
            Assert.Equal(403, json.GetProperty("status").GetInt32());
            Assert.True(json.TryGetProperty("permisoRequerido", out var perm),
                "Response must contain 'permisoRequerido'");
            // PUT /roles/{c}/permisos migra a administracion:roles_permisos:gestionar
            Assert.Equal("administracion:roles_permisos:gestionar", perm.GetString());
        }
        finally
        {
            await DeleteUsuarioIfExistsAsync(username);
        }
    }
}
