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.Usuarios;

/// <summary>
/// Integration tests for GET /api/v1/users/{id}/permisos and PUT /api/v1/users/{id}/permisos/overrides.
/// SUITE-B-GET-PERMISOS (GP-01..GP-06) + SUITE-B-PUT-OVERRIDES (PO-01..PO-11) — UDT-009.
/// </summary>
[Collection("ApiIntegration")]
public sealed class UsuarioPermisosEndpointTests : IAsyncLifetime
{
    private const string TestConnectionString = TestConnectionStrings.ApiTestDb;

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

    private readonly HttpClient _client;
    private string? _adminToken;

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

    public async Task InitializeAsync()
    {
        _adminToken = await GetBearerTokenAsync(AdminUsername, AdminPassword);
    }

    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 });
        response.EnsureSuccessStatusCode();
        var json = await response.Content.ReadFromJsonAsync<JsonElement>();
        return json.GetProperty("accessToken").GetString()!;
    }

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

    private async Task<int> GetAdminIdAsync()
    {
        await using var conn = new SqlConnection(TestConnectionString);
        await conn.OpenAsync();
        return await conn.QuerySingleAsync<int>("SELECT Id FROM dbo.Usuario WHERE Username = 'admin'");
    }

    private async Task SetPermisosJsonAsync(int userId, string json)
    {
        await using var conn = new SqlConnection(TestConnectionString);
        await conn.OpenAsync();
        await conn.ExecuteAsync(
            "UPDATE dbo.Usuario SET PermisosJson = @Json WHERE Id = @Id",
            new { Json = json, Id = userId });
    }

    private async Task<string> GetPermisosJsonAsync(int userId)
    {
        await using var conn = new SqlConnection(TestConnectionString);
        await conn.OpenAsync();
        return await conn.QuerySingleAsync<string>(
            "SELECT PermisosJson FROM dbo.Usuario WHERE Id = @Id",
            new { Id = userId });
    }

    // ── SUITE-B-GET-PERMISOS ─────────────────────────────────────────────────

    // GP-01: Admin → 200 con shape correcto {rolPermisos, overrides, effective}
    [Fact]
    public async Task GetPermisos_Admin_Returns200_WithCorrectShape()
    {
        var adminId = await GetAdminIdAsync();
        await SetPermisosJsonAsync(adminId, """{"grant":[],"deny":[]}""");

        var request = BuildRequest(HttpMethod.Get, $"/api/v1/users/{adminId}/permisos");
        var response = await _client.SendAsync(request);

        Assert.Equal(HttpStatusCode.OK, response.StatusCode);

        var json = await response.Content.ReadFromJsonAsync<JsonElement>();

        Assert.True(json.TryGetProperty("rolPermisos", out var rolPermisos));
        Assert.Equal(JsonValueKind.Array, rolPermisos.ValueKind);

        Assert.True(json.TryGetProperty("overrides", out var overrides));
        Assert.True(overrides.TryGetProperty("grant", out _));
        Assert.True(overrides.TryGetProperty("deny", out _));

        Assert.True(json.TryGetProperty("effective", out var effective));
        Assert.Equal(JsonValueKind.Array, effective.ValueKind);
    }

    // GP-02: Usuario con overrides no vacíos → shape refleja overrides.grant, effective incluye el grant
    [Fact]
    public async Task GetPermisos_UserWithGrant_EffectiveContainsGrantedPermiso()
    {
        var adminId = await GetAdminIdAsync();
        // Admin ya tiene 21 permisos del rol — grant con uno que tiene para probar idempotencia
        await SetPermisosJsonAsync(adminId, """{"grant":["textos:editar"],"deny":[]}""");

        var request = BuildRequest(HttpMethod.Get, $"/api/v1/users/{adminId}/permisos");
        var response = await _client.SendAsync(request);

        Assert.Equal(HttpStatusCode.OK, response.StatusCode);

        var json = await response.Content.ReadFromJsonAsync<JsonElement>();
        var overrides = json.GetProperty("overrides");
        var grantArr = overrides.GetProperty("grant").EnumerateArray().Select(e => e.GetString()).ToArray();

        Assert.Contains("textos:editar", grantArr);

        var effectiveArr = json.GetProperty("effective").EnumerateArray().Select(e => e.GetString()).ToArray();
        Assert.Contains("textos:editar", effectiveArr);
    }

    // GP-03: Usuario con overrides vacíos → effective == rolPermisos, overrides vacíos
    [Fact]
    public async Task GetPermisos_UserWithEmptyOverrides_EffectiveEqualsRolPermisos()
    {
        var adminId = await GetAdminIdAsync();
        await SetPermisosJsonAsync(adminId, """{"grant":[],"deny":[]}""");

        var request = BuildRequest(HttpMethod.Get, $"/api/v1/users/{adminId}/permisos");
        var response = await _client.SendAsync(request);

        Assert.Equal(HttpStatusCode.OK, response.StatusCode);

        var json = await response.Content.ReadFromJsonAsync<JsonElement>();
        var rolPermisos = json.GetProperty("rolPermisos").EnumerateArray().Select(e => e.GetString()).OrderBy(x => x).ToArray();
        var effective = json.GetProperty("effective").EnumerateArray().Select(e => e.GetString()).OrderBy(x => x).ToArray();

        Assert.Equal(rolPermisos, effective);

        var grantArr = json.GetProperty("overrides").GetProperty("grant").EnumerateArray().ToArray();
        var denyArr = json.GetProperty("overrides").GetProperty("deny").EnumerateArray().ToArray();
        Assert.Empty(grantArr);
        Assert.Empty(denyArr);
    }

    // GP-04: Usuario inexistente → 404
    [Fact]
    public async Task GetPermisos_NonExistentUser_Returns404()
    {
        var request = BuildRequest(HttpMethod.Get, "/api/v1/users/99999/permisos");
        var response = await _client.SendAsync(request);

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

    // GP-05: Sin permiso administracion:usuarios:gestionar → 403
    [Fact]
    public async Task GetPermisos_WithoutRequiredPermission_Returns403()
    {
        // Create a cajero user without the required permission
        var cajeroToken = await CreateCajeroAndGetTokenAsync("cajero_gp05");
        try
        {
            var adminId = await GetAdminIdAsync();
            var request = BuildRequest(HttpMethod.Get, $"/api/v1/users/{adminId}/permisos", token: cajeroToken);
            var response = await _client.SendAsync(request);

            Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
        }
        finally
        {
            await DeleteUsuarioAsync("cajero_gp05");
        }
    }

    // GP-06: Sin auth → 401
    [Fact]
    public async Task GetPermisos_WithoutAuth_Returns401()
    {
        var adminId = await GetAdminIdAsync();
        var request = new HttpRequestMessage(HttpMethod.Get, $"/api/v1/users/{adminId}/permisos");
        var response = await _client.SendAsync(request);

        Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
    }

    // ── SUITE-B-PUT-OVERRIDES ────────────────────────────────────────────────

    // PO-01: Grant válido → 200, DB persistido, FechaModificacion actualizado
    [Fact]
    public async Task PutOverrides_ValidGrant_Returns200_AndPersistsInDB()
    {
        var adminId = await GetAdminIdAsync();
        await SetPermisosJsonAsync(adminId, """{"grant":[],"deny":[]}""");

        var request = BuildRequest(HttpMethod.Put, $"/api/v1/users/{adminId}/permisos/overrides",
            body: new { grant = new[] { "textos:editar" }, deny = Array.Empty<string>() });
        var response = await _client.SendAsync(request);

        Assert.Equal(HttpStatusCode.OK, response.StatusCode);

        var stored = await GetPermisosJsonAsync(adminId);
        var parsed = JsonDocument.Parse(stored).RootElement;
        var grant = parsed.GetProperty("grant").EnumerateArray().Select(e => e.GetString()).ToArray();
        Assert.Contains("textos:editar", grant);
    }

    // PO-02: Deny válido → 200
    [Fact]
    public async Task PutOverrides_ValidDeny_Returns200()
    {
        var adminId = await GetAdminIdAsync();
        await SetPermisosJsonAsync(adminId, """{"grant":[],"deny":[]}""");

        var request = BuildRequest(HttpMethod.Put, $"/api/v1/users/{adminId}/permisos/overrides",
            body: new { grant = Array.Empty<string>(), deny = new[] { "ventas:contado:cobrar" } });
        var response = await _client.SendAsync(request);

        Assert.Equal(HttpStatusCode.OK, response.StatusCode);

        var stored = await GetPermisosJsonAsync(adminId);
        var parsed = JsonDocument.Parse(stored).RootElement;
        var deny = parsed.GetProperty("deny").EnumerateArray().Select(e => e.GetString()).ToArray();
        Assert.Contains("ventas:contado:cobrar", deny);
    }

    // PO-03: Código fuera del catálogo → 400, error code "invalid-permiso-codes"
    [Fact]
    public async Task PutOverrides_InvalidPermisoCode_Returns400_InvalidCodes()
    {
        var adminId = await GetAdminIdAsync();

        var request = BuildRequest(HttpMethod.Put, $"/api/v1/users/{adminId}/permisos/overrides",
            body: new { grant = new[] { "modulo:fake:accion" }, deny = Array.Empty<string>() });
        var response = await _client.SendAsync(request);

        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);

        var json = await response.Content.ReadFromJsonAsync<JsonElement>();
        var title = json.GetProperty("title").GetString();
        Assert.Equal("invalid-permiso-codes", title);

        // Should contain the list of invalid codes
        Assert.True(json.TryGetProperty("invalidCodes", out var invalidCodes));
        var codes = invalidCodes.EnumerateArray().Select(e => e.GetString()).ToArray();
        Assert.Contains("modulo:fake:accion", codes);
    }

    // PO-04: Mismo código en grant Y deny → 400, "grant-deny-overlap"
    [Fact]
    public async Task PutOverrides_GrantDenyOverlap_Returns400_Overlap()
    {
        var adminId = await GetAdminIdAsync();

        var request = BuildRequest(HttpMethod.Put, $"/api/v1/users/{adminId}/permisos/overrides",
            body: new { grant = new[] { "textos:editar" }, deny = new[] { "textos:editar" } });
        var response = await _client.SendAsync(request);

        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);

        var json = await response.Content.ReadFromJsonAsync<JsonElement>();
        var title = json.GetProperty("title").GetString();
        Assert.Equal("grant-deny-overlap", title);

        Assert.True(json.TryGetProperty("overlap", out var overlap));
        var codes = overlap.EnumerateArray().Select(e => e.GetString()).ToArray();
        Assert.Contains("textos:editar", codes);
    }

    // PO-05: Usuario inexistente → 404
    [Fact]
    public async Task PutOverrides_NonExistentUser_Returns404()
    {
        var request = BuildRequest(HttpMethod.Put, "/api/v1/users/99999/permisos/overrides",
            body: new { grant = Array.Empty<string>(), deny = Array.Empty<string>() });
        var response = await _client.SendAsync(request);

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

    // PO-06: Sin permiso → 403
    [Fact]
    public async Task PutOverrides_WithoutRequiredPermission_Returns403()
    {
        var cajeroToken = await CreateCajeroAndGetTokenAsync("cajero_po06");
        try
        {
            var adminId = await GetAdminIdAsync();
            var request = BuildRequest(HttpMethod.Put, $"/api/v1/users/{adminId}/permisos/overrides",
                body: new { grant = Array.Empty<string>(), deny = Array.Empty<string>() },
                token: cajeroToken);
            var response = await _client.SendAsync(request);

            Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
        }
        finally
        {
            await DeleteUsuarioAsync("cajero_po06");
        }
    }

    // PO-07: Sin auth → 401
    [Fact]
    public async Task PutOverrides_WithoutAuth_Returns401()
    {
        var adminId = await GetAdminIdAsync();
        var request = new HttpRequestMessage(HttpMethod.Put, $"/api/v1/users/{adminId}/permisos/overrides")
        {
            Content = JsonContent.Create(new { grant = Array.Empty<string>(), deny = Array.Empty<string>() })
        };
        var response = await _client.SendAsync(request);

        Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
    }

    // PO-08: Body JSON malformado → 400
    [Fact]
    public async Task PutOverrides_MalformedBody_Returns400()
    {
        var adminId = await GetAdminIdAsync();
        var request = new HttpRequestMessage(HttpMethod.Put, $"/api/v1/users/{adminId}/permisos/overrides")
        {
            Headers = { Authorization = new AuthenticationHeaderValue("Bearer", _adminToken) },
            Content = new StringContent("{grant: not-json", System.Text.Encoding.UTF8, "application/json")
        };
        var response = await _client.SendAsync(request);

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

    // PO-09: PUT idempotente — dos veces el mismo body → estado igual
    [Fact]
    public async Task PutOverrides_Idempotent_SameBodyTwice_StateUnchanged()
    {
        var adminId = await GetAdminIdAsync();
        var body = new { grant = new[] { "textos:editar" }, deny = Array.Empty<string>() };

        var r1 = await _client.SendAsync(BuildRequest(HttpMethod.Put, $"/api/v1/users/{adminId}/permisos/overrides", body));
        var r2 = await _client.SendAsync(BuildRequest(HttpMethod.Put, $"/api/v1/users/{adminId}/permisos/overrides", body));

        Assert.Equal(HttpStatusCode.OK, r1.StatusCode);
        Assert.Equal(HttpStatusCode.OK, r2.StatusCode);

        var stored = await GetPermisosJsonAsync(adminId);
        var parsed = JsonDocument.Parse(stored).RootElement;
        var grant = parsed.GetProperty("grant").EnumerateArray().Select(e => e.GetString()).ToArray();
        Assert.Single(grant);
        Assert.Equal("textos:editar", grant[0]);
    }

    // PO-10: PUT con grants vacíos (reset overrides) → effective == rolPermisos
    [Fact]
    public async Task PutOverrides_EmptyPayload_ResetsOverrides()
    {
        var adminId = await GetAdminIdAsync();

        // First set some overrides
        await _client.SendAsync(BuildRequest(HttpMethod.Put, $"/api/v1/users/{adminId}/permisos/overrides",
            body: new { grant = new[] { "textos:editar" }, deny = Array.Empty<string>() }));

        // Then reset
        var resetRequest = BuildRequest(HttpMethod.Put, $"/api/v1/users/{adminId}/permisos/overrides",
            body: new { grant = Array.Empty<string>(), deny = Array.Empty<string>() });
        var response = await _client.SendAsync(resetRequest);

        Assert.Equal(HttpStatusCode.OK, response.StatusCode);

        var stored = await GetPermisosJsonAsync(adminId);
        Assert.Equal("""{"grant":[],"deny":[]}""", stored);
    }

    // PO-11: Response de PUT tiene shape {rolPermisos, overrides, effective}
    [Fact]
    public async Task PutOverrides_ResponseHasCorrectShape()
    {
        var adminId = await GetAdminIdAsync();
        await SetPermisosJsonAsync(adminId, """{"grant":[],"deny":[]}""");

        var request = BuildRequest(HttpMethod.Put, $"/api/v1/users/{adminId}/permisos/overrides",
            body: new { grant = Array.Empty<string>(), deny = Array.Empty<string>() });
        var response = await _client.SendAsync(request);

        Assert.Equal(HttpStatusCode.OK, response.StatusCode);

        var json = await response.Content.ReadFromJsonAsync<JsonElement>();
        Assert.True(json.TryGetProperty("rolPermisos", out _));
        Assert.True(json.TryGetProperty("overrides", out var overrides));
        Assert.True(overrides.TryGetProperty("grant", out _));
        Assert.True(overrides.TryGetProperty("deny", out _));
        Assert.True(json.TryGetProperty("effective", out _));
    }

    // ── helpers ───────────────────────────────────────────────────────────────

    private async Task<string> CreateCajeroAndGetTokenAsync(string username)
    {
        // Seed a cajero user without administracion:usuarios:gestionar
        await using var conn = new SqlConnection(TestConnectionString);
        await conn.OpenAsync();
        await conn.ExecuteAsync("""
            IF NOT EXISTS (SELECT 1 FROM dbo.Usuario WHERE Username = @Username)
            INSERT INTO dbo.Usuario (Username, PasswordHash, Nombre, Apellido, Rol, PermisosJson, Activo, MustChangePassword)
            VALUES (@Username, '$2a$12$rmq6tlSAQ8WXhR2CwLCSeuwCJKz/.8Eab95UQCUNfwe4dokeOqMcW',
                    'Cajero', 'Test', 'cajero', '{"grant":[],"deny":[]}', 1, 0)
            """, new { Username = username });

        return await GetBearerTokenAsync(username, "@Diego550@");
    }

    private async Task DeleteUsuarioAsync(string username)
    {
        await using var conn = new SqlConnection(TestConnectionString);
        await conn.OpenAsync();
        // Must delete RefreshTokens first due to FK constraint
        await conn.ExecuteAsync("""
            DELETE rt FROM dbo.RefreshToken rt
            INNER JOIN dbo.Usuario u ON rt.UsuarioId = u.Id
            WHERE u.Username = @Username
            """, new { Username = username });
        await conn.ExecuteAsync("DELETE FROM dbo.Usuario WHERE Username = @Username", new { Username = username });
    }
}
