using FrameProcessor.Configuration;

namespace FrameProcessor.Tests;

public class FramesOptionsValidatorTests
{
    private readonly FramesOptionsValidator _validator = new();

    [Fact]
    public void NoFrames_IsValid()
    {
        var result = _validator.Validate(null, new FramesOptions());

        Assert.True(result.Succeeded);
    }

    [Fact]
    public void ValidFrame_Succeeds()
    {
        var options = new FramesOptions { Frames = { ValidFrame() } };

        var result = _validator.Validate(null, options);

        Assert.True(result.Succeeded, JoinFailures(result));
    }

    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("   ")]
    public void Name_MissingOrBlank_Fails(string? badName)
    {
        var frame = ValidFrame();
        frame.Name = badName;

        var result = _validator.Validate(null, new FramesOptions { Frames = { frame } });

        Assert.False(result.Succeeded);
        Assert.Contains(result.Failures!, f => f.Contains("Name is required"));
    }

    [Theory]
    [InlineData("living room")]
    [InlineData("living/room")]
    [InlineData("li:vingroom")]
    public void Name_NotUrlSafe_Fails(string badName)
    {
        var frame = ValidFrame();
        frame.Name = badName;

        var result = _validator.Validate(null, new FramesOptions { Frames = { frame } });

        Assert.False(result.Succeeded);
        Assert.Contains(result.Failures!, f => f.Contains("is not a valid frame name"));
    }

    [Theory]
    [InlineData(null)]
    [InlineData("")]
    public void Mac_Missing_Fails(string? badMac)
    {
        var frame = ValidFrame();
        frame.Mac = badMac;

        var result = _validator.Validate(null, new FramesOptions { Frames = { frame } });

        Assert.False(result.Succeeded);
        Assert.Contains(result.Failures!, f => f.Contains("Mac is required"));
    }

    [Theory]
    [InlineData("ZZ:ZZ:ZZ:ZZ:ZZ:ZZ")]
    [InlineData("AABBCC")]
    [InlineData("not-a-mac")]
    public void Mac_Unparseable_Fails(string badMac)
    {
        var frame = ValidFrame();
        frame.Mac = badMac;

        var result = _validator.Validate(null, new FramesOptions { Frames = { frame } });

        Assert.False(result.Succeeded);
        Assert.Contains(result.Failures!, f => f.Contains("is not a valid MAC address"));
    }

    [Fact]
    public void Resolution_Missing_Fails()
    {
        var frame = ValidFrame();
        frame.Resolution = null;

        var result = _validator.Validate(null, new FramesOptions { Frames = { frame } });

        Assert.False(result.Succeeded);
        Assert.Contains(result.Failures!, f => f.Contains("Resolution is required"));
    }

    [Theory]
    [InlineData(0, 1200)]
    [InlineData(1600, 0)]
    [InlineData(-1, 1200)]
    [InlineData(1600, -1)]
    public void Resolution_NonPositive_Fails(int width, int height)
    {
        var frame = ValidFrame();
        frame.Resolution = new ResolutionOptions { Width = width, Height = height };

        var result = _validator.Validate(null, new FramesOptions { Frames = { frame } });

        Assert.False(result.Succeeded);
        Assert.Contains(result.Failures!, f => f.Contains("must be positive"));
    }

    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("LANDSCAPE")]
    [InlineData("sideways")]
    public void Orientation_Invalid_Fails(string? badOrientation)
    {
        var frame = ValidFrame();
        frame.Orientation = badOrientation;

        var result = _validator.Validate(null, new FramesOptions { Frames = { frame } });

        Assert.False(result.Succeeded);
        Assert.Contains(result.Failures!, f => f.Contains("Orientation"));
    }

    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("FloydSteinberg")]
    [InlineData("ordered")]
    public void Dithering_Invalid_Fails(string? badDithering)
    {
        var frame = ValidFrame();
        frame.Dithering = badDithering;

        var result = _validator.Validate(null, new FramesOptions { Frames = { frame } });

        Assert.False(result.Succeeded);
        Assert.Contains(result.Failures!, f => f.Contains("Dithering"));
    }

    [Fact]
    public void Palette_FewerThanTwoEntries_Fails()
    {
        var frame = ValidFrame();
        frame.Palette = new List<PaletteEntryOptions>
        {
            new() { Name = "black", Color = "#000000", DeviceColor = "#000000" },
        };

        var result = _validator.Validate(null, new FramesOptions { Frames = { frame } });

        Assert.False(result.Succeeded);
        Assert.Contains(result.Failures!, f => f.Contains("at least 2 entries"));
    }

    [Fact]
    public void Palette_EntryWithBadHex_Fails()
    {
        var frame = ValidFrame();
        frame.Palette[0].Color = "not-a-color";

        var result = _validator.Validate(null, new FramesOptions { Frames = { frame } });

        Assert.False(result.Succeeded);
        Assert.Contains(result.Failures!, f => f.Contains("Color 'not-a-color' is not a valid hex color"));
    }

    [Fact]
    public void Palette_EntryWithBadDeviceHex_Fails()
    {
        var frame = ValidFrame();
        frame.Palette[1].DeviceColor = "zzzzzz";

        var result = _validator.Validate(null, new FramesOptions { Frames = { frame } });

        Assert.False(result.Succeeded);
        Assert.Contains(result.Failures!, f => f.Contains("DeviceColor 'zzzzzz' is not a valid hex color"));
    }

    [Fact]
    public void Palette_EntryMissingName_Fails()
    {
        var frame = ValidFrame();
        frame.Palette[0].Name = "";

        var result = _validator.Validate(null, new FramesOptions { Frames = { frame } });

        Assert.False(result.Succeeded);
        Assert.Contains(result.Failures!, f => f.Contains("Palette[0].Name is required"));
    }

    [Fact]
    public void DuplicateName_Fails()
    {
        var a = ValidFrame();
        var b = ValidFrame();
        b.Mac = "11:22:33:44:55:66";

        var result = _validator.Validate(null, new FramesOptions { Frames = { a, b } });

        Assert.False(result.Succeeded);
        Assert.Contains(result.Failures!, f => f.Contains("Name 'living-room' is a duplicate"));
    }

    [Fact]
    public void DuplicateMac_DetectedAfterCanonicalization()
    {
        var a = ValidFrame();
        a.Mac = "AA:BB:CC:DD:EE:FF";
        var b = ValidFrame();
        b.Name = "kitchen";
        b.Mac = "aabbccddeeff";

        var result = _validator.Validate(null, new FramesOptions { Frames = { a, b } });

        Assert.False(result.Succeeded);
        Assert.Contains(result.Failures!, f => f.Contains("is a duplicate"));
    }

    [Fact]
    public void AllErrors_AreReportedTogether()
    {
        var frame = new FrameOptions
        {
            Name = "bad name",
            Mac = "not-a-mac",
            Resolution = null,
            Orientation = "diagonal",
            Dithering = "ordered",
            Palette = new List<PaletteEntryOptions>(),
        };

        var result = _validator.Validate(null, new FramesOptions { Frames = { frame } });

        Assert.False(result.Succeeded);
        var failures = result.Failures!.ToList();
        Assert.Contains(failures, f => f.Contains("Name"));
        Assert.Contains(failures, f => f.Contains("Mac"));
        Assert.Contains(failures, f => f.Contains("Resolution is required"));
        Assert.Contains(failures, f => f.Contains("Orientation"));
        Assert.Contains(failures, f => f.Contains("Dithering"));
        Assert.Contains(failures, f => f.Contains("at least 2 entries"));
    }

    private static FrameOptions ValidFrame() => new()
    {
        Name = "living-room",
        Mac = "AA:BB:CC:DD:EE:FF",
        Resolution = new ResolutionOptions { Width = 1600, Height = 1200 },
        Orientation = "landscape",
        Dithering = "floyd-steinberg",
        Palette = new List<PaletteEntryOptions>
        {
            new() { Name = "black", Color = "#1F2226", DeviceColor = "#000000" },
            new() { Name = "white", Color = "#B9C7C9", DeviceColor = "#FFFFFF" },
        },
    };

    private static string JoinFailures(Microsoft.Extensions.Options.ValidateOptionsResult result)
        => result.Failures is null ? string.Empty : string.Join("; ", result.Failures);
}
