using Theriapolis.Core;
using Theriapolis.Core.Tactical;
using Theriapolis.Core.Util;
using Xunit;

namespace Theriapolis.Tests.Tactical;

/// <summary>
/// Streamer-level invariants: caching, eviction, and delta round-trip.
/// </summary>
public sealed class ChunkStreamerTests : IClassFixture<WorldCache>
{
    private const ulong TestSeed = 0xCAFEBABEUL;
    private readonly WorldCache _cache;
    public ChunkStreamerTests(WorldCache c) => _cache = c;

    [Fact]
    public void Get_CachesSubsequentCalls()
    {
        var w = _cache.Get(TestSeed).World;
        var streamer = new ChunkStreamer(TestSeed, w, new InMemoryChunkDeltaStore());
        var cc = new ChunkCoord(3, 3);
        var first = streamer.Get(cc);
        var second = streamer.Get(cc);
        Assert.Same(first, second);
    }

    [Fact]
    public void EnsureLoaded_PopulatesCacheNearPlayer()
    {
        var w = _cache.Get(TestSeed).World;
        var streamer = new ChunkStreamer(TestSeed, w, new InMemoryChunkDeltaStore());
        // Centre on world tile (50, 50) → tactical-pixel (1600, 1600).
        var pos = new Vec2(50 * C.WORLD_TILE_PIXELS, 50 * C.WORLD_TILE_PIXELS);
        streamer.EnsureLoadedAround(pos, worldTileRadius: C.TACTICAL_WINDOW_WORLD_TILES);
        Assert.NotEmpty(streamer.Loaded);
    }

    [Fact]
    public void DeltaRoundtrip_PreservesTileEdits()
    {
        var w = _cache.Get(TestSeed).World;
        var deltas = new InMemoryChunkDeltaStore();
        var streamer = new ChunkStreamer(TestSeed, w, deltas);
        var cc = new ChunkCoord(8, 8);

        var chunk = streamer.Get(cc);
        // Pick a known tile, edit it, mark the chunk dirty.
        ref var t = ref chunk.Tiles[10, 10];
        var origSurface = t.Surface;
        var newSurface  = origSurface == TacticalSurface.Cobble ? TacticalSurface.Sand : TacticalSurface.Cobble;
        t.Surface = newSurface;
        t.Deco    = TacticalDeco.None;
        chunk.HasDelta = true;

        // Force the streamer above the cache cap so the chunk gets evicted
        // (and its delta flushed). Easiest way: load enough other chunks.
        for (int i = 0; i < C.CHUNK_CACHE_SOFT_MAX + 2; i++)
            streamer.Get(new ChunkCoord(100 + i, 100));
        streamer.EnsureLoadedAround(new Vec2(100 * C.WORLD_TILE_PIXELS, 100 * C.WORLD_TILE_PIXELS),
                                     worldTileRadius: 1);

        // Now reload our edited chunk and verify the delta was reapplied.
        var reloaded = streamer.Get(cc);
        Assert.Equal(newSurface, reloaded.Tiles[10, 10].Surface);
    }

    [Fact]
    public void FlushAll_PersistsModifiedChunks()
    {
        var w = _cache.Get(TestSeed).World;
        var deltas = new InMemoryChunkDeltaStore();
        var streamer = new ChunkStreamer(TestSeed, w, deltas);
        var cc = new ChunkCoord(2, 2);
        var chunk = streamer.Get(cc);
        chunk.Tiles[5, 5].Deco = TacticalDeco.Boulder;
        chunk.HasDelta = true;

        streamer.FlushAll();
        Assert.NotNull(deltas.Get(cc));
        Assert.NotEmpty(deltas.Get(cc)!.TileMods);
    }
}
