using System.Net; using System.Net.Http.Json; using System.Text.Json; using GerbilManagerWebAPI.Import; using GerbilManagerWebAPI.Models; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; namespace GerbilManager.Tests; /// /// ABGABE-STATUS (Reservierungs-/Abgabe-Status): the pre-handover pipeline /// verfügbar → reserviert → abgegeben. /// - Full CRUD round-trip via /reservations. /// - GET ?gerbilId= filters to one animal. /// - Invalid status falls back to "verfuegbar". /// - CRITICAL: rows survive the import re-ingest wipe (loose, FK-free GerbilId/ReservedForContactId). /// public class SaleReservationEndpointTests : IClassFixture { private readonly ApiFactory _factory; public SaleReservationEndpointTests(ApiFactory factory) => _factory = factory; [Fact] public async Task Reservation_crud_lifecycle_create_reserve_handover_delete() { var client = _factory.CreateClient(); var gerbilId = Guid.NewGuid(); var contactId = Guid.NewGuid(); // Create -> defaults to verfuegbar. var create = await client.PostAsJsonAsync("/reservations", new { gerbilId, gerbilName = "Pippa", }); Assert.Equal(HttpStatusCode.Created, create.StatusCode); var created = JsonDocument.Parse(await create.Content.ReadAsStringAsync()).RootElement; var id = created.GetProperty("id").GetString(); Assert.False(string.IsNullOrEmpty(id)); Assert.Equal("verfuegbar", created.GetProperty("status").GetString()); Assert.Equal(gerbilId.ToString(), created.GetProperty("gerbilId").GetString()); Assert.Equal("Pippa", created.GetProperty("gerbilName").GetString()); // List contains it. var listed = JsonDocument.Parse(await client.GetStringAsync("/reservations")).RootElement; Assert.Contains(listed.EnumerateArray(), r => r.GetProperty("id").GetString() == id); // Filter by gerbilId returns exactly this row. var byGerbil = JsonDocument.Parse(await client.GetStringAsync($"/reservations?gerbilId={gerbilId}")).RootElement; Assert.All(byGerbil.EnumerateArray(), r => Assert.Equal(gerbilId.ToString(), r.GetProperty("gerbilId").GetString())); Assert.Contains(byGerbil.EnumerateArray(), r => r.GetProperty("id").GetString() == id); // PUT -> reserve for a contact with an appointment + price. var reserve = await client.PutAsJsonAsync($"/reservations/{id}", new { status = "reserviert", reservedForContactId = contactId, contactName = "Familie Huber", appointmentDate = "2026-07-01T10:00:00Z", price = 25.50m, note = "Käfig wird mitgebracht.", }); Assert.Equal(HttpStatusCode.OK, reserve.StatusCode); var reserved = JsonDocument.Parse(await reserve.Content.ReadAsStringAsync()).RootElement; Assert.Equal("reserviert", reserved.GetProperty("status").GetString()); Assert.Equal(contactId.ToString(), reserved.GetProperty("reservedForContactId").GetString()); Assert.Equal("Familie Huber", reserved.GetProperty("contactName").GetString()); Assert.Equal(25.50m, reserved.GetProperty("price").GetDecimal()); // PUT -> hand over. var handover = await client.PutAsJsonAsync($"/reservations/{id}", new { status = "abgegeben", reservedForContactId = contactId, handedOverDate = "2026-07-01T10:30:00Z", }); Assert.Equal(HttpStatusCode.OK, handover.StatusCode); var handed = JsonDocument.Parse(await handover.Content.ReadAsStringAsync()).RootElement; Assert.Equal("abgegeben", handed.GetProperty("status").GetString()); Assert.NotEqual(JsonValueKind.Null, handed.GetProperty("handedOverDate").ValueKind); // Delete -> 204, then 404. Assert.Equal(HttpStatusCode.NoContent, (await client.DeleteAsync($"/reservations/{id}")).StatusCode); Assert.Equal(HttpStatusCode.NotFound, (await client.DeleteAsync($"/reservations/{id}")).StatusCode); Assert.Equal(HttpStatusCode.NotFound, (await client.PutAsJsonAsync($"/reservations/{id}", new { status = "verfuegbar" })).StatusCode); } [Fact] public async Task Post_with_unknown_status_falls_back_to_verfuegbar() { var client = _factory.CreateClient(); var create = await client.PostAsJsonAsync("/reservations", new { gerbilId = Guid.NewGuid(), status = "bananen" }); Assert.Equal(HttpStatusCode.Created, create.StatusCode); var dto = JsonDocument.Parse(await create.Content.ReadAsStringAsync()).RootElement; Assert.Equal("verfuegbar", dto.GetProperty("status").GetString()); } [Fact] public async Task Post_without_gerbilId_is_rejected() { var client = _factory.CreateClient(); var create = await client.PostAsJsonAsync("/reservations", new { gerbilId = Guid.Empty, status = "reserviert" }); Assert.Equal(HttpStatusCode.BadRequest, create.StatusCode); } [Fact] public async Task Reservation_survives_ingest_wipe() { // Fresh in-memory DB seeded with a resolved import file (mirrors IngestResolvedServiceTests). var dir = Path.Combine(Path.GetTempPath(), "reservation-ingest-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(dir); try { var contactId = Guid.NewGuid(); var fatherId = Guid.NewGuid(); var motherId = Guid.NewGuid(); var litterId = Guid.NewGuid(); var data = new { Contacts = new[] { new { Id = contactId, Name = "Test Breeder", Email = "t@e.de", Phone = "", Address = "", Notes = (string?)null, IsBreeder = true, IsReceiver = false, NameSuffix = (string?)null, Provenance = (string?)null } }, Litters = new[] { new { Id = litterId, Name = "Wurf A", Date = "2026-01-01", TotalBorn = 5, DeathsWithin8Weeks = 0, FatherId = fatherId, MotherId = motherId, ExpectedGoHomeDate = (string?)null, Notes = "", PairingCode = "PC01", ExternalRef = "ext-litter-1", LitterLetter = "A" } }, Gerbils = new[] { Animal(fatherId, "Papa", "male", contactId), Animal(motherId, "Mama", "female", contactId), }, GerbilPhotos = Array.Empty(), }; File.WriteAllText(Path.Combine(dir, "resolved_import.json"), JsonSerializer.Serialize(data)); var opts = new DbContextOptionsBuilder() .UseInMemoryDatabase("reservation-ingest-" + Guid.NewGuid().ToString("N")) .Options; using var db = new ApplicationContext(opts); db.Database.EnsureCreated(); // A reservation referencing the gerbil + contact that the wipe will delete. var resId = Guid.NewGuid(); db.SaleReservations.Add(new SaleReservation { Id = resId, GerbilId = fatherId, GerbilName = "Papa", Status = "reserviert", ReservedForContactId = contactId, ContactName = "Test Breeder", AppointmentDate = new DateTime(2026, 7, 1, 10, 0, 0, DateTimeKind.Utc), Price = 30m, Note = "Abholung am Wochenende.", CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow, }); await db.SaveChangesAsync(); var config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { { "Import:SourcePath", dir } }) .Build(); // Run the ingest wipe + reload. var result = await new IngestResolvedService(db, config, null!).RunAsync(); Assert.Contains("Ingestion successful!", result); // Gerbils/contacts were wiped & re-created, but the reservation is untouched. var survivor = await db.SaleReservations.SingleAsync(r => r.Id == resId); Assert.Equal(fatherId, survivor.GerbilId); // loose id preserved Assert.Equal(contactId, survivor.ReservedForContactId); Assert.Equal("Papa", survivor.GerbilName); Assert.Equal("Test Breeder", survivor.ContactName); Assert.Equal("reserviert", survivor.Status); Assert.Equal(30m, survivor.Price); Assert.Equal(1, await db.SaleReservations.CountAsync()); } finally { try { Directory.Delete(dir, recursive: true); } catch { /* best effort */ } } } private static object Animal(Guid id, string name, string gender, Guid contactId) => new { Id = id, Name = name, Gender = gender, Status = "Breeding", LitterId = (Guid?)null, OriginContactId = contactId, ReceiverContactId = (Guid?)null, EnclosureId = (Guid?)null, ColorVarietyId = new Guid("00000000-0000-0000-0000-000000000006"), DateOfBirth = "2025-01-01", DateOfDeath = (string?)null, CauseOfDeath = (string?)null, GoHomeDate = (string?)null, Genotype = "aa CC DD EE GG PP spsp rere", Notes = "", ImportSource = "docx-export", ExternalRef = "ext-" + name, RawImportData = "{}", OriginBreeder = "Test Zucht", NameSearch = name.ToLowerInvariant(), CharacterTraits = Array.Empty(), CharacterNote = (string?)null, IsDeaf = false, IsResident = true, }; }