using FluentValidation.TestHelper;
using SatelliteProvider.Api.Validators;
using SatelliteProvider.Common.DTO;

namespace SatelliteProvider.Tests.Validators;

// AZ-809: unit tests for CreateRouteRequestValidator. Each RuleFor /
// RuleForEach in the root validator has at least one passing case + one
// failing case. Required-field detection lives at the deserializer layer
// ([JsonRequired] + UnmappedMemberHandling.Disallow), covered separately
// at the integration layer in CreateRouteValidationTests.
public class CreateRouteRequestValidatorTests
{
    private readonly CreateRouteRequestValidator _validator;

    public CreateRouteRequestValidatorTests()
    {
        GlobalValidatorConfig.ApplyOnce();
        _validator = new CreateRouteRequestValidator();
    }

    private static CreateRouteRequest ValidRequest()
    {
        return new CreateRouteRequest
        {
            Id = Guid.NewGuid(),
            Name = "derkachi-flight-1",
            Description = "AZ-777 Phase 2 seed route",
            RegionSizeMeters = 1000.0,
            ZoomLevel = 18,
            Points = new List<RoutePoint>
            {
                new() { Latitude = 50.10, Longitude = 36.10 },
                new() { Latitude = 50.11, Longitude = 36.11 },
            },
            RequestMaps = true,
            CreateTilesZip = false,
        };
    }

    [Fact]
    public void Validate_AllValid_Passes()
    {
        // Arrange
        var request = ValidRequest();

        // Act
        var result = _validator.TestValidate(request);

        // Assert
        result.ShouldNotHaveAnyValidationErrors();
    }

    [Fact]
    public void Validate_IdEmpty_FailsNotEmptyRule()
    {
        // Arrange — reproduces the 2026-05-22 probe finding (silent zero-Guid).
        var request = ValidRequest();
        request.Id = Guid.Empty;

        // Act
        var result = _validator.TestValidate(request);

        // Assert
        result.ShouldHaveValidationErrorFor("id")
            .WithErrorMessage("`id` must be a non-zero GUID (the caller's idempotency key).");
    }

    [Theory]
    [InlineData("")]
    [InlineData("   ")]
    public void Validate_NameMissing_FailsNotEmptyRule(string name)
    {
        // Arrange
        var request = ValidRequest();
        request.Name = name;

        // Act
        var result = _validator.TestValidate(request);

        // Assert
        result.ShouldHaveValidationErrorFor("name");
    }

    [Fact]
    public void Validate_NameTooLong_FailsLengthRule()
    {
        // Arrange — name length 201 (cap is 200).
        var request = ValidRequest();
        request.Name = new string('a', 201);

        // Act
        var result = _validator.TestValidate(request);

        // Assert
        result.ShouldHaveValidationErrorFor("name");
    }

    [Fact]
    public void Validate_DescriptionTooLong_FailsLengthRule()
    {
        // Arrange — description length 1001 (cap is 1000).
        var request = ValidRequest();
        request.Description = new string('d', 1001);

        // Act
        var result = _validator.TestValidate(request);

        // Assert
        result.ShouldHaveValidationErrorFor("description");
    }

    [Theory]
    [InlineData(99.999)]
    [InlineData(0.0)]
    [InlineData(10000.001)]
    [InlineData(100000.0)]
    public void Validate_RegionSizeMetersOutOfRange_FailsRangeRule(double size)
    {
        // Arrange
        var request = ValidRequest();
        request.RegionSizeMeters = size;

        // Act
        var result = _validator.TestValidate(request);

        // Assert
        result.ShouldHaveValidationErrorFor("regionSizeMeters");
    }

    [Theory]
    [InlineData(-1)]
    [InlineData(23)]
    [InlineData(100)]
    public void Validate_ZoomLevelOutOfRange_FailsRangeRule(int zoom)
    {
        // Arrange
        var request = ValidRequest();
        request.ZoomLevel = zoom;

        // Act
        var result = _validator.TestValidate(request);

        // Assert
        result.ShouldHaveValidationErrorFor("zoomLevel");
    }

    [Fact]
    public void Validate_PointsTooFew_FailsCountRule()
    {
        // Arrange — only 1 point; min is 2 (Flow F4 precondition).
        var request = ValidRequest();
        request.Points = new List<RoutePoint>
        {
            new() { Latitude = 50.10, Longitude = 36.10 },
        };

        // Act
        var result = _validator.TestValidate(request);

        // Assert
        result.ShouldHaveValidationErrorFor("points");
    }

    [Fact]
    public void Validate_PointsTooMany_FailsCountRule()
    {
        // Arrange — 501 points; max is 500.
        var request = ValidRequest();
        request.Points = Enumerable
            .Range(0, 501)
            .Select(_ => new RoutePoint { Latitude = 50.10, Longitude = 36.10 })
            .ToList();

        // Act
        var result = _validator.TestValidate(request);

        // Assert
        result.ShouldHaveValidationErrorFor("points");
    }

    [Fact]
    public void Validate_PointLatOutOfRange_FailsChildRule()
    {
        // Arrange — second point's lat is out of range
        var request = ValidRequest();
        request.Points[1].Latitude = 91.0;

        // Act
        var result = _validator.TestValidate(request);

        // Assert
        result.ShouldHaveValidationErrorFor("points[1].lat");
    }

    [Fact]
    public void Validate_PointLonOutOfRange_FailsChildRule()
    {
        // Arrange — second point's lon is out of range
        var request = ValidRequest();
        request.Points[1].Longitude = 181.0;

        // Act
        var result = _validator.TestValidate(request);

        // Assert
        result.ShouldHaveValidationErrorFor("points[1].lon");
    }

    [Fact]
    public void Validate_GeofencePolygonNwSwapped_FailsChildInvariant()
    {
        // Arrange — NW.Lat <= SE.Lat (NW not north-of SE)
        var request = ValidRequest();
        request.Geofences = new Geofences
        {
            Polygons = new List<GeofencePolygon>
            {
                new()
                {
                    NorthWest = new GeoPoint(50.05, 36.05),
                    SouthEast = new GeoPoint(50.05, 36.15),
                }
            }
        };

        // Act
        var result = _validator.TestValidate(request);

        // Assert — the GeofencePolygonValidator child-validator's `.WithName("northWest")`
        // is prefixed with the RuleForEach path which we OverridePropertyName to
        // "geofences.polygons", producing the full wire path
        // `geofences.polygons[0].northWest`.
        result.ShouldHaveValidationErrorFor("geofences.polygons[0].northWest");
    }

    [Fact]
    public void Validate_GeofencesPresentButEmpty_FailsNotEmptyRule()
    {
        // Arrange — geofences object exists, polygons list is empty
        var request = ValidRequest();
        request.Geofences = new Geofences { Polygons = new List<GeofencePolygon>() };

        // Act
        var result = _validator.TestValidate(request);

        // Assert — OverridePropertyName makes the empty-list rule fire at the
        // wire-format path `geofences.polygons` instead of the leaf-only `polygons`.
        result.ShouldHaveValidationErrorFor("geofences.polygons");
    }

    [Fact]
    public void Validate_GeofencePolygonsTooMany_FailsCountRule()
    {
        // Arrange — 51 polygons; cap is 50 (security-audit F-AZ809-1 fix).
        // Each polygon is a valid bbox so only the count rule should fire.
        var request = ValidRequest();
        request.Geofences = new Geofences
        {
            Polygons = Enumerable
                .Range(0, 51)
                .Select(_ => new GeofencePolygon
                {
                    NorthWest = new GeoPoint(50.15, 36.05),
                    SouthEast = new GeoPoint(50.05, 36.15),
                })
                .ToList(),
        };

        // Act
        var result = _validator.TestValidate(request);

        // Assert — OverridePropertyName makes the count rule fire at the
        // wire-format path `geofences.polygons` (not the leaf-only `polygons`).
        result.ShouldHaveValidationErrorFor("geofences.polygons")
            .WithErrorMessage("`geofences.polygons` must contain at most 50 polygons.");
    }

    [Fact]
    public void Validate_CreateTilesZipWithoutRequestMaps_FailsCrossFieldRule()
    {
        // Arrange — cannot zip what wasn't downloaded
        var request = ValidRequest();
        request.RequestMaps = false;
        request.CreateTilesZip = true;

        // Act
        var result = _validator.TestValidate(request);

        // Assert
        result.ShouldHaveValidationErrorFor("createTilesZip")
            .WithErrorMessage("`createTilesZip` requires `requestMaps` to be true (can't zip what wasn't downloaded).");
    }

    [Fact]
    public void Validate_CreateTilesZipWithRequestMaps_Passes()
    {
        // Arrange — both true is valid
        var request = ValidRequest();
        request.RequestMaps = true;
        request.CreateTilesZip = true;

        // Act
        var result = _validator.TestValidate(request);

        // Assert
        result.ShouldNotHaveValidationErrorFor("createTilesZip");
    }
}
