using System.Threading;
using BigRegister.Api.Data;
using BigRegister.Domain.Applications;
using BigRegister.Domain.Beoordeling;
namespace BigRegister.Tests.Builders;
/// Fixture identities test builders share across the suite.
public static class TestIdentities
{
/// The default owner for a builder-made — matches
/// (the demo's only seeded user) so a fixture that
/// doesn't care about identity gets a realistic, elfproef-valid BSN for free.
public const string DemoBsn = DocumentStore.DemoOwner;
}
///
/// Type-state test-data builder for (WP-70). "Build test data through the
/// same door production code uses" — a Concept can only ever become Submitted, and only a
/// Submitted aanvraag can be Decided, so the compiler refuses a fixture built through an illegal
/// path (e.g. deciding a still-Concept aanvraag) instead of that being a runtime assertion nobody
/// wrote. Start at .
///
/// ponytail: itself stays exactly what it always was — a mutable,
/// EF-backed bag with no invariants of its own (that's Data/ApplicationStore.cs's job in
/// production, via its own lock + checks). This builder does not
/// refactor it into an immutable aggregate; it's the one enforced DOOR through which TEST code
/// builds one, so the invariants a real request path enforces don't quietly go missing from a
/// fixture assembled by hand.
///
public static class Given
{
/// A fresh, unsubmitted wizard draft — step 0 of 0 until
/// says otherwise, exactly what ApplicationStore.CreateConcept hands back.
public static ConceptAanvraag Concept(string type = "registratie", string owner = TestIdentities.DemoBsn) =>
new(type, owner);
}
/// A not-yet-submitted aanvraag. The only next step is — there
/// is deliberately no Decided here, since only a submitted aanvraag can be decided.
public sealed class ConceptAanvraag
{
private readonly string _type;
private readonly string _owner;
private int _stepIndex;
private int _stepCount;
internal ConceptAanvraag(string type, string owner)
{
_type = type;
_owner = owner;
}
/// The wizard's current position — step of .
public ConceptAanvraag AtStep(int index, int of)
{
_stepIndex = index;
_stepCount = of;
return this;
}
/// Submits the draft — always assigns a Referentie AND SubmittedAt together (mirrors
/// ApplicationStore.Submit), so Aanvraag.StatusAt's Referentie! is honest
/// for every fixture built this way, never a null-ref waiting to happen.
public SubmittedAanvraag Submitted(bool autoApprovable = false) =>
new(_type, _owner, _stepIndex, _stepCount, autoApprovable);
public Aanvraag Build() => new()
{
Id = Guid.NewGuid().ToString(),
Type = _type,
Owner = _owner,
StepIndex = _stepIndex,
StepCount = _stepCount,
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow,
};
}
/// A submitted aanvraag, open for a behandelaar's decision. The only next step is
/// — there is no way back to ConceptAanvraag.
public sealed class SubmittedAanvraag
{
private static int _referentieSeq;
private readonly string _type;
private readonly string _owner;
private readonly int _stepIndex;
private readonly int _stepCount;
private readonly bool _autoApprovable;
private readonly string _referentie;
private readonly DateTimeOffset _submittedAt;
internal SubmittedAanvraag(string type, string owner, int stepIndex, int stepCount, bool autoApprovable)
{
_type = type;
_owner = owner;
_stepIndex = stepIndex;
_stepCount = stepCount;
_autoApprovable = autoApprovable;
// A plausible reference in SubmissionRules.NewReference's shape ("BIG-2026-" + a number) —
// sequential (not random) so a fixture's value is reproducible across a test run.
_referentie = $"BIG-2026-{Interlocked.Increment(ref _referentieSeq)}";
_submittedAt = DateTimeOffset.UtcNow;
}
/// Records a behandelaar's decision — reusing ,
/// the SAME rule production's besluit endpoint runs, rather than restating it here where it
/// could quietly drift. Throws for an Afwijzen/MeerInfoOpvragen
/// with a null/blank — exactly what that endpoint rejects with
/// a 400, just caught here at fixture-build time instead.
public DecidedAanvraag Decided(Besluit besluit, string? toelichting = null)
{
if (BeoordelingRules.RequiresToelichting(besluit) && string.IsNullOrWhiteSpace(toelichting))
throw new ArgumentException($"{besluit} requires a toelichting.", nameof(toelichting));
return new DecidedAanvraag(this, besluit, toelichting);
}
public Aanvraag Build() => new()
{
Id = Guid.NewGuid().ToString(),
Type = _type,
Owner = _owner,
StepIndex = _stepIndex,
StepCount = _stepCount,
Submitted = true,
Referentie = _referentie,
AutoApprovable = _autoApprovable,
SubmittedAt = _submittedAt,
CreatedAt = _submittedAt,
UpdatedAt = _submittedAt,
};
}
/// A submitted aanvraag with a behandelaar's decision already recorded. Terminal in the
/// builder too — there's nothing past , matching Goedgekeurd/Afgewezen being
/// terminal in the domain (); a fixture that needs a
/// SECOND besluit (the MeerInfoGevraagd "still decidable" case) builds fresh from
/// again, exactly as a real second request would.
public sealed class DecidedAanvraag
{
private readonly SubmittedAanvraag _submitted;
private readonly Besluit _besluit;
private readonly string? _toelichting;
internal DecidedAanvraag(SubmittedAanvraag submitted, Besluit besluit, string? toelichting)
{
_submitted = submitted;
_besluit = besluit;
_toelichting = toelichting;
}
public Aanvraag Build()
{
var aanvraag = _submitted.Build();
aanvraag.BesluitStatus = _besluit;
aanvraag.BesluitToelichting = _toelichting;
return aanvraag;
}
}