using System.Net;
using System.Net.Http.Json;
using BigRegister.Api.Contracts;
using BigRegister.Api.Data;
using BigRegister.Domain.Applications;
using BigRegister.Tests.Builders;
namespace BigRegister.Tests.Acceptance;
///
/// Behaviour-level tests for the scholing-threshold enforcement (WP-69) over both live HTTP
/// paths — POST /applications/{id}/submit (the wizard's real path) and the legacy
/// POST /intakes (dead from the UI, still a live crafted-POST surface). Built through
/// the type-state builder, mirroring
/// rather than the full wizard/upload dance — the builder's default owner IS
/// 's default caller,
/// so no header juggling.
///
public class IntakeSubmissionTests(TestWebApplicationFactory factory) : IClassFixture
{
private readonly HttpClient _client = factory.CreateClient();
private static void Persist(Aanvraag aanvraag)
{
using var db = Db.Create();
db.Applications.Add(aanvraag);
db.SaveChanges();
}
private Task Submit(string id, object body) =>
_client.PostAsJsonAsync($"/api/v1/applications/{id}/submit", body);
[Fact]
public async Task Below_threshold_without_an_answer_is_rejected_and_stays_a_concept()
{
// Given an intake-typed Concept aanvraag (not yet submitted).
var aanvraag = Given.Concept(type: "intake").Build();
Persist(aanvraag);
// When it is submitted with uren below the threshold and no scholing answer at all...
var res = await Submit(aanvraag.Id, new { uren = 500 });
// Then the request is rejected as a contract violation (400, not a merit rejection)...
Assert.Equal(HttpStatusCode.BadRequest, res.StatusCode);
// ...and the aanvraag is left a retryable Concept, never marked Submitted.
var stillConcept = ApplicationStore.GetAny(aanvraag.Id)!;
Assert.False(stillConcept.Submitted);
}
[Fact]
public async Task Below_threshold_with_an_answer_succeeds()
{
// Given an intake-typed Concept.
var aanvraag = Given.Concept(type: "intake").Build();
Persist(aanvraag);
// When submitted below the threshold with "niet gevolgd" — a complete, legal answer...
var res = await Submit(aanvraag.Id, new { uren = 500, aanvullendeScholing = false });
// Then the submission succeeds.
res.EnsureSuccessStatusCode();
}
[Fact]
public async Task Above_threshold_needs_no_answer()
{
// Given an intake-typed Concept.
var aanvraag = Given.Concept(type: "intake").Build();
Persist(aanvraag);
// When submitted with uren at/above the threshold and no scholing answer...
var res = await Submit(aanvraag.Id, new { uren = 1000 });
// Then it succeeds — the question is moot above the threshold.
res.EnsureSuccessStatusCode();
}
[Fact]
public async Task Punten_without_gevolgd_is_rejected()
{
// Given an intake-typed Concept.
var aanvraag = Given.Concept(type: "intake").Build();
Persist(aanvraag);
// When submitted above the threshold with punten but no "gevolgd" answer — the stale
// shape §6 fixes on the frontend, still reachable as a crafted POST...
var res = await Submit(aanvraag.Id, new { uren = 1500, scholingPunten = 150 });
// Then it is rejected.
Assert.Equal(HttpStatusCode.BadRequest, res.StatusCode);
}
[Fact]
public async Task Herregistratie_is_unaffected_by_the_intake_only_gate()
{
// Given a herregistratie-typed Concept (no scholing question in that wizard).
var aanvraag = Given.Concept(type: "herregistratie").Build();
Persist(aanvraag);
// When submitted below the intake threshold with no scholing answer at all...
var res = await Submit(aanvraag.Id, new { uren = 500 });
// Then it still succeeds — the gate is intake-only.
res.EnsureSuccessStatusCode();
}
[Fact]
public async Task Zero_uren_is_still_afgewezen_not_a_400()
{
// Given an intake-typed Concept.
var aanvraag = Given.Concept(type: "intake").Build();
Persist(aanvraag);
// When submitted with zero uren and no scholing answer — completeness would also
// reject this, but the merit rejection (RejectZeroUren) must win (the ordering guard)...
var res = await Submit(aanvraag.Id, new { uren = 0 });
// Then the submission is accepted and resolves to Afgewezen — not a 400.
res.EnsureSuccessStatusCode();
var body = (await res.Content.ReadFromJsonAsync())!;
Assert.Equal("Afgewezen", body.Status.Tag);
}
[Fact]
public async Task Legacy_intakes_endpoint_enforces_it_too()
{
// Given no aanvraag needed — the legacy endpoint mints its own reference.
// When a crafted POST hits the dead-from-the-UI /intakes endpoint below threshold,
// with no scholing answer...
var res = await _client.PostAsJsonAsync("/api/v1/intakes", new { uren = 500 });
// Then it is rejected too — the crafted-POST surface this WP closes.
Assert.Equal(HttpStatusCode.BadRequest, res.StatusCode);
}
}