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

/// <summary>
/// Integration tests for /api/v1/roles (UDT-004).
/// </summary>
[Collection("ApiIntegration")]
public sealed class RolesEndpointTests : IAsyncLifetime
{
    private const string TestConnectionString = TestConnectionStrings.ApiTestDb;

    private const string Endpoint = "/api/v1/roles";
    private const string AdminUsername = "admin";
    private const string AdminPassword = "@Diego550@";

    private readonly HttpClient _client;

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

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

    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 DeleteRolIfExistsAsync(string codigo)
    {
        await using var conn = new SqlConnection(TestConnectionString);
        await conn.OpenAsync();
        await conn.ExecuteAsync("DELETE FROM dbo.Rol WHERE Codigo = @Codigo", new { Codigo = codigo });
    }

    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 });
    }

    // ── 401 / 403 guards ────────────────────────────────────────────────────

    [Fact]
    public async Task List_WithoutAuth_Returns401()
    {
        var resp = await _client.SendAsync(BuildRequest(HttpMethod.Get, Endpoint));
        Assert.Equal(HttpStatusCode.Unauthorized, resp.StatusCode);
    }

    [Fact]
    public async Task Create_WithNonAdmin_Returns403()
    {
        var adminToken = await GetBearerTokenAsync(AdminUsername, AdminPassword);
        const string nonAdminUser = "rolestest_nonadmin";

        // Create a non-admin user via endpoint (admin can still create users).
        using var mkUser = BuildRequest(HttpMethod.Post, "/api/v1/users", new
        {
            username = nonAdminUser,
            password = "Secure1234!",
            nombre = "Non",
            apellido = "Admin",
            email = (string?)null,
            rol = "cajero"
        }, 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}");

        try
        {
            var cajeroToken = await GetBearerTokenAsync(nonAdminUser, "Secure1234!");
            using var req = BuildRequest(HttpMethod.Post, Endpoint, new
            {
                codigo = "nuevo_test",
                nombre = "Test",
                descripcion = (string?)null
            }, cajeroToken);
            var resp = await _client.SendAsync(req);
            Assert.Equal(HttpStatusCode.Forbidden, resp.StatusCode);
        }
        finally
        {
            await DeleteUsuarioIfExistsAsync(nonAdminUser);
        }
    }

    // ── List ────────────────────────────────────────────────────────────────

    [Fact]
    public async Task List_WithAdmin_Returns200WithCanonicalSeeds()
    {
        var token = await GetBearerTokenAsync(AdminUsername, AdminPassword);
        using var req = BuildRequest(HttpMethod.Get, Endpoint, bearerToken: token);
        var resp = await _client.SendAsync(req);

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

        var list = await resp.Content.ReadFromJsonAsync<JsonElement>();
        var codes = list.EnumerateArray().Select(r => r.GetProperty("codigo").GetString()).ToHashSet();
        foreach (var c in new[] { "admin", "cajero", "operador_ctacte", "picadora", "jefe_publicidad", "productor", "diagramacion", "reportes" })
            Assert.Contains(c, codes);
    }

    // ── Get ─────────────────────────────────────────────────────────────────

    [Fact]
    public async Task GetByCodigo_Existing_Returns200()
    {
        var token = await GetBearerTokenAsync(AdminUsername, AdminPassword);
        var resp = await _client.SendAsync(BuildRequest(HttpMethod.Get, $"{Endpoint}/cajero", bearerToken: token));

        Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
        var body = await resp.Content.ReadFromJsonAsync<JsonElement>();
        Assert.Equal("cajero", body.GetProperty("codigo").GetString());
        Assert.Equal("Cajero", body.GetProperty("nombre").GetString());
        Assert.True(body.GetProperty("activo").GetBoolean());
    }

    [Fact]
    public async Task GetByCodigo_NonExistent_Returns404()
    {
        var token = await GetBearerTokenAsync(AdminUsername, AdminPassword);
        var resp = await _client.SendAsync(BuildRequest(HttpMethod.Get, $"{Endpoint}/no_existe_xyz", bearerToken: token));

        Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode);
        var body = await resp.Content.ReadFromJsonAsync<JsonElement>();
        Assert.Equal("rol_not_found", body.GetProperty("error").GetString());
    }

    // ── Create ──────────────────────────────────────────────────────────────

    [Fact]
    public async Task Create_NewRol_Returns201()
    {
        var token = await GetBearerTokenAsync(AdminUsername, AdminPassword);
        const string codigo = "endpoint_new_rol";

        try
        {
            using var req = BuildRequest(HttpMethod.Post, Endpoint, new
            {
                codigo,
                nombre = "Endpoint New",
                descripcion = "Creado por integration test"
            }, token);
            var resp = await _client.SendAsync(req);

            Assert.Equal(HttpStatusCode.Created, resp.StatusCode);
            var body = await resp.Content.ReadFromJsonAsync<JsonElement>();
            Assert.Equal(codigo, body.GetProperty("codigo").GetString());
            Assert.True(body.GetProperty("id").GetInt32() > 0);
            Assert.True(body.GetProperty("activo").GetBoolean());
        }
        finally
        {
            await DeleteRolIfExistsAsync(codigo);
        }
    }

    [Fact]
    public async Task Create_CodigoDuplicado_Returns409()
    {
        var token = await GetBearerTokenAsync(AdminUsername, AdminPassword);
        using var req = BuildRequest(HttpMethod.Post, Endpoint, new
        {
            codigo = "cajero",
            nombre = "Duplicate",
            descripcion = (string?)null
        }, token);

        var resp = await _client.SendAsync(req);

        Assert.Equal(HttpStatusCode.Conflict, resp.StatusCode);
        var body = await resp.Content.ReadFromJsonAsync<JsonElement>();
        Assert.Equal("rol_already_exists", body.GetProperty("error").GetString());
    }

    [Fact]
    public async Task Create_InvalidCodigoFormat_Returns400()
    {
        var token = await GetBearerTokenAsync(AdminUsername, AdminPassword);
        using var req = BuildRequest(HttpMethod.Post, Endpoint, new
        {
            codigo = "Cajero Senior",  // uppercase + space — invalid
            nombre = "Bad",
            descripcion = (string?)null
        }, token);

        var resp = await _client.SendAsync(req);
        Assert.Equal(HttpStatusCode.BadRequest, resp.StatusCode);
    }

    // ── Update ──────────────────────────────────────────────────────────────

    [Fact]
    public async Task Update_Existing_Returns200WithUpdatedNombre()
    {
        var token = await GetBearerTokenAsync(AdminUsername, AdminPassword);
        const string codigo = "endpoint_upd_rol";

        // Seed a rol
        await using var conn = new SqlConnection(TestConnectionString);
        await conn.OpenAsync();
        await conn.ExecuteAsync(
            "INSERT INTO dbo.Rol (Codigo, Nombre, Descripcion, Activo) VALUES (@Codigo, N'Viejo', N'Desc vieja', 1);",
            new { Codigo = codigo });

        try
        {
            using var req = BuildRequest(HttpMethod.Put, $"{Endpoint}/{codigo}", new
            {
                nombre = "Nuevo Nombre",
                descripcion = "Desc nueva",
                activo = true
            }, token);
            var resp = await _client.SendAsync(req);

            Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
            var body = await resp.Content.ReadFromJsonAsync<JsonElement>();
            Assert.Equal("Nuevo Nombre", body.GetProperty("nombre").GetString());
            Assert.Equal("Desc nueva", body.GetProperty("descripcion").GetString());
            Assert.Equal(codigo, body.GetProperty("codigo").GetString());
        }
        finally
        {
            await DeleteRolIfExistsAsync(codigo);
        }
    }

    [Fact]
    public async Task Update_NonExistent_Returns404()
    {
        var token = await GetBearerTokenAsync(AdminUsername, AdminPassword);
        using var req = BuildRequest(HttpMethod.Put, $"{Endpoint}/inexistente_abc", new
        {
            nombre = "X",
            descripcion = (string?)null,
            activo = true
        }, token);

        var resp = await _client.SendAsync(req);
        Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode);
    }

    // ── Delete (soft) ───────────────────────────────────────────────────────

    [Fact]
    public async Task Delete_WithoutActiveUsuarios_Returns204()
    {
        var token = await GetBearerTokenAsync(AdminUsername, AdminPassword);
        const string codigo = "endpoint_del_rol";

        await using var conn = new SqlConnection(TestConnectionString);
        await conn.OpenAsync();
        await conn.ExecuteAsync(
            "INSERT INTO dbo.Rol (Codigo, Nombre, Activo) VALUES (@Codigo, N'Temp', 1);",
            new { Codigo = codigo });

        try
        {
            var resp = await _client.SendAsync(BuildRequest(HttpMethod.Delete, $"{Endpoint}/{codigo}", bearerToken: token));
            Assert.Equal(HttpStatusCode.NoContent, resp.StatusCode);

            var activo = await conn.ExecuteScalarAsync<bool>(
                "SELECT Activo FROM dbo.Rol WHERE Codigo = @Codigo", new { Codigo = codigo });
            Assert.False(activo);
        }
        finally
        {
            await DeleteRolIfExistsAsync(codigo);
        }
    }

    [Fact]
    public async Task Delete_WithActiveUsuarios_Returns409()
    {
        var token = await GetBearerTokenAsync(AdminUsername, AdminPassword);
        const string codigo = "endpoint_del_inuse";
        const string testUser = "endpoint_del_user";

        await using var conn = new SqlConnection(TestConnectionString);
        await conn.OpenAsync();
        await conn.ExecuteAsync(
            "INSERT INTO dbo.Rol (Codigo, Nombre, Activo) VALUES (@Codigo, N'InUse', 1);",
            new { Codigo = codigo });
        await conn.ExecuteAsync(
            "INSERT INTO dbo.Usuario (Username, PasswordHash, Nombre, Apellido, Rol, PermisosJson, Activo) " +
            "VALUES (@Username, '$2a$12$hash', 'Test', 'User', @Codigo, '[]', 1);",
            new { Username = testUser, Codigo = codigo });

        try
        {
            var resp = await _client.SendAsync(BuildRequest(HttpMethod.Delete, $"{Endpoint}/{codigo}", bearerToken: token));
            Assert.Equal(HttpStatusCode.Conflict, resp.StatusCode);

            var body = await resp.Content.ReadFromJsonAsync<JsonElement>();
            Assert.Equal("rol_in_use", body.GetProperty("error").GetString());

            var activo = await conn.ExecuteScalarAsync<bool>(
                "SELECT Activo FROM dbo.Rol WHERE Codigo = @Codigo", new { Codigo = codigo });
            Assert.True(activo);
        }
        finally
        {
            await DeleteUsuarioIfExistsAsync(testUser);
            await DeleteRolIfExistsAsync(codigo);
        }
    }

    [Fact]
    public async Task Delete_NonExistent_Returns404()
    {
        var token = await GetBearerTokenAsync(AdminUsername, AdminPassword);
        var resp = await _client.SendAsync(BuildRequest(HttpMethod.Delete, $"{Endpoint}/no_existe_del", bearerToken: token));
        Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode);
    }

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

    [Fact]
    public async Task GetRoles_WithCajeroToken_Returns403WithProblemDetailsShape()
    {
        const string username = "udt006_roles_403_cajero";
        try
        {
            var token = await CreateCajeroTokenAsync(username);
            var resp = await _client.SendAsync(BuildRequest(HttpMethod.Get, Endpoint, bearerToken: token));

            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'");
            // RolesController migra a administracion:roles:gestionar
            Assert.Equal("administracion:roles:gestionar", perm.GetString());
        }
        finally
        {
            await DeleteUsuarioIfExistsAsync(username);
        }
    }

    // Helper: create cajero user via SQL and return token
    private async Task<string> CreateCajeroTokenAsync(string username)
    {
        var adminToken = await GetBearerTokenAsync(AdminUsername, AdminPassword);
        using var mkUser = BuildRequest(HttpMethod.Post, "/api/v1/users", new
        {
            username,
            password = "Secure1234!",
            nombre = "Cajero",
            apellido = "Test",
            email = (string?)null,
            rol = "cajero"
        }, adminToken);
        var mkResp = await _client.SendAsync(mkUser);
        if (mkResp.StatusCode != HttpStatusCode.Created && mkResp.StatusCode != HttpStatusCode.Conflict)
            Assert.Fail($"Seed cajero failed: {mkResp.StatusCode}");
        return await GetBearerTokenAsync(username, "Secure1234!");
    }

}
