using FrameProcessor.Configuration;
using FrameProcessor.Domain;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;

namespace FrameProcessor.Tests;

public class FramesRegistryTests
{
    [Fact]
    public void Construction_StrictOnStartup_ThrowsOnInvalidFrame()
    {
        var monitor = new TestOptionsMonitor<FramesOptions>(new FramesOptions
        {
            Frames = { Invalid() },
        });

        var ex = Assert.Throws<OptionsValidationException>(
            () => new FramesRegistry(monitor, new FramesOptionsValidator(), NullLogger<FramesRegistry>.Instance));

        Assert.NotEmpty(ex.Failures);
    }

    [Fact]
    public void Construction_StrictOnStartup_FailsIfOneFrameIsBad()
    {
        var monitor = new TestOptionsMonitor<FramesOptions>(new FramesOptions
        {
            Frames = { LivingRoom(), Invalid() },
        });

        Assert.Throws<OptionsValidationException>(
            () => new FramesRegistry(monitor, new FramesOptionsValidator(), NullLogger<FramesRegistry>.Instance));
    }

    [Fact]
    public void TryGetByName_FindsParsedFrame()
    {
        var registry = BuildWith(LivingRoom());

        Assert.True(registry.TryGetByName(FrameName.Parse("living-room"), out var frame));
        Assert.Equal("living-room", frame.Name.Value);
        Assert.Equal("aabbccddeeff", frame.Mac.ToString());
        Assert.Equal(1600, frame.Resolution.Width);
        Assert.Equal(1200, frame.Resolution.Height);
        Assert.Equal(Orientation.Landscape, frame.Orientation);
        Assert.Equal(DitherAlgorithm.FloydSteinberg, frame.Dithering);
        Assert.Equal(2, frame.Palette.Count);
    }

    [Fact]
    public void TryGetByMac_LooksUpRegardlessOfInputFormat()
    {
        var registry = BuildWith(LivingRoom());

        Assert.True(MacAddress.TryParse("AA-BB-CC-DD-EE-FF", out var mac));
        Assert.True(registry.TryGetByMac(mac, out var frame));
        Assert.Equal("living-room", frame.Name.Value);
    }

    [Fact]
    public void TryGetByName_UnknownReturnsFalse()
    {
        var registry = BuildWith(LivingRoom());

        Assert.False(registry.TryGetByName(FrameName.Parse("kitchen"), out _));
    }

    [Fact]
    public void TryGetByMac_UnknownReturnsFalse()
    {
        var registry = BuildWith(LivingRoom());

        Assert.True(MacAddress.TryParse("11:22:33:44:55:66", out var mac));
        Assert.False(registry.TryGetByMac(mac, out _));
    }

    [Fact]
    public void Reload_DropsInvalidFrameAndKeepsValidOnes()
    {
        var monitor = new TestOptionsMonitor<FramesOptions>(new FramesOptions
        {
            Frames = { LivingRoom() },
        });
        var registry = new FramesRegistry(monitor, new FramesOptionsValidator(), NullLogger<FramesRegistry>.Instance);

        monitor.Emit(new FramesOptions
        {
            Frames = { LivingRoom(), Invalid(), Kitchen() },
        });

        Assert.True(registry.TryGetByName(FrameName.Parse("living-room"), out _));
        Assert.True(registry.TryGetByName(FrameName.Parse("kitchen"), out _));
        Assert.Equal(2, registry.All.Count);
    }

    [Fact]
    public void Reload_DoesNotThrowOnAllInvalid()
    {
        var monitor = new TestOptionsMonitor<FramesOptions>(new FramesOptions
        {
            Frames = { LivingRoom() },
        });
        var registry = new FramesRegistry(monitor, new FramesOptionsValidator(), NullLogger<FramesRegistry>.Instance);

        monitor.Emit(new FramesOptions { Frames = { Invalid() } });

        Assert.Empty(registry.All);
    }

    [Fact]
    public void Reload_LogsWarningForSkippedFrame()
    {
        var logger = new ListLogger<FramesRegistry>();
        var monitor = new TestOptionsMonitor<FramesOptions>(new FramesOptions
        {
            Frames = { LivingRoom() },
        });
        var registry = new FramesRegistry(monitor, new FramesOptionsValidator(), logger);

        monitor.Emit(new FramesOptions { Frames = { LivingRoom(), Invalid() } });

        Assert.Contains(logger.Entries,
            e => e.Level == LogLevel.Warning && e.Message.Contains("Skipping invalid frame"));
    }

    [Fact]
    public void Reload_SkipsDuplicateNameAcrossSurvivingFrames()
    {
        var logger = new ListLogger<FramesRegistry>();
        var monitor = new TestOptionsMonitor<FramesOptions>(new FramesOptions
        {
            Frames = { LivingRoom() },
        });
        var registry = new FramesRegistry(monitor, new FramesOptionsValidator(), logger);

        var firstWithName = LivingRoom();
        firstWithName.Mac = "11:22:33:44:55:66";
        var secondWithSameName = LivingRoom();
        secondWithSameName.Mac = "77:88:99:AA:BB:CC";

        monitor.Emit(new FramesOptions { Frames = { firstWithName, secondWithSameName } });

        Assert.Single(registry.All);
        Assert.Contains(logger.Entries,
            e => e.Level == LogLevel.Warning && e.Message.Contains("duplicate name"));
    }

    [Fact]
    public void Reload_SkipsDuplicateMacAcrossSurvivingFrames()
    {
        var logger = new ListLogger<FramesRegistry>();
        var monitor = new TestOptionsMonitor<FramesOptions>(new FramesOptions
        {
            Frames = { LivingRoom() },
        });
        var registry = new FramesRegistry(monitor, new FramesOptionsValidator(), logger);

        var firstWithMac = LivingRoom();
        var secondWithSameMac = LivingRoom();
        secondWithSameMac.Name = "kitchen";
        secondWithSameMac.Mac = "aabbccddeeff";

        monitor.Emit(new FramesOptions { Frames = { firstWithMac, secondWithSameMac } });

        Assert.Single(registry.All);
        Assert.Contains(logger.Entries,
            e => e.Level == LogLevel.Warning && e.Message.Contains("duplicate MAC"));
    }

    [Fact]
    public void Dispose_UnsubscribesFromMonitor()
    {
        var monitor = new TestOptionsMonitor<FramesOptions>(new FramesOptions
        {
            Frames = { LivingRoom() },
        });
        var registry = new FramesRegistry(monitor, new FramesOptionsValidator(), NullLogger<FramesRegistry>.Instance);

        registry.Dispose();

        monitor.Emit(new FramesOptions { Frames = { LivingRoom(), Kitchen() } });

        Assert.Single(registry.All);
        Assert.True(registry.TryGetByName(FrameName.Parse("living-room"), out _));
    }

    private static FramesRegistry BuildWith(params FrameOptions[] frames)
    {
        var monitor = new TestOptionsMonitor<FramesOptions>(new FramesOptions
        {
            Frames = frames.ToList(),
        });
        return new FramesRegistry(monitor, new FramesOptionsValidator(), NullLogger<FramesRegistry>.Instance);
    }

    private static FrameOptions LivingRoom() => 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 FrameOptions Kitchen() => new()
    {
        Name = "kitchen",
        Mac = "11:22:33:44:55:66",
        Resolution = new ResolutionOptions { Width = 800, Height = 600 },
        Orientation = "portrait",
        Dithering = "atkinson",
        Palette = new List<PaletteEntryOptions>
        {
            new() { Name = "black", Color = "#000000", DeviceColor = "#000000" },
            new() { Name = "white", Color = "#FFFFFF", DeviceColor = "#FFFFFF" },
        },
    };

    private static FrameOptions Invalid() => new()
    {
        Name = "bad name",
        Mac = "not-a-mac",
        Resolution = null,
        Orientation = "diagonal",
        Dithering = "ordered",
        Palette = new List<PaletteEntryOptions>(),
    };

    private sealed class TestOptionsMonitor<T> : IOptionsMonitor<T>
    {
        private T _current;
        private Action<T, string?>? _listeners;

        public TestOptionsMonitor(T initial) { _current = initial; }

        public T CurrentValue => _current;

        public T Get(string? name) => _current;

        public IDisposable? OnChange(Action<T, string?> listener)
        {
            _listeners += listener;
            return new Unsubscriber(() => _listeners -= listener);
        }

        public void Emit(T newValue)
        {
            _current = newValue;
            _listeners?.Invoke(newValue, null);
        }

        private sealed class Unsubscriber : IDisposable
        {
            private readonly Action _onDispose;

            public Unsubscriber(Action onDispose) { _onDispose = onDispose; }

            public void Dispose() => _onDispose();
        }
    }

    private sealed record LogEntry(LogLevel Level, string Message);

    private sealed class ListLogger<T> : ILogger<T>
    {
        public List<LogEntry> Entries { get; } = new();

        public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;

        public bool IsEnabled(LogLevel logLevel) => true;

        public void Log<TState>(
            LogLevel logLevel,
            EventId eventId,
            TState state,
            Exception? exception,
            Func<TState, Exception?, string> formatter)
        {
            Entries.Add(new LogEntry(logLevel, formatter(state, exception)));
        }
    }
}
