using System.Net.Http.Json;
using System.Text.Json;
using FluentAssertions;
using foodmarket.IntegrationTests.Support;
using Xunit;
namespace foodmarket.IntegrationTests;
[Collection(ApiCollection.Name)]
public class StockReportTests
{
private readonly ApiFactory _factory;
public StockReportTests(ApiFactory factory) => _factory = factory;
private static string RandomBarcode()
=> string.Concat(Enumerable.Range(0, 13).Select(_ => Random.Shared.Next(0, 10)));
/// Реконструкция остатков на «сейчас» совпадает с текущим Stock.
[Fact]
public async Task Today_matches_current_stock()
{
var api = new ApiActor(_factory.CreateClient());
await api.SignupAndLoginAsync($"strpt-{Guid.NewGuid():N}");
var refs = await api.LoadRefsAsync();
var p1 = await api.CreateProductAsync(refs, $"P-{Guid.NewGuid():N}", 100m, RandomBarcode());
var e = await api.Http.PostAsJsonAsync("/api/inventory/enters", new {
date = DateTime.UtcNow, storeId = refs.StoreId, currencyId = refs.CurrencyId,
notes = "seed", lines = new[] { new { productId = p1, quantity = 7m, unitCost = 30m } } });
e.EnsureSuccessStatusCode();
var eid = (await e.Content.ReadFromJsonAsync()).GetProperty("id").GetString();
(await api.Http.PostAsJsonAsync($"/api/inventory/enters/{eid}/post", new { })).EnsureSuccessStatusCode();
var rep = await api.GetJsonAsync("/api/reports/stock");
var rows = rep.EnumerateArray().ToList();
var r = rows.First(x => x.GetProperty("productId").GetString() == p1);
r.GetProperty("quantity").GetDecimal().Should().Be(7m);
r.GetProperty("cost").GetDecimal().Should().Be(30m);
r.GetProperty("value").GetDecimal().Should().Be(210m);
}
/// Запрос на дату до первой операции — пусто.
[Fact]
public async Task Date_before_first_movement_returns_empty()
{
var api = new ApiActor(_factory.CreateClient());
await api.SignupAndLoginAsync($"strpt-pre-{Guid.NewGuid():N}");
var refs = await api.LoadRefsAsync();
var p1 = await api.CreateProductAsync(refs, $"P-{Guid.NewGuid():N}", 100m, RandomBarcode());
// Принять товар прямо сейчас.
var e = await api.Http.PostAsJsonAsync("/api/inventory/enters", new {
date = DateTime.UtcNow, storeId = refs.StoreId, currencyId = refs.CurrencyId,
notes = "seed", lines = new[] { new { productId = p1, quantity = 3m, unitCost = 10m } } });
e.EnsureSuccessStatusCode();
var eid = (await e.Content.ReadFromJsonAsync()).GetProperty("id").GetString();
(await api.Http.PostAsJsonAsync($"/api/inventory/enters/{eid}/post", new { })).EnsureSuccessStatusCode();
// Запрос на год назад — ничего.
var pastDate = DateTime.UtcNow.AddYears(-1).ToString("o");
var rep = await api.GetJsonAsync($"/api/reports/stock?date={Uri.EscapeDataString(pastDate)}");
var rows = rep.EnumerateArray().ToList();
rows.Should().NotContain(x => x.GetProperty("productId").GetString() == p1);
}
/// Дата в будущем — отчёт = текущий остаток.
[Fact]
public async Task Future_date_equals_current()
{
var api = new ApiActor(_factory.CreateClient());
await api.SignupAndLoginAsync($"strpt-fut-{Guid.NewGuid():N}");
var refs = await api.LoadRefsAsync();
var p1 = await api.CreateProductAsync(refs, $"P-{Guid.NewGuid():N}", 100m, RandomBarcode());
var e = await api.Http.PostAsJsonAsync("/api/inventory/enters", new {
date = DateTime.UtcNow, storeId = refs.StoreId, currencyId = refs.CurrencyId,
notes = "seed", lines = new[] { new { productId = p1, quantity = 5m, unitCost = 20m } } });
e.EnsureSuccessStatusCode();
var eid = (await e.Content.ReadFromJsonAsync()).GetProperty("id").GetString();
(await api.Http.PostAsJsonAsync($"/api/inventory/enters/{eid}/post", new { })).EnsureSuccessStatusCode();
var future = DateTime.UtcNow.AddYears(1).ToString("o");
var rep = await api.GetJsonAsync($"/api/reports/stock?date={Uri.EscapeDataString(future)}");
var r = rep.EnumerateArray().First(x => x.GetProperty("productId").GetString() == p1);
r.GetProperty("quantity").GetDecimal().Should().Be(5m);
}
/// Снимок «вчера» — без учёта сегодняшней операции.
[Fact]
public async Task Date_before_operation_excludes_it()
{
var api = new ApiActor(_factory.CreateClient());
await api.SignupAndLoginAsync($"strpt-bef-{Guid.NewGuid():N}");
var refs = await api.LoadRefsAsync();
var p1 = await api.CreateProductAsync(refs, $"P-{Guid.NewGuid():N}", 100m, RandomBarcode());
// Принять с future-датой (так контроллер использует входящую дату).
var date = DateTime.UtcNow.AddDays(2);
var e = await api.Http.PostAsJsonAsync("/api/inventory/enters", new {
date, storeId = refs.StoreId, currencyId = refs.CurrencyId,
notes = "future-supply", lines = new[] { new { productId = p1, quantity = 4m, unitCost = 10m } } });
e.EnsureSuccessStatusCode();
var eid = (await e.Content.ReadFromJsonAsync()).GetProperty("id").GetString();
(await api.Http.PostAsJsonAsync($"/api/inventory/enters/{eid}/post", new { })).EnsureSuccessStatusCode();
// На сейчас (раньше OccurredAt = date) — товара ещё нет.
var now = DateTime.UtcNow.ToString("o");
var rep = await api.GetJsonAsync($"/api/reports/stock?date={Uri.EscapeDataString(now)}");
var rows = rep.EnumerateArray().ToList();
rows.Should().NotContain(x => x.GetProperty("productId").GetString() == p1);
// На дату ПОСЛЕ операции — товар появился.
var after = date.AddDays(1).ToString("o");
var rep2 = await api.GetJsonAsync($"/api/reports/stock?date={Uri.EscapeDataString(after)}");
var r = rep2.EnumerateArray().First(x => x.GetProperty("productId").GetString() == p1);
r.GetProperty("quantity").GetDecimal().Should().Be(4m);
}
[Fact]
public async Task Tenant_isolation_stock_report()
{
var a = new ApiActor(_factory.CreateClient());
var b = new ApiActor(_factory.CreateClient());
await a.SignupAndLoginAsync($"strpt-iso-a-{Guid.NewGuid():N}");
await b.SignupAndLoginAsync($"strpt-iso-b-{Guid.NewGuid():N}");
var refsA = await a.LoadRefsAsync();
var pA = await a.CreateProductAsync(refsA, $"PA-{Guid.NewGuid():N}", 100m, RandomBarcode());
var e = await a.Http.PostAsJsonAsync("/api/inventory/enters", new {
date = DateTime.UtcNow, storeId = refsA.StoreId, currencyId = refsA.CurrencyId,
notes = "seed", lines = new[] { new { productId = pA, quantity = 1m, unitCost = 5m } } });
e.EnsureSuccessStatusCode();
var eid = (await e.Content.ReadFromJsonAsync()).GetProperty("id").GetString();
(await a.Http.PostAsJsonAsync($"/api/inventory/enters/{eid}/post", new { })).EnsureSuccessStatusCode();
var repB = await b.GetJsonAsync("/api/reports/stock");
repB.EnumerateArray().Should().NotContain(x => x.GetProperty("productId").GetString() == pA);
}
}