using System.Drawing;
using System.Net;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Windows.Forms;
using OpenNest.Data;
using OpenNest.Forms;
using OpenNest.Forms.Commands;
using static OpenNest.WinForms.Tests.FormTesting;
namespace OpenNest.WinForms.Tests.Forms;
///
/// My Server Jobs against an in-memory job server: the rows and what each row allows, Cancel
/// Job, Remove and Open, the status-bar count of results to review, and a Keep refused because
/// another desktop kept the result first.
///
public class ServerJobsFormTests
{
private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web)
{
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) },
};
/// Just enough of /api/nest-jobs for the desktop: list, cancel, remove, keep.
private sealed class FakeServer : HttpMessageHandler
{
public readonly List Jobs = new();
public readonly List Requests = new();
public bool Offline { get; set; }
private int version = 1;
protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (Offline)
throw new HttpRequestException("connection refused");
var path = request.RequestUri!.AbsolutePath.TrimStart('/');
var query = request.RequestUri.Query;
lock (Jobs)
{
Requests.Add($"{request.Method} {path}{query} {request.Headers.IfMatch}".TrimEnd());
if (request.Method == HttpMethod.Get && path == "api/nest-jobs")
{
var etag = $"\"{version}\"";
if (request.Headers.IfNoneMatch.Any(t => t.Tag == etag))
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotModified));
var listed = query.Contains("filter=unreviewed") ? Jobs.Where(j => j.IsUnreviewed) : Jobs;
var response = Answer(HttpStatusCode.OK, listed.ToList());
response.Headers.ETag = new System.Net.Http.Headers.EntityTagHeaderValue(etag);
return Task.FromResult(response);
}
var job = Jobs.FirstOrDefault(j => path.StartsWith($"api/nest-jobs/{j.Id}", StringComparison.Ordinal));
if (job == null)
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound));
version++;
if (request.Method == HttpMethod.Delete)
{
Jobs.Remove(job);
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NoContent));
}
if (path.EndsWith("/cancel", StringComparison.Ordinal))
{
if (job.State == NestJobState.Queued)
job.State = NestJobState.Cancelled;
else
job.CancelRequested = true;
job.Revision++;
return Task.FromResult(Answer(HttpStatusCode.OK, job));
}
if (path.EndsWith("/keep", StringComparison.Ordinal))
{
if (!job.IsUnreviewed)
return Task.FromResult(Answer(HttpStatusCode.Conflict, job));
job.Review = NestJobReview.Kept;
job.Revision++;
return Task.FromResult(Answer(HttpStatusCode.OK, job));
}
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound));
}
}
private static HttpResponseMessage Answer(HttpStatusCode status, object body) =>
new(status) { Content = new StringContent(JsonSerializer.Serialize(body, Json), Encoding.UTF8, "application/json") };
}
private static (FakeServer Server, Func Connect) Server(params NestJobRecord[] jobs)
{
var server = new FakeServer();
server.Jobs.AddRange(jobs);
var http = new HttpClient(server);
return (server, () => new RemoteNestJobs(http, "http://nest-server/"));
}
private static NestJobRecord Ready(string title = "N26-0042") => new()
{
Id = Guid.NewGuid(),
Title = title,
EngineName = "Rectangles",
SubmittedBy = "aj@shop-pc",
State = NestJobState.Succeeded,
SubmittedAt = new DateTime(2026, 11, 2, 13, 0, 0, DateTimeKind.Utc),
FinishedAt = new DateTime(2026, 11, 2, 13, 5, 0, DateTimeKind.Utc),
Revision = 7,
Summary = new NestJobSummary { PartsPlaced = 23, PartsUnplaced = 2, Sheets = 3, Utilization = 0.784 },
};
private static NestJobRecord Queued() => new()
{
Id = Guid.NewGuid(),
Title = "N26-0050",
EngineName = "Default",
SubmittedBy = "aj@other-pc",
State = NestJobState.Queued,
QueuePosition = 1,
Revision = 1,
};
private static NestJobRecord Kept() => new()
{
Id = Guid.NewGuid(),
Title = "N26-0040",
EngineName = "Default",
SubmittedBy = "aj@shop-pc",
State = NestJobState.Succeeded,
Review = NestJobReview.Kept,
ReviewedBy = "bob@office",
Revision = 9,
};
private static string Cell(ServerJobsForm form, Guid id, int column) =>
form.JobsList.Items.Cast().Single(i => ((NestJobRecord)i.Tag).Id == id).SubItems[column].Text;
[Fact]
public void RowsDescribeEachJobAndAllowOnlyWhatItsStateAllows()
{
var ready = Ready();
var queued = Queued();
var kept = Kept();
var (server, connect) = Server(ready, queued, kept);
StaTestThread.Run(() =>
{
using var form = new ServerJobsForm(_ => Task.CompletedTask, connect, "aj");
Show(form);
PumpUntil(() => form.Jobs.Count == 3, "the job list");
Assert.Equal("Ready to review", Cell(form, ready.Id, 1));
Assert.StartsWith("23 parts on 3 sheets, ", Cell(form, ready.Id, 2));
Assert.EndsWith(", 2 not placed", Cell(form, ready.Id, 2));
Assert.Equal("Queued (1 ahead)", Cell(form, queued.Id, 1));
Assert.Equal("Kept", Cell(form, kept.Id, 1));
Assert.Contains("Kept by bob@office", form.JobsList.Items[2].ToolTipText);
Assert.Contains(server.Requests, r => r.StartsWith("GET api/nest-jobs?filter=all", StringComparison.Ordinal) && r.Contains("submittedBy=aj"));
form.Select(ready.Id);
Assert.True(form.OpenButton.Enabled);
Assert.False(form.CancelJobButton.Enabled);
Assert.True(form.RemoveButton.Enabled);
form.Select(queued.Id);
Assert.False(form.OpenButton.Enabled);
Assert.True(form.CancelJobButton.Enabled);
Assert.False(form.RemoveButton.Enabled);
// A kept result can't be committed again; it can only leave the list.
form.Select(kept.Id);
Assert.False(form.OpenButton.Enabled);
Assert.False(form.CancelJobButton.Enabled);
Assert.True(form.RemoveButton.Enabled);
}, TimeSpan.FromSeconds(30), "The STA test did not complete.");
}
[Fact]
public void CancelRemoveAndOpenActOnTheSelectedJob()
{
var ready = Ready();
var queued = Queued();
var (server, connect) = Server(ready, queued);
var opened = new List();
StaTestThread.Run(() =>
{
using var form = new ServerJobsForm(job =>
{
opened.Add(job.Id);
return Task.CompletedTask;
}, connect, "aj");
Show(form);
PumpUntil(() => form.Jobs.Count == 2, "the job list");
form.Select(queued.Id);
form.CancelJobButton.PerformClick();
PumpUntil(() => form.Jobs.Any(j => j.Id == queued.Id && j.State == NestJobState.Cancelled), "the cancelled job");
Assert.Equal("Cancelled", Cell(form, queued.Id, 1));
form.Select(ready.Id);
form.OpenButton.PerformClick();
PumpUntil(() => opened.Count == 1, "the review to open");
Assert.Equal(ready.Id, opened[0]);
// Removing a result nobody kept asks first; declining keeps it.
var asked = 0;
form.Confirm = _ => ++asked > 1;
form.Select(ready.Id);
form.RemoveButton.PerformClick();
Assert.Equal(1, asked);
Assert.Contains(ready, server.Jobs);
form.RemoveButton.PerformClick();
PumpUntil(() => form.Jobs.All(j => j.Id != ready.Id), "the removed job to leave the list");
Assert.Contains(server.Requests, r => r == $"DELETE api/nest-jobs/{ready.Id} \"7\"");
}, TimeSpan.FromSeconds(30), "The STA test did not complete.");
}
[Fact]
public void TheStatusBarCountsResultsWaitingForReview()
{
var (server, connect) = Server(Ready("a"), Ready("b"), Queued(), Kept());
var enabled = true;
StaTestThread.Run(() =>
{
SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());
using var strip = new StatusStrip();
using var status = new ServerJobsStatus(strip, 0, () => { }, SystemFonts.StatusFont!, () => enabled, connect, "aj");
Run(status.RefreshAsync());
Assert.True(status.Label.Available);
Assert.Equal("2 server nests to review", status.Label.Text);
Assert.Contains(server.Requests, r => r.Contains("filter=unreviewed") && r.Contains("submittedBy=aj"));
server.Jobs.RemoveAll(j => j.Title == "a");
Run(status.RefreshAsync());
Assert.Equal("1 server nest to review", status.Label.Text);
server.Offline = true;
Run(status.RefreshAsync());
Assert.False(status.Label.Available);
server.Offline = false;
enabled = false;
Run(status.RefreshAsync());
Assert.False(status.Label.Available);
}, TimeSpan.FromSeconds(30), "The STA test did not complete.");
static void Run(Task task) => PumpUntil(() => task.IsCompleted, "the status refresh");
}
[Fact]
public async Task AKeepIsRefusedWhenAnotherDesktopKeptTheResultFirst()
{
var job = Ready();
var (server, connect) = Server(job);
var seen = JsonSerializer.Deserialize(JsonSerializer.Serialize(job, Json), Json)!;
var warnings = new List();
Assert.True(await new ServerNestDecision(seen, connect) { Warn = warnings.Add }.KeepAsync(null));
Assert.Empty(warnings);
lock (server.Jobs)
server.Jobs[0].ReviewedBy = "aj@shop-pc";
// A second desktop that reviewed the same result is told, and commits nothing.
Assert.False(await new ServerNestDecision(seen, connect) { Warn = warnings.Add }.KeepAsync(null));
Assert.Contains("already kept by aj@shop-pc", Assert.Single(warnings));
Assert.Contains("Nothing was added", warnings[0]);
// A job the server no longer has is refused too.
server.Jobs.Clear();
Assert.False(await new ServerNestDecision(seen, connect) { Warn = warnings.Add }.KeepAsync(null));
Assert.Contains("no longer has this job", warnings[1]);
}
[Fact]
public void JobTextNamesQueuePlacesAndReviews()
{
Assert.Equal("Queued (next)", ServerJobText.State(new NestJobRecord { State = NestJobState.Queued }));
Assert.Equal("Stopping", ServerJobText.State(new NestJobRecord { State = NestJobState.Running, CancelRequested = true }));
Assert.Equal("Discarded", ServerJobText.State(new NestJobRecord { State = NestJobState.Succeeded, Review = NestJobReview.Discarded }));
Assert.Equal("Cancelled", ServerJobText.State(new NestJobRecord { State = NestJobState.Cancelled }));
Assert.Equal("out of memory", ServerJobText.Result(new NestJobRecord { State = NestJobState.Failed, Error = "out of memory" }));
Assert.Equal("", ServerJobText.Result(new NestJobRecord { State = NestJobState.Succeeded }));
Assert.Equal("(unnamed nest)", ServerJobText.Title(new NestJobRecord()));
Assert.Null(ServerJobText.Unreviewed(0));
Assert.Equal("1 server nest to review", ServerJobText.Unreviewed(1));
}
}