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;
///
/// FEEDBACK: the "Fehler melden" report sink.
/// - POST /feedback persists a row (with captured debug context) and GET /feedback returns it.
/// - Validation: an empty message is rejected with 400.
/// - CRITICAL: feedback rows survive the import re-ingest wipe (loose, FK-free GerbilId/LitterId).
///
public class FeedbackEndpointTests : IClassFixture
{
private readonly ApiFactory _factory;
public FeedbackEndpointTests(ApiFactory factory) => _factory = factory;
[Fact]
public async Task Post_feedback_persists_row_with_debug_context()
{
var client = _factory.CreateClient();
var gerbilId = Guid.NewGuid();
var resp = await client.PostAsJsonAsync("/feedback", new
{
message = "Der Stammbaum zeigt den falschen Vater.",
context = "stammbaum",
gerbilId,
litterId = (Guid?)null,
entityName = "Krümel",
url = "http://localhost:5173/rennmaeuse/kruemel/stammbaum",
clientTimestamp = "2026-06-22T12:00:00Z",
});
Assert.Equal(HttpStatusCode.Created, resp.StatusCode);
var created = JsonDocument.Parse(await resp.Content.ReadAsStringAsync()).RootElement;
Assert.False(string.IsNullOrEmpty(created.GetProperty("id").GetString()));
Assert.Equal("stammbaum", created.GetProperty("context").GetString());
Assert.Equal("Krümel", created.GetProperty("entityName").GetString());
Assert.Equal(gerbilId.ToString(), created.GetProperty("gerbilId").GetString());
// A freshly created ticket starts Open with no ResolvedAt.
Assert.Equal("Open", created.GetProperty("status").GetString());
Assert.Equal(JsonValueKind.Null, created.GetProperty("resolvedAt").ValueKind);
// GET returns it (newest first)
var listed = JsonDocument.Parse(await client.GetStringAsync("/feedback")).RootElement;
Assert.Contains(listed.EnumerateArray(),
f => f.GetProperty("entityName").GetString() == "Krümel"
&& f.GetProperty("message").GetString() == "Der Stammbaum zeigt den falschen Vater."
&& f.GetProperty("status").GetString() == "Open");
}
[Fact]
public async Task Feedback_crud_lifecycle_edit_resolve_reopen_delete()
{
var client = _factory.CreateClient();
// Create
var create = await client.PostAsJsonAsync("/feedback", new
{
message = "Gewicht wird falsch gerundet.",
context = "gerbil-detail",
entityName = "Balu",
});
Assert.Equal(HttpStatusCode.Created, create.StatusCode);
var id = JsonDocument.Parse(await create.Content.ReadAsStringAsync()).RootElement.GetProperty("id").GetString();
Assert.False(string.IsNullOrEmpty(id));
// List contains it
var listed = JsonDocument.Parse(await client.GetStringAsync("/feedback")).RootElement;
Assert.Contains(listed.EnumerateArray(), f => f.GetProperty("id").GetString() == id);
// Edit message
var edit = await client.PutAsJsonAsync($"/feedback/{id}", new { message = "Gewicht wird falsch gerundet (auf der Verlaufskurve)." });
Assert.Equal(HttpStatusCode.OK, edit.StatusCode);
var edited = JsonDocument.Parse(await edit.Content.ReadAsStringAsync()).RootElement;
Assert.Equal("Gewicht wird falsch gerundet (auf der Verlaufskurve).", edited.GetProperty("message").GetString());
Assert.Equal("Open", edited.GetProperty("status").GetString());
// Empty message is rejected
var empty = await client.PutAsJsonAsync($"/feedback/{id}", new { message = " " });
Assert.Equal(HttpStatusCode.BadRequest, empty.StatusCode);
// Mark resolved -> ResolvedAt is set
var resolve = await client.PutAsJsonAsync($"/feedback/{id}", new { status = "Resolved" });
Assert.Equal(HttpStatusCode.OK, resolve.StatusCode);
var resolved = JsonDocument.Parse(await resolve.Content.ReadAsStringAsync()).RootElement;
Assert.Equal("Resolved", resolved.GetProperty("status").GetString());
Assert.NotEqual(JsonValueKind.Null, resolved.GetProperty("resolvedAt").ValueKind);
// Reopen -> ResolvedAt cleared
var reopen = await client.PutAsJsonAsync($"/feedback/{id}", new { status = "Open" });
Assert.Equal(HttpStatusCode.OK, reopen.StatusCode);
var reopened = JsonDocument.Parse(await reopen.Content.ReadAsStringAsync()).RootElement;
Assert.Equal("Open", reopened.GetProperty("status").GetString());
Assert.Equal(JsonValueKind.Null, reopened.GetProperty("resolvedAt").ValueKind);
// Delete -> 204, then 404 on subsequent edit/delete
var del = await client.DeleteAsync($"/feedback/{id}");
Assert.Equal(HttpStatusCode.NoContent, del.StatusCode);
var delAgain = await client.DeleteAsync($"/feedback/{id}");
Assert.Equal(HttpStatusCode.NotFound, delAgain.StatusCode);
var editGone = await client.PutAsJsonAsync($"/feedback/{id}", new { message = "noch da?" });
Assert.Equal(HttpStatusCode.NotFound, editGone.StatusCode);
}
[Fact]
public async Task Put_and_delete_unknown_id_return_404()
{
var client = _factory.CreateClient();
var missing = Guid.NewGuid();
Assert.Equal(HttpStatusCode.NotFound, (await client.PutAsJsonAsync($"/feedback/{missing}", new { message = "x" })).StatusCode);
Assert.Equal(HttpStatusCode.NotFound, (await client.DeleteAsync($"/feedback/{missing}")).StatusCode);
}
[Fact]
public async Task Post_feedback_rejects_empty_message()
{
var client = _factory.CreateClient();
var resp = await client.PostAsJsonAsync("/feedback", new { message = " ", context = "gerbil-detail" });
Assert.Equal(HttpStatusCode.BadRequest, resp.StatusCode);
}
[Fact]
public async Task Feedback_survives_ingest_wipe()
{
// Fresh in-memory DB seeded with a resolved import file (mirrors IngestResolvedServiceTests).
var dir = Path.Combine(Path.GetTempPath(), "feedback-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