using System.IO.Compression;
using System.Text;
using FluentAssertions;
using foodmarket.Api.Infrastructure.Invoices;
using Xunit;
namespace foodmarket.UnitTests;
/// Табличная читалка (xlsx + csv) + :
/// порт шаблона накладной из sklad (handoff §7). Генерим xlsx руками (zip + XML),
/// как и в scripts/make_supplier_template.py — так тестируется и наша
/// собственная читалка, и формат, который сдаём поставщикам.
public class TableReaderTests
{
private const string M = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
private static string WriteXlsx(IEnumerable> rows)
{
var tmp = Path.Combine(Path.GetTempPath(), $"fm-tr-{Guid.NewGuid():N}.xlsx");
using var z = new ZipArchive(File.Create(tmp), ZipArchiveMode.Create);
void Put(string name, string content)
{
var e = z.CreateEntry(name);
using var w = new StreamWriter(e.Open(), new UTF8Encoding(false));
w.Write(content);
}
Put("[Content_Types].xml", $@"
");
Put("_rels/.rels", $@"
");
Put("xl/_rels/workbook.xml.rels", $@"
");
Put("xl/workbook.xml", $@"
");
var sb = new StringBuilder();
sb.Append($@"");
int r = 1;
foreach (var row in rows)
{
sb.Append($"");
int c = 0;
foreach (var v in row)
{
var ref1 = $"{(char)('A' + c)}{r}";
if (v is null) { c++; continue; }
if (v is string s)
sb.Append($"{System.Security.SecurityElement.Escape(s)}");
else
sb.Append($"{v}");
c++;
}
sb.Append("
");
r++;
}
sb.Append("");
Put("xl/worksheets/sheet1.xml", sb.ToString());
return tmp;
}
[Fact]
public void Reads_xlsx_cells()
{
var tmp = WriteXlsx(new object?[][]
{
new object?[] { "Штрихкод", "Наименование товара", "Количество", "Цена за единицу" },
new object?[] { "4870001234560", "Молоко 3.2%", 10, 450 },
});
try
{
var rows = TableReader.Read(tmp);
rows[0][0].Should().Be("Штрихкод");
rows[1][2].Should().Be("10");
}
finally { File.Delete(tmp); }
}
[Fact]
public void Parses_table_invoice_with_header_rows()
{
var tmp = WriteXlsx(new object?[][]
{
new object?[] { "Поставщик", "ТОО Пример" },
new object?[] { "Номер накладной", "7777" },
new object?[] { "Дата", "2026-10-05" },
new object?[] { },
new object?[] { "Штрихкод", "Наименование товара", "Количество", "Ед. изм.", "Цена за единицу", "Сумма" },
new object?[] { "4870001234560", "Молоко 3.2%", 10, "шт", 450, 4500 },
new object?[] { "4607031440148", "Хлеб серый", 20, "шт", 180, 3600 },
});
try
{
var inv = TableInvoiceParser.Parse(tmp, "накладная.xlsx");
inv.SupplierName.Should().Be("ТОО Пример");
inv.InvoiceNumber.Should().Be("7777");
inv.InvoiceDate!.Value.Date.Should().Be(new DateTime(2026, 10, 5));
inv.Lines.Should().HaveCount(2);
inv.Lines[0].Barcode.Should().Be("4870001234560");
inv.Lines[0].Quantity.Should().Be(10);
inv.Lines[0].Price.Should().Be(450);
inv.Lines[0].Sum.Should().Be(4500);
inv.Source.Should().Be("excel");
}
finally { File.Delete(tmp); }
}
[Fact]
public void Parses_csv_utf8_with_semicolons()
{
var tmp = Path.Combine(Path.GetTempPath(), $"fm-{Guid.NewGuid():N}.csv");
File.WriteAllText(tmp,
"Штрихкод;Наименование;Количество;Цена\n" +
"4870001234560;Молоко 3.2%;10;450\n", new UTF8Encoding(true)); // BOM
try
{
var inv = TableInvoiceParser.Parse(tmp, "накладная.csv");
inv.Lines.Should().HaveCount(1);
inv.Lines[0].Barcode.Should().Be("4870001234560");
inv.Lines[0].Name.Should().Be("Молоко 3.2%");
}
finally { File.Delete(tmp); }
}
[Fact]
public void Parses_csv_windows1251()
{
// Excel в русской локали сохраняет CSV в 1251 без BOM — читалка должна
// определить и переключиться на 1251.
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
var enc = Encoding.GetEncoding(1251);
var tmp = Path.Combine(Path.GetTempPath(), $"fm-{Guid.NewGuid():N}.csv");
File.WriteAllBytes(tmp, enc.GetBytes(
"Штрихкод;Наименование;Количество;Цена\n" +
"4870001234560;Молоко 3.2%;10;450\n"));
try
{
var inv = TableInvoiceParser.Parse(tmp, "накладная.csv");
inv.Lines.Should().HaveCount(1);
inv.Lines[0].Name.Should().Be("Молоко 3.2%");
}
finally { File.Delete(tmp); }
}
[Fact]
public void Xls_old_format_throws_friendly()
{
var tmp = Path.Combine(Path.GetTempPath(), $"fm-{Guid.NewGuid():N}.xls");
File.WriteAllText(tmp, "stub");
try
{
FluentActions.Invoking(() => TableReader.Read(tmp, "накладная.xls"))
.Should().Throw()
.WithMessage("*сохраните файл как .xlsx*");
}
finally { File.Delete(tmp); }
}
}