using OpenNest.Data;
using OpenNest.Data.MaterialLedger;
using OpenNest.Geometry;
namespace OpenNest.Tests.Data;
///
/// Codes other open nests are coded with leave Auto Nest's ledger rows: every copy of a code,
/// with its quantity and location, and rows left empty disappear.
///
public class LedgerCodeFilterTests
{
[Fact]
public void ABundleLosesEveryCopyOfItsCode_AndAnEmptiedRowIsDropped()
{
var bundle = Row("ledger:1", ["B-1", "B-1", "B-1"]);
var sheet = Row("ledger:2", ["S-1"]);
var free = Row("ledger:3", ["S-2"]);
var rows = LedgerCodeFilter.Exclude([bundle, sheet, free], ["B-1", "S-1"], out var removed);
Assert.Equal(["ledger:3"], rows.Select(r => r.Id));
Assert.Same(free, rows[0]);
Assert.Equal(4, removed);
}
[Fact]
public void DropsKeepTheirOtherCodesLocationsAndQuantity()
{
var drops = Row("ledger:1", ["D-1", "D-2", "D-3"], ("D-1", "R1"), ("D-2", "R2"), ("D-3", "R1")) with
{
Rank = 2,
Label = "A36",
};
var row = Assert.Single(LedgerCodeFilter.Exclude([drops], ["D-2", "UNRELATED"], out var removed));
Assert.Equal(1, removed);
Assert.Equal(2, row.Quantity);
Assert.Equal(["D-1", "D-3"], row.ItemCodes);
Assert.Equal(["R1"], row.Locations);
Assert.Equal(new[] { "D-1", "D-3" }, row.ItemLocations.Keys.Order());
Assert.Equal(("ledger:1", 2, "A36"), (row.Id, row.Rank, row.Label));
// The original row is left alone.
Assert.Equal(3, drops.Quantity);
}
[Fact]
public void NoCodesInUse_KeepsEveryRowAsItWas()
{
var rows = new[] { Row("ledger:1", ["S-1"]) };
var kept = LedgerCodeFilter.Exclude(rows, Array.Empty(), out var removed);
Assert.Same(rows[0], Assert.Single(kept));
Assert.Equal(0, removed);
}
[Fact]
public void CodesMatchExactly()
{
var kept = LedgerCodeFilter.Exclude([Row("ledger:1", ["s-1"])], ["S-1"], out var removed);
Assert.Single(kept);
Assert.Equal(0, removed);
}
[Fact]
public void MappingSummary_CountsTheSheetsOnOtherNests()
{
var mapping = LedgerStockMapper.Map(
[
Item("B-1", LedgerItemKinds.FullSheetBundle, 3),
Item("D-1", LedgerItemKinds.Drop, 1),
Item("D-2", LedgerItemKinds.Drop, 1),
], Units.Inches);
var filtered = LedgerCodeFilter.Exclude(mapping, ["B-1", "D-1"]);
Assert.Equal(4, filtered.OnOtherNests);
Assert.Equal("Ledger: 0 sheet sizes, 1 drop (4 on other nests)", filtered.Summary);
Assert.Equal("Ledger: 1 sheet size, 2 drops", mapping.Summary);
}
[Fact]
public void CodesInUse_NameEachNestOnceAndReportWhyTheyAreUnknown()
{
var known = LedgerCodesInUse.Known(
[
Use("S-1", "Nest B"),
Use("S-1", "Nest A"),
Use("S-2", "Nest A"),
]);
Assert.True(known.IsKnown);
Assert.Equal(["S-1", "S-2"], known.Codes.Order());
Assert.Equal(["Nest A", "Nest B"], known.NestsUsing("S-1"));
var unknown = LedgerCodesInUse.Unknown("nests are saved as files");
Assert.False(unknown.IsKnown);
Assert.Empty(unknown.Codes);
}
[Fact]
public async Task ReadingCodes_NeverThrowsForAnExpectedFailure()
{
Assert.Equal("nests are saved as files", (await LedgerCodesInUse.ReadAsync(null, Guid.Empty, default)).Unavailable);
Assert.Equal("the nest server needs an update",
(await LedgerCodesInUse.ReadAsync(new Repository(_ => throw new NotSupportedException()), Guid.Empty, default)).Unavailable);
Assert.Equal("the nest server could not be reached",
(await LedgerCodesInUse.ReadAsync(new Repository(_ => throw new IOException("down")), Guid.Empty, default)).Unavailable);
var slow = new Repository(async token =>
{
await Task.Delay(Timeout.Infinite, token);
return [];
});
Assert.Equal("the nest server did not answer",
(await LedgerCodesInUse.ReadAsync(slow, Guid.Empty, default, TimeSpan.FromMilliseconds(20))).Unavailable);
using var cancel = new CancellationTokenSource();
cancel.Cancel();
await Assert.ThrowsAnyAsync(() => LedgerCodesInUse.ReadAsync(slow, Guid.Empty, cancel.Token));
}
[Fact]
public async Task ReadingCodes_ExcludesTheNestBeingEdited()
{
var editing = Guid.NewGuid();
Guid asked = default;
var repository = new Repository(_ => Task.FromResult>([Use("S-1", "Other")]))
{
OnAsk = id => asked = id,
};
var codes = await LedgerCodesInUse.ReadAsync(repository, editing, default);
Assert.Equal(editing, asked);
Assert.Equal(["S-1"], codes.Codes);
}
internal static NestStockCodeUse Use(string code, string nest) =>
new() { ItemCode = code, NestId = Guid.NewGuid(), NestName = nest, Status = NestStatus.Quote };
private static LedgerStockRow Row(string id, string[] codes, params (string Code, string Location)[] locations) =>
new(id, new Size(48, 96), codes.Length, IsDrop: codes.Distinct().Count() > 1, IsIrregular: false, codes,
locations.Select(l => l.Location).Distinct().ToList())
{
ItemLocations = locations.ToDictionary(l => l.Code, l => l.Location, StringComparer.Ordinal),
};
private static LedgerStockItem Item(string code, string kind, int quantity) =>
new(code, 7, kind, kind == LedgerItemKinds.Drop ? 24 : 48, kind == LedgerItemKinds.Drop ? 36 : 96, quantity,
LedgerItemShapes.Rectangular, "R1", "Rack 1", null, null, DateTime.UtcNow);
private sealed class Repository(Func>> read) : INestRepository
{
public Action? OnAsk { get; init; }
public Task> GetStockCodesInUseAsync(Guid excludeNestId, CancellationToken cancellationToken)
{
OnAsk?.Invoke(excludeNestId);
return read(cancellationToken);
}
public Task> ListAsync(CancellationToken cancellationToken) => throw new NotImplementedException();
public Task QueryAsync(NestQuery query, CancellationToken cancellationToken) => throw new NotImplementedException();
public Task GetMetadataAsync(Guid id, CancellationToken cancellationToken) => throw new NotImplementedException();
public Task GetFileAsync(Guid id, CancellationToken cancellationToken) => throw new NotImplementedException();
public Task UploadAsync(byte[] nestFile, NestRecord record, CancellationToken cancellationToken) => throw new NotImplementedException();
public Task UpdateFileAsync(Guid id, byte[] nestFile, NestRecord record, CancellationToken cancellationToken) => throw new NotImplementedException();
public Task UpdateMetadataAsync(Guid id, NestRecord record, CancellationToken cancellationToken) => throw new NotImplementedException();
public Task DeleteAsync(Guid id, CancellationToken cancellationToken) => throw new NotImplementedException();
}
}