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

/// <summary>
/// CAT-001 — Integration tests for /api/v1/rubros and /api/v1/admin/rubros.
/// Read endpoints require authentication (any role).
/// Write endpoints require permission 'catalogo:rubros:gestionar'.
/// Verifies audit events after each mutating operation.
/// </summary>
[Collection("ApiIntegration")]
public sealed class RubrosControllerTests : IAsyncLifetime
{
    private const string TestConnectionString = TestConnectionStrings.ApiTestDb;

    private const string ReadEndpoint = "/api/v1/rubros";
    private const string AdminEndpoint = "/api/v1/admin/rubros";
    private const string AdminUsername = "admin";
    private const string AdminPassword = "@Diego550@";

    private readonly HttpClient _client;

    public RubrosControllerTests(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;
    }

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

    private static async Task DeleteRubroIfExistsAsync(int id)
    {
        await using var conn = new SqlConnection(TestConnectionString);
        await conn.OpenAsync();

        // Need to disable system versioning to delete from history + main table
        await conn.ExecuteAsync("ALTER TABLE dbo.Rubro SET (SYSTEM_VERSIONING = OFF)");
        await conn.ExecuteAsync("DELETE FROM dbo.Rubro_History WHERE Id = @Id", new { Id = id });
        // Delete children first (recursive), then the target
        await conn.ExecuteAsync("""
            WITH ToDelete AS (
                SELECT Id FROM dbo.Rubro WHERE Id = @Id
                UNION ALL
                SELECT r.Id FROM dbo.Rubro r INNER JOIN ToDelete t ON r.ParentId = t.Id
            )
            DELETE r FROM dbo.Rubro r INNER JOIN ToDelete td ON r.Id = td.Id
            """, new { Id = id });
        await conn.ExecuteAsync(
            "ALTER TABLE dbo.Rubro SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.Rubro_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 });
    }

    // ── 401 / 403 guards on READ endpoints ────────────────────────────────────

    [Fact]
    public async Task GetTree_WithoutAuth_Returns401()
    {
        using var req = BuildRequest(HttpMethod.Get, $"{ReadEndpoint}/tree");
        var resp = await _client.SendAsync(req);
        Assert.Equal(HttpStatusCode.Unauthorized, resp.StatusCode);
    }

    [Fact]
    public async Task GetById_WithoutAuth_Returns401()
    {
        using var req = BuildRequest(HttpMethod.Get, $"{ReadEndpoint}/999999");
        var resp = await _client.SendAsync(req);
        Assert.Equal(HttpStatusCode.Unauthorized, resp.StatusCode);
    }

    // ── 401 / 403 guards on WRITE endpoints ───────────────────────────────────

    [Fact]
    public async Task CreateRubro_WithoutAuth_Returns401()
    {
        using var req = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "Test", parentId = (int?)null });
        var resp = await _client.SendAsync(req);
        Assert.Equal(HttpStatusCode.Unauthorized, resp.StatusCode);
    }

    [Fact]
    public async Task CreateRubro_WithCajeroRole_Returns403()
    {
        const string username = "cat001_rubro_cajero_403";
        try
        {
            var token = await GetCajeroTokenAsync(username);
            using var req = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "Test403", parentId = (int?)null }, token);
            var resp = await _client.SendAsync(req);
            Assert.Equal(HttpStatusCode.Forbidden, resp.StatusCode);
        }
        finally
        {
            await DeleteUsuarioIfExistsAsync(username);
        }
    }

    // ── GET /api/v1/rubros/tree ────────────────────────────────────────────────

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

        // Create a root rubro for the tree
        using var createReq = BuildRequest(HttpMethod.Post, AdminEndpoint, new
        {
            nombre = "TreeRoot_GetTree",
            parentId = (int?)null,
            tarifarioBaseId = (int?)null
        }, token);
        var createResp = await _client.SendAsync(createReq);
        Assert.Equal(HttpStatusCode.Created, createResp.StatusCode);
        var created = await createResp.Content.ReadFromJsonAsync<JsonElement>();
        var rootId = created.GetProperty("id").GetInt32();

        try
        {
            using var req = BuildRequest(HttpMethod.Get, $"{ReadEndpoint}/tree", bearerToken: token);
            var resp = await _client.SendAsync(req);

            Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
            var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
            Assert.Equal(JsonValueKind.Array, json.ValueKind);
            // Should contain our created root
            var nombres = json.EnumerateArray().Select(n => n.GetProperty("nombre").GetString()).ToList();
            Assert.Contains("TreeRoot_GetTree", nombres);
        }
        finally
        {
            await DeleteRubroIfExistsAsync(rootId);
        }
    }

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

        // Create then deactivate a rubro
        using var createReq = BuildRequest(HttpMethod.Post, AdminEndpoint, new
        {
            nombre = "RubroInactivo_GetTree",
            parentId = (int?)null,
        }, token);
        var createResp = await _client.SendAsync(createReq);
        Assert.Equal(HttpStatusCode.Created, createResp.StatusCode);
        var created = await createResp.Content.ReadFromJsonAsync<JsonElement>();
        var rubroId = created.GetProperty("id").GetInt32();

        try
        {
            // Deactivate it
            using var deleteReq = BuildRequest(HttpMethod.Delete, $"{AdminEndpoint}/{rubroId}", bearerToken: token);
            await _client.SendAsync(deleteReq);

            // Without incluirInactivos → should not appear
            using var req1 = BuildRequest(HttpMethod.Get, $"{ReadEndpoint}/tree", bearerToken: token);
            var resp1 = await _client.SendAsync(req1);
            var json1 = await resp1.Content.ReadFromJsonAsync<JsonElement>();
            var nombres1 = json1.EnumerateArray().Select(n => n.GetProperty("nombre").GetString()).ToList();
            Assert.DoesNotContain("RubroInactivo_GetTree", nombres1);

            // With incluirInactivos=true → should appear
            using var req2 = BuildRequest(HttpMethod.Get, $"{ReadEndpoint}/tree?incluirInactivos=true", bearerToken: token);
            var resp2 = await _client.SendAsync(req2);
            var json2 = await resp2.Content.ReadFromJsonAsync<JsonElement>();
            var nombres2 = json2.EnumerateArray().Select(n => n.GetProperty("nombre").GetString()).ToList();
            Assert.Contains("RubroInactivo_GetTree", nombres2);
        }
        finally
        {
            await DeleteRubroIfExistsAsync(rubroId);
        }
    }

    // ── GET /api/v1/rubros/{id} ────────────────────────────────────────────────

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

        using var createReq = BuildRequest(HttpMethod.Post, AdminEndpoint, new
        {
            nombre = "RubroGetById",
            parentId = (int?)null,
        }, token);
        var createResp = await _client.SendAsync(createReq);
        Assert.Equal(HttpStatusCode.Created, createResp.StatusCode);
        var created = await createResp.Content.ReadFromJsonAsync<JsonElement>();
        var rubroId = created.GetProperty("id").GetInt32();

        try
        {
            using var req = BuildRequest(HttpMethod.Get, $"{ReadEndpoint}/{rubroId}", bearerToken: token);
            var resp = await _client.SendAsync(req);

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

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

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

    // ── POST /api/v1/admin/rubros ──────────────────────────────────────────────

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

        using var req = BuildRequest(HttpMethod.Post, AdminEndpoint, new
        {
            nombre = "RubroCreate201",
            parentId = (int?)null,
            tarifarioBaseId = (int?)null
        }, 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>();
        var id = json.GetProperty("id").GetInt32();
        Assert.True(id > 0);
        Assert.Equal("RubroCreate201", json.GetProperty("nombre").GetString());
        Assert.True(json.GetProperty("activo").GetBoolean());

        try
        {
            var auditCount = await CountAuditEventsAsync("rubro.created", "Rubro", id.ToString());
            Assert.Equal(1, auditCount);
        }
        finally
        {
            await DeleteRubroIfExistsAsync(id);
        }
    }

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

        using var parentReq = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "ParentCreate", parentId = (int?)null }, token);
        var parentResp = await _client.SendAsync(parentReq);
        Assert.Equal(HttpStatusCode.Created, parentResp.StatusCode);
        var parentJson = await parentResp.Content.ReadFromJsonAsync<JsonElement>();
        var parentId = parentJson.GetProperty("id").GetInt32();

        try
        {
            using var childReq = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "ChildCreate", parentId }, token);
            var childResp = await _client.SendAsync(childReq);

            Assert.Equal(HttpStatusCode.Created, childResp.StatusCode);
            var childJson = await childResp.Content.ReadFromJsonAsync<JsonElement>();
            Assert.Equal(parentId, childJson.GetProperty("parentId").GetInt32());
        }
        finally
        {
            await DeleteRubroIfExistsAsync(parentId);
        }
    }

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

        using var parentReq = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "ParentDup409", parentId = (int?)null }, token);
        var parentResp = await _client.SendAsync(parentReq);
        Assert.Equal(HttpStatusCode.Created, parentResp.StatusCode);
        var parentJson = await parentResp.Content.ReadFromJsonAsync<JsonElement>();
        var parentId = parentJson.GetProperty("id").GetInt32();

        try
        {
            // First child
            using var child1 = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "Duplicado", parentId }, token);
            var r1 = await _client.SendAsync(child1);
            Assert.Equal(HttpStatusCode.Created, r1.StatusCode);

            // Second child with same nombre
            using var child2 = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "Duplicado", parentId }, token);
            var r2 = await _client.SendAsync(child2);
            Assert.Equal(HttpStatusCode.Conflict, r2.StatusCode);
            var json = await r2.Content.ReadFromJsonAsync<JsonElement>();
            Assert.Equal("rubro_nombre_duplicado", json.GetProperty("error").GetString());
        }
        finally
        {
            await DeleteRubroIfExistsAsync(parentId);
        }
    }

    [Fact]
    public async Task CreateRubro_ParentNotFound_Returns404()
    {
        var token = await GetAdminTokenAsync();
        using var req = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "OrphanChild", parentId = 999999 }, token);
        var resp = await _client.SendAsync(req);
        Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode);
    }

    // ── PUT /api/v1/admin/rubros/{id} ─────────────────────────────────────────

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

        using var createReq = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "OriginalNombre", parentId = (int?)null }, token);
        var createResp = await _client.SendAsync(createReq);
        Assert.Equal(HttpStatusCode.Created, createResp.StatusCode);
        var created = await createResp.Content.ReadFromJsonAsync<JsonElement>();
        var id = created.GetProperty("id").GetInt32();

        try
        {
            using var updateReq = BuildRequest(HttpMethod.Put, $"{AdminEndpoint}/{id}", new { nombre = "NombreActualizado" }, token);
            var updateResp = await _client.SendAsync(updateReq);

            Assert.Equal(HttpStatusCode.OK, updateResp.StatusCode);
            var updated = await updateResp.Content.ReadFromJsonAsync<JsonElement>();
            Assert.Equal("NombreActualizado", updated.GetProperty("nombre").GetString());

            var auditCount = await CountAuditEventsAsync("rubro.updated", "Rubro", id.ToString());
            Assert.Equal(1, auditCount);

            // Verify Rubro_History row
            await using var conn = new SqlConnection(TestConnectionString);
            await conn.OpenAsync();
            var histCount = await conn.QuerySingleAsync<int>(
                "SELECT COUNT(*) FROM dbo.Rubro_History WHERE Id = @Id", new { Id = id });
            Assert.True(histCount >= 1, "Should have ≥1 row in Rubro_History after update");
        }
        finally
        {
            await DeleteRubroIfExistsAsync(id);
        }
    }

    [Fact]
    public async Task UpdateRubro_NotFound_Returns404()
    {
        var token = await GetAdminTokenAsync();
        using var req = BuildRequest(HttpMethod.Put, $"{AdminEndpoint}/999999", new { nombre = "Test" }, token);
        var resp = await _client.SendAsync(req);
        Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode);
    }

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

        using var parent = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "ParentUpdate409", parentId = (int?)null }, token);
        var parentResp = await _client.SendAsync(parent);
        var parentJson = await parentResp.Content.ReadFromJsonAsync<JsonElement>();
        var parentId = parentJson.GetProperty("id").GetInt32();

        try
        {
            using var c1 = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "Sibling1", parentId }, token);
            var r1 = await _client.SendAsync(c1);
            var j1 = await r1.Content.ReadFromJsonAsync<JsonElement>();
            var id1 = j1.GetProperty("id").GetInt32();

            using var c2 = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "Sibling2", parentId }, token);
            var r2 = await _client.SendAsync(c2);
            var j2 = await r2.Content.ReadFromJsonAsync<JsonElement>();
            // Try to rename Sibling1 → Sibling2 (conflict)
            using var updateReq = BuildRequest(HttpMethod.Put, $"{AdminEndpoint}/{id1}", new { nombre = "Sibling2" }, token);
            var updateResp = await _client.SendAsync(updateReq);
            Assert.Equal(HttpStatusCode.Conflict, updateResp.StatusCode);
        }
        finally
        {
            await DeleteRubroIfExistsAsync(parentId);
        }
    }

    // ── DELETE /api/v1/admin/rubros/{id} ──────────────────────────────────────

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

        using var createReq = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "RubroToDelete", parentId = (int?)null }, token);
        var createResp = await _client.SendAsync(createReq);
        Assert.Equal(HttpStatusCode.Created, createResp.StatusCode);
        var created = await createResp.Content.ReadFromJsonAsync<JsonElement>();
        var id = created.GetProperty("id").GetInt32();

        try
        {
            using var deleteReq = BuildRequest(HttpMethod.Delete, $"{AdminEndpoint}/{id}", bearerToken: token);
            var deleteResp = await _client.SendAsync(deleteReq);

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

            // Verify audit event (handler uses "rubro.deleted")
            var auditCount = await CountAuditEventsAsync("rubro.deleted", "Rubro", id.ToString());
            Assert.Equal(1, auditCount);
        }
        finally
        {
            await DeleteRubroIfExistsAsync(id);
        }
    }

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

        using var parentReq = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "ParentWithChildren", parentId = (int?)null }, token);
        var parentResp = await _client.SendAsync(parentReq);
        var parentJson = await parentResp.Content.ReadFromJsonAsync<JsonElement>();
        var parentId = parentJson.GetProperty("id").GetInt32();

        try
        {
            // Add a child
            using var childReq = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "ChildActive", parentId }, token);
            await _client.SendAsync(childReq);

            // Try to delete parent (has active children → 409)
            using var deleteReq = BuildRequest(HttpMethod.Delete, $"{AdminEndpoint}/{parentId}", bearerToken: token);
            var deleteResp = await _client.SendAsync(deleteReq);

            Assert.Equal(HttpStatusCode.Conflict, deleteResp.StatusCode);
            var json = await deleteResp.Content.ReadFromJsonAsync<JsonElement>();
            Assert.Equal("rubro_tiene_hijos_activos", json.GetProperty("error").GetString());
        }
        finally
        {
            await DeleteRubroIfExistsAsync(parentId);
        }
    }

    [Fact]
    public async Task DeleteRubro_NotFound_Returns404()
    {
        var token = await GetAdminTokenAsync();
        using var req = BuildRequest(HttpMethod.Delete, $"{AdminEndpoint}/999999", bearerToken: token);
        var resp = await _client.SendAsync(req);
        Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode);
    }

    // ── PATCH /api/v1/admin/rubros/{id}/mover ─────────────────────────────────

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

        using var p1 = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "MoveParent1", parentId = (int?)null }, token);
        var p1Resp = await _client.SendAsync(p1);
        var p1Json = await p1Resp.Content.ReadFromJsonAsync<JsonElement>();
        var parent1Id = p1Json.GetProperty("id").GetInt32();

        try
        {
            using var p2 = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "MoveParent2", parentId = (int?)null }, token);
            var p2Resp = await _client.SendAsync(p2);
            var p2Json = await p2Resp.Content.ReadFromJsonAsync<JsonElement>();
            var parent2Id = p2Json.GetProperty("id").GetInt32();

            using var childReq = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "MoveChild", parentId = parent1Id }, token);
            var childResp = await _client.SendAsync(childReq);
            var childJson = await childResp.Content.ReadFromJsonAsync<JsonElement>();
            var childId = childJson.GetProperty("id").GetInt32();

            // Move child from parent1 to parent2
            using var moveReq = BuildRequest(HttpMethod.Patch, $"{AdminEndpoint}/{childId}/mover", new
            {
                nuevoParentId = parent2Id,
                nuevoOrden = 0
            }, token);
            var moveResp = await _client.SendAsync(moveReq);

            Assert.Equal(HttpStatusCode.OK, moveResp.StatusCode);
            var moved = await moveResp.Content.ReadFromJsonAsync<JsonElement>();
            Assert.Equal(parent2Id, moved.GetProperty("parentId").GetInt32());

            var auditCount = await CountAuditEventsAsync("rubro.moved", "Rubro", childId.ToString());
            Assert.Equal(1, auditCount);
        }
        finally
        {
            await DeleteRubroIfExistsAsync(parent1Id);
        }
    }

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

        using var rootReq = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "CycleRoot", parentId = (int?)null }, token);
        var rootResp = await _client.SendAsync(rootReq);
        var rootJson = await rootResp.Content.ReadFromJsonAsync<JsonElement>();
        var rootId = rootJson.GetProperty("id").GetInt32();

        try
        {
            using var childReq = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "CycleChild", parentId = rootId }, token);
            var childResp = await _client.SendAsync(childReq);
            var childJson = await childResp.Content.ReadFromJsonAsync<JsonElement>();
            var childId = childJson.GetProperty("id").GetInt32();

            // Try to move root under its own child → cycle
            using var moveReq = BuildRequest(HttpMethod.Patch, $"{AdminEndpoint}/{rootId}/mover", new
            {
                nuevoParentId = childId,
                nuevoOrden = 0
            }, token);
            var moveResp = await _client.SendAsync(moveReq);

            Assert.Equal(HttpStatusCode.BadRequest, moveResp.StatusCode);
            var json = await moveResp.Content.ReadFromJsonAsync<JsonElement>();
            Assert.Equal("rubro_cycle_detected", json.GetProperty("error").GetString());
        }
        finally
        {
            await DeleteRubroIfExistsAsync(rootId);
        }
    }

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

        using var p1 = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "MoveDupParent1", parentId = (int?)null }, token);
        var p1Resp = await _client.SendAsync(p1);
        var p1Json = await p1Resp.Content.ReadFromJsonAsync<JsonElement>();
        var parent1Id = p1Json.GetProperty("id").GetInt32();

        try
        {
            using var p2 = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "MoveDupParent2", parentId = (int?)null }, token);
            var p2Resp = await _client.SendAsync(p2);
            var p2Json = await p2Resp.Content.ReadFromJsonAsync<JsonElement>();
            var parent2Id = p2Json.GetProperty("id").GetInt32();

            // Add "SameName" under parent1
            using var c1 = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "SameName", parentId = parent1Id }, token);
            var c1Resp = await _client.SendAsync(c1);
            var c1Json = await c1Resp.Content.ReadFromJsonAsync<JsonElement>();
            var c1Id = c1Json.GetProperty("id").GetInt32();

            // Add "SameName" under parent2 already
            using var c2 = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "SameName", parentId = parent2Id }, token);
            await _client.SendAsync(c2);

            // Try to move c1 (SameName) under parent2 → duplicate
            using var moveReq = BuildRequest(HttpMethod.Patch, $"{AdminEndpoint}/{c1Id}/mover", new
            {
                nuevoParentId = parent2Id,
                nuevoOrden = 0
            }, token);
            var moveResp = await _client.SendAsync(moveReq);

            Assert.Equal(HttpStatusCode.Conflict, moveResp.StatusCode);
        }
        finally
        {
            await DeleteRubroIfExistsAsync(parent1Id);
        }
    }
}
