using OpenNest.Engine.Fill;
using OpenNest.Geometry;
using OpenNest.IO;

namespace OpenNest.Tests.Fill;

public class RemnantFinderTests
{
    [Fact]
    public void EmptyPlate_ReturnsWholeWorkArea()
    {
        var finder = new RemnantFinder(new Box(0, 0, 100, 100));
        var remnants = finder.FindRemnants();

        Assert.Single(remnants);
        Assert.Equal(100 * 100, remnants[0].Area(), 0.1);
    }

    [Fact]
    public void SingleObstacle_InCorner_FindsLShapedRemnants()
    {
        var finder = new RemnantFinder(new Box(0, 0, 100, 100));
        finder.AddObstacle(new Box(0, 0, 40, 40));
        var remnants = finder.FindRemnants();

        Assert.True(remnants.Count >= 2);
        var largest = remnants[0];
        Assert.Equal(60 * 100, largest.Area(), 0.1);
    }

    [Fact]
    public void SingleObstacle_InCenter_FindsFourRemnants()
    {
        var finder = new RemnantFinder(new Box(0, 0, 100, 100));
        finder.AddObstacle(new Box(30, 30, 40, 40));
        var remnants = finder.FindRemnants();

        Assert.True(remnants.Count >= 4);
    }

    [Fact]
    public void MinDimension_FiltersSmallRemnants()
    {
        var finder = new RemnantFinder(new Box(0, 0, 100, 100));
        finder.AddObstacle(new Box(0, 0, 95, 100));
        var all = finder.FindRemnants(0);
        var filtered = finder.FindRemnants(10);

        Assert.True(all.Count > filtered.Count);
        foreach (var r in filtered)
        {
            Assert.True(r.Width >= 10);
            Assert.True(r.Length >= 10);
        }
    }

    [Fact]
    public void ResultsSortedByAreaDescending()
    {
        var finder = new RemnantFinder(new Box(0, 0, 100, 100));
        finder.AddObstacle(new Box(0, 0, 50, 50));
        var remnants = finder.FindRemnants();

        for (var i = 1; i < remnants.Count; i++)
            Assert.True(remnants[i - 1].Area() >= remnants[i].Area());
    }

    [Fact]
    public void AddObstacle_UpdatesResults()
    {
        var finder = new RemnantFinder(new Box(0, 0, 100, 100));
        var before = finder.FindRemnants();
        Assert.Single(before);

        finder.AddObstacle(new Box(0, 0, 50, 50));
        var after = finder.FindRemnants();
        Assert.True(after.Count > 1);
    }

    [Fact]
    public void ClearObstacles_ResetsToFullWorkArea()
    {
        var finder = new RemnantFinder(new Box(0, 0, 100, 100));
        finder.AddObstacle(new Box(0, 0, 50, 50));
        finder.ClearObstacles();
        var remnants = finder.FindRemnants();

        Assert.Single(remnants);
        Assert.Equal(100 * 100, remnants[0].Area(), 0.1);
    }

    [Fact]
    public void FullyCovered_ReturnsEmpty()
    {
        var finder = new RemnantFinder(new Box(0, 0, 100, 100));
        finder.AddObstacle(new Box(0, 0, 100, 100));
        var remnants = finder.FindRemnants();

        Assert.Empty(remnants);
    }

    [Fact]
    public void MultipleObstacles_FindsGapBetween()
    {
        var finder = new RemnantFinder(new Box(0, 0, 100, 100));
        finder.AddObstacle(new Box(0, 0, 40, 100));
        finder.AddObstacle(new Box(60, 0, 40, 100));
        var remnants = finder.FindRemnants();

        var gap = remnants.FirstOrDefault(r =>
            r.Length >= 19.9 && r.Length <= 20.1 &&
            r.Width >= 99.9);
        Assert.NotNull(gap);
    }

    [Fact]
    public void FromPlate_CreatesFinderWithPartsAsObstacles()
    {
        var plate = TestHelpers.MakePlate(60, 120,
            TestHelpers.MakePartAt(0, 0, 20));
        var finder = RemnantFinder.FromPlate(plate);
        var remnants = finder.FindRemnants();

        Assert.True(remnants.Count >= 1);
        Assert.True(remnants[0].Area() < plate.WorkArea().Area());
    }

    [Fact]
    public void ObstacleOutsideWorkArea_IsIgnored()
    {
        var finder = new RemnantFinder(new Box(0, 0, 100, 100));
        finder.AddObstacle(new Box(200, 200, 50, 50));
        var remnants = finder.FindRemnants();

        Assert.Single(remnants);
        Assert.Equal(100 * 100, remnants[0].Area(), 0.1);
    }

    [Fact]
    public void ObstaclePartiallyOutsideWorkArea_IsClipped()
    {
        var finder = new RemnantFinder(new Box(0, 0, 100, 100));
        // Obstacle extends 20 units past the right edge
        finder.AddObstacle(new Box(80, 0, 40, 100));
        var remnants = finder.FindRemnants();

        // Should find the 80x100 strip on the left
        var left = remnants.FirstOrDefault(r =>
            r.Length >= 79.9 && r.Length <= 80.1 &&
            r.Width >= 99.9);
        Assert.NotNull(left);
    }

    [Fact]
    public void OverlappingObstacles_HandledCorrectly()
    {
        var finder = new RemnantFinder(new Box(0, 0, 100, 100));
        finder.AddObstacle(new Box(0, 0, 60, 60));
        finder.AddObstacle(new Box(40, 40, 60, 60)); // overlaps first
        var remnants = finder.FindRemnants();

        // No remnant should overlap either obstacle
        foreach (var r in remnants)
        {
            Assert.False(
                r.Left < 60 && r.Right > 0 && r.Bottom < 60 && r.Top > 0
                && r.Left < 100 && r.Right > 40 && r.Bottom < 100 && r.Top > 40,
                "Remnant should not overlap both obstacles simultaneously in their shared region");
        }

        // Total remnant area + obstacle coverage should not exceed work area
        var totalRemnantArea = remnants.Sum(r => r.Area());
        Assert.True(totalRemnantArea < 100 * 100);
        Assert.True(totalRemnantArea > 0);
    }

    [Fact]
    public void ConstructorWithObstaclesList()
    {
        var obstacles = new List<Box>
        {
            new Box(0, 0, 40, 100),
            new Box(60, 0, 40, 100)
        };
        var finder = new RemnantFinder(new Box(0, 0, 100, 100), obstacles);
        var remnants = finder.FindRemnants();

        var gap = remnants.FirstOrDefault(r =>
            r.Length >= 19.9 && r.Length <= 20.1);
        Assert.NotNull(gap);
    }

    [Fact]
    public void AddObstacles_Plural_AddsMultiple()
    {
        var finder = new RemnantFinder(new Box(0, 0, 100, 100));
        finder.AddObstacles(new[]
        {
            new Box(0, 0, 40, 100),
            new Box(60, 0, 40, 100)
        });
        var remnants = finder.FindRemnants();

        var gap = remnants.FirstOrDefault(r =>
            r.Length >= 19.9 && r.Length <= 20.1);
        Assert.NotNull(gap);
    }

    [Fact]
    public void IterativeWorkflow_AddObstacleThenRequery()
    {
        var finder = new RemnantFinder(new Box(0, 0, 100, 100));

        // First fill: obstacle in bottom-left
        finder.AddObstacle(new Box(0, 0, 50, 50));
        var pass1 = finder.FindRemnants();
        Assert.True(pass1.Count >= 2);

        // Simulate filling the largest remnant by adding another obstacle
        var largest = pass1[0];
        finder.AddObstacle(new Box(largest.X, largest.Y, largest.Width / 2, largest.Length));
        var pass2 = finder.FindRemnants();

        // Should have more, smaller remnants now
        var pass2TotalArea = pass2.Sum(r => r.Area());
        var pass1TotalArea = pass1.Sum(r => r.Area());
        Assert.True(pass2TotalArea < pass1TotalArea);
    }

    [Fact]
    public void NoRemnantOverlapsObstacle()
    {
        var finder = new RemnantFinder(new Box(0, 0, 100, 100));
        finder.AddObstacle(new Box(20, 20, 30, 30));
        finder.AddObstacle(new Box(60, 10, 25, 80));
        var remnants = finder.FindRemnants();

        foreach (var r in remnants)
        {
            // Check no remnant overlaps obstacle 1
            var overlaps1 = r.Left < 50 && r.Right > 20 && r.Bottom < 50 && r.Top > 20;
            Assert.False(overlaps1, $"Remnant ({r.X},{r.Y} {r.Width}x{r.Length}) overlaps obstacle 1");

            // Check no remnant overlaps obstacle 2
            var overlaps2 = r.Left < 85 && r.Right > 60 && r.Bottom < 90 && r.Top > 10;
            Assert.False(overlaps2, $"Remnant ({r.X},{r.Y} {r.Width}x{r.Length}) overlaps obstacle 2");
        }
    }

    [Fact]
    public void ManyObstacles_GridPattern()
    {
        var finder = new RemnantFinder(new Box(0, 0, 100, 100));

        // Place a 5x5 grid of 10x10 obstacles with 10-unit gaps
        for (var row = 0; row < 5; row++)
            for (var col = 0; col < 5; col++)
                finder.AddObstacle(new Box(col * 20, row * 20, 10, 10));

        var remnants = finder.FindRemnants();

        // All remnants should be within the work area
        foreach (var r in remnants)
        {
            Assert.True(r.Left >= 0);
            Assert.True(r.Bottom >= 0);
            Assert.True(r.Right <= 100);
            Assert.True(r.Top <= 100);
        }

        // Should find gaps between obstacles
        Assert.True(remnants.Count > 0);
    }

    [Fact]
    public void SingleObstacle_NearEdge_FindsRemnantsOnAllSides()
    {
        // Obstacle near top-left: should find remnants above, below, and to the right.
        var finder = new RemnantFinder(new Box(0, 0, 120, 60));
        finder.AddObstacle(new Box(0, 47, 21, 6));
        var remnants = finder.FindRemnants();

        var above = remnants.FirstOrDefault(r => r.Bottom >= 53 - 0.1 && r.Length > 50);
        var below = remnants.FirstOrDefault(r => r.Top <= 47 + 0.1 && r.Length > 50);
        var right = remnants.FirstOrDefault(r => r.Left >= 21 - 0.1 && r.Width > 50);

        Assert.NotNull(above);
        Assert.NotNull(below);
        Assert.NotNull(right);
    }

    [Fact]
    public void LoadNestFile_FindsGapAboveMainGrid()
    {
        var nestFile = @"C:\Users\AJ\Desktop\no_remnant_found.nest";
        if (!File.Exists(nestFile))
            return; // Skip if file not available.

        var reader = new NestReader(nestFile);
        var nest = reader.Read();
        var plate = nest.Plates[0];

        var finder = RemnantFinder.FromPlate(plate);

        // Use smallest drawing bbox dimension as minDim (same as UI).
        var minDim = nest.Drawings.Min(d =>
            System.Math.Min(d.Program.BoundingBox().Width, d.Program.BoundingBox().Length));

        var tiered = finder.FindTieredRemnants(minDim);

        // Should find a remnant near (0.25, 53.13) — the gap above the main grid.
        var topGap = tiered.FirstOrDefault(t =>
            t.Box.Bottom > 50 && t.Box.Bottom < 55 &&
            t.Box.Left < 1 &&
            t.Box.Length > 100 &&
            t.Box.Width > 5);

        Assert.True(topGap.Box.Length > 0, "Expected remnant above main grid");
    }

    [Fact]
    public void DensePack_FindsGapAtTop()
    {
        // Reproduce real plate: 120x60, 68 parts of SULLYS-004.
        // Main grid tops out at y=53.14 (obstacle). Two rotated parts on the
        // right extend to y=58.49 but only at x > 106. The gap at x < 106
        // from y=53.14 to y=59.8 is ~106 x 6.66 — should be found.
        var workArea = new Box(0.2, 0.8, 119.5, 59.0);
        var obstacles = new List<Box>();
        var spacing = 0.25;

        // Main grid: 5 columns x 12 rows (6 pairs).
        // Even rows: bbox bottom offsets, odd rows: different offsets.
        double[] colX = { 0.25, 21.08, 41.90, 62.73, 83.56 };
        double[] colXOdd = { 0.81, 21.64, 42.46, 63.29, 84.12 };
        double[] evenY = { 3.67, 12.41, 21.14, 29.87, 38.60, 47.33 };
        double[] oddY = { 0.75, 9.48, 18.21, 26.94, 35.67, 44.40 };

        foreach (var cx in colX)
            foreach (var ey in evenY)
                obstacles.Add(new Box(cx - spacing, ey - spacing, 20.65 + spacing * 2, 5.56 + spacing * 2));
        foreach (var cx in colXOdd)
            foreach (var oy in oddY)
                obstacles.Add(new Box(cx - spacing, oy - spacing, 20.65 + spacing * 2, 5.56 + spacing * 2));

        // Right-side rotated parts (only 2 extend high: parts 62 and 66).
        obstacles.Add(new Box(106.70 - spacing, 37.59 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));
        obstacles.Add(new Box(114.19 - spacing, 37.59 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));
        // Parts 63, 67 (lower rotated)
        obstacles.Add(new Box(105.02 - spacing, 29.35 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));
        obstacles.Add(new Box(112.51 - spacing, 29.35 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));
        // Parts 60, 64 (upper-right rotated, lower)
        obstacles.Add(new Box(106.70 - spacing, 8.99 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));
        obstacles.Add(new Box(114.19 - spacing, 8.99 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));
        // Parts 61, 65
        obstacles.Add(new Box(105.02 - spacing, 0.75 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));
        obstacles.Add(new Box(112.51 - spacing, 0.75 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));

        var finder = new RemnantFinder(workArea, obstacles);
        var remnants = finder.FindRemnants(5.375);

        // The gap at x < 106 from y=53.14 to y=59.8 should be found.
        Assert.True(remnants.Count > 0, "Should find gap above main grid");
        var topRemnant = remnants.FirstOrDefault(r => r.Width >= 5.375 && r.Length > 50);
        Assert.NotNull(topRemnant);

        // Verify dimensions are close to the expected ~104 x 6.6 gap.
        Assert.True(topRemnant.Length > 100, $"Expected length > 100, got {topRemnant.Length:F1}");
        Assert.True(topRemnant.Width > 6, $"Expected width > 6, got {topRemnant.Width:F1}");
    }
}
