using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using SIGCM2.TestSupport;

namespace SIGCM2.Api.Tests.Products;

/// <summary>
/// PRD-002 — Integration tests for /api/v1/products and /api/v1/admin/products.
/// Read endpoints require authentication (any role).
/// Write endpoints require permission 'catalogo:productos:gestionar'.
/// Verifies HTTP status codes, response shapes, and ExceptionFilter mappings.
/// </summary>
[Collection("ApiIntegration")]
public sealed class ProductsControllerTests : IAsyncLifetime
{
    private const string ReadEndpoint = "/api/v1/products";
    private const string AdminEndpoint = "/api/v1/admin/products";
    private const string AdminUsername = "admin";
    private const string AdminPassword = "@Diego550@";

    private readonly HttpClient _client;

    public ProductsControllerTests(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 async Task<(int MedioId, int ProductTypeId)> EnsureMedioAndProductTypeAsync(string token)
    {
        // Create a Medio via SQL (we don't have a Medio controller endpoint available here)
        // Use product-types endpoint to create a ProductType and insert Medio directly
        var medioResp = await _client.SendAsync(BuildRequest(HttpMethod.Post, "/api/v1/admin/medios", new
        {
            codigo = $"PM{Guid.NewGuid():N}"[..6],
            nombre = $"Medio Test {Guid.NewGuid():N}"[..30],
            tipo = 1
        }, token));
        medioResp.EnsureSuccessStatusCode();
        var medioJson = await medioResp.Content.ReadFromJsonAsync<JsonElement>();
        var medioId = medioJson.GetProperty("id").GetInt32();

        var ptResp = await _client.SendAsync(BuildRequest(HttpMethod.Post, "/api/v1/admin/product-types", new
        {
            nombre = $"PT_{Guid.NewGuid():N}"[..30],
            hasDuration = false, requiresText = false, requiresCategory = false, isBundle = false,
            allowImages = false
        }, token));
        ptResp.EnsureSuccessStatusCode();
        var ptJson = await ptResp.Content.ReadFromJsonAsync<JsonElement>();
        var productTypeId = ptJson.GetProperty("id").GetInt32();

        return (medioId, productTypeId);
    }

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

    [Fact]
    public async Task List_WithoutAuth_Returns401()
    {
        using var req = BuildRequest(HttpMethod.Get, ReadEndpoint);
        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 guards on WRITE endpoints ──────────────────────────────────────────

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

    [Fact]
    public async Task Update_WithoutAuth_Returns401()
    {
        using var req = BuildRequest(HttpMethod.Put, $"{AdminEndpoint}/1", new { nombre = "Test", basePrice = 10m });
        var resp = await _client.SendAsync(req);
        Assert.Equal(HttpStatusCode.Unauthorized, resp.StatusCode);
    }

    [Fact]
    public async Task Deactivate_WithoutAuth_Returns401()
    {
        using var req = BuildRequest(HttpMethod.Delete, $"{AdminEndpoint}/1");
        var resp = await _client.SendAsync(req);
        Assert.Equal(HttpStatusCode.Unauthorized, resp.StatusCode);
    }

    // ── POST /api/v1/admin/products ───────────────────────────────────────────

    [Fact]
    public async Task Create_WithAdmin_Returns201WithId()
    {
        var token = await GetAdminTokenAsync();
        var (medioId, productTypeId) = await EnsureMedioAndProductTypeAsync(token);
        var uniqueName = $"Prod_{Guid.NewGuid():N}"[..30];

        using var req = BuildRequest(HttpMethod.Post, AdminEndpoint, new
        {
            nombre = uniqueName,
            medioId,
            productTypeId,
            basePrice = 100.50m
        }, 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(uniqueName, json.GetProperty("nombre").GetString());
        Assert.True(json.GetProperty("isActive").GetBoolean());
    }

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

        using var req = BuildRequest(HttpMethod.Post, AdminEndpoint, new
        {
            nombre = string.Empty,  // invalid
            medioId = 1,
            productTypeId = 1,
            basePrice = 10m
        }, token);

        var resp = await _client.SendAsync(req);

        Assert.Equal(HttpStatusCode.BadRequest, resp.StatusCode);
    }

    [Fact]
    public async Task Create_DuplicateNombre_Returns409()
    {
        var token = await GetAdminTokenAsync();
        var (medioId, productTypeId) = await EnsureMedioAndProductTypeAsync(token);
        var uniqueName = $"Dup_{Guid.NewGuid():N}"[..30];

        var body = new { nombre = uniqueName, medioId, productTypeId, basePrice = 50m };

        using var req1 = BuildRequest(HttpMethod.Post, AdminEndpoint, body, token);
        var resp1 = await _client.SendAsync(req1);
        Assert.Equal(HttpStatusCode.Created, resp1.StatusCode);

        using var req2 = BuildRequest(HttpMethod.Post, AdminEndpoint, body, token);
        var resp2 = await _client.SendAsync(req2);

        Assert.Equal(HttpStatusCode.Conflict, resp2.StatusCode);
    }

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

        using var req = BuildRequest(HttpMethod.Post, AdminEndpoint, new
        {
            nombre = $"Prod_{Guid.NewGuid():N}"[..30],
            medioId = 999999,
            productTypeId = 1,
            basePrice = 50m
        }, token);

        var resp = await _client.SendAsync(req);

        Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode);
    }

    // ── GET /api/v1/products ──────────────────────────────────────────────────

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

        using var req = BuildRequest(HttpMethod.Get, $"{ReadEndpoint}?page=1&pageSize=10", 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 _));
        Assert.True(json.TryGetProperty("total", out _));
    }

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

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

    [Fact]
    public async Task GetById_ExistingId_Returns200()
    {
        var token = await GetAdminTokenAsync();
        var (medioId, productTypeId) = await EnsureMedioAndProductTypeAsync(token);
        var uniqueName = $"GetById_{Guid.NewGuid():N}"[..28];

        using var createReq = BuildRequest(HttpMethod.Post, AdminEndpoint, new
        {
            nombre = uniqueName, medioId, productTypeId, basePrice = 75m
        }, token);
        var createResp = await _client.SendAsync(createReq);
        Assert.Equal(HttpStatusCode.Created, createResp.StatusCode);

        var createJson = await createResp.Content.ReadFromJsonAsync<JsonElement>();
        var productId = createJson.GetProperty("id").GetInt32();

        using var getReq = BuildRequest(HttpMethod.Get, $"{ReadEndpoint}/{productId}", bearerToken: token);
        var getResp = await _client.SendAsync(getReq);

        Assert.Equal(HttpStatusCode.OK, getResp.StatusCode);
        var getJson = await getResp.Content.ReadFromJsonAsync<JsonElement>();
        Assert.Equal(productId, getJson.GetProperty("id").GetInt32());
        Assert.Equal(uniqueName, getJson.GetProperty("nombre").GetString());
    }

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

    [Fact]
    public async Task Deactivate_ExistingId_Returns204()
    {
        var token = await GetAdminTokenAsync();
        var (medioId, productTypeId) = await EnsureMedioAndProductTypeAsync(token);
        var uniqueName = $"Del_{Guid.NewGuid():N}"[..28];

        using var createReq = BuildRequest(HttpMethod.Post, AdminEndpoint, new
        {
            nombre = uniqueName, medioId, productTypeId, basePrice = 50m
        }, token);
        var createResp = await _client.SendAsync(createReq);
        Assert.Equal(HttpStatusCode.Created, createResp.StatusCode);

        var createJson = await createResp.Content.ReadFromJsonAsync<JsonElement>();
        var productId = createJson.GetProperty("id").GetInt32();

        using var delReq = BuildRequest(HttpMethod.Delete, $"{AdminEndpoint}/{productId}", bearerToken: token);
        var delResp = await _client.SendAsync(delReq);

        Assert.Equal(HttpStatusCode.NoContent, delResp.StatusCode);
    }

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

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

    [Fact]
    public async Task Update_ExistingProduct_Returns200()
    {
        var token = await GetAdminTokenAsync();
        var (medioId, productTypeId) = await EnsureMedioAndProductTypeAsync(token);
        var uniqueName = $"Upd_{Guid.NewGuid():N}"[..28];

        using var createReq = BuildRequest(HttpMethod.Post, AdminEndpoint, new
        {
            nombre = uniqueName, medioId, productTypeId, basePrice = 50m
        }, token);
        var createResp = await _client.SendAsync(createReq);
        Assert.Equal(HttpStatusCode.Created, createResp.StatusCode);

        var createJson = await createResp.Content.ReadFromJsonAsync<JsonElement>();
        var productId = createJson.GetProperty("id").GetInt32();

        var newName = $"Upd2_{Guid.NewGuid():N}"[..28];
        using var updateReq = BuildRequest(HttpMethod.Put, $"{AdminEndpoint}/{productId}", new
        {
            nombre = newName,
            basePrice = 200m
        }, token);
        var updateResp = await _client.SendAsync(updateReq);

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

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

    // ── PRD-002 W1: ExceptionFilter 409 for ProductTypeInactivo ──────────────

    [Fact]
    public async Task Create_WithInactiveProductType_Returns409()
    {
        var token = await GetAdminTokenAsync();
        var (medioId, productTypeId) = await EnsureMedioAndProductTypeAsync(token);

        // Deactivate the ProductType first
        using var deactivatePtReq = BuildRequest(HttpMethod.Delete, $"/api/v1/admin/product-types/{productTypeId}", bearerToken: token);
        var deactivatePtResp = await _client.SendAsync(deactivatePtReq);
        Assert.Equal(HttpStatusCode.NoContent, deactivatePtResp.StatusCode);

        // Now attempt to create a product with the inactive ProductType
        using var createReq = BuildRequest(HttpMethod.Post, AdminEndpoint, new
        {
            nombre = $"Prod_{Guid.NewGuid():N}"[..28],
            medioId,
            productTypeId,
            basePrice = 50m
        }, token);
        var createResp = await _client.SendAsync(createReq);

        Assert.Equal(HttpStatusCode.Conflict, createResp.StatusCode);
        var body = await createResp.Content.ReadFromJsonAsync<JsonElement>();
        Assert.Equal("product_type_inactivo", body.GetProperty("error").GetString());
    }
}
