using System.Runtime.Versioning;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using TeamsISO.Engine.Domain;
using TeamsISO.Engine.NdiInterop;
using TeamsISO.Engine.Pipeline;

namespace TeamsISO.Engine.IntegrationTests;

/// <summary>
/// End-to-end pipeline test: synthesize a fake Teams source, run an
/// <see cref="IsoPipeline"/> against it, and assert the resulting normalized
/// output stream yields frames at the configured 1080p target size. This is
/// the closest unit-test analog to "did the engine actually do its job?"
/// — exercises NdiReceiver, FrameProcessor, ManagedNearestNeighborFrameScaler,
/// and NdiSender as a single chain through the production P/Invoke shim.
///
/// Marked [Trait("requires","ndi")] so default CI skips it; run locally with
///     dotnet test --filter requires=ndi
/// </summary>
[SupportedOSPlatform("windows")]
public class PipelineFrameRoundTripTests
{
    [Fact]
    [Trait("requires", "ndi")]
    public async Task Pipeline_ReceivesFromTeamsLikeSource_AndEmitsAt1080pTarget()
    {
        // ─── 1. Source pump ─────────────────────────────────────────────────
        // Spin up an NDI sender broadcasting an unambiguous, unique source
        // name. We use the literal "Teams - <token>" form so this works even
        // if a parallel test happens to be running at the same time.
        var token = Guid.NewGuid().ToString("N")[..6].ToUpperInvariant();
        var sourceShortName = $"Teams - FrameRT_{token}";
        var outputName = $"TEAMSISO_RT_{token}";

        using var interop = new NdiInteropPInvoke(NullLogger<NdiInteropPInvoke>.Instance);
        using var fakeSender = interop.CreateSender(sourceShortName);

        // Synthesize 640x360 BGRA frames in cyan so the receive side has
        // something obviously-not-zero to pick up. Pump at ~30 fps; the engine
        // is configured for 59.94 fps target so it'll either re-emit our
        // frames (slate threshold not yet reached) or interpolate via the
        // last-frame re-emit path — either is fine for the dimensions check.
        const int srcW = 640, srcH = 360;
        var pixelBuffer = new byte[srcW * srcH * 4];
        for (var i = 0; i < pixelBuffer.Length; i += 4)
        {
            pixelBuffer[i + 0] = 0xF0; // B
            pixelBuffer[i + 1] = 0xED; // G
            pixelBuffer[i + 2] = 0x97; // R  → ~Wild Dragon cyan #97EDF0 in BGRA
            pixelBuffer[i + 3] = 0xFF; // A
        }

        using var pumpCts = new CancellationTokenSource();
        var pumpTask = Task.Run(() =>
        {
            var pixelMemory = new ReadOnlyMemory<byte>(pixelBuffer);
            while (!pumpCts.IsCancellationRequested)
            {
                var frame = new ProcessedFrame(srcW, srcH, DateTime.UtcNow.Ticks, pixelMemory, PixelFormat.Bgra);
                try { interop.SendFrame(fakeSender, frame); }
                catch { /* sender may be torn down during cancellation */ }
                Thread.Sleep(33); // ~30 fps source
            }
        }, pumpCts.Token);

        try
        {
            // ─── 2. Wait for our fake source to be discoverable ─────────────
            using var finder = interop.CreateFinder();
            var fullSourceName = await WaitForSourceAsync(interop, finder, sourceShortName, TimeSpan.FromSeconds(5));
            fullSourceName.Should().NotBeNullOrEmpty(
                because: "fake Teams sender should be visible to a same-process finder within 5s");

            // ─── 3. Build the production IsoPipeline against it ────────────
            var settings = FrameProcessingSettings.Default; // 1080p, 59.94 fps, Pillarbox
            var clock = new PeriodicTimerFrameClock(settings.FramerateHz);
            var scaler = new ManagedNearestNeighborFrameScaler();
            var config = new IsoPipelineConfig(Guid.NewGuid(), fullSourceName!, outputName, settings);

            await using var pipeline = new IsoPipeline(
                config, interop, scaler, clock,
                ExponentialBackoff.Default,
                (delay, ct) => Task.Delay(delay, ct),
                NullLoggerFactory.Instance);
            await pipeline.StartAsync();

            // ─── 4. Wait for the pipeline's output sender to appear ────────
            var outputFullName = await WaitForSourceAsync(interop, finder, outputName, TimeSpan.FromSeconds(5));
            outputFullName.Should().NotBeNullOrEmpty(
                because: "IsoPipeline must broadcast a TEAMSISO_* sender within 5s of StartAsync");

            // ─── 5. Receive a frame from the normalized output ─────────────
            using var receiver = interop.CreateReceiver(outputFullName!);
            RawFrame? captured = null;
            var captureDeadline = DateTime.UtcNow.AddSeconds(8);
            while (DateTime.UtcNow < captureDeadline)
            {
                captured = interop.CaptureFrame(receiver, 1000);
                if (captured is not null) break;
                await Task.Delay(50);
            }

            captured.Should().NotBeNull(
                because: "the pipeline output must yield at least one normalized frame within 8s");

            // ─── 6. Assert the normalized dimensions match the target ──────
            var (expectedW, expectedH) = settings.ResolutionSize;
            captured!.Width.Should().Be(expectedW,
                because: $"the pipeline normalizes to {expectedW}x{expectedH} per FrameProcessingSettings.Default");
            captured.Height.Should().Be(expectedH);

            // ─── 7. Stop pipeline cleanly ──────────────────────────────────
            await pipeline.StopAsync();
        }
        finally
        {
            pumpCts.Cancel();
            try { await pumpTask; }
            catch (OperationCanceledException) { /* expected */ }
        }
    }

    /// <summary>
    /// Polls the finder until a source whose name contains <paramref name="needle"/>
    /// appears, or the timeout elapses. Returns the full NDI name (machine + paren
    /// short-name) on success, or null on timeout.
    /// </summary>
    private static async Task<string?> WaitForSourceAsync(
        NdiInteropPInvoke interop,
        TeamsISO.Engine.Interop.NdiFindHandle finder,
        string needle,
        TimeSpan timeout)
    {
        var deadline = DateTime.UtcNow + timeout;
        while (DateTime.UtcNow < deadline)
        {
            var sources = interop.GetCurrentSources(finder);
            var match = sources.FirstOrDefault(s => s.Contains(needle, StringComparison.Ordinal));
            if (match is not null) return match;
            await Task.Delay(150);
        }
        return null;
    }
}
