using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using FluentAssertions;
using foodmarket.Api.Infrastructure.Invoices;
using foodmarket.Domain.Purchases;
using foodmarket.Infrastructure.Persistence;
using foodmarket.IntegrationTests.Support;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace foodmarket.IntegrationTests;
/// Sprint 31 — приёмка по фото. Проверяем pipeline без реального
/// Claude CLI: подставляем парсированный
/// напрямую через InvoiceProcessor.BuildMatcherAsync, т.е. тестим
/// matcher-on-real-catalog + alias upsert + apply → Supply Draft.
[Collection(ApiCollection.Name)]
public class InvoicePipelineTests
{
private readonly ApiFactory _factory;
public InvoicePipelineTests(ApiFactory factory) => _factory = factory;
[Fact]
public async Task Matcher_hits_product_by_barcode_from_real_catalog()
{
using var scope = _factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService();
// Берём любую org из seed'ов (Demo Market или первую не-архивную)
var demoOrg = await db.Organizations.IgnoreQueryFilters()
.FirstAsync(o => !o.IsArchived && o.Name == "Demo Market");
// Нам нужен товар + штрихкод в этой org. Если нет — создадим.
var (productId, barcode) = await EnsureProductWithBarcodeAsync(db, demoOrg.Id);
var matcher = await InvoiceProcessor.BuildMatcherAsync(db, demoOrg.Id, "ТОО Поставщик", CancellationToken.None);
var line = new InvoiceLine { Barcode = barcode, Name = "Что угодно" };
matcher.Match(line, "тоо поставщик");
line.MatchedBy.Should().Be("barcode");
line.ProductId.Should().Be(productId);
}
[Fact]
public async Task Multi_tenant_isolation_matcher_does_not_see_other_orgs_barcodes()
{
using var scope = _factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService();
var orgA = await db.Organizations.IgnoreQueryFilters()
.FirstAsync(o => !o.IsArchived && o.Name == "Demo Market");
var orgB = await db.Organizations.IgnoreQueryFilters()
.Where(o => !o.IsArchived && o.Id != orgA.Id).FirstOrDefaultAsync();
if (orgB is null)
{
// создадим вторую org для теста изоляции
orgB = new foodmarket.Domain.Organizations.Organization
{
Name = $"InvOrgB-{Guid.NewGuid():N}", CountryCode = "KZ",
Bin = "999999999999",
};
db.Organizations.Add(orgB);
await db.SaveChangesAsync();
}
var (_, bcA) = await EnsureProductWithBarcodeAsync(db, orgA.Id);
// Matcher для orgB — не должен находить barcode из orgA.
var matcher = await InvoiceProcessor.BuildMatcherAsync(db, orgB.Id, null, CancellationToken.None);
var line = new InvoiceLine { Barcode = bcA, Name = "X" };
matcher.Match(line, "any");
line.MatchedBy.Should().Be("none");
}
private static async Task<(Guid productId, string barcode)> EnsureProductWithBarcodeAsync(
AppDbContext db, Guid orgId)
{
var existing = await (from pb in db.ProductBarcodes.IgnoreQueryFilters()
where pb.OrganizationId == orgId
select new { pb.ProductId, pb.Code }).FirstOrDefaultAsync();
if (existing is not null) return (existing.ProductId, existing.Code);
// Создаём минимальный товар + штрихкод (идемпотентный barcode).
var unit = await db.UnitsOfMeasure.IgnoreQueryFilters()
.FirstAsync(u => u.OrganizationId == null && u.Code == "796");
var group = await db.ProductGroups.IgnoreQueryFilters()
.FirstAsync(g => g.OrganizationId == orgId && g.IsSystem);
var p = new foodmarket.Domain.Catalog.Product
{
OrganizationId = orgId,
Name = $"ТестТовар-{Guid.NewGuid():N}",
UnitOfMeasureId = unit.Id,
ProductGroupId = group.Id,
Vat = 12m,
};
db.Products.Add(p);
var bc = $"4870{Random.Shared.Next(100000000, 999999999)}";
db.ProductBarcodes.Add(new foodmarket.Domain.Catalog.ProductBarcode
{
OrganizationId = orgId, ProductId = p.Id, Code = bc,
Type = foodmarket.Domain.Catalog.BarcodeType.Ean13, IsPrimary = true,
});
await db.SaveChangesAsync();
return (p.Id, bc);
}
}