using Theriapolis.Core;
using Theriapolis.Core.Data;
using Theriapolis.Core.Persistence;
using Theriapolis.Core.Rules.Character;
using Theriapolis.Core.Rules.Stats;
using Xunit;

namespace Theriapolis.Tests.Persistence;

/// <summary>
/// Phase 6.5 M0 — level-up history + subclass + learned-features round-trip.
/// Save a character at level 4 (with subclass + ASI history); load; assert
/// every per-level delta survives.
/// </summary>
public sealed class LevelUpRoundTripTests
{
    private readonly ContentResolver _content = new(new ContentLoader(TestHelpers.DataDirectory));

    private Character MakeWolfFangsworn()
    {
        var b = new CharacterBuilder
        {
            Clade        = _content.Clades["canidae"],
            Species      = _content.Species["wolf"],
            ClassDef     = _content.Classes["fangsworn"],
            Background   = _content.Backgrounds["pack_raised"],
            BaseAbilities = new AbilityScores(15, 12, 13, 10, 13, 8),
            Name         = "Tester",
        };
        int n = b.ClassDef.SkillsChoose;
        foreach (var raw in b.ClassDef.SkillOptions)
        {
            if (b.ChosenClassSkills.Count >= n) break;
            try { b.ChosenClassSkills.Add(SkillIdExtensions.FromJson(raw)); } catch { }
        }
        return b.Build();
    }

    private void LevelTo(Character c, int target)
    {
        while (c.Level < target)
        {
            int next = c.Level + 1;
            ulong seed = 0xCAFE_F00D_CAFE_F00DUL ^ (ulong)next;
            var r = LevelUpFlow.Compute(c, next, seed, takeAverage: true);
            var ch = new LevelUpChoices { TakeAverageHp = true };
            if (r.GrantsSubclassChoice && c.ClassDef.SubclassIds.Length > 0)
                ch.SubclassId = c.ClassDef.SubclassIds[0];
            if (r.GrantsAsiChoice)
                ch.AsiAdjustments = new() { { AbilityId.STR, 2 } };
            c.ApplyLevelUp(r, ch);
        }
    }

    [Fact]
    public void Character_AtLevel4_RoundTripsThroughCharacterCodec()
    {
        var c = MakeWolfFangsworn();
        c.Xp = 6_500;       // beyond level 4 threshold
        LevelTo(c, target: 4);

        Assert.Equal(4, c.Level);
        Assert.NotEmpty(c.SubclassId);    // L3 picker fired
        Assert.NotEmpty(c.LearnedFeatureIds);
        Assert.Equal(3, c.LevelUpHistory.Count);  // L2, L3, L4

        var snap     = CharacterCodec.Capture(c);
        var restored = CharacterCodec.Restore(snap, _content);

        Assert.Equal(4, restored.Level);
        Assert.Equal(c.SubclassId, restored.SubclassId);
        Assert.Equal(c.MaxHp, restored.MaxHp);
        Assert.Equal(c.LearnedFeatureIds.Count, restored.LearnedFeatureIds.Count);
        Assert.Equal(c.LearnedFeatureIds, restored.LearnedFeatureIds);
        Assert.Equal(c.LevelUpHistory.Count, restored.LevelUpHistory.Count);

        for (int i = 0; i < c.LevelUpHistory.Count; i++)
        {
            var a = c.LevelUpHistory[i];
            var b = restored.LevelUpHistory[i];
            Assert.Equal(a.Level,           b.Level);
            Assert.Equal(a.HpGained,        b.HpGained);
            Assert.Equal(a.HpWasAveraged,   b.HpWasAveraged);
            Assert.Equal(a.HpHitDieResult,  b.HpHitDieResult);
            Assert.Equal(a.SubclassChosen,  b.SubclassChosen);
            Assert.Equal(a.FeaturesUnlocked, b.FeaturesUnlocked);
            Assert.Equal(a.AsiAdjustmentsKeys,   b.AsiAdjustmentsKeys);
            Assert.Equal(a.AsiAdjustmentsValues, b.AsiAdjustmentsValues);
        }

        // ASI raised STR — level-4 history entry should record +2 STR.
        var lv4 = c.LevelUpHistory[^1];
        Assert.Equal(4, lv4.Level);
        Assert.Single(lv4.AsiAdjustmentsValues);
        Assert.Equal(2, lv4.AsiAdjustmentsValues[0]);
        // And the ability is preserved across the round-trip.
        Assert.Equal(c.Abilities.STR, restored.Abilities.STR);
    }

    [Fact]
    public void Character_AtLevel4_RoundTripsThroughBinarySaveCodec()
    {
        var c = MakeWolfFangsworn();
        c.Xp = 6_500;
        LevelTo(c, target: 4);

        var header = new SaveHeader { Version = C.SAVE_SCHEMA_VERSION, WorldSeedHex = "0xFEED" };
        var body   = new SaveBody { PlayerCharacter = CharacterCodec.Capture(c) };
        body.Player.Id   = 1;
        body.Player.Name = "Tester";

        var bytes = SaveCodec.Serialize(header, body);
        var (h2, body2) = SaveCodec.Deserialize(bytes);

        Assert.Equal(header.Version, h2.Version);
        Assert.NotNull(body2.PlayerCharacter);
        Assert.Equal(4, body2.PlayerCharacter!.Level);
        Assert.NotEmpty(body2.PlayerCharacter.SubclassId);
        Assert.Equal(3, body2.PlayerCharacter.LevelUpHistory.Length);
        Assert.Equal(c.LearnedFeatureIds.Count, body2.PlayerCharacter.LearnedFeatureIds.Length);
    }

    [Fact]
    public void V6Save_WithoutLevelUpFields_LoadsAsLevel1Character()
    {
        // Simulate a v6 save by writing a character without the v7 trailing
        // fields. Easiest path: hand-construct a minimal PlayerCharacterState
        // (the codec's EOS-check pattern handles missing trailing data on read).
        var c = MakeWolfFangsworn();
        var snap = CharacterCodec.Capture(c);
        // Force the v7 fields to defaults to simulate a v6 save.
        snap.SubclassId        = "";
        snap.LearnedFeatureIds = Array.Empty<string>();
        snap.LevelUpHistory    = Array.Empty<LevelUpRecordState>();

        var restored = CharacterCodec.Restore(snap, _content);
        Assert.Equal(1, restored.Level);
        Assert.Empty(restored.SubclassId);
        Assert.Empty(restored.LevelUpHistory);
    }
}
