using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.RegularExpressions;

namespace GerbilManager.Tests;

/// <summary>CR-2 regression: PUT /gerbils/{id} must not silently destroy fields that
/// the caller omits (PATCH semantics). The ForSale-toggle and Charakterbogen-save
/// send partial bodies — all other fields must be preserved unchanged.</summary>
public class PartialUpdateTests : IClassFixture<ApiFactory>
{
    private readonly HttpClient _client;

    public PartialUpdateTests(ApiFactory factory) => _client = factory.CreateClient();

    // ------------------------------------------------------------------ helpers

    private static Guid ExtractId(string json) =>
        Guid.Parse(Regex.Match(json, "\"id\":\"([^\"]+)\"").Groups[1].Value);

    private static string? GetStr(JsonElement el, string prop) =>
        el.TryGetProperty(prop, out var v) && v.ValueKind == JsonValueKind.String
            ? v.GetString() : null;

    private static string? GetNested(JsonElement el, string prop) =>
        el.TryGetProperty(prop, out var v) ? v.ToString() : null;

    /// <summary>Create a fully-populated gerbil and return its id.</summary>
    private async Task<Guid> CreateFullGerbil(string name = "TestTier")
    {
        // Use name-derived unique externalRef so the DB-1 unique constraint doesn't fire
        // when multiple tests in the same fixture share the SQLite connection.
        var resp = await _client.PostAsync("/gerbils", JsonContent.Create(new
        {
            name,
            gender = "female",
            genotype = "aa CC DD ee GG PP spsp rere",
            originBreeder = "Zucht der Kleinen Chaoten",
            isDeaf = false,
            characterTraits = new[] { "neugierig", "zutraulich" },
            characterNote = "Liebling der Familie",
            notes = "Eine Notiz",
            externalRef = $"ext-{name.GetHashCode():X8}",
        }));
        Assert.Equal(HttpStatusCode.Created, resp.StatusCode);
        return ExtractId(await resp.Content.ReadAsStringAsync());
    }

    // ------------------------------------------------------------------ tests

    [Fact]
    public async Task ForSale_toggle_does_not_wipe_other_fields()
    {
        // Arrange: create a fully-populated gerbil
        var id = await CreateFullGerbil("ForSale-Tier");

        // Act: send only { status: 'ForSale' } — exactly what the detail page does
        var put = await _client.PutAsync($"/gerbils/{id}",
            JsonContent.Create(new { status = "ForSale" }));
        Assert.Equal(HttpStatusCode.NoContent, put.StatusCode);

        // Assert: re-read and check all fields survive
        var json = JsonDocument.Parse(await _client.GetStringAsync($"/gerbils/{id}")).RootElement;

        Assert.Equal("ForSale", GetStr(json, "status"));
        Assert.Equal("ForSale-Tier", GetStr(json, "name"));
        Assert.Equal("female", GetStr(json, "gender"));
        Assert.Equal("aa CC DD ee GG PP spsp rere", GetStr(json, "genotype"));
        Assert.Equal("Zucht der Kleinen Chaoten", GetStr(json, "originBreeder"));
        Assert.Equal("Eine Notiz", GetStr(json, "notes"));
        Assert.NotNull(GetStr(json, "externalRef")); // externalRef set during create, not cleared by partial PUT
        Assert.Contains("neugierig", GetNested(json, "characterTraits") ?? "");
        Assert.Equal("Liebling der Familie", GetStr(json, "characterNote"));
    }

    [Fact]
    public async Task CharacterSave_does_not_wipe_other_fields()
    {
        // Arrange
        var id = await CreateFullGerbil("Charakter-Tier");

        // Act: send only { characterTraits, characterNote } — exactly what the Charakterbogen save does
        var put = await _client.PutAsync($"/gerbils/{id}", JsonContent.Create(new
        {
            characterTraits = new[] { "handzahm" },
            characterNote = "Neue Notiz",
        }));
        Assert.Equal(HttpStatusCode.NoContent, put.StatusCode);

        // Assert: name / genotype / originBreeder / status / externalRef all intact
        var json = JsonDocument.Parse(await _client.GetStringAsync($"/gerbils/{id}")).RootElement;

        Assert.Equal("Active", GetStr(json, "status"));
        Assert.Equal("Charakter-Tier", GetStr(json, "name"));
        Assert.Equal("female", GetStr(json, "gender"));
        Assert.Equal("aa CC DD ee GG PP spsp rere", GetStr(json, "genotype"));
        Assert.Equal("Zucht der Kleinen Chaoten", GetStr(json, "originBreeder"));
        Assert.Equal("Eine Notiz", GetStr(json, "notes"));
        Assert.NotNull(GetStr(json, "externalRef")); // externalRef set during create, not cleared by partial PUT
        Assert.Contains("handzahm", GetNested(json, "characterTraits") ?? "");
        Assert.Equal("Neue Notiz", GetStr(json, "characterNote"));
    }

    [Fact]
    public async Task EditForm_omitting_import_fields_keeps_them()
    {
        // Arrange: gerbil with import-set fields
        var id = await CreateFullGerbil("EditForm-Tier");

        // Act: send a typical edit-form body that omits originBreeder/isDeaf/externalRef/characterTraits/characterNote
        var put = await _client.PutAsync($"/gerbils/{id}", JsonContent.Create(new
        {
            name = "EditForm-Tier (umbenannt)",
            gender = "female",
            genotype = "aa CC DD ee GG PP spsp rere",
            notes = "Aktualisierte Notiz",
        }));
        Assert.Equal(HttpStatusCode.NoContent, put.StatusCode);

        // Assert: name updated, import fields survive
        var json = JsonDocument.Parse(await _client.GetStringAsync($"/gerbils/{id}")).RootElement;

        Assert.Equal("EditForm-Tier (umbenannt)", GetStr(json, "name"));
        Assert.Equal("Aktualisierte Notiz", GetStr(json, "notes"));
        Assert.Equal("Zucht der Kleinen Chaoten", GetStr(json, "originBreeder"));
        Assert.NotNull(GetStr(json, "externalRef")); // externalRef set during create, not cleared by partial PUT
        Assert.Contains("neugierig", GetNested(json, "characterTraits") ?? "");
        Assert.Equal("Liebling der Familie", GetStr(json, "characterNote"));
    }

    [Fact]
    public async Task Create_with_minimal_body_uses_safe_defaults()
    {
        // A minimal POST (only name) must still produce a valid gerbil
        var resp = await _client.PostAsync("/gerbils", JsonContent.Create(new { name = "Minimal" }));
        Assert.Equal(HttpStatusCode.Created, resp.StatusCode);

        var json = JsonDocument.Parse(await resp.Content.ReadAsStringAsync()).RootElement;
        Assert.Equal("Minimal", GetStr(json, "name"));
        Assert.Equal("Active", GetStr(json, "status"));
        // characterTraits defaults to [] on create when not provided
        Assert.Contains("[]", GetNested(json, "characterTraits") ?? "");
    }
}
