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

namespace Theriapolis.Tests.Rules;

/// <summary>
/// Phase 6.5 M2 — SubclassResolver covers the pure look-up surface
/// (subclass id → unlocked feature ids per level, feature def lookup).
/// All subclass mechanics are JSON-driven; tests run against the live
/// content set so authoring drift is caught here.
/// </summary>
public sealed class SubclassResolverTests
{
    private readonly ContentResolver _content = new(new ContentLoader(TestHelpers.DataDirectory));

    [Fact]
    public void TryFindSubclass_ReturnsDefForKnownId()
    {
        var def = SubclassResolver.TryFindSubclass(_content.Subclasses, "pack_forged");
        Assert.NotNull(def);
        Assert.Equal("fangsworn", def!.ClassId);
    }

    [Fact]
    public void TryFindSubclass_NullForEmptyId()
    {
        Assert.Null(SubclassResolver.TryFindSubclass(_content.Subclasses, ""));
        Assert.Null(SubclassResolver.TryFindSubclass(_content.Subclasses, null));
    }

    [Fact]
    public void TryFindSubclass_NullForUnknownId()
    {
        Assert.Null(SubclassResolver.TryFindSubclass(_content.Subclasses, "definitely_not_a_subclass"));
    }

    [Fact]
    public void UnlockedFeaturesAt_Level3_ReturnsFirstFeature()
    {
        var features = SubclassResolver.UnlockedFeaturesAt(_content.Subclasses, "pack_forged", 3);
        Assert.NotEmpty(features);
        Assert.Contains("packmates_howl", features);
    }

    [Fact]
    public void UnlockedFeaturesAt_LevelWithoutEntry_ReturnsEmpty()
    {
        // Pack-Forged has entries at L3, L7, L10, L15, L18 — L4 is empty.
        var features = SubclassResolver.UnlockedFeaturesAt(_content.Subclasses, "pack_forged", 4);
        Assert.Empty(features);
    }

    [Fact]
    public void UnlockedFeaturesAt_NullSubclassId_ReturnsEmpty()
    {
        Assert.Empty(SubclassResolver.UnlockedFeaturesAt(_content.Subclasses, null, 3));
        Assert.Empty(SubclassResolver.UnlockedFeaturesAt(_content.Subclasses, "", 3));
    }

    [Fact]
    public void ResolveFeatureDef_FindsSubclassFeature()
    {
        var subclass = _content.Subclasses["pack_forged"];
        var classDef = _content.Classes["fangsworn"];
        var fdef = SubclassResolver.ResolveFeatureDef(classDef, subclass, "packmates_howl");
        Assert.NotNull(fdef);
        Assert.Equal("Packmate's Howl", fdef!.Name);
    }

    [Fact]
    public void ResolveFeatureDef_FallsThroughToClassDefForSharedIds()
    {
        var subclass = _content.Subclasses["pack_forged"];
        var classDef = _content.Classes["fangsworn"];
        // 'asi' is in the class feature_definitions (shared across subclasses).
        var fdef = SubclassResolver.ResolveFeatureDef(classDef, subclass, "asi");
        Assert.NotNull(fdef);
    }

    [Fact]
    public void ResolveFeatureDef_NullForUnknownId()
    {
        var subclass = _content.Subclasses["pack_forged"];
        var classDef = _content.Classes["fangsworn"];
        Assert.Null(SubclassResolver.ResolveFeatureDef(classDef, subclass, "completely_made_up_feature"));
    }

    [Fact]
    public void EveryClass_HasAtLeastOneSubclass()
    {
        // Smoke: every class declared in classes.json should resolve to at
        // least one entry in subclasses.json.
        foreach (var cls in _content.Classes.Values)
        {
            Assert.NotEmpty(cls.SubclassIds);
            foreach (var sid in cls.SubclassIds)
            {
                Assert.True(_content.Subclasses.ContainsKey(sid),
                    $"class '{cls.Id}' references unknown subclass '{sid}'");
            }
        }
    }

    [Fact]
    public void EverySubclass_HasLevel3Features()
    {
        // M2 ship-point: every authored subclass should have at least one
        // level-3 feature so the L3 unlock fires meaningfully.
        foreach (var sub in _content.Subclasses.Values)
        {
            var l3 = SubclassResolver.UnlockedFeaturesAt(_content.Subclasses, sub.Id, 3);
            Assert.NotEmpty(l3);
        }
    }
}
