using System.Runtime.InteropServices;
using DragonISO.Engine.Pipeline;
using Xunit;

namespace DragonISO.Engine.Tests.Pipeline;

public class AudioPeakComputerTests
{
    [Fact]
    public void EmptyBuffer_ReturnsZero()
    {
        Assert.Equal(0.0, AudioPeakComputer.ComputePeak(ReadOnlySpan<byte>.Empty, AudioPeakComputer.FourCC_FLTP, 0));
    }

    [Fact]
    public void ZeroSamples_ReturnsZero()
    {
        var data = new byte[16];
        Assert.Equal(0.0, AudioPeakComputer.ComputePeak(data, AudioPeakComputer.FourCC_FLTP, 0));
    }

    [Fact]
    public void UnknownFourCC_ReturnsZero_RatherThanThrow()
    {
        // Receiver loop must never crash on an unrecognized format — better to
        // show silence on the meter than to take down the pipeline.
        var floats = new[] { 0.5f, -0.5f };
        var bytes = AsBytes(floats);
        Assert.Equal(0.0, AudioPeakComputer.ComputePeak(bytes, fourCC: 0xDEADBEEF, totalSamples: floats.Length));
    }

    [Fact]
    public void FltpAllZeros_PeakIsZero()
    {
        var floats = new float[256];
        var bytes = AsBytes(floats);
        Assert.Equal(0.0, AudioPeakComputer.ComputePeak(bytes, AudioPeakComputer.FourCC_FLTP, floats.Length));
    }

    [Fact]
    public void Fltp_PicksLargestAbsoluteSample()
    {
        // Mix of small + one large negative spike. Peak = abs(largest) = 0.85.
        var floats = new[] { 0.1f, -0.2f, 0.3f, -0.85f, 0.4f, -0.05f };
        var bytes = AsBytes(floats);
        var peak = AudioPeakComputer.ComputePeak(bytes, AudioPeakComputer.FourCC_FLTP, floats.Length);
        Assert.Equal(0.85, peak, precision: 5);
    }

    [Fact]
    public void Fltp_FullScalePositive_ReturnsOne()
    {
        var floats = new[] { 0.0f, 1.0f, 0.5f };
        var bytes = AsBytes(floats);
        Assert.Equal(1.0, AudioPeakComputer.ComputePeak(bytes, AudioPeakComputer.FourCC_FLTP, floats.Length), precision: 5);
    }

    [Fact]
    public void Fltp_FullScaleNegative_ReturnsOne()
    {
        var floats = new[] { 0.0f, -1.0f, 0.5f };
        var bytes = AsBytes(floats);
        Assert.Equal(1.0, AudioPeakComputer.ComputePeak(bytes, AudioPeakComputer.FourCC_FLTP, floats.Length), precision: 5);
    }

    [Fact]
    public void Fltp_OverflowsClampToOne()
    {
        // Real audio sometimes briefly exceeds [-1,1] during a clip. Engine
        // clamps so the meter never reads >1.0 (UI assumes [0,1] for the bar width).
        var floats = new[] { 0.0f, 1.5f, -2.3f };
        var bytes = AsBytes(floats);
        Assert.Equal(1.0, AudioPeakComputer.ComputePeak(bytes, AudioPeakComputer.FourCC_FLTP, floats.Length), precision: 5);
    }

    [Fact]
    public void Fltp_TotalSamplesSmallerThanBuffer_OnlyConsumesReportedRange()
    {
        // The reported range covers only the first 3 floats. The 4th
        // (largest) is past `totalSamples` and must be ignored — otherwise we'd
        // be reading beyond what the source said it wrote.
        var floats = new[] { 0.1f, -0.2f, 0.3f, 0.99f };
        var bytes = AsBytes(floats);
        var peak = AudioPeakComputer.ComputePeak(bytes, AudioPeakComputer.FourCC_FLTP, totalSamples: 3);
        Assert.Equal(0.3, peak, precision: 5);
    }

    [Fact]
    public void Fltp_TotalSamplesLargerThanBuffer_CapsAtBuffer()
    {
        // Defensive: a misreporting source claims more samples than the buffer
        // actually holds. We must not read past the end of `data`.
        var floats = new[] { 0.5f, -0.7f };
        var bytes = AsBytes(floats);
        var peak = AudioPeakComputer.ComputePeak(bytes, AudioPeakComputer.FourCC_FLTP, totalSamples: 999);
        Assert.Equal(0.7, peak, precision: 5);
    }

    [Fact]
    public void Pcms16_ZeroBuffer_ReturnsZero()
    {
        var samples = new short[64];
        var bytes = AsBytes(samples);
        Assert.Equal(0.0, AudioPeakComputer.ComputePeak(bytes, AudioPeakComputer.FourCC_PCMs16, samples.Length));
    }

    [Fact]
    public void Pcms16_FullScalePositive_ReturnsOne()
    {
        var samples = new[] { (short)0, short.MaxValue, (short)100 };
        var bytes = AsBytes(samples);
        Assert.Equal(1.0, AudioPeakComputer.ComputePeak(bytes, AudioPeakComputer.FourCC_PCMs16, samples.Length), precision: 5);
    }

    [Fact]
    public void Pcms16_MinValue_ClampsToOne()
    {
        // short.MinValue == -32768; abs() overflows back to short.MinValue in
        // two's complement, so we need explicit handling. Verify it pins to 1.0.
        var samples = new[] { (short)0, short.MinValue, (short)100 };
        var bytes = AsBytes(samples);
        Assert.Equal(1.0, AudioPeakComputer.ComputePeak(bytes, AudioPeakComputer.FourCC_PCMs16, samples.Length), precision: 5);
    }

    [Fact]
    public void Pcms16_HalfScale_ReturnsHalf()
    {
        var samples = new[] { (short)0, (short)16384, (short)-16383 };
        var bytes = AsBytes(samples);
        var peak = AudioPeakComputer.ComputePeak(bytes, AudioPeakComputer.FourCC_PCMs16, samples.Length);
        // 16384 / 32767 ≈ 0.500015; tolerate small precision drift.
        Assert.InRange(peak, 0.49, 0.51);
    }

    // ============================================================
    // FourCC constant integrity — regression guard
    // ============================================================
    //
    // The NDI SDK packs FourCCs little-endian: the first ASCII character
    // lands in the least-significant byte. These tests pin each public
    // constant to the little-endian packing of its documented characters,
    // so a transposed/typo'd literal (the PCMs16 0x73334d50 → 0x734D4350
    // fix) fails loudly instead of silently routing real audio to the
    // "unknown format → 0.0" branch.

    [Fact]
    public void FourCC_FLTP_MatchesAsciiPacking()
    {
        AudioPeakComputer.FourCC_FLTP.Should().Be(Pack('F', 'L', 'T', 'p'));
    }

    [Fact]
    public void FourCC_FLT_MatchesAsciiPacking()
    {
        AudioPeakComputer.FourCC_FLT.Should().Be(Pack('F', 'L', 'T', ' '));
    }

    [Fact]
    public void FourCC_PCMs16_MatchesAsciiPacking()
    {
        // Regression: was 0x73334d50 ('P','M','3','s'), should be 'P','C','M','s'.
        AudioPeakComputer.FourCC_PCMs16.Should().Be(Pack('P', 'C', 'M', 's'));
        AudioPeakComputer.FourCC_PCMs16.Should().Be(0x734D4350u);
    }

    [Fact]
    public void Pcms16_DecodedViaLiteralFourCC_NotJustSymbol()
    {
        // Belt-and-braces: feed a PCM16 buffer tagged with the *literal* FourCC
        // a real legacy Teams/NDI sender emits, not the symbol. If the constant
        // ever drifts from the wire value again, this peak collapses to 0.0
        // because ComputePeak falls through to the unknown branch.
        const uint wirePcms16 = 0x734D4350; // little-endian 'P','C','M','s'
        var samples = new[] { (short)0, (short)16384, (short)-200 };
        var bytes = AsBytes(samples);
        var peak = AudioPeakComputer.ComputePeak(bytes, wirePcms16, samples.Length);
        peak.Should().BeInRange(0.49, 0.51);
    }

    /// <summary>Little-endian ASCII FourCC packing: first char in the low byte.</summary>
    private static uint Pack(char a, char b, char c, char d) =>
        (uint)a | ((uint)b << 8) | ((uint)c << 16) | ((uint)d << 24);

    private static byte[] AsBytes<T>(T[] arr) where T : struct
    {
        var span = MemoryMarshal.Cast<T, byte>(arr.AsSpan());
        return span.ToArray();
    }
}
