using Theriapolis.Core;
using Theriapolis.Core.World;
using Theriapolis.Core.World.Generation;
using Theriapolis.Core.World.Polylines;
using Xunit;

namespace Theriapolis.Tests.Worldgen;

/// <summary>
/// Hydrology correctness: rivers must be generated, endpoints must reach water.
/// </summary>
public sealed class HydrologyTests : IClassFixture<WorldCache>
{
    private const ulong TestSeed = 0xCAFEBABEUL;

    // HydrologyGen is stage 10 → 0-based index 9 (fast-path for hydrology-only tests).
    private const int HydrologyStageIndex = 9;

    private readonly WorldCache _cache;

    public HydrologyTests(WorldCache cache) => _cache = cache;

    [Fact]
    public void Pipeline_GeneratesAtLeastOneRiver()
    {
        var ctx = _cache.GetThrough(TestSeed, HydrologyStageIndex);
        Assert.NotEmpty(ctx.World.Rivers);
    }

    [Fact]
    public void AllRivers_HaveAtLeastTwoPoints()
    {
        var ctx = _cache.GetThrough(TestSeed, HydrologyStageIndex);
        foreach (var river in ctx.World.Rivers)
            Assert.True(river.Points.Count >= 2,
                $"River {river.Id} has fewer than 2 points");
    }

    [Fact]
    public void RiverPolylines_AreInWorldPixelSpace()
    {
        var ctx = _cache.GetThrough(TestSeed, HydrologyStageIndex);
        float maxCoord = C.WORLD_WIDTH_TILES * C.WORLD_TILE_PIXELS;
        foreach (var river in ctx.World.Rivers)
        foreach (var pt in river.Points)
        {
            Assert.True(pt.X >= 0 && pt.X <= maxCoord,
                $"River {river.Id} point X={pt.X} out of world-pixel range");
            Assert.True(pt.Y >= 0 && pt.Y <= maxCoord,
                $"River {river.Id} point Y={pt.Y} out of world-pixel range");
        }
    }

    [Theory]
    [InlineData(0xCAFEBABEUL)]
    [InlineData(0x12345678UL)]
    [InlineData(0xDEADBEEFUL)]
    public void RiverEndpoints_DrainToWaterOrWorldEdge(ulong seed)
    {
        var ctx   = _cache.GetThrough(seed, HydrologyStageIndex);
        var world = ctx.World;
        int W     = C.WORLD_WIDTH_TILES;
        int H     = C.WORLD_HEIGHT_TILES;

        int nonDrainingCount = 0;
        foreach (var river in world.Rivers)
        {
            if (river.Points.Count < 2) continue;
            var last = river.Points[^1];
            int lx   = Math.Clamp((int)(last.X / C.WORLD_TILE_PIXELS), 0, W - 1);
            int ly   = Math.Clamp((int)(last.Y / C.WORLD_TILE_PIXELS), 0, H - 1);

            var biome = world.Tiles[lx, ly].Biome;
            bool atWater = biome == BiomeId.Ocean || biome == BiomeId.Wetland ||
                           (world.Tiles[lx, ly].Features & FeatureFlags.HasRiver) != 0;
            bool atEdge  = lx == 0 || ly == 0 || lx == W - 1 || ly == H - 1;

            if (!atWater && !atEdge)
                nonDrainingCount++;
        }

        // Allow up to 10% non-draining rivers (noise from complex terrain)
        double failRate = world.Rivers.Count > 0
            ? (double)nonDrainingCount / world.Rivers.Count
            : 0.0;
        Assert.True(failRate <= 0.10,
            $"Seed {seed:X}: {nonDrainingCount}/{world.Rivers.Count} rivers do not drain to water ({failRate:P0})");
    }

    [Fact]
    public void EncounterDensityMap_IsPopulated()
    {
        var ctx = _cache.Get(TestSeed);
        Assert.NotNull(ctx.World.EncounterDensity);

        float sum = 0;
        int   W   = C.WORLD_WIDTH_TILES;
        int   H   = C.WORLD_HEIGHT_TILES;
        for (int y = 0; y < H; y++)
        for (int x = 0; x < W; x++)
            sum += ctx.World.EncounterDensity![x, y];
        Assert.True(sum > 0, "EncounterDensity map is all zeros");
    }
}
