using System.Text;

namespace SatelliteProvider.IntegrationTests;

// AZ-809: end-to-end coverage for POST /api/satellite/route strict input
// validation. Each test exercises one rule from the AZ-809 validator triplet
// (CreateRouteRequestValidator + RoutePointValidator + GeofencePolygonValidator)
// and asserts the response body conforms to the RFC 7807
// ValidationProblemDetails contract in `_docs/02_document/contracts/api/error-shape.md`
// v1.0.0. Required-field detection is enforced at the deserializer layer via
// [JsonRequired] + UnmappedMemberHandling.Disallow (AZ-795).
//
// The route-creation happy path is intentionally `requestMaps=false` here to
// keep this suite fast; the existing RouteCreationTests.cs exercises the
// `requestMaps=true` flow (with background F5 processing).
public static class CreateRouteValidationTests
{
    private const string RoutePath = "/api/satellite/route";

    public static async Task RunAll(HttpClient httpClient)
    {
        RouteTestHelpers.PrintTestHeader("Test: POST /api/satellite/route strict validation (AZ-809)");

        await HappyPath_Returns200(httpClient);

        // Rule 1: body present
        await EmptyBody_Returns400(httpClient);

        // Rule 2: id required, non-zero Guid (probe-confirmed gap)
        await MissingId_Returns400(httpClient);
        await ZeroGuidId_Returns400(httpClient);

        // Rule 3: name required, length [1, 200]
        await EmptyName_Returns400(httpClient);

        // Rule 5: regionSizeMeters required, [100, 10000]
        await RegionSizeOutOfRange_Returns400(httpClient);

        // Rule 6: zoomLevel required, [0, 22]
        await ZoomLevelOutOfRange_Returns400(httpClient);

        // Rule 7: points required, [2, 500]
        await PointsTooFew_Returns400(httpClient);

        // Rule 8: per-point lat/lon ranges
        await PointLatOutOfRange_Returns400(httpClient);
        await PointLonOutOfRange_Returns400(httpClient);

        // Rule 9: geofence corners + NW-of-SE invariant
        await GeofenceNwLatNotGreaterThanSeLat_Returns400(httpClient);

        // Rule 9b: geofence polygon-count cap (F-AZ809-1 security-audit fix)
        await GeofencePolygonsTooMany_Returns400(httpClient);

        // Rule 10/11: requestMaps + createTilesZip required
        await MissingRequestMaps_Returns400(httpClient);

        // Rule 12: cross-field createTilesZip implies requestMaps
        await CreateTilesZipWithoutRequestMaps_Returns400(httpClient);

        // Rule 13: unknown root field rejected
        await UnknownRootField_Returns400(httpClient);

        // Rule 14: type mismatch (per-point lat)
        await PointLatTypeMismatch_Returns400(httpClient);

        Console.WriteLine("✓ Create-route validation tests: PASSED");
    }

    private static async Task HappyPath_Returns200(HttpClient httpClient)
    {
        Console.WriteLine();
        Console.WriteLine("AZ-809 AC-2: well-formed body → HTTP 200 (no background processing — requestMaps=false)");

        // Arrange
        var routeId = Guid.NewGuid();
        var body = BuildValidBody(routeId, requestMaps: false, createTilesZip: false);

        // Act
        var response = await PostJsonAsync(httpClient, body);
        var status = (int)response.StatusCode;
        var bodyText = await response.Content.ReadAsStringAsync();

        // Assert
        if (status != 200)
        {
            throw new Exception($"AZ-809 happy path: expected HTTP 200, got {status}. Body: {bodyText}");
        }

        Console.WriteLine("  ✓ Well-formed body accepted with HTTP 200");
    }

    private static async Task EmptyBody_Returns400(HttpClient httpClient)
    {
        Console.WriteLine();
        Console.WriteLine("AZ-809 rule 1: empty body → HTTP 400");

        // Act
        var response = await PostJsonAsync(httpClient, "");
        var status = (int)response.StatusCode;

        // Assert
        if (status != 400)
        {
            throw new Exception($"AZ-809 rule 1: expected HTTP 400, got {status}.");
        }

        Console.WriteLine("  ✓ Empty body rejected with HTTP 400");
    }

    private static async Task MissingId_Returns400(HttpClient httpClient)
    {
        Console.WriteLine();
        Console.WriteLine("AZ-809 rule 2 (probe-confirmed gap): missing `id` → HTTP 400 (no silent zero-Guid coercion)");

        // Arrange — same exact pattern as the AZ-808 probe finding.
        var body = """
            {
              "name": "derkachi-flight-1",
              "regionSizeMeters": 1000,
              "zoomLevel": 18,
              "points": [
                { "lat": 50.10, "lon": 36.10 },
                { "lat": 50.11, "lon": 36.11 }
              ],
              "requestMaps": false,
              "createTilesZip": false
            }
            """;

        // Act
        var response = await PostJsonAsync(httpClient, body);
        var problem = await ProblemDetailsAssertions.ReadProblemDetailsAsync(response, "AZ-809 missing id");

        // Assert
        ProblemDetailsAssertions.AssertValidationProblem(
            problem,
            expectedStatus: 400,
            label: "AZ-809 missing id",
            expectedErrorPath: "$",
            expectedErrorContains: "The field value is invalid.");

        Console.WriteLine("  ✓ Missing `id` rejected with HTTP 400 (no silent coercion)");
    }

    private static async Task ZeroGuidId_Returns400(HttpClient httpClient)
    {
        Console.WriteLine();
        Console.WriteLine("AZ-809 rule 2: zero-Guid `id` → HTTP 400");

        // Arrange
        var body = BuildValidBody(Guid.Empty, requestMaps: false, createTilesZip: false);

        // Act
        var response = await PostJsonAsync(httpClient, body);
        var problem = await ProblemDetailsAssertions.ReadProblemDetailsAsync(response, "AZ-809 zero-Guid id");

        // Assert
        ProblemDetailsAssertions.AssertValidationProblem(problem, expectedStatus: 400, label: "AZ-809 zero-Guid id", expectedErrorPath: "id");

        Console.WriteLine("  ✓ Zero-Guid `id` rejected with errors[\"id\"]");
    }

    private static async Task EmptyName_Returns400(HttpClient httpClient)
    {
        Console.WriteLine();
        Console.WriteLine("AZ-809 rule 3: empty `name` → HTTP 400");

        // Arrange
        var routeId = Guid.NewGuid();
        var body = $$"""
            {
              "id": "{{routeId}}",
              "name": "",
              "regionSizeMeters": 1000,
              "zoomLevel": 18,
              "points": [
                { "lat": 50.10, "lon": 36.10 },
                { "lat": 50.11, "lon": 36.11 }
              ],
              "requestMaps": false,
              "createTilesZip": false
            }
            """;

        // Act
        var response = await PostJsonAsync(httpClient, body);
        var problem = await ProblemDetailsAssertions.ReadProblemDetailsAsync(response, "AZ-809 empty name");

        // Assert
        ProblemDetailsAssertions.AssertValidationProblem(problem, expectedStatus: 400, label: "AZ-809 empty name", expectedErrorPath: "name");

        Console.WriteLine("  ✓ Empty `name` rejected with errors[\"name\"]");
    }

    private static async Task RegionSizeOutOfRange_Returns400(HttpClient httpClient)
    {
        Console.WriteLine();
        Console.WriteLine("AZ-809 rule 5: `regionSizeMeters` out of range (100..10000) → HTTP 400");

        // Arrange — same 1M cap-exceeder as AZ-808.
        var routeId = Guid.NewGuid();
        var body = BuildValidBody(routeId, regionSize: 1_000_000, requestMaps: false, createTilesZip: false);

        // Act
        var response = await PostJsonAsync(httpClient, body);
        var problem = await ProblemDetailsAssertions.ReadProblemDetailsAsync(response, "AZ-809 regionSize out of range");

        // Assert
        ProblemDetailsAssertions.AssertValidationProblem(problem, expectedStatus: 400, label: "AZ-809 regionSize out of range", expectedErrorPath: "regionSizeMeters");

        Console.WriteLine("  ✓ `regionSizeMeters=1000000` rejected with errors[\"regionSizeMeters\"]");
    }

    private static async Task ZoomLevelOutOfRange_Returns400(HttpClient httpClient)
    {
        Console.WriteLine();
        Console.WriteLine("AZ-809 rule 6: `zoomLevel` out of range (0..22) → HTTP 400");

        // Arrange
        var routeId = Guid.NewGuid();
        var body = BuildValidBody(routeId, zoom: 30, requestMaps: false, createTilesZip: false);

        // Act
        var response = await PostJsonAsync(httpClient, body);
        var problem = await ProblemDetailsAssertions.ReadProblemDetailsAsync(response, "AZ-809 zoomLevel out of range");

        // Assert
        ProblemDetailsAssertions.AssertValidationProblem(problem, expectedStatus: 400, label: "AZ-809 zoomLevel out of range", expectedErrorPath: "zoomLevel");

        Console.WriteLine("  ✓ `zoomLevel=30` rejected with errors[\"zoomLevel\"]");
    }

    private static async Task PointsTooFew_Returns400(HttpClient httpClient)
    {
        Console.WriteLine();
        Console.WriteLine("AZ-809 rule 7: `points` count < 2 → HTTP 400");

        // Arrange — single point.
        var routeId = Guid.NewGuid();
        var body = $$"""
            {
              "id": "{{routeId}}",
              "name": "single-point-route",
              "regionSizeMeters": 1000,
              "zoomLevel": 18,
              "points": [
                { "lat": 50.10, "lon": 36.10 }
              ],
              "requestMaps": false,
              "createTilesZip": false
            }
            """;

        // Act
        var response = await PostJsonAsync(httpClient, body);
        var problem = await ProblemDetailsAssertions.ReadProblemDetailsAsync(response, "AZ-809 points too few");

        // Assert
        ProblemDetailsAssertions.AssertValidationProblem(problem, expectedStatus: 400, label: "AZ-809 points too few", expectedErrorPath: "points");

        Console.WriteLine("  ✓ `points` count=1 rejected with errors[\"points\"]");
    }

    private static async Task PointLatOutOfRange_Returns400(HttpClient httpClient)
    {
        Console.WriteLine();
        Console.WriteLine("AZ-809 rule 8: per-point `lat` out of range → HTTP 400 (errors[points[i].lat])");

        // Arrange
        var routeId = Guid.NewGuid();
        var body = $$"""
            {
              "id": "{{routeId}}",
              "name": "out-of-range-lat",
              "regionSizeMeters": 1000,
              "zoomLevel": 18,
              "points": [
                { "lat": 50.10, "lon": 36.10 },
                { "lat": 91.0,  "lon": 36.11 }
              ],
              "requestMaps": false,
              "createTilesZip": false
            }
            """;

        // Act
        var response = await PostJsonAsync(httpClient, body);
        var problem = await ProblemDetailsAssertions.ReadProblemDetailsAsync(response, "AZ-809 point lat out of range");

        // Assert
        ProblemDetailsAssertions.AssertValidationProblem(problem, expectedStatus: 400, label: "AZ-809 point lat out of range");
        ProblemDetailsAssertions.AssertErrorsContainsMention(problem, expectedMention: "points[1].lat", label: "AZ-809 point lat out of range");

        Console.WriteLine("  ✓ `points[1].lat=91` rejected with errors[\"points[1].lat\"]");
    }

    private static async Task PointLonOutOfRange_Returns400(HttpClient httpClient)
    {
        Console.WriteLine();
        Console.WriteLine("AZ-809 rule 8: per-point `lon` out of range → HTTP 400 (errors[points[i].lon])");

        // Arrange
        var routeId = Guid.NewGuid();
        var body = $$"""
            {
              "id": "{{routeId}}",
              "name": "out-of-range-lon",
              "regionSizeMeters": 1000,
              "zoomLevel": 18,
              "points": [
                { "lat": 50.10, "lon": 36.10 },
                { "lat": 50.11, "lon": 181.0 }
              ],
              "requestMaps": false,
              "createTilesZip": false
            }
            """;

        // Act
        var response = await PostJsonAsync(httpClient, body);
        var problem = await ProblemDetailsAssertions.ReadProblemDetailsAsync(response, "AZ-809 point lon out of range");

        // Assert
        ProblemDetailsAssertions.AssertValidationProblem(problem, expectedStatus: 400, label: "AZ-809 point lon out of range");
        ProblemDetailsAssertions.AssertErrorsContainsMention(problem, expectedMention: "points[1].lon", label: "AZ-809 point lon out of range");

        Console.WriteLine("  ✓ `points[1].lon=181` rejected with errors[\"points[1].lon\"]");
    }

    private static async Task GeofenceNwLatNotGreaterThanSeLat_Returns400(HttpClient httpClient)
    {
        Console.WriteLine();
        Console.WriteLine("AZ-809 rule 9: geofence NW.lat <= SE.lat → HTTP 400 (cross-field invariant)");

        // Arrange — NW.lat == SE.lat → NW not north-of SE.
        var routeId = Guid.NewGuid();
        var body = $$"""
            {
              "id": "{{routeId}}",
              "name": "inverted-geofence",
              "regionSizeMeters": 1000,
              "zoomLevel": 18,
              "points": [
                { "lat": 50.10, "lon": 36.10 },
                { "lat": 50.11, "lon": 36.11 }
              ],
              "geofences": {
                "polygons": [
                  { "northWest": { "lat": 50.05, "lon": 36.05 },
                    "southEast": { "lat": 50.05, "lon": 36.15 } }
                ]
              },
              "requestMaps": false,
              "createTilesZip": false
            }
            """;

        // Act
        var response = await PostJsonAsync(httpClient, body);
        var problem = await ProblemDetailsAssertions.ReadProblemDetailsAsync(response, "AZ-809 NW lat not > SE lat");

        // Assert
        ProblemDetailsAssertions.AssertValidationProblem(problem, expectedStatus: 400, label: "AZ-809 NW lat not > SE lat");
        ProblemDetailsAssertions.AssertErrorsContainsMention(problem, expectedMention: "northWest", label: "AZ-809 NW lat not > SE lat");

        Console.WriteLine("  ✓ NW.lat <= SE.lat rejected by cross-field invariant");
    }

    private static async Task GeofencePolygonsTooMany_Returns400(HttpClient httpClient)
    {
        Console.WriteLine();
        Console.WriteLine("AZ-809 rule 9b (security-audit F-AZ809-1): geofence polygon-count > 50 → HTTP 400");

        // Arrange — 51 polygons, each valid bbox. Only the count rule should fire.
        var routeId = Guid.NewGuid();
        var polygonsJson = string.Join(
            ",\n        ",
            Enumerable
                .Range(0, 51)
                .Select(_ => "{ \"northWest\": { \"lat\": 50.15, \"lon\": 36.05 }, \"southEast\": { \"lat\": 50.05, \"lon\": 36.15 } }"));
        var body = $$"""
            {
              "id": "{{routeId}}",
              "name": "too-many-polygons",
              "regionSizeMeters": 1000,
              "zoomLevel": 18,
              "points": [
                { "lat": 50.10, "lon": 36.10 },
                { "lat": 50.11, "lon": 36.11 }
              ],
              "geofences": {
                "polygons": [
                  {{polygonsJson}}
                ]
              },
              "requestMaps": false,
              "createTilesZip": false
            }
            """;

        // Act
        var response = await PostJsonAsync(httpClient, body);
        var problem = await ProblemDetailsAssertions.ReadProblemDetailsAsync(response, "AZ-809 geofence polygons too many");

        // Assert
        ProblemDetailsAssertions.AssertValidationProblem(problem, expectedStatus: 400, label: "AZ-809 geofence polygons too many", expectedErrorPath: "geofences.polygons");

        Console.WriteLine("  ✓ 51 polygons rejected with errors[\"geofences.polygons\"] (cap is 50)");
    }

    private static async Task MissingRequestMaps_Returns400(HttpClient httpClient)
    {
        Console.WriteLine();
        Console.WriteLine("AZ-809 rule 10: missing `requestMaps` → HTTP 400 (no defaulting)");

        // Arrange
        var routeId = Guid.NewGuid();
        var body = $$"""
            {
              "id": "{{routeId}}",
              "name": "no-requestMaps",
              "regionSizeMeters": 1000,
              "zoomLevel": 18,
              "points": [
                { "lat": 50.10, "lon": 36.10 },
                { "lat": 50.11, "lon": 36.11 }
              ],
              "createTilesZip": false
            }
            """;

        // Act
        var response = await PostJsonAsync(httpClient, body);
        var problem = await ProblemDetailsAssertions.ReadProblemDetailsAsync(response, "AZ-809 missing requestMaps");

        // Assert
        ProblemDetailsAssertions.AssertValidationProblem(
            problem,
            expectedStatus: 400,
            label: "AZ-809 missing requestMaps",
            expectedErrorPath: "$",
            expectedErrorContains: "The field value is invalid.");

        Console.WriteLine("  ✓ Missing `requestMaps` rejected");
    }

    private static async Task CreateTilesZipWithoutRequestMaps_Returns400(HttpClient httpClient)
    {
        Console.WriteLine();
        Console.WriteLine("AZ-809 rule 12: `createTilesZip=true` AND `requestMaps=false` → HTTP 400 (cross-field invariant)");

        // Arrange
        var routeId = Guid.NewGuid();
        var body = BuildValidBody(routeId, requestMaps: false, createTilesZip: true);

        // Act
        var response = await PostJsonAsync(httpClient, body);
        var problem = await ProblemDetailsAssertions.ReadProblemDetailsAsync(response, "AZ-809 createTilesZip without requestMaps");

        // Assert
        ProblemDetailsAssertions.AssertValidationProblem(problem, expectedStatus: 400, label: "AZ-809 createTilesZip without requestMaps", expectedErrorPath: "createTilesZip");

        Console.WriteLine("  ✓ `createTilesZip=true requestMaps=false` rejected by cross-field invariant");
    }

    private static async Task UnknownRootField_Returns400(HttpClient httpClient)
    {
        Console.WriteLine();
        Console.WriteLine("AZ-809 rule 13: unknown root field → HTTP 400 (UnmappedMemberHandling.Disallow)");

        // Arrange
        var routeId = Guid.NewGuid();
        var body = $$"""
            {
              "id": "{{routeId}}",
              "name": "with-unknown-field",
              "regionSizeMeters": 1000,
              "zoomLevel": 18,
              "points": [
                { "lat": 50.10, "lon": 36.10 },
                { "lat": 50.11, "lon": 36.11 }
              ],
              "requestMaps": false,
              "createTilesZip": false,
              "debug": "fingerprint-probe"
            }
            """;

        // Act
        var response = await PostJsonAsync(httpClient, body);
        var problem = await ProblemDetailsAssertions.ReadProblemDetailsAsync(response, "AZ-809 unknown root field");

        // Assert
        ProblemDetailsAssertions.AssertValidationProblem(problem, expectedStatus: 400, label: "AZ-809 unknown root field");
        ProblemDetailsAssertions.AssertErrorsContainsMention(problem, expectedMention: "debug", label: "AZ-809 unknown root field");

        Console.WriteLine("  ✓ Unknown root field `debug` rejected with errors mention");
    }

    private static async Task PointLatTypeMismatch_Returns400(HttpClient httpClient)
    {
        Console.WriteLine();
        Console.WriteLine("AZ-809 rule 14: nested type mismatch (`points[0].lat` as string) → HTTP 400");

        // Arrange
        var routeId = Guid.NewGuid();
        var body = $$"""
            {
              "id": "{{routeId}}",
              "name": "nested-type-mismatch",
              "regionSizeMeters": 1000,
              "zoomLevel": 18,
              "points": [
                { "lat": "fifty", "lon": 36.10 },
                { "lat": 50.11,   "lon": 36.11 }
              ],
              "requestMaps": false,
              "createTilesZip": false
            }
            """;

        // Act
        var response = await PostJsonAsync(httpClient, body);
        var problem = await ProblemDetailsAssertions.ReadProblemDetailsAsync(response, "AZ-809 point lat type mismatch");

        // Assert — GlobalExceptionHandler converts BadHttpRequestException to
        // ValidationProblemDetails when the inner JsonException's Path is set.
        ProblemDetailsAssertions.AssertValidationProblem(problem, expectedStatus: 400, label: "AZ-809 point lat type mismatch");

        Console.WriteLine("  ✓ `points[0].lat:\"fifty\"` rejected with HTTP 400");
    }

    private static string BuildValidBody(
        Guid routeId,
        double regionSize = 1000.0,
        int zoom = 18,
        bool requestMaps = false,
        bool createTilesZip = false)
    {
        // Lat/lon picked from gps-denied-onboard AZ-777 Phase 2 probe.
        return $$"""
            {
              "id": "{{routeId}}",
              "name": "az-809-integration-test",
              "description": "AZ-809 integration test route",
              "regionSizeMeters": {{regionSize.ToString(System.Globalization.CultureInfo.InvariantCulture)}},
              "zoomLevel": {{zoom}},
              "points": [
                { "lat": 50.10, "lon": 36.10 },
                { "lat": 50.11, "lon": 36.11 }
              ],
              "requestMaps": {{(requestMaps ? "true" : "false")}},
              "createTilesZip": {{(createTilesZip ? "true" : "false")}}
            }
            """;
    }

    private static Task<HttpResponseMessage> PostJsonAsync(HttpClient httpClient, string body)
    {
        var content = new StringContent(body, Encoding.UTF8, "application/json");
        return httpClient.PostAsync(RoutePath, content);
    }
}
