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;
///
/// 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).
///
[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(
"SELECT TOP 1 Id FROM dbo.Medio WHERE Activo = 1 ORDER BY Id");
var ptId = await conn.QuerySingleOrDefaultAsync(
"SELECT TOP 1 Id FROM dbo.ProductType WHERE IsActive = 1 ORDER BY Id");
if (ptId is null)
{
ptId = await conn.QuerySingleAsync("""
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(
"SELECT COUNT(1) FROM dbo.ProductPrices WHERE ProductId = @ProductId",
new { ProductId = productId });
var auditCountBefore = await conn.ExecuteScalarAsync(
"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();
throwingAuditLogger
.LogAsync(
Arg.Any(), Arg.Any(), Arg.Any(),
Arg.Any