using Theriapolis.Core;
using Theriapolis.Core.Entities;
using Theriapolis.Core.Persistence;
using Theriapolis.Core.Tactical;
using Theriapolis.Core.Time;
using Xunit;

namespace Theriapolis.Tests.Persistence;

/// <summary>
/// SaveCodec round-trip: every field we write should reappear after Serialize
/// → Deserialize. The Phase-4 reserved fields (Flags, Factions, etc.) are
/// covered with at least one entry each so future schema bumps notice if a
/// section gets accidentally dropped.
/// </summary>
public sealed class SaveCodecRoundTripTests
{
    [Fact]
    public void Roundtrip_PreservesPlayerAndClock()
    {
        var header = new SaveHeader
        {
            WorldSeedHex  = "0xDEADBEEF",
            PlayerName    = "Grev",
            PlayerTier    = 2,
            InGameSeconds = 12345,
            SavedAtUtc    = "2026-04-21T09:00:00Z",
        };
        header.StageHashes["ElevationGen"] = "0xABCDEF01";
        header.StageHashes["BiomeAssign"]  = "0x11223344";

        var body = new SaveBody
        {
            Player = new PlayerActorState
            {
                Id = 7,
                Name = "Grev",
                PositionX = 1234.5f, PositionY = 678.9f,
                FacingAngleRad = MathF.PI * 0.5f,
                SpeedWorldPxPerSec = 80f,
                HighestTierReached = 2,
                DiscoveredPoiIds = new[] { 3, 14, 159 },
            },
            Clock = new WorldClockState { InGameSeconds = 12345 },
        };
        body.Flags["acts:1:complete"] = 1;

        var bytes = SaveCodec.Serialize(header, body);
        var (rh, rb) = SaveCodec.Deserialize(bytes);

        Assert.Equal(header.WorldSeedHex, rh.WorldSeedHex);
        Assert.Equal(header.PlayerName,   rh.PlayerName);
        Assert.Equal(2,                    rh.StageHashes.Count);
        Assert.Equal("0xABCDEF01",         rh.StageHashes["ElevationGen"]);

        Assert.Equal(7, rb.Player.Id);
        Assert.Equal("Grev", rb.Player.Name);
        Assert.Equal(1234.5f, rb.Player.PositionX);
        Assert.Equal(MathF.PI * 0.5f, rb.Player.FacingAngleRad);
        Assert.Equal(2, rb.Player.HighestTierReached);
        Assert.Equal(new[] { 3, 14, 159 }, rb.Player.DiscoveredPoiIds);
        Assert.Equal(12345L, rb.Clock.InGameSeconds);
        Assert.Equal(1, rb.Flags["acts:1:complete"]);
    }

    [Fact]
    public void Roundtrip_PreservesChunkDeltas()
    {
        var header = new SaveHeader { WorldSeedHex = "0x1" };
        var body = new SaveBody
        {
            Player = new PlayerActorState { Name = "X" },
            Clock  = new WorldClockState  { InGameSeconds = 0 },
        };
        var d = new ChunkDelta { SpawnsConsumed = true };
        d.TileMods.Add(new TileMod(5, 7, TacticalSurface.Cobble, TacticalDeco.None, (byte)TacticalFlags.Road));
        d.TileMods.Add(new TileMod(8, 8, TacticalSurface.Mud,    TacticalDeco.Boulder, 0));
        body.ModifiedChunks[new ChunkCoord(3, 4)] = d;

        var bytes = SaveCodec.Serialize(header, body);
        var (_, rb) = SaveCodec.Deserialize(bytes);

        Assert.True(rb.ModifiedChunks.ContainsKey(new ChunkCoord(3, 4)));
        var rd = rb.ModifiedChunks[new ChunkCoord(3, 4)];
        Assert.True(rd.SpawnsConsumed);
        Assert.Equal(2, rd.TileMods.Count);
        Assert.Equal(TacticalSurface.Cobble, rd.TileMods[0].Surface);
        Assert.Equal(TacticalDeco.Boulder,    rd.TileMods[1].Deco);
    }

    [Fact]
    public void Roundtrip_PreservesWorldTileDeltas()
    {
        var header = new SaveHeader { WorldSeedHex = "0x2" };
        var body = new SaveBody
        {
            Player = new PlayerActorState { Name = "Y" },
            Clock  = new WorldClockState  { InGameSeconds = 0 },
            ModifiedWorldTiles = { new WorldTileDelta(50, 80, 7, 0xAB) },
        };

        var bytes = SaveCodec.Serialize(header, body);
        var (_, rb) = SaveCodec.Deserialize(bytes);

        Assert.Single(rb.ModifiedWorldTiles);
        Assert.Equal(50, rb.ModifiedWorldTiles[0].X);
        Assert.Equal(80, rb.ModifiedWorldTiles[0].Y);
        Assert.Equal(7,  rb.ModifiedWorldTiles[0].NewBiome);
        Assert.Equal(0xAB, rb.ModifiedWorldTiles[0].NewFeatures);
    }

    [Fact]
    public void DeserializeHeaderOnly_DoesNotTouchBody()
    {
        var header = new SaveHeader { WorldSeedHex = "0xCAFE", PlayerName = "Solo" };
        var body = new SaveBody
        {
            Player = new PlayerActorState { Id = 99, Name = "Solo" },
            Clock  = new WorldClockState  { InGameSeconds = 1 },
        };
        var bytes = SaveCodec.Serialize(header, body);
        var only = SaveCodec.DeserializeHeaderOnly(bytes);
        Assert.Equal("0xCAFE", only.WorldSeedHex);
        Assert.Equal("Solo",   only.PlayerName);
    }

    [Fact]
    public void Roundtrip_HandlesEmptyBody()
    {
        var header = new SaveHeader { WorldSeedHex = "0x0" };
        var body = new SaveBody();
        var bytes = SaveCodec.Serialize(header, body);
        var (rh, rb) = SaveCodec.Deserialize(bytes);
        Assert.Equal("0x0", rh.WorldSeedHex);
        Assert.Equal(0L, rb.Clock.InGameSeconds);
        Assert.Empty(rb.ModifiedChunks);
        Assert.Empty(rb.ModifiedWorldTiles);
    }

    [Fact]
    public void SchemaVersion_IsCurrent()
    {
        // Bumped to 8 in Phase 7 M0 (Phase 6.5 was 7; Phase 6 was 6; Phase 5
        // was 5; Phase 4 was 4). The header auto-stamps the current schema
        // version on construction so a save written today carries the latest
        // version. Each bump must add a chained ISaveMigration in
        // <see cref="Theriapolis.Core.Persistence.SaveMigrations.Migrations"/>.
        Assert.Equal(8, C.SAVE_SCHEMA_VERSION);
        var h = new SaveHeader();
        Assert.Equal(C.SAVE_SCHEMA_VERSION, h.Version);
    }
}
