using RioJoy.Core.Overlay;
using Xunit;

namespace RioJoy.Core.Tests.Overlay;

public class OverlayLayoutEngineTests
{
    // LinearTextMeasurer(0.6, 1.2): width = len*size*0.6, height = size*1.2.
    private static readonly OverlayLayoutEngine Engine = new(new LinearTextMeasurer());

    private static OverlayRegion Region(
        string name = "b-00", double x = 0, double y = 0, double w = 200, double h = 40, double size = 10) =>
        new() { Name = name, X = x, Y = y, Width = w, Height = h, FontSizePx = size, FontFamily = "Sans" };

    [Fact]
    public void Place_TextFits_KeepsBaseSize_AndCenters()
    {
        // "FIRE" at size 10 -> 24 x 12, well inside 200 x 40.
        PlacedLabel p = Engine.Place(Region(x: 10, y: 20), "FIRE");

        Assert.Equal(10, p.FontSizePx, 6);
        Assert.Equal(24, p.Width, 6);
        Assert.Equal(12, p.Height, 6);
        Assert.Equal(10 + (200 - 24) / 2, p.X, 6); // 98
        Assert.Equal(20 + (40 - 12) / 2, p.Y, 6);  // 34
    }

    [Fact]
    public void Place_Overflow_Fit_ShrinksFont()
    {
        // Narrow cell: "FIRE" at base 10 is 24 wide > 20 -> auto-fit shrinks it.
        PlacedLabel p = Engine.Place(Region(w: 20, h: 40), "FIRE");

        Assert.True(p.FontSizePx < 10);
        Assert.True(p.Width <= 20, $"fitted width {p.Width} must be <= 20");
    }

    [Fact]
    public void Place_Overflow_Crop_KeepsBaseSize()
    {
        var opts = new OverlayLayoutOptions { SizeMode = OverlaySizeMode.Crop };
        PlacedLabel p = Engine.Place(Region(w: 20, h: 40), "FIRE", opts);

        Assert.Equal(10, p.FontSizePx, 6); // not shrunk
        Assert.Equal(24, p.Width, 6);      // overflows the 20-wide cell
    }

    [Theory]
    [InlineData(OverlayHJust.Left, 0)]
    [InlineData(OverlayHJust.Center, 44)]  // (100-12)/2
    [InlineData(OverlayHJust.Right, 88)]   //  100-12
    public void Place_HorizontalJustification(OverlayHJust hjust, double expectedX)
    {
        // "AB" at size 10 -> 12 x 12 inside a 100 x 20 cell at origin.
        var opts = new OverlayLayoutOptions { HJust = hjust };
        PlacedLabel p = Engine.Place(Region(w: 100, h: 20), "AB", opts);
        Assert.Equal(expectedX, p.X, 6);
    }

    [Theory]
    [InlineData(OverlayVJust.Top, 0)]
    [InlineData(OverlayVJust.Center, 4)]   // (20-12)/2
    [InlineData(OverlayVJust.Bottom, 8)]   //  20-12
    public void Place_VerticalJustification(OverlayVJust vjust, double expectedY)
    {
        var opts = new OverlayLayoutOptions { VJust = vjust };
        PlacedLabel p = Engine.Place(Region(w: 100, h: 20), "AB", opts);
        Assert.Equal(expectedY, p.Y, 6);
    }

    [Fact]
    public void Place_RegionOverride_BeatsEngineDefault()
    {
        OverlayRegion region = Region(w: 100, h: 20);
        region.HJust = OverlayHJust.Right;
        region.VJust = OverlayVJust.Top;

        var opts = new OverlayLayoutOptions { HJust = OverlayHJust.Left, VJust = OverlayVJust.Bottom };
        PlacedLabel p = Engine.Place(region, "AB", opts);

        Assert.Equal(88, p.X, 6); // Right wins over Left
        Assert.Equal(0, p.Y, 6);  // Top wins over Bottom
    }

    [Fact]
    public void Place_Rotated_CentersInCell_AndCarriesAngle()
    {
        // "HELLO" at size 10 -> 30 x 12; fits the swapped 200 x 50 envelope, so size holds.
        OverlayRegion region = Region(w: 50, h: 200, size: 10);
        region.RotationDegrees = 90;

        PlacedLabel p = Engine.Place(region, "HELLO");

        Assert.Equal(90, p.RotationDegrees, 6);
        Assert.Equal(10, p.FontSizePx, 6);
        Assert.Equal((50 - 30) / 2.0, p.X, 6);    // centered horizontally -> 10
        Assert.Equal((200 - 12) / 2.0, p.Y, 6);   // centered vertically   -> 94
    }

    [Fact]
    public void Place_Rotated_FitsAgainstSwappedDimensions()
    {
        // A wide, short cell: upright the text fits height-bound; rotated it must fit
        // the (swapped) narrow height, forcing a smaller font.
        OverlayRegion upright = Region(w: 200, h: 20, size: 30);
        OverlayRegion rotated = Region(w: 200, h: 20, size: 30);
        rotated.RotationDegrees = 90;

        double uprightSize = Engine.Place(upright, "HELLO").FontSizePx;
        double rotatedSize = Engine.Place(rotated, "HELLO").FontSizePx;

        Assert.True(rotatedSize < uprightSize,
            $"rotated {rotatedSize} should be smaller than upright {uprightSize} (fit uses swapped axes)");
    }

    [Fact]
    public void Layout_SkipsEmptyAndMissingLabels_PreservesRegionOrder()
    {
        var template = new OverlayTemplate
        {
            Regions =
            {
                Region("b-00"),
                Region("b-01"),
                Region("b-02"),
            },
        };
        var labels = new Dictionary<string, string>
        {
            ["b-00"] = "FIRE",
            ["b-01"] = "",        // empty -> skipped
            ["b-02"] = "GEAR",
            ["b-99"] = "ORPHAN",  // no such region -> ignored
        };

        IList<PlacedLabel> placed = Engine.Layout(template, labels);

        Assert.Equal(2, placed.Count);
        Assert.Equal("b-00", placed[0].RegionName);
        Assert.Equal("b-02", placed[1].RegionName);
        Assert.Equal("GEAR", placed[1].Text);
    }
}
