using System.Threading;
using BigRegister.Api.Data;
using BigRegister.Domain.Applications;
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 . "Build
/// test data through the same door production code uses" — itself is now
/// the closed Concept/Submitted/Decided union, so this builder no longer needs
/// to mirror production's guards (step-index bounds, "Afwijzen needs a toelichting") by hand —
/// it just calls the real nested constructors/required members, which enforce them. A call that
/// would build an illegal Aanvraag (e.g. deciding a still-Concept aanvraag, or an Afwijzen with
/// no toelichting) is refused the same way production refuses it: a still-Concept aanvraag has
/// no .Decided(...) to call in the first place, and a missing toelichting is a runtime
/// guard identical to ApplicationStore.RecordBesluit's own. Start at
/// .
///
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 .
/// Bounds are 's OWN constructor's to enforce, not this
/// builder's — an out-of-range pair fails at , the same
/// production throws, not a guard restated here.
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 a fixture built this way can never hit the
/// null-forgiving derefs the earlier flat Aanvraag needed (there's nothing to force any
/// more: both are required, non-null members of ).
public SubmittedAanvraag Submitted(bool autoApprovable = false) => new(_type, _owner, autoApprovable);
public Aanvraag.Concept Build() => new(_stepIndex, _stepCount)
{
Id = Guid.NewGuid().ToString(),
Type = _type,
Owner = _owner,
DocumentIds = Array.Empty(),
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 .
public sealed class SubmittedAanvraag
{
private static int _referentieSeq;
private readonly string _type;
private readonly string _owner;
private readonly bool _autoApprovable;
private readonly string _referentie;
private readonly DateTimeOffset _submittedAt;
private string? _zaakUrl;
internal SubmittedAanvraag(string type, string owner, bool autoApprovable)
{
_type = type;
_owner = owner;
_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;
}
/// Registers this aanvraag's already-known OpenZaak zaak URL — mirrors
/// , the one production writer of this field, so a
/// fixture that needs a pre-existing zaak doesn't reach past Build() to mutate the
/// result by hand.
public SubmittedAanvraag WithZaakUrl(string zaakUrl)
{
_zaakUrl = zaakUrl;
return this;
}
/// Records a behandelaar's decision. Unlike the earlier builder, there is no
/// hand-written toelichting guard mirroring BeoordelingRules.RequiresToelichting any
/// more — /
/// simply have a `required string Toelichting` member; the null-coalescing throw below is the
/// one place a null has to turn into an exception (this method's own parameter is still the
/// nullable string? a wire request would carry), same failure production's own
/// ApplicationStore.RecordBesluit raises for the identical input.
public DecidedAanvraag Decided(Besluit besluit, string? toelichting = null) => new(BuildDecided(besluit, toelichting));
private Aanvraag.Decided BuildDecided(Besluit besluit, string? toelichting)
{
var (id, createdAt) = (Guid.NewGuid().ToString(), _submittedAt);
return besluit switch
{
Besluit.Goedkeuren => new Aanvraag.Decided.Goedgekeurd
{
Id = id,
Type = _type,
Owner = _owner,
DocumentIds = Array.Empty(),
CreatedAt = createdAt,
UpdatedAt = createdAt,
ZaakUrl = _zaakUrl,
Referentie = _referentie,
SubmittedAt = _submittedAt,
},
Besluit.Afwijzen => new Aanvraag.Decided.Afgewezen
{
Id = id,
Type = _type,
Owner = _owner,
DocumentIds = Array.Empty(),
CreatedAt = createdAt,
UpdatedAt = createdAt,
ZaakUrl = _zaakUrl,
Referentie = _referentie,
SubmittedAt = _submittedAt,
Toelichting = toelichting ?? throw new ArgumentException("Afwijzen requires a toelichting.", nameof(toelichting)),
},
Besluit.MeerInfoOpvragen => new Aanvraag.Decided.MeerInfoGevraagd
{
Id = id,
Type = _type,
Owner = _owner,
DocumentIds = Array.Empty(),
CreatedAt = createdAt,
UpdatedAt = createdAt,
ZaakUrl = _zaakUrl,
Referentie = _referentie,
SubmittedAt = _submittedAt,
Toelichting = toelichting ?? throw new ArgumentException("MeerInfoOpvragen requires a toelichting.", nameof(toelichting)),
},
_ => throw new ArgumentOutOfRangeException(nameof(besluit), besluit, "Unknown besluit."),
};
}
public Aanvraag.Submitted Build() => new()
{
Id = Guid.NewGuid().ToString(),
Type = _type,
Owner = _owner,
DocumentIds = Array.Empty(),
CreatedAt = _submittedAt,
UpdatedAt = _submittedAt,
ZaakUrl = _zaakUrl,
Referentie = _referentie,
SubmittedAt = _submittedAt,
AutoApprovable = _autoApprovable,
};
}
/// A submitted aanvraag with a behandelaar's decision already recorded — terminal in
/// the builder too, 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. Just a one-line
/// wrapper around the already-fully-built value — construction moved
/// all the actual construction (and its invariant enforcement) into
/// itself, so there's nothing left for this type to do
/// except keep .Decided(...).Build() a valid two-call chain for the existing test
/// suite.
public sealed class DecidedAanvraag(Aanvraag.Decided value)
{
public Aanvraag.Decided Build() => value;
}