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

namespace Theriapolis.Tests.Persistence;

/// <summary>
/// End-to-end M5 smoke test:
///   1. Generate a chunk via the streamer.
///   2. Mutate a tile (chop a tree → set Deco=None).
///   3. Flush → save → load.
///   4. Re-stream the chunk and verify the mutation is still applied.
///
/// This is the single most important save-correctness test — exercises the
/// streamer/delta-store/codec/restore loop in one shot.
/// </summary>
public sealed class DeltaPersistenceTests : IClassFixture<WorldCache>
{
    private const ulong TestSeed = 0xCAFEBABEUL;
    private readonly WorldCache _cache;
    public DeltaPersistenceTests(WorldCache c) => _cache = c;

    [Fact]
    public void ChopTree_PersistsAcrossSaveLoad()
    {
        var w = _cache.Get(TestSeed).World;

        // Find a chunk + tile that the baseline generator put a tree on.
        var (cc, lx, ly) = FindTreeTile(w);

        var deltasA = new InMemoryChunkDeltaStore();
        var streamerA = new ChunkStreamer(TestSeed, w, deltasA);
        var chunk = streamerA.Get(cc);
        Assert.Equal(TacticalDeco.Tree, chunk.Tiles[lx, ly].Deco);

        // Chop it.
        chunk.Tiles[lx, ly].Deco = TacticalDeco.None;
        chunk.HasDelta = true;

        // Flush + save.
        streamerA.FlushAll();
        var header = new SaveHeader { WorldSeedHex = $"0x{TestSeed:X}" };
        var body   = new SaveBody { Clock = new() { InGameSeconds = 0 } };
        body.Player = new() { Name = "Tester" };
        foreach (var kv in deltasA.All) body.ModifiedChunks[kv.Key] = kv.Value;
        var bytes = SaveCodec.Serialize(header, body);

        // ── Load on a fresh streamer ──
        var (_, rb) = SaveCodec.Deserialize(bytes);
        var deltasB = new InMemoryChunkDeltaStore();
        foreach (var kv in rb.ModifiedChunks) deltasB.Put(kv.Key, kv.Value);
        var streamerB = new ChunkStreamer(TestSeed, w, deltasB);
        var reloaded = streamerB.Get(cc);

        Assert.Equal(TacticalDeco.None, reloaded.Tiles[lx, ly].Deco);
    }

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

        // Touch a chunk without modifying it.
        var cc = new ChunkCoord(20, 20);
        streamer.Get(cc);
        streamer.FlushAll();
        Assert.Null(deltas.Get(cc));
    }

    private static (ChunkCoord cc, int lx, int ly) FindTreeTile(WorldState w)
    {
        // Walk a band of chunks centred near a known land settlement so we
        // don't waste time scanning ocean.
        var anchor = w.Settlements.First(s => !s.IsPoi && s.Tier <= 3);
        var centre = ChunkCoord.ForWorldTile(anchor.TileX, anchor.TileY);
        for (int dy = -3; dy <= 3; dy++)
        for (int dx = -3; dx <= 3; dx++)
        {
            var cc = new ChunkCoord(centre.X + dx, centre.Y + dy);
            var chunk = TacticalChunkGen.Generate(0xCAFEBABEUL, cc, w);
            for (int ly = 0; ly < C.TACTICAL_CHUNK_SIZE; ly++)
            for (int lx = 0; lx < C.TACTICAL_CHUNK_SIZE; lx++)
                if (chunk.Tiles[lx, ly].Deco == TacticalDeco.Tree)
                    return (cc, lx, ly);
        }
        throw new Xunit.Sdk.XunitException("no tree near settlement to chop in test");
    }
}
