using Xunit;
using ytLive.Models;
using ytLive.Services;
using ytLive.Services.Compositor;

namespace ytLive.Tests;

/// <summary>
/// The output compositor (TASK 4 ship step 1): renders a scene into the encoder's
/// master frame, mirroring the XAML preview minus the editing chrome. The integration
/// test composites a full scene (backdrop + round webcam + images + mirror + border)
/// and asserts per-layer probe pixels; the vertical-tier test asserts the 9:16 crop
/// and upscale. Frames are pushed by the test — no capture managers involved.
/// </summary>
public class SceneCompositorTests
{
    public static VideoFrame Solid(int w, int h, byte r, byte g, byte b)
    {
        var pixels = new byte[w * h * 4];
        for (var i = 0; i < pixels.Length; i += 4)
        {
            pixels[i] = b;
            pixels[i + 1] = g;
            pixels[i + 2] = r;
            pixels[i + 3] = 255;
        }
        return new VideoFrame(w, h, pixels);
    }

    /// <summary>Left half = leftColor, right half = rightColor.</summary>
    private static VideoFrame Split(int w, int h, byte lr, byte lg, byte lb, byte rr, byte rg, byte rb)
    {
        var pixels = new byte[w * h * 4];
        var half = w / 2;
        for (var y = 0; y < h; y++)
        {
            for (var x = 0; x < w; x++)
            {
                var i = (y * w + x) * 4;
                pixels[i] = x < half ? lb : rb;
                pixels[i + 1] = x < half ? lg : rg;
                pixels[i + 2] = x < half ? lr : rr;
                pixels[i + 3] = 255;
            }
        }
        return new VideoFrame(w, h, pixels);
    }

    private static void AssertColor(VideoFrame frame, int x, int y, byte r, byte g, byte b)
    {
        var i = (y * frame.Width + x) * 4;
        var br = frame.BgraPixels[i + 2];
        var bg = frame.BgraPixels[i + 1];
        var bb = frame.BgraPixels[i];
        Assert.True(
            Math.Abs(br - r) <= 2 && Math.Abs(bg - g) <= 2 && Math.Abs(bb - b) <= 2,
            $"pixel ({x},{y}): expected rgb({r},{g},{b}), got rgb({br},{bg},{bb})");
    }

    [Fact]
    public void Composite_FullScene_MasterPixels()
    {
        var red = Solid(1920, 1080, 255, 0, 0);        // backdrop
        var green = Solid(1280, 720, 0, 255, 0);       // webcam
        var split = Split(100, 100, 0, 255, 255, 255, 0, 255); // image: left cyan, right magenta

        var backdrop = new Source { Type = SourceType.DisplayCapture, IsBackdrop = true, CaptureKey = "monitor:0" };
        var roundWebcam = new WebcamSceneConfig { X = 100, Y = 100, Width = 300, Height = 300, ClipShape = ClipShape.Round };
        var imageA = new Source { Type = SourceType.Image, X = 1200, Y = 600, Width = 200, Height = 200 };
        var imageB = new Source
        {
            Type = SourceType.Image, X = 500, Y = 700, Width = 100, Height = 100, IsMirrored = true,
            BorderColor = "#ffffff", BorderOpacity = 1, BorderWidth = 4,
        };

        var scene = new Scene { Name = "Live" };
        scene.Elements.Add(backdrop);
        scene.Elements.Add(roundWebcam);
        scene.Elements.Add(imageA);
        scene.Elements.Add(imageB);

        VideoFrame? FrameFor(SceneElement e) => e switch
        {
            WebcamSceneConfig => green,
            Source { IsBackdrop: true } => red,
            Source { Type: SourceType.Image } => split,
            _ => null,
        };

        var options = new CompositorOptions
        {
            SourceRectX = 0, SourceRectY = 0, SourceRectWidth = 1920, SourceRectHeight = 1080,
            OutputWidth = 1920, OutputHeight = 1080,
        };

        var output = new SceneCompositor().Render(scene, FrameFor, null, options);

        Assert.Equal(1920, output.Width);
        Assert.Equal(1080, output.Height);

        // backdrop at the frame corner
        AssertColor(output, 0, 0, 255, 0, 0);
        // round webcam: center is the (square-cropped) webcam feed
        AssertColor(output, 250, 250, 0, 255, 0);
        // round webcam: element-square corner is OUTSIDE the circle -> backdrop shows
        AssertColor(output, 101, 101, 255, 0, 0);
        // imageA (unmirrored): left half cyan, right half magenta
        AssertColor(output, 1220, 700, 0, 255, 255);
        AssertColor(output, 1380, 700, 255, 0, 255);
        // imageB (mirrored): halves swap — element left shows the source's right (magenta)
        AssertColor(output, 520, 750, 255, 0, 255);
        AssertColor(output, 580, 750, 0, 255, 255);
        // imageB border: white ring at the top edge (drawn over the content)
        AssertColor(output, 550, 700, 255, 255, 255);
    }

    [Fact]
    public void Render_VerticalTier_Outputs_1080x1920_From_The_Center_Crop()
    {
        var red = Solid(1920, 1080, 255, 0, 0);
        var green = Solid(1280, 720, 0, 255, 0);

        var backdrop = new Source { Type = SourceType.DisplayCapture, IsBackdrop = true, CaptureKey = "monitor:0" };
        var roundWebcam = new WebcamSceneConfig { X = 656, Y = 100, Width = 300, Height = 300, ClipShape = ClipShape.Round };
        var scene = new Scene { Name = "Live" };
        scene.Elements.Add(backdrop);
        scene.Elements.Add(roundWebcam);

        VideoFrame? FrameFor(SceneElement e) => e switch
        {
            WebcamSceneConfig => green,
            Source { IsBackdrop: true } => red,
            _ => null,
        };

        var options = new CompositorOptions
        {
            SourceRectX = 656, SourceRectY = 0, SourceRectWidth = 607, SourceRectHeight = 1080,
            OutputWidth = 1080, OutputHeight = 1920,
        };

        var output = new SceneCompositor().Render(scene, FrameFor, null, options);

        Assert.Equal(1080, output.Width);
        Assert.Equal(1920, output.Height);
        // top-left of the crop is pure backdrop (webcam starts at crop y=100, inset from the corner)
        AssertColor(output, 0, 0, 255, 0, 0);
        // the webcam center (crop 150,250) scales to output ~(267,444) and stays green
        AssertColor(output, 267, 444, 0, 255, 0);
        // far from the webcam, still backdrop
        AssertColor(output, 978, 1688, 255, 0, 0);
    }

    [Fact]
    public void Composite_WithFlash_BlendsOverContent()
    {
        var red = Solid(1920, 1080, 255, 0, 0);
        var backdrop = new Source { Type = SourceType.DisplayCapture, IsBackdrop = true, CaptureKey = "monitor:0" };
        var scene = new Scene { Name = "Live" };
        scene.Elements.Add(backdrop);

        // flash: a semi-transparent white pixel at the center of a master-sized frame
        var flash = new VideoFrame(1920, 1080, new byte[1920 * 1080 * 4]);
        var ci = (1080 / 2 * 1920 + 1920 / 2) * 4;
        flash.BgraPixels[ci] = 255;
        flash.BgraPixels[ci + 1] = 255;
        flash.BgraPixels[ci + 2] = 255;
        flash.BgraPixels[ci + 3] = 64; // ~25% alpha

        var options = new CompositorOptions
        {
            SourceRectX = 0, SourceRectY = 0, SourceRectWidth = 1920, SourceRectHeight = 1080,
            OutputWidth = 1920, OutputHeight = 1080,
        };

        var output = new SceneCompositor().Render(scene, _ => red, flash, options);

        // red lightened by 25% white: ~(255, 63, 63)
        AssertColor(output, 960, 540, 255, 64, 64);
        // untouched corner stays pure red
        AssertColor(output, 0, 0, 255, 0, 0);
    }

    [Fact]
    public void Composite_WithSocialBar_OverlaysAboveFlash_AtTopOrBottom()
    {
        var red = Solid(1920, 1080, 255, 0, 0);
        var backdrop = new Source { Type = SourceType.DisplayCapture, IsBackdrop = true, CaptureKey = "monitor:0" };
        var scene = new Scene { Name = "Live" };
        scene.Elements.Add(backdrop);

        var options = new CompositorOptions
        {
            SourceRectX = 0, SourceRectY = 0, SourceRectWidth = 1920, SourceRectHeight = 1080,
            OutputWidth = 1920, OutputHeight = 1080,
        };
        var compositor = new SceneCompositor();

        // master-sized overlay: opaque green at (0,0), 50%-white at the center
        var bar = new VideoFrame(1920, 1080, new byte[1920 * 1080 * 4]);
        var g = 0;
        bar.BgraPixels[g] = 0; bar.BgraPixels[g + 1] = 255; bar.BgraPixels[g + 2] = 0; bar.BgraPixels[g + 3] = 255;
        var w = (540 * 1920 + 960) * 4;
        bar.BgraPixels[w] = 255; bar.BgraPixels[w + 1] = 255; bar.BgraPixels[w + 2] = 255; bar.BgraPixels[w + 3] = 128;

        // flash: opaque magenta at (0,0) — the bar must cover it
        var flash = new VideoFrame(1920, 1080, new byte[1920 * 1080 * 4]);
        flash.BgraPixels[0] = 255; flash.BgraPixels[1] = 0; flash.BgraPixels[2] = 255; flash.BgraPixels[3] = 255;

        var top = compositor.Render(scene, _ => red, flash, options, bar, 0);
        AssertColor(top, 0, 0, 0, 255, 0); // bar above flash at the top-left
        AssertColor(top, 960, 540, 255, 127, 127); // 50% white over red
        AssertColor(top, 100, 100, 255, 0, 0); // empty overlay area: backdrop

        var bottom = compositor.Render(scene, _ => red, flash, options, bar, 1040);
        AssertColor(bottom, 0, 1040, 0, 255, 0); // bar drawn at the bottom edge
        AssertColor(bottom, 0, 1039, 255, 0, 0); // backdrop just above the bar
        AssertColor(bottom, 960, 540, 255, 0, 0); // bar region moved away from center
    }
}

public class StretchMathTests
{
    [Fact]
    public void UniformToFill_SameAspect_Is_Exact_Fit_With_No_Offset()
    {
        var (scale, ox, oy) = StretchMath.UniformToFill(400, 300, 800, 600);
        Assert.Equal(0.5f, scale, 3);
        Assert.Equal(0f, ox, 3);
        Assert.Equal(0f, oy, 3);
    }

    [Fact]
    public void UniformToFill_WiderSource_Crops_And_Centers_Vertically()
    {
        var (scale, ox, oy) = StretchMath.UniformToFill(400, 200, 800, 600);
        Assert.Equal(0.5f, scale, 3);
        Assert.Equal(0f, ox, 3);
        Assert.Equal(-50f, oy, 3); // drawn 400x300 into 400x200 -> 50px crop top and bottom
    }

    [Fact]
    public void BilinearScale_SameSize_ReturnsTheInput()
    {
        var src = SceneCompositorTests.Solid(4, 4, 10, 20, 30);
        Assert.Same(src, StretchMath.BilinearScale(src, 4, 4));
    }

    [Fact]
    public void BilinearScale_Downscales_To_Target_Size()
    {
        var src = SceneCompositorTests.Solid(16, 16, 0, 255, 0);
        var scaled = StretchMath.BilinearScale(src, 8, 8);
        Assert.Equal(8, scaled.Width);
        Assert.Equal(8, scaled.Height);
        var i = 0;
        Assert.Equal(255, scaled.BgraPixels[i + 1]); // solid green survives the scale
    }
}
