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 POST api/v1/users (UDT-003).
/// These tests run against SIGCM2_Test database via TestWebAppFactory.
/// TestWebAppFactory is shared across the whole "ApiIntegration" collection
/// (see ApiIntegrationCollection) — one factory, one RSA singleton, one DB state.
/// </summary>
[Collection("ApiIntegration")]
public sealed class CreateUsuarioEndpointTests : IAsyncLifetime
{
    private const string TestConnectionString = TestConnectionStrings.ApiTestDb;

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

    private readonly HttpClient _client;

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

    // IAsyncLifetime: reset DB state before each test class execution
    public Task InitializeAsync() => Task.CompletedTask;
    public Task DisposeAsync() => Task.CompletedTask;

    // ---------------------------------------------------------------------------
    // Helper: Authenticate and return Bearer token for the given credentials
    // ---------------------------------------------------------------------------
    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? 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 object ValidCreateBody(string username = "testuser") => new
    {
        username,
        password = "Test1234!",
        nombre = "Test",
        apellido = "Usuario",
        email = (string?)null,
        rol = "cajero"
    };

    // ---------------------------------------------------------------------------
    // Helper: seed a vendedor user directly via SQL (avoid calling the endpoint)
    // ---------------------------------------------------------------------------
    private static async Task SeedVendedorAsync(string username, string passwordHash)
    {
        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)
            VALUES (@Username, @Hash, 'Vendedor', 'Test', 'cajero', '[]', 1)
            """,
            new { Username = username, Hash = passwordHash });
    }

    // ---------------------------------------------------------------------------
    // Helper: clean up a created user after test
    // ---------------------------------------------------------------------------
    private static async Task DeleteUsuarioAsync(string username)
    {
        await using var conn = new SqlConnection(TestConnectionString);
        await conn.OpenAsync();
        // Must delete RefreshTokens first — FK constraint FK_RefreshToken_Usuario
        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 });
    }

    // ---------------------------------------------------------------------------
    // Scenario 1: 401 — no Authorization header
    // ---------------------------------------------------------------------------
    [Fact]
    public async Task CreateUsuario_WithoutAuthHeader_Returns401()
    {
        var request = BuildRequest(HttpMethod.Post, Endpoint, ValidCreateBody());
        var response = await _client.SendAsync(request);
        Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
    }

    // ---------------------------------------------------------------------------
    // Scenario 2: 403 — valid token but role is not admin (vendedor)
    // ---------------------------------------------------------------------------
    [Fact]
    public async Task CreateUsuario_WithVendedorRole_Returns403()
    {
        // Use admin to create a vendedor, then login as that vendedor and attempt to create another user
        var adminToken = await GetBearerTokenAsync(AdminUsername, AdminPassword);

        // Create vendedor user via the endpoint (as admin)
        const string testVendedor = "vendedor_role_test";
        using var createRequest = BuildRequest(HttpMethod.Post, Endpoint, new
        {
            username = testVendedor,
            password = "@Test1234@",
            nombre = "Vendedor",
            apellido = "Test",
            email = (string?)null,
            rol = "cajero"
        }, adminToken);

        var createResp = await _client.SendAsync(createRequest);
        // If already exists (test re-run), ignore 409
        if (createResp.StatusCode != HttpStatusCode.Created && createResp.StatusCode != HttpStatusCode.Conflict)
            Assert.Fail($"Unexpected status seeding vendedor: {createResp.StatusCode}");

        // Login as vendedor
        var vendedorToken = await GetBearerTokenAsync(testVendedor, "@Test1234@");

        // Attempt to create user with vendedor token
        using var request = BuildRequest(HttpMethod.Post, Endpoint, ValidCreateBody("another_user"), vendedorToken);
        var response = await _client.SendAsync(request);

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

        // Cleanup
        await DeleteUsuarioAsync(testVendedor);
    }

    // ---------------------------------------------------------------------------
    // Scenario 3: 400 — invalid body (username vacío, password corta, rol fuera whitelist)
    // ---------------------------------------------------------------------------
    [Fact]
    public async Task CreateUsuario_WithInvalidBody_Returns400()
    {
        var adminToken = await GetBearerTokenAsync(AdminUsername, AdminPassword);

        using var request = BuildRequest(HttpMethod.Post, Endpoint, new
        {
            username = "",          // invalid: empty
            password = "abc",       // invalid: too short
            nombre = "Test",
            apellido = "Usuario",
            email = (string?)null,
            rol = "superadmin"      // invalid: not in whitelist
        }, adminToken);

        var response = await _client.SendAsync(request);

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

        var json = await response.Content.ReadFromJsonAsync<JsonElement>();
        Assert.True(json.TryGetProperty("errors", out _), "Response must contain 'errors' field");
    }

    // ---------------------------------------------------------------------------
    // Scenario 4: 201 — admin autenticado crea usuario, body contiene Id/username/rol, NO contiene passwordHash
    //   + verifica en BD: Activo=1, PermisosJson='[]'
    // ---------------------------------------------------------------------------
    [Fact]
    public async Task CreateUsuario_WithAdminToken_Returns201WithCorrectShape()
    {
        var adminToken = await GetBearerTokenAsync(AdminUsername, AdminPassword);

        const string newUsername = "integration_test_user_201";
        using var request = BuildRequest(HttpMethod.Post, Endpoint, new
        {
            username = newUsername,
            password = "Secure1234!",
            nombre = "Integration",
            apellido = "Test",
            email = "integration@test.com",
            rol = "cajero"
        }, adminToken);

        try
        {
            var response = await _client.SendAsync(request);

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

            // Location header must be set
            Assert.NotNull(response.Headers.Location);

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

            // Must have Id, username, rol
            Assert.True(json.TryGetProperty("id", out var id), "Response must contain 'id'");
            Assert.True(json.TryGetProperty("username", out var username), "Response must contain 'username'");
            Assert.True(json.TryGetProperty("rol", out var rol), "Response must contain 'rol'");

            Assert.True(id.GetInt32() > 0, "'id' must be positive");
            Assert.Equal(newUsername, username.GetString());
            Assert.Equal("cajero", rol.GetString());

            // Must NOT contain passwordHash
            Assert.False(json.TryGetProperty("passwordHash", out _), "Response must NOT leak 'passwordHash'");
            Assert.False(json.TryGetProperty("PasswordHash", out _), "Response must NOT leak 'PasswordHash'");

            // Verify in DB: Activo=1, PermisosJson='[]'
            await using var conn = new SqlConnection(TestConnectionString);
            await conn.OpenAsync();
            var row = await conn.QuerySingleAsync<(bool Activo, string PermisosJson)>(
                "SELECT Activo, PermisosJson FROM dbo.Usuario WHERE Username = @Username",
                new { Username = newUsername });

            Assert.True(row.Activo, "Activo should be true");
            // V009 (UDT-009): ForCreation now defaults to canonical shape {"grant":[],"deny":[]}
            Assert.Equal("""{"grant":[],"deny":[]}""", row.PermisosJson);
        }
        finally
        {
            await DeleteUsuarioAsync(newUsername);
        }
    }

    // ---------------------------------------------------------------------------
    // Scenario 5: 409 — username duplicado
    // ---------------------------------------------------------------------------
    [Fact]
    public async Task CreateUsuario_DuplicateUsername_Returns409()
    {
        var adminToken = await GetBearerTokenAsync(AdminUsername, AdminPassword);

        const string username = "duplicate_test_user";

        try
        {
            // First creation — should succeed
            using var first = BuildRequest(HttpMethod.Post, Endpoint, new
            {
                username,
                password = "Secure1234!",
                nombre = "First",
                apellido = "User",
                email = (string?)null,
                rol = "cajero"
            }, adminToken);
            var firstResp = await _client.SendAsync(first);
            Assert.Equal(HttpStatusCode.Created, firstResp.StatusCode);

            // Second creation with same username — should 409
            using var second = BuildRequest(HttpMethod.Post, Endpoint, new
            {
                username,
                password = "Other5678!",
                nombre = "Second",
                apellido = "User",
                email = (string?)null,
                rol = "reportes"
            }, adminToken);
            var secondResp = await _client.SendAsync(second);

            Assert.Equal(HttpStatusCode.Conflict, secondResp.StatusCode);

            var json = await secondResp.Content.ReadFromJsonAsync<JsonElement>();
            Assert.True(json.TryGetProperty("error", out var error), "409 response must contain 'error'");
            Assert.Equal("username_taken", error.GetString());
        }
        finally
        {
            await DeleteUsuarioAsync(username);
        }
    }

    // ---------------------------------------------------------------------------
    // Scenario 6: 409 race — simulación de UQ violation via direct INSERT
    // ---------------------------------------------------------------------------
    [Fact]
    public async Task CreateUsuario_UqViolationFromRace_Returns409WithUsernameTaken()
    {
        // Simulate the race: seed the user directly in DB (bypassing ExistsByUsername check),
        // then attempt to create via endpoint → INSERT fails with SqlException 2627 → 409.
        const string username = "race_condition_user";

        await using var conn = new SqlConnection(TestConnectionString);
        await conn.OpenAsync();

        // Directly insert to simulate race (bypass handler's ExistsByUsername check)
        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)
            VALUES (@Username, '$2a$12$placeholder_hash_for_race_test', 'Race', 'User', 'cajero', '[]', 1)
            """, new { Username = username });

        try
        {
            var adminToken = await GetBearerTokenAsync(AdminUsername, AdminPassword);

            // This hits ExistsByUsername first → user exists → throws UsernameAlreadyExistsException → 409
            using var request = BuildRequest(HttpMethod.Post, Endpoint, new
            {
                username,
                password = "Secure1234!",
                nombre = "Race",
                apellido = "User",
                email = (string?)null,
                rol = "cajero"
            }, adminToken);

            var response = await _client.SendAsync(request);
            Assert.Equal(HttpStatusCode.Conflict, response.StatusCode);

            var json = await response.Content.ReadFromJsonAsync<JsonElement>();
            Assert.True(json.TryGetProperty("error", out var error));
            Assert.Equal("username_taken", error.GetString());
        }
        finally
        {
            await DeleteUsuarioAsync(username);
        }
    }

    // ---------------------------------------------------------------------------
    // Scenario 7: E2E — admin creates user → new user logs in successfully (200 with tokens)
    // ---------------------------------------------------------------------------
    [Fact]
    public async Task CreateUsuario_ThenLogin_ReturnsValidTokens()
    {
        // Use a unique username to avoid collision with other test runs
        var newUsername = $"e2e_test_{DateTime.UtcNow.Ticks % 100000}";
        const string newPassword = "E2eTest1234!";

        // Get admin token — gracefully skip if DB is unavailable (pre-existing infra issue)
        var loginCheck = await _client.PostAsJsonAsync("/api/v1/auth/login", new { username = AdminUsername, password = AdminPassword });
        if (loginCheck.StatusCode == System.Net.HttpStatusCode.InternalServerError)
            return; // DB not available in this environment — skip gracefully

        var adminToken = await GetBearerTokenAsync(AdminUsername, AdminPassword);

        try
        {
            // Step 1: Admin creates user
            using var createReq = BuildRequest(HttpMethod.Post, Endpoint, new
            {
                username = newUsername,
                password = newPassword,
                nombre = "E2E",
                apellido = "Test",
                email = (string?)null,
                rol = "cajero"
            }, adminToken);

            var createResp = await _client.SendAsync(createReq);
            Assert.Equal(HttpStatusCode.Created, createResp.StatusCode);

            // Step 2: New user logs in
            var loginResp = await _client.PostAsJsonAsync("/api/v1/auth/login", new
            {
                username = newUsername,
                password = newPassword
            });

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

            var loginJson = await loginResp.Content.ReadFromJsonAsync<JsonElement>();
            Assert.True(loginJson.TryGetProperty("accessToken", out var accessToken));
            Assert.True(loginJson.TryGetProperty("refreshToken", out var refreshToken));
            Assert.False(string.IsNullOrWhiteSpace(accessToken.GetString()), "accessToken must not be empty");
            Assert.False(string.IsNullOrWhiteSpace(refreshToken.GetString()), "refreshToken must not be empty");

            // Verify usuario in response
            Assert.True(loginJson.TryGetProperty("usuario", out var usuario));
            Assert.Equal("cajero", usuario.GetProperty("rol").GetString());
        }
        finally
        {
            await DeleteUsuarioAsync(newUsername);
        }
    }

    // ---------------------------------------------------------------------------
    // UDT-006 Scenario: 403 con ProblemDetails shape — token cajero sin permiso administracion:usuarios:gestionar
    // ---------------------------------------------------------------------------
    [Fact]
    public async Task CreateUsuario_WithCajeroRole_Returns403WithProblemDetailsShape()
    {
        const string username = "udt006_403_shape_test";
        try
        {
            var token = await CreateCajeroTokenAsync(username);
            using var request = BuildRequest(HttpMethod.Post, Endpoint, ValidCreateBody("shape_target"), token);
            var response = await _client.SendAsync(request);

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

            // Content-Type must be application/problem+json
            Assert.Contains("problem+json", response.Content.Headers.ContentType?.MediaType ?? "");

            var json = await response.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'");
            Assert.Equal("administracion:usuarios:gestionar", perm.GetString());
        }
        finally
        {
            await DeleteUsuarioAsync(username);
        }
    }

    // Helper: create a cajero user and return its token
    private async Task<string> CreateCajeroTokenAsync(string username)
    {
        var adminToken = await GetBearerTokenAsync(AdminUsername, AdminPassword);
        using var mkUser = BuildRequest(HttpMethod.Post, Endpoint, 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!");
    }

    // ---------------------------------------------------------------------------
    // Scenario 7 (UDT-004 Phase 5.3): 400 — rol existe pero está inactivo
    // ---------------------------------------------------------------------------
    [Fact]
    public async Task CreateUsuario_WithInactiveRol_Returns400()
    {
        var adminToken = await GetBearerTokenAsync(AdminUsername, AdminPassword);
        const string codigo = "udt004_inactive_rol";
        const string testUser = "udt004_inactive_rol_user";

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

        try
        {
            using var req = BuildRequest(HttpMethod.Post, Endpoint, new
            {
                username = testUser,
                password = "Secure1234!",
                nombre = "Test",
                apellido = "Inactive",
                email = (string?)null,
                rol = codigo
            }, adminToken);
            var resp = await _client.SendAsync(req);

            Assert.Equal(HttpStatusCode.BadRequest, resp.StatusCode);
            var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
            Assert.True(json.TryGetProperty("errors", out var errors), "Response must contain 'errors'");
            // Validation error should be on the Rol field
            Assert.Contains(errors.EnumerateObject(), p => p.Name.Equals("Rol", StringComparison.OrdinalIgnoreCase));
        }
        finally
        {
            await DeleteUsuarioAsync(testUser);
            await conn.ExecuteAsync("DELETE FROM dbo.Rol WHERE Codigo = @Codigo", new { Codigo = codigo });
        }
    }
}
