UnitTests/SlotDispatcherTests.cs
#nullable enable
using System.Collections.Generic;
using HitShapes;
using Sandbox;
using Xunit;

namespace HitShapes.Tests;

public class SlotDispatcherTests
{
    // 2-slot horizontal split: left half = slot 0, right half = slot 1, outside rect = null.
    static IHitShape TwoSlot()
        => HitShape.CustomRaw(slotCount: 2, (pos, size) =>
        {
            if (pos.x < 0 || pos.y < 0 || pos.x >= size.x || pos.y >= size.y) return null;
            return pos.x < size.x * 0.5f ? 0 : 1;
        });

    [Fact]
    public void Null_to_A_fires_OnSlotEnter_only()
    {
        var events = new List<string>();
        var disp = new SlotDispatcher(TwoSlot())
        {
            OnSlotEnter = s => events.Add($"enter:{s}"),
            OnSlotLeave = s => events.Add($"leave:{s}"),
        };

        disp.UpdateHoverAt(new Vector2(20, 50), new Vector2(100, 100));

        Assert.Equal(new[] { "enter:0" }, events);
        Assert.Equal(0, disp.CurrentSlot);
    }

    [Fact]
    public void A_to_A_fires_nothing()
    {
        var events = new List<string>();
        var disp = new SlotDispatcher(TwoSlot())
        {
            OnSlotEnter = s => events.Add($"enter:{s}"),
            OnSlotLeave = s => events.Add($"leave:{s}"),
        };

        disp.UpdateHoverAt(new Vector2(20, 50), new Vector2(100, 100));
        events.Clear();
        disp.UpdateHoverAt(new Vector2(30, 50), new Vector2(100, 100));

        Assert.Empty(events);
    }

    [Fact]
    public void A_to_B_fires_leave_then_enter_in_order()
    {
        var events = new List<string>();
        var disp = new SlotDispatcher(TwoSlot())
        {
            OnSlotEnter = s => events.Add($"enter:{s}"),
            OnSlotLeave = s => events.Add($"leave:{s}"),
        };

        disp.UpdateHoverAt(new Vector2(20, 50), new Vector2(100, 100));
        events.Clear();
        disp.UpdateHoverAt(new Vector2(80, 50), new Vector2(100, 100));

        Assert.Equal(new[] { "leave:0", "enter:1" }, events);
        Assert.Equal(1, disp.CurrentSlot);
    }

    [Fact]
    public void B_to_null_fires_leave_only()
    {
        var events = new List<string>();
        var disp = new SlotDispatcher(TwoSlot())
        {
            OnSlotEnter = s => events.Add($"enter:{s}"),
            OnSlotLeave = s => events.Add($"leave:{s}"),
        };

        disp.UpdateHoverAt(new Vector2(80, 50), new Vector2(100, 100));
        events.Clear();
        disp.UpdateHoverAt(new Vector2(-10, 50), new Vector2(100, 100));

        Assert.Equal(new[] { "leave:1" }, events);
        Assert.Null(disp.CurrentSlot);
    }

    [Fact]
    public void Reset_clears_state_without_firing_leave()
    {
        var events = new List<string>();
        var disp = new SlotDispatcher(TwoSlot())
        {
            OnSlotEnter = s => events.Add($"enter:{s}"),
            OnSlotLeave = s => events.Add($"leave:{s}"),
        };

        disp.UpdateHoverAt(new Vector2(20, 50), new Vector2(100, 100));
        events.Clear();
        disp.Reset();

        Assert.Empty(events);
        Assert.Null(disp.CurrentSlot);
    }

    [Fact]
    public void Null_shape_throws()
    {
        Assert.Throws<System.ArgumentNullException>(() => new SlotDispatcher(null!));
    }
}