using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging.Abstractions;
using SIGCM2.Api.Filters;
using SIGCM2.Domain.Exceptions;
using SIGCM2.TestSupport;

namespace SIGCM2.Api.Tests.Rubros;

/// <summary>
/// CAT-002 — Regla de Oro Rama vs Hoja.
///
/// Unit tests: ExceptionFilter mapping for new 409 cases (no DB needed).
/// Integration: GET /arbol returns tieneAvisos field per node (stub = false).
///
/// Design note: the 409 guard behavior is fully covered by unit tests in
/// SIGCM2.Application.Tests (CreateRubroCommandHandlerTests, MoveRubroCommandHandlerTests).
/// e2e 409 verification via a separate factory is skipped here because the shared
/// ApiIntegration singleton factory cannot be safely augmented with per-test DI overrides
/// (RSA key singleton issue documented in ApiIntegrationCollection.cs).
/// </summary>
[Collection("ApiIntegration")]
public sealed class RubrosReglaDeOroTests : IAsyncLifetime
{
    private const string AdminEndpoint = "/api/v1/admin/rubros";
    private const string ReadEndpoint = "/api/v1/rubros";
    private const string AdminUsername = "admin";
    private const string AdminPassword = "@Diego550@";

    private readonly HttpClient _client;

    public RubrosReglaDeOroTests(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 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 DeleteRubroIfExistsAsync(int id)
    {
        await using var conn = new Microsoft.Data.SqlClient.SqlConnection(TestConnectionStrings.ApiTestDb);
        await conn.OpenAsync();
        await Dapper.SqlMapper.ExecuteAsync(conn, "ALTER TABLE dbo.Rubro SET (SYSTEM_VERSIONING = OFF)");
        await Dapper.SqlMapper.ExecuteAsync(conn, "DELETE FROM dbo.Rubro_History WHERE Id = @Id", new { Id = id });
        await Dapper.SqlMapper.ExecuteAsync(conn, """
            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 Dapper.SqlMapper.ExecuteAsync(conn,
            "ALTER TABLE dbo.Rubro SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.Rubro_History, HISTORY_RETENTION_PERIOD = 10 YEARS))");
    }

    // ── ExceptionFilter unit tests (no DB, no HTTP) ───────────────────────────

    private static ExceptionContext MakeExceptionContext(Exception exception)
    {
        var httpContext = new DefaultHttpContext();
        var routeData = new Microsoft.AspNetCore.Routing.RouteData();
        var actionDescriptor = new Microsoft.AspNetCore.Mvc.Abstractions.ActionDescriptor();
        var modelState = new Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary();
        var actionContext = new ActionContext(httpContext, routeData, actionDescriptor, modelState);
        return new ExceptionContext(actionContext, new List<IFilterMetadata>())
        {
            Exception = exception
        };
    }

    [Fact]
    public void ExceptionFilter_MapsRubroPadreEsHojaConAvisos_To409()
    {
        var filter = new ExceptionFilter(NullLogger<ExceptionFilter>.Instance);
        var ctx = MakeExceptionContext(new RubroPadreEsHojaConAvisosException(parentId: 1, cantidadAvisos: 3));

        filter.OnException(ctx);

        var result = Assert.IsType<ObjectResult>(ctx.Result);
        Assert.Equal(StatusCodes.Status409Conflict, result.StatusCode);
        var json = System.Text.Json.JsonSerializer.Serialize(result.Value);
        Assert.Contains("rubro_padre_es_hoja_con_avisos", json);
    }

    [Fact]
    public void ExceptionFilter_MapsRubroEsRamaConHijosActivos_To409()
    {
        var filter = new ExceptionFilter(NullLogger<ExceptionFilter>.Instance);
        var ctx = MakeExceptionContext(new RubroEsRamaConHijosActivosException(rubroId: 7, cantidadHijos: 2));

        filter.OnException(ctx);

        var result = Assert.IsType<ObjectResult>(ctx.Result);
        Assert.Equal(StatusCodes.Status409Conflict, result.StatusCode);
        var json = System.Text.Json.JsonSerializer.Serialize(result.Value);
        Assert.Contains("rubro_es_rama_con_hijos_activos", json);
    }

    // ── Integration: GET /arbol includes tieneAvisos field (stub = false) ─────

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

        // Create a root rubro to ensure tree is non-empty
        using var createReq = BuildRequest(HttpMethod.Post, AdminEndpoint, new
        {
            nombre = "TieneAvisosCheck_CAT002",
            parentId = (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>();
            var ourNode = json.EnumerateArray()
                .FirstOrDefault(n => n.GetProperty("id").GetInt32() == rootId);

            Assert.True(ourNode.ValueKind != JsonValueKind.Undefined, "Our rubro must appear in tree");
            Assert.True(ourNode.TryGetProperty("tieneAvisos", out var tieneAvisos),
                "tieneAvisos must be present in every tree node (CAT-002 additive field)");
            Assert.False(tieneAvisos.GetBoolean(),
                "Stub (NullAvisoQueryRepository) must always return false");
        }
        finally
        {
            await DeleteRubroIfExistsAsync(rootId);
        }
    }

    // ── Integration: POST returns 409 message format (guard path) ─────────────
    // NOTE: these tests rely on the unit-tested handler behavior. The 409 is proven by:
    //   - CreateRubroCommandHandlerTests.Handle_ParentTieneAvisos_Throws_RubroPadreEsHojaConAvisosException
    //   - ExceptionFilter_MapsRubroPadreEsHojaConAvisos_To409 (above)
    // The combined e2e 409 test is omitted here because it requires per-factory DI override
    // which conflicts with the shared ApiIntegration RSA singleton pattern.
    // See: ApiIntegrationCollection.cs for the rationale.
}
