using Mbproxy.Admin;
using Mbproxy.Bcd;
using Mbproxy.Options;
using Mbproxy.Proxy;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Serilog;
using Shouldly;
using Xunit;

namespace Mbproxy.Tests.Admin;

/// <summary>
/// Unit tests for <see cref="StatusBroadcaster"/>'s push-cycle logic — fleet always
/// pushed, per-PLC pushed only for PLCs with a detail-page subscriber, and every
/// capture disarmed on stop. The SignalR sink is faked; a real
/// <see cref="StatusSnapshotBuilder"/> is resolved from a minimal in-process host.
/// </summary>
[Trait("Category", "Unit")]
public sealed class StatusBroadcasterTests
{
    private sealed record Harness(
        IHost Host,
        StatusBroadcaster Broadcaster,
        FakeStatusPushSink Sink,
        StatusSnapshotBuilder Builder,
        TagCaptureRegistry Registry,
        PlcSubscriptionTracker Tracker) : IAsyncDisposable
    {
        public async ValueTask DisposeAsync()
        {
            await Broadcaster.DisposeAsync();
            using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
            try { await Host.StopAsync(cts.Token); } catch { }
            Host.Dispose();
        }
    }

    private static async Task<Harness> BuildAsync()
    {
        var hostBuilder = Host.CreateApplicationBuilder();
        hostBuilder.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
        {
            ["Mbproxy:AdminPort"] = "0",
        });
        hostBuilder.Services.AddSerilog(
            new LoggerConfiguration().MinimumLevel.Fatal().CreateLogger(), dispose: false);
        hostBuilder.AddMbproxyOptions();
        hostBuilder.Services.AddSingleton<IPduPipeline, NoopPduPipeline>();
        hostBuilder.Services.AddSingleton<ProxyWorker>();
        hostBuilder.Services.AddHostedService(sp => sp.GetRequiredService<ProxyWorker>());
        hostBuilder.Services.AddSingleton<AssemblyVersionAccessor>();
        hostBuilder.Services.AddSingleton<StatusSnapshotBuilder>();

        var host = hostBuilder.Build();
        using var startCts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
        await host.StartAsync(startCts.Token);

        var builder  = host.Services.GetRequiredService<StatusSnapshotBuilder>();
        var registry = host.Services.GetRequiredService<TagCaptureRegistry>();
        var options  = host.Services.GetRequiredService<IOptionsMonitor<MbproxyOptions>>();
        var tracker  = new PlcSubscriptionTracker();
        var sink     = new FakeStatusPushSink();

        var broadcaster = new StatusBroadcaster(
            sink, builder, tracker, registry, options, NullLogger.Instance);

        return new Harness(host, broadcaster, sink, builder, registry, tracker);
    }

    [Fact]
    public async Task PushOnce_AlwaysPushesFleet()
    {
        await using var h = await BuildAsync();

        await h.Broadcaster.PushOnceAsync(TestContext.Current.CancellationToken);

        h.Sink.FleetPushes.Count.ShouldBe(1);
    }

    [Fact]
    public async Task PushOnce_NoActivePlcs_SkipsPerPlcPush()
    {
        await using var h = await BuildAsync();

        await h.Broadcaster.PushOnceAsync(TestContext.Current.CancellationToken);

        h.Sink.PlcPushes.ShouldBeEmpty();
    }

    [Fact]
    public async Task PushOnce_ActivePlc_PushesDetailWithDebugSnapshot()
    {
        await using var h = await BuildAsync();
        h.Registry.GetOrCreate("plc-x", BcdTagMap.Empty);
        h.Tracker.Add("conn-1", "plc-x");

        await h.Broadcaster.PushOnceAsync(TestContext.Current.CancellationToken);

        var push = h.Sink.PlcPushes.ShouldHaveSingleItem();
        push.Plc.ShouldBe("plc-x");
        push.Detail.Debug.ShouldNotBeNull();
    }

    [Fact]
    public async Task StopAsync_DisarmsEveryCapture()
    {
        await using var h = await BuildAsync();
        h.Registry.GetOrCreate("plc-x", BcdTagMap.Empty);
        h.Registry.Arm("plc-x");

        await h.Broadcaster.StopAsync();

        h.Registry.TryGet("plc-x", out var capture).ShouldBeTrue();
        capture.IsArmed.ShouldBeFalse();
    }
}
