using OpenNest.Data; namespace OpenNest.Tests.Data; /// /// An in-memory drawing set server: revisions, If-Match conflicts, archive, a search over /// name, customer and notes, and switches that lose or fail the next create. Linked into /// OpenNest.WinForms.Tests as well. /// internal sealed class InMemoryDrawingSets : IDrawingSetStore { private readonly Dictionary> _revisions = new(); public List Creates { get; } = new(); public List<(Guid Id, int ExpectedRevision, DrawingSetRecord Record)> Updates { get; } = new(); public List Queries { get; } = new(); public bool LoseNextCreateReply { get; set; } public bool FailNextCreate { get; set; } public Exception? QueryFailure { get; set; } public IEnumerable Current => _revisions.Values.Select(r => r[^1]); public Task CreateAsync(DrawingSetRecord record, CancellationToken cancellationToken = default) { Creates.Add(record); if (FailNextCreate) { FailNextCreate = false; throw new IOException("Synthetic connection refused."); } if (_revisions.TryGetValue(record.Id, out var existing)) throw new DrawingSetSaveConflictException(existing[^1]); var stored = Store(record, record.Id, 1, false); _revisions[record.Id] = new List { stored }; if (LoseNextCreateReply) { LoseNextCreateReply = false; throw new IOException("Synthetic reply lost."); } return Task.FromResult(stored); } public Task UpdateAsync(Guid id, DrawingSetRecord record, int expectedRevision, CancellationToken cancellationToken = default) { Updates.Add((id, expectedRevision, record)); var revisions = _revisions[id]; var current = revisions[^1]; if (current.Revision != expectedRevision || current.Archived) throw new DrawingSetSaveConflictException(current); var stored = Store(record, id, current.Revision + 1, false); revisions.Add(stored); return Task.FromResult(stored); } public Task ArchiveAsync(Guid id, int expectedRevision, CancellationToken cancellationToken = default) => SetArchived(id, expectedRevision, true); public Task RestoreAsync(Guid id, int expectedRevision, CancellationToken cancellationToken = default) => SetArchived(id, expectedRevision, false); public Task GetAsync(Guid id, CancellationToken cancellationToken = default) => Task.FromResult(_revisions.TryGetValue(id, out var revisions) ? revisions[^1] : null); public Task QueryAsync(DrawingSetQuery query, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); Queries.Add(query); if (QueryFailure != null) throw QueryFailure; var matches = Current .Where(r => query.IncludeArchived || !r.Archived) .Where(r => (r.Name + " " + r.Customer + " " + r.Notes).Contains(query.NormalizedSearch, StringComparison.OrdinalIgnoreCase)) .OrderBy(r => r.Name, StringComparer.OrdinalIgnoreCase) .ToList(); return Task.FromResult(new DrawingSetPage { Items = matches.Skip(query.Offset).Take(query.Limit).Select(Listed).ToList(), Total = matches.Count, Offset = query.Offset, Limit = query.Limit, }); } public Task GetRevisionsAsync(Guid id, int offset = 0, int limit = 100, CancellationToken cancellationToken = default) { if (!_revisions.TryGetValue(id, out var revisions)) return Task.FromResult(null); var newest = Enumerable.Reverse(revisions).ToList(); return Task.FromResult(new DrawingSetPage { Items = newest.Skip(offset).Take(limit).Select(Listed).ToList(), Total = newest.Count, Offset = offset, Limit = limit, }); } public Task GetRevisionAsync(Guid id, int revision, CancellationToken cancellationToken = default) => Task.FromResult(_revisions.TryGetValue(id, out var revisions) ? revisions.FirstOrDefault(r => r.Revision == revision) : null); public Task GetSetsContainingAsync(Guid drawingId, int offset = 0, int limit = 100, CancellationToken cancellationToken = default) { var matches = Current.Where(r => r.Members.Any(m => m.DrawingId == drawingId)).OrderBy(r => r.Name).ToList(); return Task.FromResult(new DrawingSetPage { Items = matches.Skip(offset).Take(limit).Select(Listed).ToList(), Total = matches.Count, Offset = offset, Limit = limit, }); } /// Stored releases, oldest first; tests may add ones with joined nest metadata. public List Releases { get; } = new(); public int PostAttempts { get; private set; } public bool FailNextPost { get; set; } /// Plays a server from before drawings released on their own. public bool WithoutDrawingReleases { get; set; } /// Library drawing names joined onto stored releases, by id. public Dictionary DrawingNames { get; } = new(); public Task PostReleaseAsync(SetReleaseRecord release, CancellationToken cancellationToken = default) { PostAttempts++; if (FailNextPost) { FailNextPost = false; throw new IOException("Synthetic release post failure."); } if (Releases.FirstOrDefault(r => r.Id == release.Id) is { } existing) throw new SetReleasePostConflictException(existing); var stored = new SetReleaseRecord { Id = release.Id, Sets = release.Sets.Select(s => new SetReleaseLine { SetId = s.SetId, SetRevision = s.SetRevision, Quantity = s.Quantity, SetName = _revisions[s.SetId].First(r => r.Revision == s.SetRevision).Name, }).ToList(), Drawings = release.Drawings.Select(d => new SetReleaseDrawing { DrawingId = d.DrawingId, Quantity = d.Quantity, DrawingName = DrawingNames.TryGetValue(d.DrawingId, out var name) ? name : "", }).ToList(), Notes = release.Notes, ReleasedBy = release.ReleasedBy, ReleasedAt = new DateTime(2026, 10, 20, 15, 0, 0, DateTimeKind.Utc), Nests = release.Nests.ToList(), }; Releases.Add(stored); return Task.FromResult(stored); } public Task GetReleaseAsync(Guid id, CancellationToken cancellationToken = default) => Task.FromResult(Releases.FirstOrDefault(r => r.Id == id)); public Task QueryReleasesAsync(Guid? setId = null, Guid? nestId = null, Guid? drawingId = null, int offset = 0, int limit = 100, CancellationToken cancellationToken = default) { if (drawingId is not null && WithoutDrawingReleases) throw new ServerTooOldException("releasing library drawings on their own"); var newest = Enumerable.Reverse(Releases .Where(r => setId is null || r.Sets.Any(s => s.SetId == setId)) .Where(r => nestId is null || r.Nests.Any(n => n.NestId == nestId)) .Where(r => drawingId is null || r.Drawings.Any(d => d.DrawingId == drawingId)) .ToList()).ToList(); return Task.FromResult(new SetReleasePage { Items = newest.Skip(offset).Take(limit).ToList(), Total = newest.Count, Offset = offset, Limit = limit, }); } public Task EnsureDrawingReleasesSupportedAsync(CancellationToken cancellationToken = default) => WithoutDrawingReleases ? Task.FromException(new ServerTooOldException("releasing library drawings on their own")) : Task.CompletedTask; /// Every undo request, in order. public List<(Guid Id, SetReleaseUndoRequest Request)> UndoRequests { get; } = new(); /// Answers an undo; this store holds no nests, so a test decides what the server says. public Func? UndoHandler { get; set; } public Task UndoReleaseAsync(Guid id, SetReleaseUndoRequest request, CancellationToken cancellationToken = default) { UndoRequests.Add((id, new SetReleaseUndoRequest { UndoneBy = request.UndoneBy, SkipNests = request.SkipNests.ToList() })); return UndoHandler is { } handler ? Task.FromResult(handler(id, request)) : throw new NotSupportedException("The in-memory store does not hold nests, so it cannot undo a release."); } private Task SetArchived(Guid id, int expectedRevision, bool archived) { var revisions = _revisions[id]; var current = revisions[^1]; if (current.Revision != expectedRevision || current.Archived == archived) throw new DrawingSetSaveConflictException(current); var stored = Store(current, id, current.Revision + 1, archived); revisions.Add(stored); return Task.FromResult(stored); } private static DrawingSetRecord Store(DrawingSetRecord record, Guid id, int revision, bool archived) => new() { Id = id, Revision = revision, Name = record.Name, Customer = record.Customer, Notes = record.Notes, SavedBy = record.SavedBy, Archived = archived, SavedAt = new DateTime(2026, 10, 20, 12, 0, 0, DateTimeKind.Utc).AddMinutes(revision), MemberCount = record.Members.Count, Members = record.Members.Select(m => new DrawingSetMember { DrawingId = m.DrawingId, Quantity = m.Quantity, Drawing = m.Drawing }).ToList(), }; private static DrawingSetRecord Listed(DrawingSetRecord record) => new() { Id = record.Id, Revision = record.Revision, Name = record.Name, Customer = record.Customer, Notes = record.Notes, SavedBy = record.SavedBy, Archived = record.Archived, SavedAt = record.SavedAt, MemberCount = record.MemberCount, }; }