using System.Net;
using System.Text;
using System.Text.Json;

namespace SatelliteProvider.IntegrationTests;

public static class IdempotentPostTests
{
    public static async Task RunAll(HttpClient httpClient)
    {
        RouteTestHelpers.PrintTestHeader("Test: Idempotent POST contract (AZ-362)");

        await RegionPost_SameIdTwice_BothReturn200_NoDuplicateProcessing_AZ362_AC1(httpClient);
        await RoutePost_SameIdTwice_BothReturn200_NoReinsertion_AZ362_AC2(httpClient);

        Console.WriteLine("✓ Idempotent POST tests: PASSED");
    }

    private static readonly JsonSerializerOptions JsonOptions = new()
    {
        PropertyNameCaseInsensitive = true,
    };

    private static async Task RegionPost_SameIdTwice_BothReturn200_NoDuplicateProcessing_AZ362_AC1(HttpClient httpClient)
    {
        Console.WriteLine();
        Console.WriteLine("AZ-362 AC-1: POST /api/satellite/request twice with same id returns existing region on retry");

        var regionId = Guid.NewGuid();
        var body = JsonSerializer.Serialize(new
        {
            id = regionId,
            lat = 47.4617,
            lon = 37.6470,
            sizeMeters = 200,
            zoomLevel = 18,
            stitchTiles = false,
        });
        var content1 = new StringContent(body, Encoding.UTF8, "application/json");
        var response1 = await httpClient.PostAsync("/api/satellite/request", content1);
        var status1 = (int)response1.StatusCode;
        var body1 = await response1.Content.ReadAsStringAsync();
        if (status1 != 200)
        {
            throw new Exception($"AZ-362 AC-1: first POST expected 200, got {status1}. Body: {body1}");
        }

        var first = JsonSerializer.Deserialize<RegionStatusResponse>(body1, JsonOptions)
            ?? throw new Exception($"AZ-362 AC-1: first POST returned unparseable body: {body1}");
        if (first.Id != regionId)
        {
            throw new Exception($"AZ-362 AC-1: first POST returned id {first.Id}, expected {regionId}");
        }

        // Second POST with the same id (and same payload — retry semantics)
        var content2 = new StringContent(body, Encoding.UTF8, "application/json");
        var response2 = await httpClient.PostAsync("/api/satellite/request", content2);
        var status2 = (int)response2.StatusCode;
        var body2 = await response2.Content.ReadAsStringAsync();
        if (status2 != 200)
        {
            throw new Exception($"AZ-362 AC-1: retried POST expected 200 (idempotent), got {status2}. Body: {body2}");
        }

        var second = JsonSerializer.Deserialize<RegionStatusResponse>(body2, JsonOptions)
            ?? throw new Exception($"AZ-362 AC-1: retried POST returned unparseable body: {body2}");
        if (second.Id != regionId)
        {
            throw new Exception($"AZ-362 AC-1: retried POST returned id {second.Id}, expected {regionId}");
        }

        // The retried POST must reflect the SAME persisted resource. We tolerate
        // sub-millisecond drift because PostgreSQL TIMESTAMP truncates to microseconds
        // while .NET DateTime keeps 100ns ticks — re-reading the same row can produce
        // a value that's a few ticks off from the in-memory original. A genuine
        // re-insertion would shift CreatedAt by milliseconds (network + DB round trip).
        var createdAtDelta = (first.CreatedAt - second.CreatedAt).Duration();
        if (createdAtDelta > TimeSpan.FromMilliseconds(1))
        {
            throw new Exception(
                $"AZ-362 AC-1: retried POST has a different CreatedAt — looks like a fresh row was inserted. " +
                $"first={first.CreatedAt:O}, second={second.CreatedAt:O}, delta={createdAtDelta.TotalMilliseconds:F3}ms");
        }

        Console.WriteLine($"  ✓ First POST: HTTP 200, status={first.Status}, createdAt={first.CreatedAt:O}");
        Console.WriteLine($"  ✓ Retry POST: HTTP 200, status={second.Status}, createdAt={second.CreatedAt:O} (same row, delta={createdAtDelta.TotalMilliseconds:F3}ms)");
    }

    private static async Task RoutePost_SameIdTwice_BothReturn200_NoReinsertion_AZ362_AC2(HttpClient httpClient)
    {
        Console.WriteLine();
        Console.WriteLine("AZ-362 AC-2: POST /api/satellite/route twice with same id returns existing route on retry");

        var routeId = Guid.NewGuid();
        var payload = JsonSerializer.Serialize(new
        {
            id = routeId,
            name = "az-362 idempotency test",
            description = "first POST creates, second POST reads",
            regionSizeMeters = 500,
            zoomLevel = 18,
            requestMaps = false,
            createTilesZip = false,
            points = new[]
            {
                new { lat = 47.4617, lon = 37.6470 },
                new { lat = 47.4630, lon = 37.6485 },
            },
        });

        var content1 = new StringContent(payload, Encoding.UTF8, "application/json");
        var response1 = await httpClient.PostAsync("/api/satellite/route", content1);
        var status1 = (int)response1.StatusCode;
        var body1 = await response1.Content.ReadAsStringAsync();
        if (status1 != 200)
        {
            throw new Exception($"AZ-362 AC-2: first POST expected 200, got {status1}. Body: {body1}");
        }

        var first = JsonSerializer.Deserialize<RouteResponseShape>(body1, JsonOptions)
            ?? throw new Exception($"AZ-362 AC-2: first POST returned unparseable body: {body1}");
        if (first.Id != routeId)
        {
            throw new Exception($"AZ-362 AC-2: first POST returned id {first.Id}, expected {routeId}");
        }

        // Second POST with the same id
        var content2 = new StringContent(payload, Encoding.UTF8, "application/json");
        var response2 = await httpClient.PostAsync("/api/satellite/route", content2);
        var status2 = (int)response2.StatusCode;
        var body2 = await response2.Content.ReadAsStringAsync();
        if (status2 != 200)
        {
            throw new Exception($"AZ-362 AC-2: retried POST expected 200 (idempotent), got {status2}. Body: {body2}");
        }

        var second = JsonSerializer.Deserialize<RouteResponseShape>(body2, JsonOptions)
            ?? throw new Exception($"AZ-362 AC-2: retried POST returned unparseable body: {body2}");

        if (second.Id != routeId)
        {
            throw new Exception($"AZ-362 AC-2: retried POST returned id {second.Id}, expected {routeId}");
        }
        var createdAtDelta = (first.CreatedAt - second.CreatedAt).Duration();
        if (createdAtDelta > TimeSpan.FromMilliseconds(1))
        {
            throw new Exception(
                $"AZ-362 AC-2: retried POST has a different CreatedAt — looks like a fresh row was inserted. " +
                $"first={first.CreatedAt:O}, second={second.CreatedAt:O}, delta={createdAtDelta.TotalMilliseconds:F3}ms");
        }
        if (first.TotalPoints != second.TotalPoints)
        {
            throw new Exception(
                $"AZ-362 AC-2: retried POST has different TotalPoints ({second.TotalPoints} vs {first.TotalPoints}) — points should not have been regenerated");
        }

        Console.WriteLine($"  ✓ First POST: HTTP 200, totalPoints={first.TotalPoints}, createdAt={first.CreatedAt:O}");
        Console.WriteLine($"  ✓ Retry POST: HTTP 200, totalPoints={second.TotalPoints}, createdAt={second.CreatedAt:O} (same row, delta={createdAtDelta.TotalMilliseconds:F3}ms)");
    }

    private sealed record RegionStatusResponse(Guid Id, string? Status, DateTime CreatedAt);
    private sealed record RouteResponseShape(Guid Id, int TotalPoints, DateTime CreatedAt);
}
