using System.Buffers;
using System.IO.Pipes;
using Xunit;
using ytLive.Services.Audio;

namespace ytLive.Tests;

/// <summary>
/// TASK 9 audio milestone units: the voice chain (shelf/gate/compressor), the
/// ring buffer, the auto-ducker, and the resampler are all pure and tested in
/// isolation. The single integration test drives the REAL chain + mixer +
/// ducker + named-pipe writer against fakes and reads the float stream back —
/// the Good Dog Rule keeps it to exactly one integration test per branch.
/// </summary>
public class AudioPipelineTests
{
    // ─── Voice chain ───

    [Fact]
    public void LowShelf_DcGain_MatchesShelfGain()
    {
        var filter = new LowShelfFilter(48000, 120, 4f);
        float last = 0;
        for (var i = 0; i < 2000; i++)
            last = filter.Process(1f);

        // +4 dB → A² ≈ 1.585 at DC.
        Assert.InRange(last, 1.5f, 1.7f);
    }

    [Fact]
    public void HighShelf_PassesDc()
    {
        var filter = new HighShelfFilter(48000, 8000, 3f);
        float last = 0;
        for (var i = 0; i < 2000; i++)
            last = filter.Process(1f);

        Assert.InRange(last, 0.98f, 1.02f);
    }

    [Fact]
    public void NoiseGate_StaysClosedOnSilence()
    {
        var gate = new NoiseGate();
        for (var i = 0; i < 1000; i++)
            Assert.Equal(0f, gate.Process(0.001f));
        Assert.False(gate.IsOpen);
    }

    [Fact]
    public void NoiseGate_OpensOnLoudInput_AndHoldsThroughQuiet()
    {
        var gate = new NoiseGate();
        for (var i = 0; i < 50; i++)
            gate.Process(0.5f);
        Assert.True(gate.IsOpen);

        // A brief quiet dip must NOT close it (release hysteresis) — no chatter.
        for (var i = 0; i < 20; i++)
            Assert.Equal(0.001f, gate.Process(0.001f), 6);
        Assert.True(gate.IsOpen);
    }

    [Fact]
    public void Compressor_AtOrBelowThreshold_PassesThrough()
    {
        var compressor = new Compressor();
        Assert.Equal(0.4f, compressor.Process(0.4f));
        Assert.Equal(0.5f, compressor.Process(0.5f));
        Assert.Equal(-0.5f, compressor.Process(-0.5f));
    }

    [Fact]
    public void Compressor_FoldsLoudSignalAtRatio()
    {
        var compressor = new Compressor();
        Assert.Equal(0.625f, compressor.Process(1f), 4);
        Assert.Equal(-0.625f, compressor.Process(-1f), 4);
    }

    [Fact]
    public void VoiceChain_SilenceStaysSilent()
    {
        var chain = new VoiceFilterChain(48000);
        for (var i = 0; i < 500; i++)
            Assert.Equal(0f, chain.Process(0f), 6);
    }

    [Fact]
    public void VoiceChain_NeverClipsLoudSignal()
    {
        var chain = new VoiceFilterChain(48000);
        for (var i = 0; i < 500; i++)
        {
            var outSample = chain.Process(1f);
            Assert.InRange(outSample, -1.1f, 1.1f);
        }
    }

    // ─── Ring buffer ───

    [Fact]
    public void RingBuffer_ReadsBackInOrder()
    {
        var buffer = new AudioRingBuffer(8);
        buffer.Write(new[] { 1f, 2f, 3f });
        buffer.Write(new[] { 4f, 5f });

        var dest = new float[8];
        Assert.Equal(5, buffer.Read(dest, 8));
        Assert.Equal(new[] { 1f, 2f, 3f, 4f, 5f, 0f, 0f, 0f }, dest);
        Assert.Equal(0, buffer.Count);
    }

    [Fact]
    public void RingBuffer_OverwritesOldestWhenFull()
    {
        var buffer = new AudioRingBuffer(4);
        buffer.Write(new[] { 1f, 2f, 3f });
        buffer.Write(new[] { 4f, 5f, 6f }); // drops 1,2

        var dest = new float[4];
        Assert.Equal(4, buffer.Read(dest, 4));
        Assert.Equal(new[] { 3f, 4f, 5f, 6f }, dest);
    }

    [Fact]
    public void RingBuffer_ReadUnderrun_ReturnsFewer()
    {
        var buffer = new AudioRingBuffer(8);
        buffer.Write(new[] { 1f, 2f });

        var dest = new float[8];
        Assert.Equal(2, buffer.Read(dest, 8));
        Assert.Equal(0, buffer.Count);
    }

    [Fact]
    public void RingBuffer_WrapsAround()
    {
        var buffer = new AudioRingBuffer(4);
        buffer.Write(new[] { 1f, 2f, 3f });
        buffer.Read(new float[4], 2); // consume 1,2 — head now past the ring

        buffer.Write(new[] { 4f, 5f, 6f }); // wraps around the tail
        var dest = new float[4];
        Assert.Equal(4, buffer.Read(dest, 4));
        Assert.Equal(new[] { 3f, 4f, 5f, 6f }, dest);
    }

    // ─── Auto-duck ───

    [Fact]
    public void Ducker_Silence_KeepsUnity()
    {
        var ducker = new AutoDucker();
        for (var i = 0; i < 100; i++)
            Assert.Equal(1f, ducker.Update(0f));
    }

    [Fact]
    public void Ducker_HotMic_ConvergesToDuckGain()
    {
        var ducker = new AutoDucker();
        for (var i = 0; i < 500; i++)
            ducker.Update(0.2f);

        Assert.Equal(AutoDucker.DuckGain, ducker.CurrentGain, 2);
    }

    [Fact]
    public void Ducker_RecoversWhenMicStops()
    {
        var ducker = new AutoDucker();
        for (var i = 0; i < 500; i++)
            ducker.Update(0.2f);
        Assert.InRange(ducker.CurrentGain, 0.1f, 0.4f);

        for (var i = 0; i < 3000; i++)
            ducker.Update(0f);
        Assert.Equal(1f, ducker.CurrentGain, 2);
    }

    // ─── Resampler ───

    [Fact]
    public void Resampler_SameRate_PassesThrough()
    {
        var resampler = new TinyResampler(48000, 48000);
        var input = new float[] { 0.1f, 0.2f, 0.3f };
        Assert.Same(input, resampler.Process(input));
    }

    [Fact]
    public void Resampler_Downsample_ChangesLength()
    {
        var resampler = new TinyResampler(44100, 48000);
        var output = resampler.Process(Enumerable.Repeat(0.5f, 4410).ToArray());

        Assert.Equal(4800, output.Length);
        Assert.All(output, v => Assert.Equal(0.5f, v, 3));
    }

    [Fact]
    public void Resampler_Upsample_ChangesLength()
    {
        var resampler = new TinyResampler(48000, 44100);
        var output = resampler.Process(Enumerable.Repeat(0.25f, 4800).ToArray());

        Assert.Equal(4410, output.Length);
        Assert.All(output, v => Assert.Equal(0.25f, v, 3));
    }

    [Fact]
    public void Resampler_IsStatefulAcrossChunks()
    {
        var resampler = new TinyResampler(48000, 16000); // 3:1 downsample
        var input = Enumerable.Range(0, 4800).Select(i => (float)i).ToArray();
        var first = resampler.Process(input);
        var second = resampler.Process(input);

        // No double-counting of the boundary: the phase carries over, so the
        // first output sample of chunk 2 is the next input index, not index 0.
        Assert.Equal(1600, first.Length);
        Assert.Equal(1600, second.Length);
    }

    // ─── The ONE integration test (Good Dog Rule): real chain + mixer + ducker
    // ─── + named pipe, fakes for the capture sources, read back over the wire. ───

    private sealed class FakeSource : IAudioSource
    {
        public event Action? Started;
        public event Action<AudioSample>? SampleReady;
        public event Action<Exception>? Failed;

        public void Start() => Started?.Invoke();
        public void Stop() { }
        public void Dispose() { }

        public void Emit(AudioSample sample) => SampleReady?.Invoke(sample);
        public void Fail(Exception ex) => Failed?.Invoke(ex);
    }

    [Fact]
    public async Task Mix_WithFiltersDuckAndGain_Lands_On_AudioPipe()
    {
        var pipeName = "ytllive_test_" + Guid.NewGuid().ToString("N");
        var mic = new FakeSource();
        var loopback = new FakeSource();
        using var mixer = new AudioMixer(
            mic, loopback,
            log: null,
            micGain: () => 1.0,
            loopbackGain: () => 1.0,
            mixInterval: TimeSpan.FromMilliseconds(5));

        // Loud mic (filtered + compressed toward ~0.5) + loud loopback (ducked
        // from unity down toward 0.25). Pre-fill the ring buffers so the very
        // first tick written to the pipe already carries real audio.
        var micChunk = new float[240];
        Array.Fill(micChunk, 0.5f);
        var loopChunk = new float[480];
        Array.Fill(loopChunk, 0.8f);
        for (var i = 0; i < 200; i++)
        {
            mic.Emit(new AudioSample(micChunk, 48000, 1));
            loopback.Emit(new AudioSample(loopChunk, 48000, 2));
        }

        using var client = new NamedPipeClientStream(".", pipeName, PipeDirection.In, PipeOptions.Asynchronous);
        var connected = client.ConnectAsync();
        mixer.StartLive(pipeName);
        await connected.WaitAsync(TimeSpan.FromSeconds(5));

        var bytes = await ReadFullyAsync(client, 240 * 2 * 4, TimeSpan.FromSeconds(5));
        mixer.StopLive();

        Assert.True(bytes.Length >= 240 * 2 * 4, "expected at least one full stereo frame");
        var floats = new float[bytes.Length / 4];
        Buffer.BlockCopy(bytes, 0, floats, 0, bytes.Length);

        // The chain compresses the mic and the ducker folds the loopback down —
        // the result must be a bounded, finite, non-silent signal.
        Assert.All(floats, f => Assert.InRange(f, -2f, 2f));
        Assert.True(floats.Any(f => MathF.Abs(f) > 0.05f), "the pipe carried silence");
    }

    [Fact]
    public async Task Mix_HonorsProviderGains_AndGameMute_KillsTheLoopback()
    {
        // THE integration test for the polish batch: the AudioGainProvider's
        // Func seams are exactly what the VM hands to the mixer, so this proves
        // the desktop/game volume slider and mute button actually reach the live
        // mix (before wiring they defaulted to unity and did nothing).
        var pipeName = "ytllive_test_" + Guid.NewGuid().ToString("N");
        var mic = new FakeSource();
        var loopback = new FakeSource();

        var gameVolume = 0.5;
        var gameMuted = false;
        var provider = new AudioGainProvider(
            micMuted: () => false,
            micVolume: () => 1.0,
            gameMuted: () => gameMuted,
            gameVolume: () => gameVolume);

        using var mixer = new AudioMixer(
            mic, loopback,
            log: null,
            micGain: provider.MicGain,
            loopbackGain: provider.LoopbackGain,
            mixInterval: TimeSpan.FromMilliseconds(5));

        // Mic silent (so the ducker stays at unity) + a loud loopback bed that
        // scales to 0.8 * 0.5 = 0.4 on the wire. Pre-fill the ring buffer so the
        // first frames already carry real audio.
        var loopChunk = new float[480];
        Array.Fill(loopChunk, 0.8f);
        for (var i = 0; i < 200; i++)
            loopback.Emit(new AudioSample(loopChunk, 48000, 2));

        using var client = new NamedPipeClientStream(".", pipeName, PipeDirection.In, PipeOptions.Asynchronous);
        var connected = client.ConnectAsync();
        mixer.StartLive(pipeName);
        await connected.WaitAsync(TimeSpan.FromSeconds(5));

        // Phase 1: unmuted at 0.5 → scaled but clearly audible, bounded.
        var bytes = await ReadFullyAsync(client, 240 * 2 * 4, TimeSpan.FromSeconds(5));
        var floats = new float[bytes.Length / 4];
        Buffer.BlockCopy(bytes, 0, floats, 0, bytes.Length);
        Assert.True(floats.Any(f => MathF.Abs(f) > 0.1f), "scaled loopback should be audible");
        Assert.All(floats, f => Assert.InRange(f, -0.55f, 0.55f));

        // Phase 2: mute flips the provider's seam → the pipe must go silent.
        gameMuted = true;
        var mutedBytes = await ReadFullyAsync(client, 240 * 2 * 4, TimeSpan.FromSeconds(5));
        mixer.StopLive();

        Assert.True(mutedBytes.Length >= 240 * 2 * 4, "expected at least one full stereo frame after mute");
        var mutedFloats = new float[mutedBytes.Length / 4];
        Buffer.BlockCopy(mutedBytes, 0, mutedFloats, 0, mutedBytes.Length);
        Assert.All(mutedFloats, f => Assert.Equal(0f, f, 6));
    }

    private static async Task<byte[]> ReadFullyAsync(NamedPipeClientStream client, int count, TimeSpan timeout)
    {
        var ms = new MemoryStream();
        var buffer = ArrayPool<byte>.Shared.Rent(4096);
        try
        {
            var deadline = DateTime.UtcNow + timeout;
            while (ms.Length < count && DateTime.UtcNow < deadline)
            {
                var read = await client.ReadAsync(buffer, 0, buffer.Length);
                if (read == 0)
                    break;
                ms.Write(buffer, 0, read);
            }
            return ms.ToArray();
        }
        finally
        {
            ArrayPool<byte>.Shared.Return(buffer);
        }
    }
}
