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

namespace SIGCM2.Api.Tests.Admin;

/// <summary>
/// ADM-009 Batch 5 — Integration tests for /api/v1/admin/fiscal
/// Requires permission 'administracion:fiscal:gestionar'.
/// All tests use real JWT RS256 auth via TestWebAppFactory.
/// </summary>
[Collection("ApiIntegration")]
public sealed class FiscalControllerTests : IAsyncLifetime
{
    private const string TestConnectionString = TestConnectionStrings.ApiTestDb;

    private const string IvaEndpoint = "/api/v1/admin/fiscal/iva";
    private const string IibbEndpoint = "/api/v1/admin/fiscal/iibb";
    private const string AdminUsername = "admin";
    private const string AdminPassword = "@Diego550@";

    private readonly HttpClient _client;

    public FiscalControllerTests(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,
        string contentType = "application/json")
    {
        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 HttpRequestMessage BuildRawRequest(
        HttpMethod method,
        string url,
        string rawJson,
        string? bearerToken = null)
    {
        var request = new HttpRequestMessage(method, url);
        if (bearerToken is not null)
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken);
        request.Content = new StringContent(rawJson, Encoding.UTF8, "application/json");
        return request;
    }

    private async Task<int> CreateTipoDeIvaAsync(string codigo, string descripcion, decimal porcentaje, bool aplicaIva, string vigenciaDesde, string token)
    {
        using var req = BuildRequest(HttpMethod.Post, IvaEndpoint, new
        {
            codigo,
            descripcion,
            porcentaje,
            aplicaIVA = aplicaIva,
            vigenciaDesde
        }, token);
        var resp = await _client.SendAsync(req);
        if (!resp.IsSuccessStatusCode)
        {
            var body = await resp.Content.ReadAsStringAsync();
            Assert.Fail($"CreateTipoDeIva failed {resp.StatusCode}: {body}");
        }
        var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
        return json.GetProperty("id").GetInt32();
    }

    private async Task<int> CreateIngresosBrutosAsync(string provincia, string descripcion, decimal alicuota, string vigenciaDesde, string token)
    {
        using var req = BuildRequest(HttpMethod.Post, IibbEndpoint, new
        {
            provincia,
            descripcion,
            alicuota,
            vigenciaDesde
        }, token);
        var resp = await _client.SendAsync(req);
        if (!resp.IsSuccessStatusCode)
        {
            var body = await resp.Content.ReadAsStringAsync();
            Assert.Fail($"CreateIngresosBrutos failed {resp.StatusCode}: {body}");
        }
        var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
        return json.GetProperty("id").GetInt32();
    }

    private static async Task DeleteTipoDeIvaByCodigoAsync(string codigo)
    {
        await using var conn = new SqlConnection(TestConnectionString);
        await conn.OpenAsync();
        await conn.ExecuteAsync("ALTER TABLE dbo.TipoDeIva SET (SYSTEM_VERSIONING = OFF)");
        await conn.ExecuteAsync("DELETE FROM dbo.TipoDeIva_History WHERE Codigo = @Codigo", new { Codigo = codigo });
        await conn.ExecuteAsync("DELETE FROM dbo.TipoDeIva WHERE Codigo = @Codigo", new { Codigo = codigo });
        await conn.ExecuteAsync(
            "ALTER TABLE dbo.TipoDeIva SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.TipoDeIva_History, HISTORY_RETENTION_PERIOD = 10 YEARS))");
    }

    private static async Task DeleteIngresosBrutosByProvinciaAsync(string provincia)
    {
        await using var conn = new SqlConnection(TestConnectionString);
        await conn.OpenAsync();
        await conn.ExecuteAsync("ALTER TABLE dbo.IngresosBrutos SET (SYSTEM_VERSIONING = OFF)");
        await conn.ExecuteAsync(
            "DELETE FROM dbo.IngresosBrutos_History WHERE Provincia = @Provincia", new { Provincia = provincia });
        await conn.ExecuteAsync(
            "DELETE FROM dbo.IngresosBrutos WHERE Provincia = @Provincia", new { Provincia = provincia });
        await conn.ExecuteAsync(
            "ALTER TABLE dbo.IngresosBrutos SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.IngresosBrutos_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 });
    }

    // ── AUTH / PERMISSION GUARDS ─────────────────────────────────────────────

    /// <summary>[REQ-FISCAL-AUTH-001] GET /iva sin auth → 401.</summary>
    [Fact]
    public async Task GetIva_WithoutAuth_Returns401()
    {
        using var req = new HttpRequestMessage(HttpMethod.Get, IvaEndpoint);
        var resp = await _client.SendAsync(req);
        Assert.Equal(HttpStatusCode.Unauthorized, resp.StatusCode);
    }

    /// <summary>[REQ-FISCAL-AUTH-001] GET /iva con cajero (sin permiso fiscal) → 403.</summary>
    [Fact]
    public async Task GetIva_WithCajeroRole_Returns403()
    {
        const string username = "adm009_fiscal_cajero_403";
        try
        {
            var token = await GetCajeroTokenAsync(username);
            using var req = BuildRequest(HttpMethod.Get, IvaEndpoint, bearerToken: token);
            var resp = await _client.SendAsync(req);
            Assert.Equal(HttpStatusCode.Forbidden, resp.StatusCode);
        }
        finally
        {
            await DeleteUsuarioIfExistsAsync(username);
        }
    }

    /// <summary>[REQ-FISCAL-AUTH-002] GET /iva con admin (tiene permiso) → 200.</summary>
    [Fact]
    public async Task GetIva_WithAdmin_Returns200()
    {
        var token = await GetAdminTokenAsync();
        using var req = BuildRequest(HttpMethod.Get, IvaEndpoint, 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'");
    }

    // ── IVA: POST (CREATE) ────────────────────────────────────────────────────

    /// <summary>POST /iva → 201 con id, campos correctos.</summary>
    [Fact]
    public async Task CreateIva_WithAdmin_Returns201()
    {
        // Codigo must match ^(EXENTO|NO_GRAVADO|IVA_\d+)$
        const string codigo = "IVA_9901";
        var token = await GetAdminTokenAsync();
        try
        {
            using var req = BuildRequest(HttpMethod.Post, IvaEndpoint, new
            {
                codigo,
                descripcion = "IVA Test Creacion",
                porcentaje = 15.5m,
                aplicaIVA = true,
                vigenciaDesde = "2025-01-01"
            }, token);
            var resp = await _client.SendAsync(req);

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

    /// <summary>[REQ-TIPOIVA-CREATE-002] POST /iva con codigo duplicado en misma vigencia → 409 duplicate_codigo.</summary>
    [Fact]
    public async Task CreateIva_DuplicateCodigo_Returns409()
    {
        const string codigo = "IVA_9902";
        var token = await GetAdminTokenAsync();
        try
        {
            await CreateTipoDeIvaAsync(codigo, "IVA Original", 10m, true, "2025-01-01", token);

            using var req = BuildRequest(HttpMethod.Post, IvaEndpoint, new
            {
                codigo,
                descripcion = "IVA Duplicado",
                porcentaje = 12m,
                aplicaIVA = true,
                vigenciaDesde = "2025-01-01"
            }, token);
            var resp = await _client.SendAsync(req);

            Assert.Equal(HttpStatusCode.Conflict, resp.StatusCode);
            var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
            Assert.Equal("duplicate_codigo", json.GetProperty("error").GetString());
        }
        finally
        {
            await DeleteTipoDeIvaByCodigoAsync(codigo);
        }
    }

    // ── IVA: GET BY ID ────────────────────────────────────────────────────────

    /// <summary>GET /iva/{id} inexistente → 404 tipo_iva_not_found.</summary>
    [Fact]
    public async Task GetIvaById_NotFound_Returns404()
    {
        var token = await GetAdminTokenAsync();
        using var req = BuildRequest(HttpMethod.Get, $"{IvaEndpoint}/999999", bearerToken: token);
        var resp = await _client.SendAsync(req);

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

    /// <summary>GET /iva/{id} existente → 200 con campos correctos.</summary>
    [Fact]
    public async Task GetIvaById_Existing_Returns200()
    {
        const string codigo = "IVA_9903";
        var token = await GetAdminTokenAsync();
        try
        {
            var id = await CreateTipoDeIvaAsync(codigo, "IVA GetById", 10m, true, "2025-01-01", token);
            using var req = BuildRequest(HttpMethod.Get, $"{IvaEndpoint}/{id}", bearerToken: token);
            var resp = await _client.SendAsync(req);

            Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
            var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
            Assert.Equal(id, json.GetProperty("id").GetInt32());
            Assert.Equal(codigo, json.GetProperty("codigo").GetString());
        }
        finally
        {
            await DeleteTipoDeIvaByCodigoAsync(codigo);
        }
    }

    // ── IVA: PATCH (UPDATE COSMETICO) ─────────────────────────────────────────

    /// <summary>PATCH /iva/{id} con campos cosméticos → 200 OK.</summary>
    [Fact]
    public async Task PatchIva_CosmeticFields_Returns200()
    {
        const string codigo = "IVA_9904";
        var token = await GetAdminTokenAsync();
        try
        {
            var id = await CreateTipoDeIvaAsync(codigo, "Descripcion Original", 10m, true, "2025-01-01", token);

            using var req = BuildRawRequest(
                HttpMethod.Patch,
                $"{IvaEndpoint}/{id}",
                """{"codigo":"IVA_9904","descripcion":"Descripcion Actualizada","aplicaIVA":true,"activo":true}""",
                token);
            var resp = await _client.SendAsync(req);

            Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
            var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
            Assert.Equal("Descripcion Actualizada", json.GetProperty("descripcion").GetString());
        }
        finally
        {
            await DeleteTipoDeIvaByCodigoAsync(codigo);
        }
    }

    /// <summary>[REQ-TIPOIVA-UPDATE-002] PATCH /iva/{id} con "porcentaje" en body → 409 inmutable_usar_nueva_version.</summary>
    [Fact]
    public async Task PatchIva_WithPorcentajeInBody_Returns409Inmutable()
    {
        const string codigo = "IVA_9905";
        var token = await GetAdminTokenAsync();
        try
        {
            var id = await CreateTipoDeIvaAsync(codigo, "IVA Patch Pct", 10m, true, "2025-01-01", token);

            using var req = BuildRawRequest(
                HttpMethod.Patch,
                $"{IvaEndpoint}/{id}",
                """{"porcentaje":23.5}""",
                token);
            var resp = await _client.SendAsync(req);

            Assert.Equal(HttpStatusCode.Conflict, resp.StatusCode);
            var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
            Assert.Equal("inmutable_usar_nueva_version", json.GetProperty("error").GetString());
        }
        finally
        {
            await DeleteTipoDeIvaByCodigoAsync(codigo);
        }
    }

    // ── IVA: NUEVA VERSION ────────────────────────────────────────────────────

    /// <summary>[REQ-TIPOIVA-NUEVAVER-001] POST /iva/{id}/nueva-version → 201 con predecesoraId+nuevaVersionId correctos.</summary>
    [Fact]
    public async Task NuevaVersionIva_HappyPath_Returns201WithChain()
    {
        const string codigo = "IVA_9906";
        var token = await GetAdminTokenAsync();
        try
        {
            var predecesoraId = await CreateTipoDeIvaAsync(codigo, "IVA Nueva Version", 10m, true, "2024-01-01", token);

            using var req = BuildRequest(
                HttpMethod.Post,
                $"{IvaEndpoint}/{predecesoraId}/nueva-version",
                new { porcentaje = 12m, vigenciaDesde = "2025-01-01" },
                token);
            var resp = await _client.SendAsync(req);

            Assert.Equal(HttpStatusCode.Created, resp.StatusCode);
            var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
            Assert.Equal(predecesoraId, json.GetProperty("predecesoraId").GetInt32());
            var nuevaVersionId = json.GetProperty("nuevaVersionId").GetInt32();
            Assert.True(nuevaVersionId > predecesoraId);

            // Verificar que la predecesora quedó cerrada (VigenciaHasta != null)
            using var getReq = BuildRequest(HttpMethod.Get, $"{IvaEndpoint}/{predecesoraId}", bearerToken: token);
            var getResp = await _client.SendAsync(getReq);
            var predecesoraJson = await getResp.Content.ReadFromJsonAsync<JsonElement>();
            Assert.NotEqual(JsonValueKind.Null, predecesoraJson.GetProperty("vigenciaHasta").ValueKind);
        }
        finally
        {
            await DeleteTipoDeIvaByCodigoAsync(codigo);
        }
    }

    /// <summary>[REQ-TIPOIVA-NUEVAVER-003] POST /iva/{id}/nueva-version sobre predecesora ya cerrada → 409 predecesora_ya_cerrada.</summary>
    [Fact]
    public async Task NuevaVersionIva_PredecesoraYaCerrada_Returns409()
    {
        const string codigo = "IVA_9907";
        var token = await GetAdminTokenAsync();
        try
        {
            var predecesoraId = await CreateTipoDeIvaAsync(codigo, "IVA Predecesora Cerrada", 10m, true, "2024-01-01", token);

            // Primera nueva version — cierra la predecesora
            using var req1 = BuildRequest(
                HttpMethod.Post,
                $"{IvaEndpoint}/{predecesoraId}/nueva-version",
                new { porcentaje = 12m, vigenciaDesde = "2025-01-01" },
                token);
            var resp1 = await _client.SendAsync(req1);
            Assert.Equal(HttpStatusCode.Created, resp1.StatusCode);

            // Segunda sobre la predecesora original (ya cerrada)
            using var req2 = BuildRequest(
                HttpMethod.Post,
                $"{IvaEndpoint}/{predecesoraId}/nueva-version",
                new { porcentaje = 15m, vigenciaDesde = "2026-01-01" },
                token);
            var resp2 = await _client.SendAsync(req2);

            Assert.Equal(HttpStatusCode.Conflict, resp2.StatusCode);
            var json = await resp2.Content.ReadFromJsonAsync<JsonElement>();
            Assert.Equal("predecesora_ya_cerrada", json.GetProperty("error").GetString());
        }
        finally
        {
            await DeleteTipoDeIvaByCodigoAsync(codigo);
        }
    }

    /// <summary>POST /iva/{id}/nueva-version con vigenciaDesde inválida → 400 vigencia_desde_invalida.</summary>
    [Fact]
    public async Task NuevaVersionIva_VigenciaDesdeInvalida_Returns400()
    {
        const string codigo = "IVA_9908";
        var token = await GetAdminTokenAsync();
        try
        {
            var predecesoraId = await CreateTipoDeIvaAsync(codigo, "IVA Vig Invalida", 10m, true, "2025-06-01", token);

            // vigenciaDesde anterior a la de la predecesora
            using var req = BuildRequest(
                HttpMethod.Post,
                $"{IvaEndpoint}/{predecesoraId}/nueva-version",
                new { porcentaje = 12m, vigenciaDesde = "2024-01-01" },
                token);
            var resp = await _client.SendAsync(req);

            Assert.Equal(HttpStatusCode.BadRequest, resp.StatusCode);
            var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
            // Validate that the error body contains info about vigencia_desde_invalida
            var bodyStr = json.GetRawText();
            Assert.Contains("vigencia_desde_invalida", bodyStr);
        }
        finally
        {
            await DeleteTipoDeIvaByCodigoAsync(codigo);
        }
    }

    // ── IVA: HISTORIAL ────────────────────────────────────────────────────────

    /// <summary>GET /iva/{id}/historial → 200 con cadena ordenada.</summary>
    [Fact]
    public async Task GetHistorialIva_Returns200WithOrderedChain()
    {
        const string codigo = "IVA_9909";
        var token = await GetAdminTokenAsync();
        try
        {
            // Crear cadena de 2 versiones
            var v1Id = await CreateTipoDeIvaAsync(codigo, "IVA Historial v1", 10m, true, "2023-01-01", token);
            using var nvReq = BuildRequest(
                HttpMethod.Post,
                $"{IvaEndpoint}/{v1Id}/nueva-version",
                new { porcentaje = 15m, vigenciaDesde = "2025-01-01" },
                token);
            var nvResp = await _client.SendAsync(nvReq);
            Assert.Equal(HttpStatusCode.Created, nvResp.StatusCode);
            var nvJson = await nvResp.Content.ReadFromJsonAsync<JsonElement>();
            var v2Id = nvJson.GetProperty("nuevaVersionId").GetInt32();

            // GET historial desde v2 (la actual)
            using var req = BuildRequest(HttpMethod.Get, $"{IvaEndpoint}/{v2Id}/historial", bearerToken: token);
            var resp = await _client.SendAsync(req);

            Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
            var chain = await resp.Content.ReadFromJsonAsync<JsonElement>();
            var items = chain.EnumerateArray().ToList();
            Assert.Equal(2, items.Count);
            // Version 1 tiene version=1, version 2 tiene version=2
            Assert.Equal(1, items[0].GetProperty("version").GetInt32());
            Assert.Equal(2, items[1].GetProperty("version").GetInt32());
        }
        finally
        {
            await DeleteTipoDeIvaByCodigoAsync(codigo);
        }
    }

    // ── IVA: DEACTIVATE / REACTIVATE ─────────────────────────────────────────

    /// <summary>POST /iva/{id}/deactivate → 200 con activo=false.</summary>
    [Fact]
    public async Task DeactivateIva_Returns200WithActivoFalse()
    {
        const string codigo = "IVA_9910";
        var token = await GetAdminTokenAsync();
        try
        {
            var id = await CreateTipoDeIvaAsync(codigo, "IVA Deactivate", 10m, true, "2025-01-01", token);

            using var req = BuildRequest(HttpMethod.Post, $"{IvaEndpoint}/{id}/deactivate", bearerToken: token);
            var resp = await _client.SendAsync(req);

            Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
            var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
            Assert.False(json.GetProperty("activo").GetBoolean());
        }
        finally
        {
            await DeleteTipoDeIvaByCodigoAsync(codigo);
        }
    }

    /// <summary>POST /iva/{id}/reactivate → 200 con activo=true.</summary>
    [Fact]
    public async Task ReactivateIva_Returns200WithActivoTrue()
    {
        const string codigo = "IVA_9911";
        var token = await GetAdminTokenAsync();
        try
        {
            var id = await CreateTipoDeIvaAsync(codigo, "IVA Reactivate", 10m, true, "2025-01-01", token);

            // Deactivate primero
            using var deactReq = BuildRequest(HttpMethod.Post, $"{IvaEndpoint}/{id}/deactivate", bearerToken: token);
            await _client.SendAsync(deactReq);

            // Reactivate
            using var reactReq = BuildRequest(HttpMethod.Post, $"{IvaEndpoint}/{id}/reactivate", bearerToken: token);
            var reactResp = await _client.SendAsync(reactReq);

            Assert.Equal(HttpStatusCode.OK, reactResp.StatusCode);
            var json = await reactResp.Content.ReadFromJsonAsync<JsonElement>();
            Assert.True(json.GetProperty("activo").GetBoolean());
        }
        finally
        {
            await DeleteTipoDeIvaByCodigoAsync(codigo);
        }
    }

    // ── IIBB: Tests espejo mínimos ────────────────────────────────────────────

    /// <summary>[REQ-FISCAL-AUTH-001] GET /iibb sin auth → 401.</summary>
    [Fact]
    public async Task GetIibb_WithoutAuth_Returns401()
    {
        using var req = new HttpRequestMessage(HttpMethod.Get, IibbEndpoint);
        var resp = await _client.SendAsync(req);
        Assert.Equal(HttpStatusCode.Unauthorized, resp.StatusCode);
    }

    /// <summary>POST /iibb → 201 con id correcto.</summary>
    [Fact]
    public async Task CreateIibb_WithAdmin_Returns201()
    {
        // Usar una provincia que no tenga datos de test previos
        // Nota: El seed tiene todas las provincias con Alicuota=0.
        // Para crear un nuevo registro necesitamos una provincia+vigenciaDesde únicos.
        // Los repos aceptan combinación (Provincia, VigenciaDesde) única.
        // Usamos "Formosa" con una fecha específica de test.
        const string provincia = "Formosa";
        const string vigenciaDesde = "2030-01-01"; // fecha futura para no colisionar con seed
        var token = await GetAdminTokenAsync();
        try
        {
            using var req = BuildRequest(HttpMethod.Post, IibbEndpoint, new
            {
                provincia,
                descripcion = "IIBB Formosa Test",
                alicuota = 2.5m,
                vigenciaDesde
            }, token);
            var resp = await _client.SendAsync(req);

            Assert.Equal(HttpStatusCode.Created, resp.StatusCode);
            var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
            Assert.True(json.GetProperty("id").GetInt32() > 0);
            Assert.Equal(provincia, json.GetProperty("provincia").GetString());
            Assert.Equal(2.5m, json.GetProperty("alicuota").GetDecimal());
        }
        finally
        {
            // Limpiar solo la fila con la fecha de test, no las del seed
            await using var conn = new SqlConnection(TestConnectionString);
            await conn.OpenAsync();
            await conn.ExecuteAsync("ALTER TABLE dbo.IngresosBrutos SET (SYSTEM_VERSIONING = OFF)");
            await conn.ExecuteAsync(
                "DELETE FROM dbo.IngresosBrutos_History WHERE Provincia = 'Formosa' AND VigenciaDesde = '2030-01-01'");
            await conn.ExecuteAsync(
                "DELETE FROM dbo.IngresosBrutos WHERE Provincia = 'Formosa' AND VigenciaDesde = '2030-01-01'");
            await conn.ExecuteAsync(
                "ALTER TABLE dbo.IngresosBrutos SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.IngresosBrutos_History, HISTORY_RETENTION_PERIOD = 10 YEARS))");
        }
    }

    /// <summary>PATCH /iibb/{id} con "alicuota" en body → 409 inmutable_usar_nueva_version.</summary>
    [Fact]
    public async Task PatchIibb_WithAlicuotaInBody_Returns409Inmutable()
    {
        const string provincia = "Jujuy";
        const string vigenciaDesde = "2030-02-01";
        var token = await GetAdminTokenAsync();
        try
        {
            var id = await CreateIngresosBrutosAsync(provincia, "IIBB Jujuy Test", 1.5m, vigenciaDesde, token);

            using var req = BuildRawRequest(
                HttpMethod.Patch,
                $"{IibbEndpoint}/{id}",
                """{"alicuota":3.0}""",
                token);
            var resp = await _client.SendAsync(req);

            Assert.Equal(HttpStatusCode.Conflict, resp.StatusCode);
            var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
            Assert.Equal("inmutable_usar_nueva_version", json.GetProperty("error").GetString());
        }
        finally
        {
            await using var conn = new SqlConnection(TestConnectionString);
            await conn.OpenAsync();
            await conn.ExecuteAsync("ALTER TABLE dbo.IngresosBrutos SET (SYSTEM_VERSIONING = OFF)");
            await conn.ExecuteAsync(
                "DELETE FROM dbo.IngresosBrutos_History WHERE Provincia = 'Jujuy' AND VigenciaDesde = '2030-02-01'");
            await conn.ExecuteAsync(
                "DELETE FROM dbo.IngresosBrutos WHERE Provincia = 'Jujuy' AND VigenciaDesde = '2030-02-01'");
            await conn.ExecuteAsync(
                "ALTER TABLE dbo.IngresosBrutos SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.IngresosBrutos_History, HISTORY_RETENTION_PERIOD = 10 YEARS))");
        }
    }

    /// <summary>GET /iibb/{id} inexistente → 404 ingresos_brutos_not_found.</summary>
    [Fact]
    public async Task GetIibbById_NotFound_Returns404()
    {
        var token = await GetAdminTokenAsync();
        using var req = BuildRequest(HttpMethod.Get, $"{IibbEndpoint}/999999", bearerToken: token);
        var resp = await _client.SendAsync(req);

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

    /// <summary>GET /iibb con admin → 200 con paged result.</summary>
    [Fact]
    public async Task GetIibb_WithAdmin_Returns200PagedResult()
    {
        var token = await GetAdminTokenAsync();
        using var req = BuildRequest(HttpMethod.Get, IibbEndpoint, 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'");
    }

    // ── UDT-011: DateOnly serialization format ────────────────────────────────

    /// <summary>
    /// [UDT-011 REQ-BE-JSON-001] POST /iva → vigenciaDesde en respuesta debe ser
    /// "yyyy-MM-dd" (e.g. "2025-01-01"), no "2025-01-01T00:00:00" ni con sufijo "Z".
    /// Valida que DateOnlyJsonConverter está activo en el pipeline de controllers.
    /// </summary>
    [Fact]
    public async Task CreateIva_VigenciaDesde_SerializesAsDateOnlyString()
    {
        const string codigo = "IVA_9999";
        var token = await GetAdminTokenAsync();
        try
        {
            using var req = BuildRequest(HttpMethod.Post, IvaEndpoint, new
            {
                codigo,
                descripcion = "IVA DateOnly Format Test",
                porcentaje = 5.0m,
                aplicaIVA = true,
                vigenciaDesde = "2025-01-01"
            }, token);
            var resp = await _client.SendAsync(req);

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

            // Read raw JSON string to inspect format (not deserialized)
            var rawJson = await resp.Content.ReadAsStringAsync();

            // vigenciaDesde MUST be "2025-01-01" — short date format
            Assert.Contains("\"2025-01-01\"", rawJson);

            // Must NOT contain datetime format or UTC suffix
            Assert.DoesNotContain("T00:00:00", rawJson);
            Assert.DoesNotContain("\"2025-01-01Z\"", rawJson);
        }
        finally
        {
            await DeleteTipoDeIvaByCodigoAsync(codigo);
        }
    }
}
