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

namespace Theriapolis.Tests.Worldgen;

/// <summary>
/// Settlement placement correctness: narrative anchors, tier counts, distances,
/// reachability, and no overlaps.
/// </summary>
public sealed class SettlementTests : IClassFixture<WorldCache>
{
    private const ulong TestSeed = 0xCAFEBABEUL;
    private readonly WorldCache _cache;

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

    // ── Narrative anchors ─────────────────────────────────────────────────────

    [Fact]
    public void SanctumFidelis_IsPresent()
    {
        var ctx = _cache.Get(TestSeed);
        var capital = ctx.World.Settlements
            .FirstOrDefault(s => s.Anchor == NarrativeAnchor.SanctumFidelis);
        Assert.NotNull(capital);
        Assert.Equal(1, capital!.Tier);
    }

    [Fact]
    public void AllNarrativeAnchors_ArePlaced()
    {
        var ctx = _cache.Get(TestSeed);
        var anchors = ctx.World.Settlements
            .Where(s => s.Anchor.HasValue)
            .Select(s => s.Anchor!.Value)
            .ToHashSet();

        foreach (NarrativeAnchor anchor in Enum.GetValues<NarrativeAnchor>())
            Assert.Contains(anchor, anchors);
    }

    [Theory]
    [InlineData(0xCAFEBABEUL)]
    [InlineData(0x11111111UL)]
    [InlineData(0x99887766UL)]
    [InlineData(0xABCDEF01UL)]
    [InlineData(0x00000042UL)]
    public void NarrativeAnchors_PlacedAcrossMultipleSeeds(ulong seed)
    {
        var ctx = _cache.Get(seed);
        var capital = ctx.World.Settlements
            .FirstOrDefault(s => s.Anchor == NarrativeAnchor.SanctumFidelis);
        Assert.NotNull(capital);
    }

    // ── Tier counts ───────────────────────────────────────────────────────────

    [Fact]
    public void TierCounts_MeetMinimums()
    {
        var ctx = _cache.Get(TestSeed);
        var ss  = ctx.World.Settlements.Where(s => !s.IsPoi).ToList();

        int tier1 = ss.Count(s => s.Tier == 1);
        int tier2 = ss.Count(s => s.Tier == 2);
        int tier3 = ss.Count(s => s.Tier == 3);
        int tier4 = ss.Count(s => s.Tier == 4);

        Assert.Equal(1, tier1); // exactly one capital
        Assert.InRange(tier2, C.SETTLE_TIER2_MIN, C.SETTLE_TIER2_MAX);
        Assert.InRange(tier3, C.SETTLE_TIER3_MIN, C.SETTLE_TIER3_MAX);
        Assert.True(tier4 >= C.SETTLE_TIER4_MIN,
            $"Only {tier4} tier-4 settlements, need at least {C.SETTLE_TIER4_MIN}");
    }

    [Fact]
    public void PoICount_MeetsMinimum()
    {
        var ctx  = _cache.Get(TestSeed);
        int pois = ctx.World.Settlements.Count(s => s.IsPoi);
        Assert.True(pois >= C.SETTLE_TIER5_MIN,
            $"Only {pois} PoIs, need at least {C.SETTLE_TIER5_MIN}");
    }

    // ── No overlapping settlements ─────────────────────────────────────────────

    [Fact]
    public void NoSettlements_ShareTheSameTile()
    {
        var ctx       = _cache.Get(TestSeed);
        var positions = new HashSet<(int, int)>();
        foreach (var s in ctx.World.Settlements)
        {
            bool added = positions.Add((s.TileX, s.TileY));
            Assert.True(added,
                $"Two settlements share tile ({s.TileX},{s.TileY})");
        }
    }

    // ── Minimum separation ─────────────────────────────────────────────────────

    [Fact]
    public void Tier1And2Settlements_MeetMinimumDistance()
    {
        var ctx  = _cache.Get(TestSeed);
        var high = ctx.World.Settlements.Where(s => s.Tier <= 2 && !s.IsPoi).ToList();
        int minSq = C.SETTLE_MIN_DIST_TIER2 * C.SETTLE_MIN_DIST_TIER2;

        for (int i = 0; i < high.Count; i++)
        for (int j = i + 1; j < high.Count; j++)
        {
            int dx = high[i].TileX - high[j].TileX;
            int dy = high[i].TileY - high[j].TileY;
            Assert.True(dx * dx + dy * dy >= minSq,
                $"{high[i].Name} and {high[j].Name} are too close ({Math.Sqrt(dx*dx+dy*dy):F0} tiles, min {C.SETTLE_MIN_DIST_TIER2})");
        }
    }

    // ── Settlement attributes ─────────────────────────────────────────────────

    [Fact]
    public void AllSettlements_HaveNames()
    {
        var ctx = _cache.Get(TestSeed);
        foreach (var s in ctx.World.Settlements.Where(s => !s.IsPoi))
            Assert.False(string.IsNullOrWhiteSpace(s.Name),
                $"Settlement at ({s.TileX},{s.TileY}) Tier {s.Tier} has no name");
    }

    [Fact]
    public void AllSettlements_HaveValidTileCoordinates()
    {
        var ctx = _cache.Get(TestSeed);
        int W   = C.WORLD_WIDTH_TILES;
        int H   = C.WORLD_HEIGHT_TILES;
        foreach (var s in ctx.World.Settlements)
        {
            Assert.InRange(s.TileX, 0, W - 1);
            Assert.InRange(s.TileY, 0, H - 1);
        }
    }

    [Fact]
    public void NoSettlement_PlacedOnOcean()
    {
        var ctx = _cache.Get(TestSeed);
        foreach (var s in ctx.World.Settlements)
        {
            var biome = ctx.World.Tiles[s.TileX, s.TileY].Biome;
            Assert.NotEqual(BiomeId.Ocean, biome);
        }
    }

    // ── Faction influence ─────────────────────────────────────────────────────

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

        // Capital area should have high Enforcer influence
        var capital = ctx.World.Settlements
            .First(s => s.Anchor == NarrativeAnchor.SanctumFidelis);
        float enforcer = ctx.World.FactionInfluence!
            .Get((int)FactionId.CovenantEnforcers, capital.TileX, capital.TileY);
        Assert.True(enforcer > 0.5f,
            $"Enforcer influence at capital is only {enforcer:F3}");
    }
}
