using OpenNest.CNC;
using OpenNest.Collections;
using OpenNest.Geometry;

namespace OpenNest.Tests;

public class PlateManagerTests
{
    private static Nest CreateNest()
    {
        var nest = new Nest("test");
        return nest;
    }

    private static Part MakePart()
    {
        var pgm = new Program();
        pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
        pgm.Codes.Add(new LinearMove(new Vector(10, 0)));
        pgm.Codes.Add(new LinearMove(new Vector(10, 10)));
        pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
        var drawing = new Drawing("test", pgm);
        return new Part(drawing);
    }

    [Fact]
    public void Constructor_EmptyNest_CurrentIndexZero()
    {
        var nest = CreateNest();
        using var mgr = new PlateManager(nest);
        Assert.Equal(0, mgr.CurrentIndex);
    }

    [Fact]
    public void Constructor_NestWithPlates_CurrentIndexZero()
    {
        var nest = CreateNest();
        nest.CreatePlate();
        nest.CreatePlate();
        using var mgr = new PlateManager(nest);
        Assert.Equal(0, mgr.CurrentIndex);
    }

    [Fact]
    public void CurrentPlate_ReturnsPlateAtCurrentIndex()
    {
        var nest = CreateNest();
        var plate = nest.CreatePlate();
        using var mgr = new PlateManager(nest);
        Assert.Same(plate, mgr.CurrentPlate);
    }

    [Fact]
    public void CurrentPlate_EmptyNest_ReturnsNull()
    {
        var nest = CreateNest();
        using var mgr = new PlateManager(nest);
        Assert.Null(mgr.CurrentPlate);
    }

    [Fact]
    public void Count_DelegatesToNestPlates()
    {
        var nest = CreateNest();
        nest.CreatePlate();
        nest.CreatePlate();
        using var mgr = new PlateManager(nest);
        Assert.Equal(2, mgr.Count);
    }

    [Fact]
    public void LoadFirst_SetsCurrentIndexToZero()
    {
        var nest = CreateNest();
        nest.CreatePlate();
        nest.CreatePlate();
        using var mgr = new PlateManager(nest);
        mgr.LoadLast();
        mgr.LoadFirst();
        Assert.Equal(0, mgr.CurrentIndex);
    }

    [Fact]
    public void LoadLast_SetsCurrentIndexToLastPlate()
    {
        var nest = CreateNest();
        nest.CreatePlate();
        nest.CreatePlate();
        nest.CreatePlate();
        using var mgr = new PlateManager(nest);
        mgr.LoadLast();
        Assert.Equal(2, mgr.CurrentIndex);
    }

    [Fact]
    public void LoadNext_AdvancesIndex()
    {
        var nest = CreateNest();
        nest.CreatePlate();
        nest.CreatePlate();
        using var mgr = new PlateManager(nest);
        var result = mgr.LoadNext();
        Assert.True(result);
        Assert.Equal(1, mgr.CurrentIndex);
    }

    [Fact]
    public void LoadNext_AtEnd_ReturnsFalse()
    {
        var nest = CreateNest();
        nest.CreatePlate();
        using var mgr = new PlateManager(nest);
        var result = mgr.LoadNext();
        Assert.False(result);
        Assert.Equal(0, mgr.CurrentIndex);
    }

    [Fact]
    public void LoadPrevious_DecrementsIndex()
    {
        var nest = CreateNest();
        nest.CreatePlate();
        nest.CreatePlate();
        using var mgr = new PlateManager(nest);
        mgr.LoadLast();
        var result = mgr.LoadPrevious();
        Assert.True(result);
        Assert.Equal(0, mgr.CurrentIndex);
    }

    [Fact]
    public void LoadPrevious_AtStart_ReturnsFalse()
    {
        var nest = CreateNest();
        nest.CreatePlate();
        using var mgr = new PlateManager(nest);
        var result = mgr.LoadPrevious();
        Assert.False(result);
        Assert.Equal(0, mgr.CurrentIndex);
    }

    [Fact]
    public void LoadAt_SetsExactIndex()
    {
        var nest = CreateNest();
        nest.CreatePlate();
        nest.CreatePlate();
        nest.CreatePlate();
        using var mgr = new PlateManager(nest);
        mgr.LoadAt(2);
        Assert.Equal(2, mgr.CurrentIndex);
    }

    [Fact]
    public void IsFirst_WhenAtStart_ReturnsTrue()
    {
        var nest = CreateNest();
        nest.CreatePlate();
        using var mgr = new PlateManager(nest);
        Assert.True(mgr.IsFirst);
    }

    [Fact]
    public void IsLast_WhenAtEnd_ReturnsTrue()
    {
        var nest = CreateNest();
        nest.CreatePlate();
        using var mgr = new PlateManager(nest);
        Assert.True(mgr.IsLast);
    }

    [Fact]
    public void IsFirst_WhenNotAtStart_ReturnsFalse()
    {
        var nest = CreateNest();
        nest.CreatePlate();
        nest.CreatePlate();
        using var mgr = new PlateManager(nest);
        mgr.LoadLast();
        Assert.False(mgr.IsFirst);
    }

    [Fact]
    public void Navigation_FiresCurrentPlateChanged()
    {
        var nest = CreateNest();
        nest.CreatePlate();
        nest.CreatePlate();
        using var mgr = new PlateManager(nest);
        PlateChangedEventArgs received = null;
        mgr.CurrentPlateChanged += (s, e) => received = e;
        mgr.LoadNext();
        Assert.NotNull(received);
        Assert.Equal(1, received.Index);
        Assert.Same(nest.Plates[1], received.Plate);
    }

    [Fact]
    public void Dispose_UnsubscribesFromPlateEvents()
    {
        var nest = CreateNest();
        using var mgr = new PlateManager(nest);
        var eventFired = false;
        mgr.PlateListChanged += (s, e) => eventFired = true;
        mgr.Dispose();
        nest.CreatePlate();
        Assert.False(eventFired);
    }

    // Task 3: Sentinel plate invariant

    [Fact]
    public void EnsureSentinel_EmptyNest_CreatesOnePlate()
    {
        var nest = CreateNest();
        using var mgr = new PlateManager(nest);
        mgr.EnsureSentinel();
        Assert.Equal(1, nest.Plates.Count);
        Assert.Equal(0, nest.Plates[0].Parts.Count);
    }

    [Fact]
    public void EnsureSentinel_LastPlateHasParts_CreatesNewEmpty()
    {
        var nest = CreateNest();
        var plate = nest.CreatePlate();
        plate.Parts.Add(MakePart());
        using var mgr = new PlateManager(nest);
        mgr.EnsureSentinel();
        Assert.Equal(2, nest.Plates.Count);
        Assert.Equal(0, nest.Plates[^1].Parts.Count);
    }

    [Fact]
    public void EnsureSentinel_TwoTrailingEmpties_TrimsToOne()
    {
        var nest = CreateNest();
        nest.CreatePlate();
        nest.CreatePlate();
        using var mgr = new PlateManager(nest);
        mgr.EnsureSentinel();
        Assert.Equal(1, nest.Plates.Count);
    }

    [Fact]
    public void EnsureSentinel_PreservesCurrentIndex()
    {
        var nest = CreateNest();
        var plate = nest.CreatePlate();
        plate.Parts.Add(MakePart());
        using var mgr = new PlateManager(nest);
        mgr.EnsureSentinel();
        Assert.Equal(0, mgr.CurrentIndex);
        Assert.Same(plate, mgr.CurrentPlate);
    }

    // Task 4: Reactive tail-plate subscriptions

    [Fact]
    public void PartAddedToLastPlate_CreatesSentinel()
    {
        var nest = CreateNest();
        nest.CreatePlate();
        using var mgr = new PlateManager(nest);
        mgr.EnsureSentinel();
        var initialCount = nest.Plates.Count;
        nest.Plates[^1].Parts.Add(MakePart());
        Assert.Equal(initialCount + 1, nest.Plates.Count);
        Assert.Equal(0, nest.Plates[^1].Parts.Count);
    }

    [Fact]
    public void PartRemovedFromSecondToLast_TrimsExtraEmpty()
    {
        var nest = CreateNest();
        var plate = nest.CreatePlate();
        plate.Parts.Add(MakePart());
        using var mgr = new PlateManager(nest);
        mgr.EnsureSentinel();
        Assert.Equal(2, nest.Plates.Count);
        nest.Plates[0].Parts.RemoveAt(0);
        Assert.Equal(1, nest.Plates.Count);
    }

    [Fact]
    public void ReactiveSubscription_ResubscribesAfterPlateListChange()
    {
        var nest = CreateNest();
        var plate1 = nest.CreatePlate();
        plate1.Parts.Add(MakePart());
        using var mgr = new PlateManager(nest);
        mgr.EnsureSentinel();
        nest.Plates[^1].Parts.Add(MakePart());
        Assert.Equal(3, nest.Plates.Count);
        nest.Plates[^1].Parts.Add(MakePart());
        Assert.Equal(4, nest.Plates.Count);
        Assert.Equal(0, nest.Plates[^1].Parts.Count);
    }

    // Task 5: Batch mode and plate operations

    [Fact]
    public void BeginBatch_DefersSentinelEnforcement()
    {
        var nest = CreateNest();
        var plate = nest.CreatePlate();
        plate.Parts.Add(MakePart());
        using var mgr = new PlateManager(nest);
        mgr.EnsureSentinel();

        mgr.BeginBatch();
        nest.Plates[^1].Parts.Add(MakePart());
        Assert.Equal(2, nest.Plates.Count);
    }

    [Fact]
    public void EndBatch_EnforcesSentinelAndFiresEvents()
    {
        var nest = CreateNest();
        var plate = nest.CreatePlate();
        plate.Parts.Add(MakePart());
        using var mgr = new PlateManager(nest);
        mgr.EnsureSentinel();

        mgr.BeginBatch();
        nest.Plates[^1].Parts.Add(MakePart());

        var listChangedCount = 0;
        mgr.PlateListChanged += (s, e) => listChangedCount++;

        mgr.EndBatch();

        Assert.Equal(3, nest.Plates.Count);
        Assert.Equal(0, nest.Plates[^1].Parts.Count);
        Assert.True(listChangedCount > 0);
    }

    [Fact]
    public void GetOrCreateEmpty_ReturnsSentinelWhenEmpty()
    {
        var nest = CreateNest();
        var plate1 = nest.CreatePlate();
        plate1.Parts.Add(MakePart());
        using var mgr = new PlateManager(nest);
        mgr.EnsureSentinel();

        var result = mgr.GetOrCreateEmpty();
        Assert.Same(nest.Plates[^1], result);
        Assert.Equal(0, result.Parts.Count);
    }

    [Fact]
    public void GetOrCreateEmpty_NoEmptyPlate_CreatesNew()
    {
        var nest = CreateNest();
        var plate1 = nest.CreatePlate();
        plate1.Parts.Add(MakePart());
        using var mgr = new PlateManager(nest);

        var result = mgr.GetOrCreateEmpty();
        Assert.Equal(0, result.Parts.Count);
    }

    [Fact]
    public void RemoveCurrent_RemovesPlateAndClampsIndex()
    {
        var nest = CreateNest();
        var plate1 = nest.CreatePlate();
        plate1.Parts.Add(MakePart());
        var plate2 = nest.CreatePlate();
        plate2.Parts.Add(MakePart());
        using var mgr = new PlateManager(nest);
        mgr.LoadLast();

        mgr.RemoveCurrent();

        // plate1 + sentinel (created by reactive sentinel enforcement)
        Assert.Equal(2, nest.Plates.Count);
        Assert.Same(plate1, nest.Plates[0]);
        Assert.Equal(0, nest.Plates[^1].Parts.Count);
        Assert.Equal(0, mgr.CurrentIndex);
    }

    [Fact]
    public void CanRemoveCurrent_OnlyIfMultiplePlatesAndHasParts()
    {
        var nest = CreateNest();
        var plate1 = nest.CreatePlate();
        using var mgr = new PlateManager(nest);

        Assert.False(mgr.CanRemoveCurrent);

        plate1.Parts.Add(MakePart());
        Assert.False(mgr.CanRemoveCurrent);

        nest.CreatePlate();
        // Auto-navigated to plate2 (empty), go back to plate1 (has parts)
        mgr.LoadFirst();
        Assert.True(mgr.CanRemoveCurrent);
    }

    [Fact]
    public void RemoveEmptyPlates_SentinelIsRestored()
    {
        var nest = CreateNest();
        var plate = nest.CreatePlate();
        plate.Parts.Add(MakePart());
        using var mgr = new PlateManager(nest);
        mgr.EnsureSentinel();
        Assert.Equal(2, nest.Plates.Count);

        // RemoveEmptyPlates removes the sentinel
        nest.Plates.RemoveEmptyPlates();

        // Sentinel should be restored reactively by OnPlateRemoved
        Assert.Equal(2, nest.Plates.Count);
        Assert.Equal(0, nest.Plates[^1].Parts.Count);
    }

    [Fact]
    public void PlateRemoval_RefreshesTailSubscriptions()
    {
        var nest = CreateNest();
        var plate1 = nest.CreatePlate();
        plate1.Parts.Add(MakePart());
        var plate2 = nest.CreatePlate();
        plate2.Parts.Add(MakePart());
        using var mgr = new PlateManager(nest);
        mgr.EnsureSentinel();
        // plates: [plate1(parts), plate2(parts), sentinel(empty)]

        // Remove plate2 — tail subscriptions should refresh
        nest.Plates.Remove(plate2);
        // plates: [plate1(parts), sentinel(empty)]

        // Verify reactive subscriptions still work on the new tail
        nest.Plates[^1].Parts.Add(MakePart());
        Assert.Equal(0, nest.Plates[^1].Parts.Count);
    }
}
