using System.Collections.Concurrent;
using Mbproxy;
using Mbproxy.Options;
using Mbproxy.Proxy;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Hosting.Systemd;
using Microsoft.Extensions.Hosting.WindowsServices;
using Serilog;
using Serilog.Core;
using Serilog.Events;
using Shouldly;
using Xunit;

namespace Mbproxy.Tests;

/// <summary>
/// Smoke tests: host starts, logs <c>mbproxy.startup.ready</c>, and shuts down cleanly.
/// </summary>
[Trait("Category", "Unit")]
public sealed class HostSmokeTests
{
    [Fact]
    public async Task HostSmoke_StartsAndStops_Cleanly_AndLogs_StartupReady()
    {
        // Arrange: build a host with an in-memory Serilog sink.
        var sink = new CapturingSink();
        var serilogLogger = new LoggerConfiguration()
            .MinimumLevel.Debug()
            .WriteTo.Sink(sink)
            .CreateLogger();

        using var host = Host.CreateApplicationBuilder()
            .ConfigureForTest(serilogLogger)
            .Build();

        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));

        // Act
        await host.StartAsync(cts.Token);

        // Give ProxyWorker time to fire (it binds 0 listeners and logs startup.ready).
        await Task.Delay(500, cts.Token);

        await host.StopAsync(cts.Token);

        // Assert: the startup.ready event was logged at Information.
        var readyEvents = sink.Events
            .Where(e =>
                e.Level == LogEventLevel.Information &&
                e.MessageTemplate.Text.Contains("mbproxy service ready"))
            .ToList();

        readyEvents.ShouldNotBeEmpty("ProxyWorker should have logged mbproxy.startup.ready");
    }

    [Fact]
    public async Task HostSmoke_ShutdownIsOrdered()
    {
        // Arrange
        using var host = Host.CreateApplicationBuilder()
            .ConfigureForTest(new LoggerConfiguration().CreateLogger())
            .Build();

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

        // Act: stop must complete well within 2 s.
        using var stopCts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
        var stopTask = host.StopAsync(stopCts.Token);

        // Assert: does not throw / time out.
        await stopTask.ShouldCompleteWithinAsync(TimeSpan.FromSeconds(3));
    }

    [Fact]
    public async Task HostSmoke_BothInitSystemIntegrations_CoRegister_AndHostRunsCleanly()
    {
        // Arrange: register BOTH init-system integrations. Each is a no-op off its
        // own init system, so on a test run (neither) the default console lifetime
        // applies — they must co-register without conflict and leave the host
        // startable and stoppable.
        var builder = Host.CreateApplicationBuilder();
        builder.Services.AddWindowsService();
        builder.Services.AddSystemd();
        builder.ConfigureForTest(new LoggerConfiguration().CreateLogger());

        using var host = builder.Build();
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));

        // Act + Assert: start/stop do not throw or time out.
        await host.StartAsync(cts.Token);
        await host.StopAsync(cts.Token);
    }
}

/// <summary>
/// Helper to configure a <see cref="HostApplicationBuilder"/> for smoke tests,
/// wiring in an in-memory config and the workers under test.
/// </summary>
internal static class TestHostBuilderExtensions
{
    public static HostApplicationBuilder ConfigureForTest(
        this HostApplicationBuilder builder,
        Serilog.ILogger serilogLogger)
    {
        // Minimal in-memory config so AddMbproxyOptions doesn't fail.
        builder.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
        {
            ["Mbproxy:AdminPort"] = "8080",
        });

        builder.Services.AddSerilog(serilogLogger, dispose: false);
        builder.AddMbproxyOptions();

        // Register the no-op pipeline and ProxyWorker.
        builder.Services.AddSingleton<IPduPipeline, NoopPduPipeline>();
        builder.Services.AddHostedService<ProxyWorker>();

        return builder;
    }
}

/// <summary>Serilog <see cref="ILogEventSink"/> that stores events for assertion.</summary>
internal sealed class CapturingSink : ILogEventSink
{
    private readonly ConcurrentQueue<LogEvent> _events = new();
    public IEnumerable<LogEvent> Events => _events;
    public void Emit(LogEvent logEvent) => _events.Enqueue(logEvent);
}

internal static class TaskExtensions
{
    public static async Task ShouldCompleteWithinAsync(this Task task, TimeSpan timeout)
    {
        var completed = await Task.WhenAny(task, Task.Delay(timeout));
        completed.ShouldBe(task, $"Task did not complete within {timeout}");
        await task; // propagate any exception
    }
}
