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

/// <summary>
/// ADM-001 B6 — Integration tests for /api/v1/admin/secciones.
/// All endpoints require permission 'administracion:secciones:gestionar'.
/// </summary>
[Collection("ApiIntegration")]
public sealed class SeccionesControllerTests : IAsyncLifetime
{
    private const string TestConnectionString = TestConnectionStrings.ApiTestDb;

    private const string Endpoint = "/api/v1/admin/secciones";
    private const string MediosEndpoint = "/api/v1/admin/medios";
    private const string AdminUsername = "admin";
    private const string AdminPassword = "@Diego550@";

    private readonly HttpClient _client;

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

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

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

    private async Task<string> GetAdminTokenAsync()
    {
        var response = await _client.PostAsJsonAsync("/api/v1/auth/login", new
        {
            username = AdminUsername,
            password = AdminPassword
        });
        response.EnsureSuccessStatusCode();
        var json = await response.Content.ReadFromJsonAsync<JsonElement>();
        return json.GetProperty("accessToken").GetString()!;
    }

    private async Task<string> GetCajeroTokenAsync(string username)
    {
        var adminToken = await GetAdminTokenAsync();

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

        var loginResp = await _client.PostAsJsonAsync("/api/v1/auth/login", new
        {
            username,
            password = "Secure1234!"
        });
        loginResp.EnsureSuccessStatusCode();
        var loginJson = await loginResp.Content.ReadFromJsonAsync<JsonElement>();
        return loginJson.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;
    }

    /// <summary>Creates a Medio via the API and returns its id.</summary>
    private async Task<int> CreateMedioAsync(string codigo, string nombre, string token)
    {
        using var req = BuildRequest(HttpMethod.Post, MediosEndpoint, new
        {
            codigo,
            nombre,
            tipo = 1
        }, token);
        var resp = await _client.SendAsync(req);
        resp.EnsureSuccessStatusCode();
        var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
        return json.GetProperty("id").GetInt32();
    }

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

        var id = await conn.QuerySingleOrDefaultAsync<int?>(
            "SELECT Id FROM dbo.Medio WHERE Codigo = @Codigo", new { Codigo = codigo });
        if (id is null) return;

        // Delete dependent secciones (disable versioning to clear history too)
        await conn.ExecuteAsync("ALTER TABLE dbo.Seccion SET (SYSTEM_VERSIONING = OFF)");
        await conn.ExecuteAsync("DELETE FROM dbo.Seccion_History WHERE MedioId = @id", new { id });
        await conn.ExecuteAsync("DELETE FROM dbo.Seccion WHERE MedioId = @id", new { id });
        await conn.ExecuteAsync(
            "ALTER TABLE dbo.Seccion SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.Seccion_History, HISTORY_RETENTION_PERIOD = 10 YEARS))");

        // Delete the medio itself
        await conn.ExecuteAsync("ALTER TABLE dbo.Medio SET (SYSTEM_VERSIONING = OFF)");
        await conn.ExecuteAsync("DELETE FROM dbo.Medio_History WHERE Id = @id", new { id });
        await conn.ExecuteAsync("DELETE FROM dbo.Medio WHERE Id = @id", new { id });
        await conn.ExecuteAsync(
            "ALTER TABLE dbo.Medio SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.Medio_History, HISTORY_RETENTION_PERIOD = 10 YEARS))");
    }

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

    private static async Task<int> CountAuditEventsAsync(string action, string targetType, string targetId)
    {
        await using var conn = new SqlConnection(TestConnectionString);
        await conn.OpenAsync();
        return await conn.QuerySingleAsync<int>(
            "SELECT COUNT(*) FROM dbo.AuditEvent WHERE Action = @Action AND TargetType = @TargetType AND TargetId = @TargetId",
            new { Action = action, TargetType = targetType, TargetId = targetId });
    }

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

    [Fact]
    public async Task CreateSeccion_WithoutAuth_Returns401()
    {
        using var req = BuildRequest(HttpMethod.Post, Endpoint, new
        {
            medioId = 1,
            codigo = "SEC401",
            nombre = "Seccion Test",
            tipo = "clasificados"
        });
        var resp = await _client.SendAsync(req);
        Assert.Equal(HttpStatusCode.Unauthorized, resp.StatusCode);
    }

    [Fact]
    public async Task CreateSeccion_WithCajeroRole_Returns403()
    {
        const string username = "adm001_sec_cajero_403";
        try
        {
            var token = await GetCajeroTokenAsync(username);
            using var req = BuildRequest(HttpMethod.Post, Endpoint, new
            {
                medioId = 1,
                codigo = "SEC403",
                nombre = "Seccion Test",
                tipo = "clasificados"
            }, token);
            var resp = await _client.SendAsync(req);
            Assert.Equal(HttpStatusCode.Forbidden, resp.StatusCode);
        }
        finally
        {
            await DeleteUsuarioIfExistsAsync(username);
        }
    }

    // ── CREATE ────────────────────────────────────────────────────────────────

    [Fact]
    public async Task CreateSeccion_WithAdmin_Returns201AndAuditEvent()
    {
        const string medioCodigo = "TESTSECMED201";
        var token = await GetAdminTokenAsync();

        try
        {
            var medioId = await CreateMedioAsync(medioCodigo, "Medio Para Seccion 201", token);

            using var req = BuildRequest(HttpMethod.Post, Endpoint, new
            {
                medioId,
                codigo = "SEC201",
                nombre = "Seccion 201",
                tipo = "clasificados"
            }, token);
            var resp = await _client.SendAsync(req);

            Assert.Equal(HttpStatusCode.Created, resp.StatusCode);
            Assert.NotNull(resp.Headers.Location);

            var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
            Assert.True(json.TryGetProperty("id", out var idEl));
            var secId = idEl.GetInt32();
            Assert.True(secId > 0);
            Assert.Equal(medioId, json.GetProperty("medioId").GetInt32());
            Assert.Equal("SEC201", json.GetProperty("codigo").GetString());
            Assert.Equal("clasificados", json.GetProperty("tipo").GetString());

            // Verify AuditEvent
            var auditCount = await CountAuditEventsAsync("seccion.create", "Seccion", secId.ToString());
            Assert.Equal(1, auditCount);
        }
        finally
        {
            await DeleteMedioIfExistsAsync(medioCodigo);
        }
    }

    [Fact]
    public async Task CreateSeccion_WithNonExistentMedioId_Returns404()
    {
        var token = await GetAdminTokenAsync();

        using var req = BuildRequest(HttpMethod.Post, Endpoint, new
        {
            medioId = 99999,
            codigo = "SECNOTFOUND",
            nombre = "Seccion Not Found",
            tipo = "clasificados"
        }, token);
        var resp = await _client.SendAsync(req);

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

    [Fact]
    public async Task CreateSeccion_WithDuplicateCodigoInSameMedio_Returns409()
    {
        const string medioCodigo = "TESTSECDUP";
        var token = await GetAdminTokenAsync();

        try
        {
            var medioId = await CreateMedioAsync(medioCodigo, "Medio Dup Test", token);

            // First seccion
            using var first = BuildRequest(HttpMethod.Post, Endpoint, new
            {
                medioId,
                codigo = "DUPCODE",
                nombre = "Seccion Original",
                tipo = "clasificados"
            }, token);
            var firstResp = await _client.SendAsync(first);
            Assert.Equal(HttpStatusCode.Created, firstResp.StatusCode);

            // Second with same medioId + codigo
            using var second = BuildRequest(HttpMethod.Post, Endpoint, new
            {
                medioId,
                codigo = "DUPCODE",
                nombre = "Seccion Duplicada",
                tipo = "notables"
            }, token);
            var secondResp = await _client.SendAsync(second);

            Assert.Equal(HttpStatusCode.Conflict, secondResp.StatusCode);
            var json = await secondResp.Content.ReadFromJsonAsync<JsonElement>();
            Assert.Equal("seccion_codigo_duplicado_en_medio", json.GetProperty("error").GetString());
        }
        finally
        {
            await DeleteMedioIfExistsAsync(medioCodigo);
        }
    }

    [Fact]
    public async Task CreateSeccion_SameCodigoDifferentMedio_Returns201()
    {
        const string medio1Codigo = "TESTSECMULTI1";
        const string medio2Codigo = "TESTSECMULTI2";
        var token = await GetAdminTokenAsync();

        try
        {
            var medioId1 = await CreateMedioAsync(medio1Codigo, "Medio Multi 1", token);
            var medioId2 = await CreateMedioAsync(medio2Codigo, "Medio Multi 2", token);

            using var req1 = BuildRequest(HttpMethod.Post, Endpoint, new
            {
                medioId = medioId1,
                codigo = "SHAREDCODE",
                nombre = "Seccion en Medio 1",
                tipo = "clasificados"
            }, token);
            var resp1 = await _client.SendAsync(req1);
            Assert.Equal(HttpStatusCode.Created, resp1.StatusCode);

            // Same codigo but different medioId → should succeed (composite UQ)
            using var req2 = BuildRequest(HttpMethod.Post, Endpoint, new
            {
                medioId = medioId2,
                codigo = "SHAREDCODE",
                nombre = "Seccion en Medio 2",
                tipo = "notables"
            }, token);
            var resp2 = await _client.SendAsync(req2);
            Assert.Equal(HttpStatusCode.Created, resp2.StatusCode);
        }
        finally
        {
            await DeleteMedioIfExistsAsync(medio1Codigo);
            await DeleteMedioIfExistsAsync(medio2Codigo);
        }
    }

    [Fact]
    public async Task CreateSeccion_WithInactiveMedio_Returns404()
    {
        const string medioCodigo = "TESTSECDEACT";
        var token = await GetAdminTokenAsync();

        try
        {
            var medioId = await CreateMedioAsync(medioCodigo, "Medio Para Desactivar", token);

            // Deactivate the medio
            using var deactReq = BuildRequest(HttpMethod.Post, $"{MediosEndpoint}/{medioId}/deactivate", bearerToken: token);
            var deactResp = await _client.SendAsync(deactReq);
            Assert.Equal(HttpStatusCode.NoContent, deactResp.StatusCode);

            // Try to create seccion in inactive medio
            using var secReq = BuildRequest(HttpMethod.Post, Endpoint, new
            {
                medioId,
                codigo = "SECINACTIVE",
                nombre = "Seccion en Medio Inactivo",
                tipo = "clasificados"
            }, token);
            var secResp = await _client.SendAsync(secReq);

            Assert.Equal(HttpStatusCode.NotFound, secResp.StatusCode);
            var json = await secResp.Content.ReadFromJsonAsync<JsonElement>();
            Assert.Equal("medio_not_found", json.GetProperty("error").GetString());
        }
        finally
        {
            await DeleteMedioIfExistsAsync(medioCodigo);
        }
    }

    // ── LIST ─────────────────────────────────────────────────────────────────

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

        Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
        var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
        Assert.True(json.TryGetProperty("items", out _), "Response must have 'items'");
        Assert.True(json.TryGetProperty("total", out _), "Response must have 'total'");
    }

    // ── GET BY ID ────────────────────────────────────────────────────────────

    [Fact]
    public async Task GetSeccionById_NotFound_Returns404()
    {
        var token = await GetAdminTokenAsync();
        using var req = BuildRequest(HttpMethod.Get, $"{Endpoint}/999999", bearerToken: token);
        var resp = await _client.SendAsync(req);

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

    // ── CASCADA INACTIVIDAD (issue #16) ──────────────────────────────────────

    [Fact]
    public async Task UpdateSeccion_WhenMedioInactive_Returns409()
    {
        const string medioCodigo = "TSEC_UPD_INACT";
        var token = await GetAdminTokenAsync();

        try
        {
            var medioId = await CreateMedioAsync(medioCodigo, "Medio Inactivo Update", token);

            using var createReq = BuildRequest(HttpMethod.Post, Endpoint, new
            {
                medioId,
                codigo = "SECUPDINACT",
                nombre = "Seccion Update Inactivo",
                tipo = "clasificados"
            }, token);
            var createResp = await _client.SendAsync(createReq);
            Assert.Equal(HttpStatusCode.Created, createResp.StatusCode);
            var created = await createResp.Content.ReadFromJsonAsync<JsonElement>();
            var secId = created.GetProperty("id").GetInt32();

            // Deactivate the medio
            using var deactMedioReq = BuildRequest(HttpMethod.Post, $"{MediosEndpoint}/{medioId}/deactivate", bearerToken: token);
            var deactMedioResp = await _client.SendAsync(deactMedioReq);
            Assert.Equal(HttpStatusCode.NoContent, deactMedioResp.StatusCode);

            var auditBefore = await CountAuditEventsAsync("seccion.update", "Seccion", secId.ToString());

            // Try to update seccion with inactive medio
            using var updateReq = BuildRequest(HttpMethod.Put, $"{Endpoint}/{secId}", new
            {
                nombre = "Nombre Cambiado",
                tipo = "notables"
            }, token);
            var updateResp = await _client.SendAsync(updateReq);

            Assert.Equal(HttpStatusCode.Conflict, updateResp.StatusCode);
            var json = await updateResp.Content.ReadFromJsonAsync<JsonElement>();
            Assert.Equal("medio_inactivo", json.GetProperty("error").GetString());

            var auditAfter = await CountAuditEventsAsync("seccion.update", "Seccion", secId.ToString());
            Assert.Equal(auditBefore, auditAfter);
        }
        finally
        {
            await DeleteMedioIfExistsAsync(medioCodigo);
        }
    }

    [Fact]
    public async Task DeactivateSeccion_WhenMedioInactive_Returns409()
    {
        const string medioCodigo = "TSEC_DEACT_INACT";
        var token = await GetAdminTokenAsync();

        try
        {
            var medioId = await CreateMedioAsync(medioCodigo, "Medio Inactivo Deactivate", token);

            using var createReq = BuildRequest(HttpMethod.Post, Endpoint, new
            {
                medioId,
                codigo = "SECDEACTINACT",
                nombre = "Seccion Deactivate Inactivo",
                tipo = "clasificados"
            }, token);
            var createResp = await _client.SendAsync(createReq);
            Assert.Equal(HttpStatusCode.Created, createResp.StatusCode);
            var created = await createResp.Content.ReadFromJsonAsync<JsonElement>();
            var secId = created.GetProperty("id").GetInt32();

            // Deactivate the medio
            using var deactMedioReq = BuildRequest(HttpMethod.Post, $"{MediosEndpoint}/{medioId}/deactivate", bearerToken: token);
            var deactMedioResp = await _client.SendAsync(deactMedioReq);
            Assert.Equal(HttpStatusCode.NoContent, deactMedioResp.StatusCode);

            var auditBefore = await CountAuditEventsAsync("seccion.deactivate", "Seccion", secId.ToString());

            // Try to deactivate seccion with inactive medio
            using var deactReq = BuildRequest(HttpMethod.Post, $"{Endpoint}/{secId}/deactivate", bearerToken: token);
            var deactResp = await _client.SendAsync(deactReq);

            Assert.Equal(HttpStatusCode.Conflict, deactResp.StatusCode);
            var json = await deactResp.Content.ReadFromJsonAsync<JsonElement>();
            Assert.Equal("medio_inactivo", json.GetProperty("error").GetString());

            var auditAfter = await CountAuditEventsAsync("seccion.deactivate", "Seccion", secId.ToString());
            Assert.Equal(auditBefore, auditAfter);
        }
        finally
        {
            await DeleteMedioIfExistsAsync(medioCodigo);
        }
    }

    [Fact]
    public async Task ReactivateSeccion_WhenMedioInactive_Returns409()
    {
        const string medioCodigo = "TSEC_REACT_INACT";
        var token = await GetAdminTokenAsync();

        try
        {
            var medioId = await CreateMedioAsync(medioCodigo, "Medio Inactivo Reactivate", token);

            // Create seccion then deactivate it (while medio is still active)
            using var createReq = BuildRequest(HttpMethod.Post, Endpoint, new
            {
                medioId,
                codigo = "SECREACTINACT",
                nombre = "Seccion Reactivate Inactivo",
                tipo = "clasificados"
            }, token);
            var createResp = await _client.SendAsync(createReq);
            Assert.Equal(HttpStatusCode.Created, createResp.StatusCode);
            var created = await createResp.Content.ReadFromJsonAsync<JsonElement>();
            var secId = created.GetProperty("id").GetInt32();

            // Deactivate seccion while medio is still active
            using var deactSecReq = BuildRequest(HttpMethod.Post, $"{Endpoint}/{secId}/deactivate", bearerToken: token);
            var deactSecResp = await _client.SendAsync(deactSecReq);
            Assert.Equal(HttpStatusCode.NoContent, deactSecResp.StatusCode);

            // Now deactivate the medio
            using var deactMedioReq = BuildRequest(HttpMethod.Post, $"{MediosEndpoint}/{medioId}/deactivate", bearerToken: token);
            var deactMedioResp = await _client.SendAsync(deactMedioReq);
            Assert.Equal(HttpStatusCode.NoContent, deactMedioResp.StatusCode);

            var auditBefore = await CountAuditEventsAsync("seccion.reactivate", "Seccion", secId.ToString());

            // Try to reactivate seccion with inactive medio
            using var reactReq = BuildRequest(HttpMethod.Post, $"{Endpoint}/{secId}/reactivate", bearerToken: token);
            var reactResp = await _client.SendAsync(reactReq);

            Assert.Equal(HttpStatusCode.Conflict, reactResp.StatusCode);
            var json = await reactResp.Content.ReadFromJsonAsync<JsonElement>();
            Assert.Equal("medio_inactivo", json.GetProperty("error").GetString());

            var auditAfter = await CountAuditEventsAsync("seccion.reactivate", "Seccion", secId.ToString());
            Assert.Equal(auditBefore, auditAfter);
        }
        finally
        {
            await DeleteMedioIfExistsAsync(medioCodigo);
        }
    }

    // ── DEACTIVATE ────────────────────────────────────────────────────────────

    [Fact]
    public async Task DeactivateSeccion_WithAdmin_Returns204AndAuditEvent()
    {
        const string medioCodigo = "TESTSECDEACT2";
        var token = await GetAdminTokenAsync();

        try
        {
            var medioId = await CreateMedioAsync(medioCodigo, "Medio Para Sec Deactivate", token);

            using var createReq = BuildRequest(HttpMethod.Post, Endpoint, new
            {
                medioId,
                codigo = "SECDEACT",
                nombre = "Seccion Para Desactivar",
                tipo = "clasificados"
            }, token);
            var createResp = await _client.SendAsync(createReq);
            Assert.Equal(HttpStatusCode.Created, createResp.StatusCode);
            var created = await createResp.Content.ReadFromJsonAsync<JsonElement>();
            var secId = created.GetProperty("id").GetInt32();

            using var deactReq = BuildRequest(HttpMethod.Post, $"{Endpoint}/{secId}/deactivate", bearerToken: token);
            var deactResp = await _client.SendAsync(deactReq);

            Assert.Equal(HttpStatusCode.NoContent, deactResp.StatusCode);

            var auditCount = await CountAuditEventsAsync("seccion.deactivate", "Seccion", secId.ToString());
            Assert.Equal(1, auditCount);
        }
        finally
        {
            await DeleteMedioIfExistsAsync(medioCodigo);
        }
    }
}
