using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using Dapper;
using FluentAssertions;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using SIGCM2.Application.Abstractions.Security;
using SIGCM2.Application.Audit;
using SIGCM2.Domain.Entities;
using SIGCM2.TestSupport;
using Xunit;

namespace SIGCM2.Api.Tests.Products;

/// <summary>
/// PRD-003 — Batch 7 / T7.4: Audit failure e2e test.
///
/// Verifies that when IAuditLogger.LogAsync throws, the TransactionScope rolls back:
///   - dbo.ProductPrices row is NOT inserted (fail-closed).
///   - dbo.AuditEvent row is NOT created.
///   - HTTP response is 500 (unhandled exception propagates through ExceptionFilter).
///
/// Uses CreateClientWithOverrides (pattern from issue #36) to inject a throwing IAuditLogger
/// mock without touching the shared factory.
///
/// DB: SIGCM2_Test_Api (ApiIntegration collection).
/// </summary>
[Collection("ApiIntegration")]
public sealed class ProductPricesAuditFailureTests : IAsyncLifetime
{
    private const string ConnectionString = TestConnectionStrings.ApiTestDb;

    private readonly TestWebAppFactory _factory;
    private int _medioId;
    private int _productTypeId;

    public ProductPricesAuditFailureTests(TestWebAppFactory factory)
    {
        _factory = factory;
    }

    public async Task InitializeAsync()
    {
        await using var conn = new SqlConnection(ConnectionString);
        await conn.OpenAsync();

        _medioId = await conn.QuerySingleAsync<int>(
            "SELECT TOP 1 Id FROM dbo.Medio WHERE Activo = 1 ORDER BY Id");

        var ptId = await conn.QuerySingleOrDefaultAsync<int?>(
            "SELECT TOP 1 Id FROM dbo.ProductType WHERE IsActive = 1 ORDER BY Id");
        if (ptId is null)
        {
            ptId = await conn.QuerySingleAsync<int>("""
                INSERT INTO dbo.ProductType (Nombre, HasDuration, RequiresText, RequiresCategory, IsBundle, AllowImages, IsActive, FechaCreacion)
                VALUES ('PT_AuditFail_Test', 0, 0, 0, 0, 0, 1, SYSUTCDATETIME());
                SELECT CAST(SCOPE_IDENTITY() AS INT);
                """);
        }
        _productTypeId = ptId.Value;
    }

    public Task DisposeAsync() => Task.CompletedTask;

    // ── T7.4 — Audit failure rolls back the ProductPrice insert ──────────────

    [Fact]
    public async Task PostPrice_WhenAuditLoggerThrows_Returns500AndRollsBackInsert()
    {
        // Arrange: seed a unique product
        var productId = await SeedProductAsync();

        // Count rows before the failing POST
        await using var conn = new SqlConnection(ConnectionString);
        await conn.OpenAsync();

        var priceCountBefore = await conn.ExecuteScalarAsync<int>(
            "SELECT COUNT(1) FROM dbo.ProductPrices WHERE ProductId = @ProductId",
            new { ProductId = productId });

        var auditCountBefore = await conn.ExecuteScalarAsync<int>(
            "SELECT COUNT(1) FROM dbo.AuditEvent WHERE Action = 'product_price.created' AND TargetType = 'ProductPrice'");

        // Build a mock IAuditLogger that throws on LogAsync
        var throwingAuditLogger = Substitute.For<IAuditLogger>();
        throwingAuditLogger
            .LogAsync(
                Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
                Arg.Any<object?>(), Arg.Any<CancellationToken>())
            .ThrowsAsync(new InvalidOperationException("audit down — simulated failure"));

        // Use CreateClientWithOverrides to inject the mock without touching the shared factory
        using var client = _factory.CreateClientWithOverrides(services =>
        {
            services.RemoveAll<IAuditLogger>();
            services.AddScoped<IAuditLogger>(_ => throwingAuditLogger);
        });

        // Generate admin token (use factory services, not child)
        var jwt = _factory.Services.GetRequiredService<IJwtService>();
        var token = jwt.GenerateAccessToken(new Usuario(
            id: 1, username: "admin", passwordHash: "x",
            nombre: "Admin", apellido: "Sys", email: null,
            rol: "admin", permisosJson: """{"grant":[],"deny":[]}""", activo: true));

        var pvf = DateOnly.FromDateTime(DateTime.UtcNow).AddDays(1).ToString("yyyy-MM-dd");

        var req = new HttpRequestMessage(HttpMethod.Post, $"/api/v1/admin/products/{productId}/prices");
        req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
        req.Content = JsonContent.Create(new { price = 999m, priceValidFrom = pvf });

        // Act
        var resp = await client.SendAsync(req);

        // Assert HTTP: audit failure → unhandled exception → 500
        resp.StatusCode.Should().Be(HttpStatusCode.InternalServerError,
            because: "IAuditLogger throwing must propagate as 500 (fail-closed)");

        // Assert DB: NO new ProductPrices row was persisted (TransactionScope rolled back)
        await using var verifyConn = new SqlConnection(ConnectionString);
        await verifyConn.OpenAsync();

        var priceCountAfter = await verifyConn.ExecuteScalarAsync<int>(
            "SELECT COUNT(1) FROM dbo.ProductPrices WHERE ProductId = @ProductId",
            new { ProductId = productId });

        priceCountAfter.Should().Be(priceCountBefore,
            because: "TransactionScope must roll back the ProductPrices INSERT when audit fails");

        // Assert DB: NO new AuditEvent row for this product
        var auditCountAfter = await verifyConn.ExecuteScalarAsync<int>(
            "SELECT COUNT(1) FROM dbo.AuditEvent WHERE Action = 'product_price.created' AND TargetType = 'ProductPrice'");

        auditCountAfter.Should().Be(auditCountBefore,
            because: "no AuditEvent row must be created when the audit logger throws");
    }

    // ── Helper: seed an active product ───────────────────────────────────────

    private async Task<int> SeedProductAsync()
    {
        await using var conn = new SqlConnection(ConnectionString);
        await conn.OpenAsync();
        var nombre = $"PP_AF_{Guid.NewGuid():N}"[..35];
        return await conn.QuerySingleAsync<int>("""
            INSERT INTO dbo.Product (Nombre, MedioId, ProductTypeId, BasePrice, PriceDurationDays, IsActive, FechaCreacion)
            VALUES (@Nombre, @MedioId, @PtId, 100.00, NULL, 1, SYSUTCDATETIME());
            SELECT CAST(SCOPE_IDENTITY() AS INT);
            """,
            new { Nombre = nombre, MedioId = _medioId, PtId = _productTypeId });
    }
}
