using NAudio.Wave;
using Xunit;
using ytLive.Services.Audio;

namespace ytLive.Tests;

/// <summary>
/// TASK 4 ship step 4: WASAPI audio capture behind the <see cref="IAudioSource"/>
/// seam. The units pin down the pure pieces — byte→float conversion, the level
/// meter math, and the mixer lifecycle/forwarding against fakes. No real audio
/// devices (NAudio device resolution is a thin wrapper left to a manual smoke
/// test), no timers.
/// </summary>
public class AudioMixerTests
{
    private sealed class FakeSource : IAudioSource
    {
        public int StartCount { get; private set; }
        public int StopCount { get; private set; }
        public bool Disposed { get; private set; }
        public event Action? Started;
        public event Action<AudioSample>? SampleReady;
        public event Action<Exception>? Failed;

        public void Start() => StartCount++;
        public void Stop() => StopCount++;
        public void Dispose() => Disposed = true;

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

    [Fact]
    public void Start_StartsBothSources()
    {
        var mic = new FakeSource();
        var loopback = new FakeSource();
        var mixer = new AudioMixer(mic, loopback);

        mixer.Start();

        Assert.Equal(1, mic.StartCount);
        Assert.Equal(1, loopback.StartCount);
    }

    [Fact]
    public void Start_IsIdempotent()
    {
        var mic = new FakeSource();
        var mixer = new AudioMixer(mic, new FakeSource());

        mixer.Start();
        mixer.Start();

        Assert.Equal(1, mic.StartCount);
    }

    [Fact]
    public void Stop_StopsBothAndResetsLevel()
    {
        var mic = new FakeSource();
        var loopback = new FakeSource();
        var mixer = new AudioMixer(mic, loopback);
        mixer.Start();
        mic.Emit(new AudioSample(new[] { 0.8f }, 48000, 1));

        var last = -1f;
        mixer.MicLevelChanged += l => last = l;
        mixer.Stop();

        Assert.Equal(1, mic.StopCount);
        Assert.Equal(1, loopback.StopCount);
        Assert.Equal(0f, last);
        Assert.Equal(0f, mixer.MicLevel);
    }

    [Fact]
    public void Stop_WithNoStart_DoesNothing()
    {
        var mic = new FakeSource();
        var mixer = new AudioMixer(mic, new FakeSource());

        mixer.Stop();

        Assert.Equal(0, mic.StopCount);
    }

    [Fact]
    public void MicSamples_DriveMicLevelChanged()
    {
        var mic = new FakeSource();
        var mixer = new AudioMixer(mic, new FakeSource());
        var levels = new List<float>();
        mixer.MicLevelChanged += l => levels.Add(l);

        mixer.Start();
        mic.Emit(new AudioSample(new[] { 1f, 1f, 1f, 1f }, 48000, 1));
        mic.Emit(new AudioSample(new[] { -1f, -1f, -1f, -1f }, 48000, 1));

        Assert.NotEmpty(levels);
        Assert.All(levels, l => Assert.InRange(l, 0f, 1f));
    }

    [Fact]
    public void LoopbackSamples_DoNotChangeMicLevel()
    {
        var loopback = new FakeSource();
        var mixer = new AudioMixer(new FakeSource(), loopback);
        var levels = new List<float>();
        mixer.MicLevelChanged += l => levels.Add(l);

        mixer.Start();
        loopback.Emit(new AudioSample(new[] { 1f, 1f, 1f, 1f }, 48000, 2));

        Assert.Empty(levels);
        Assert.Equal(0f, mixer.MicLevel);
    }

    [Fact]
    public void MicSamples_DoNotChangeLoopbackLevel()
    {
        var mic = new FakeSource();
        var mixer = new AudioMixer(mic, new FakeSource());
        var levels = new List<float>();
        mixer.LoopbackLevelChanged += l => levels.Add(l);

        mixer.Start();
        mic.Emit(new AudioSample(new[] { 1f, 1f, 1f, 1f }, 48000, 1));

        Assert.Empty(levels);
        Assert.Equal(0f, mixer.LoopbackLevel);
    }

    [Fact]
    public void LoopbackSamples_DriveLoopbackLevelChanged()
    {
        var loopback = new FakeSource();
        var mixer = new AudioMixer(new FakeSource(), loopback);
        var levels = new List<float>();
        mixer.LoopbackLevelChanged += l => levels.Add(l);

        mixer.Start();
        loopback.Emit(new AudioSample(new[] { 1f, 1f, 1f, 1f }, 48000, 2));
        loopback.Emit(new AudioSample(new[] { -1f, -1f, -1f, -1f }, 48000, 2));

        Assert.NotEmpty(levels);
        Assert.All(levels, l => Assert.InRange(l, 0f, 1f));
    }

    [Fact]
    public void MicStarted_RaisesMicConnected()
    {
        var mic = new FakeSource();
        var mixer = new AudioMixer(mic, new FakeSource());
        var connected = 0;
        mixer.MicConnected += () => connected++;

        mixer.Start();
        mic.MarkStarted();

        Assert.Equal(1, connected);
    }

    [Fact]
    public void MicFailure_RaisesMicFailed()
    {
        var mic = new FakeSource();
        var mixer = new AudioMixer(mic, new FakeSource());
        Exception? failed = null;
        mixer.MicFailed += ex => failed = ex;

        mixer.Start();
        mic.Fail(new InvalidOperationException("boom"));

        Assert.NotNull(failed);
        Assert.Equal("boom", failed!.Message);
    }

    [Fact]
    public void RestartMic_StopsAndRestartsMic_KeepsLoopbackRunning()
    {
        var mic = new FakeSource();
        var loopback = new FakeSource();
        var mixer = new AudioMixer(mic, loopback);
        mixer.Start();

        mixer.RestartMic();

        Assert.Equal(2, mic.StartCount);
        Assert.Equal(1, mic.StopCount);
        Assert.Equal(1, loopback.StartCount);
        Assert.Equal(0, loopback.StopCount);
    }

    [Fact]
    public void RestartMic_ResetsLevel()
    {
        var mic = new FakeSource();
        var mixer = new AudioMixer(mic, new FakeSource());
        mixer.Start();
        mic.Emit(new AudioSample(new[] { 1f, 1f, 1f, 1f }, 48000, 1));
        Assert.True(mixer.MicLevel > 0);

        float? reset = null;
        mixer.MicLevelChanged += l => reset = l;
        mixer.RestartMic();

        Assert.Equal(0f, reset);
    }

    [Fact]
    public void MicFailure_LogsAndResetsLevel()
    {
        var mic = new FakeSource();
        var logs = new List<string>();
        var mixer = new AudioMixer(mic, new FakeSource(), m => logs.Add(m));
        mixer.Start();
        mic.Emit(new AudioSample(new[] { 0.5f }, 48000, 1));

        var last = -1f;
        mixer.MicLevelChanged += l => last = l;
        mic.Fail(new InvalidOperationException("boom"));

        Assert.Contains(logs, l => l.Contains("boom"));
        Assert.Equal(0f, last);
    }

    [Fact]
    public void LoopbackFailure_LogsButKeepsMic()
    {
        var loopback = new FakeSource();
        var logs = new List<string>();
        var mixer = new AudioMixer(new FakeSource(), loopback, m => logs.Add(m));
        mixer.Start();

        var last = -1f;
        mixer.MicLevelChanged += l => last = l;
        loopback.Fail(new InvalidOperationException("boom"));

        Assert.Contains(logs, l => l.Contains("boom"));
        Assert.Equal(-1f, last);
    }

    [Fact]
    public void Dispose_StopsAndDisposesSources()
    {
        var mic = new FakeSource();
        var loopback = new FakeSource();
        var mixer = new AudioMixer(mic, loopback);
        mixer.Start();

        mixer.Dispose();

        Assert.True(mic.Disposed);
        Assert.True(loopback.Disposed);
        Assert.Equal(1, mic.StopCount);
    }

    [Fact]
    public void Dispose_UnsubscribesEvents()
    {
        var mic = new FakeSource();
        var loopback = new FakeSource();
        var mixer = new AudioMixer(mic, loopback);
        mixer.Dispose();

        var levels = new List<float>();
        mixer.MicLevelChanged += l => levels.Add(l);
        mic.Emit(new AudioSample(new[] { 1f }, 48000, 1));

        Assert.Empty(levels);
    }
}

public class AudioLevelMeterTests
{
    [Fact]
    public void ConstantSine_ConvergesToRms()
    {
        var meter = new AudioLevelMeter();
        var sample = new AudioSample(new[] { 0.5f, -0.5f, 0.5f, -0.5f }, 48000, 1);

        float level = 0;
        for (var i = 0; i < 30; i++)
            level = meter.Push(sample);

        Assert.InRange(level, 0.45f, 0.55f);
    }

    [Fact]
    public void Silence_DrivesTowardZero()
    {
        var meter = new AudioLevelMeter();
        meter.Push(new AudioSample(new[] { 1f, 1f, 1f, 1f }, 48000, 1));
        var samples = new float[1024];
        var sample = new AudioSample(samples, 48000, 1);

        for (var i = 0; i < 50; i++)
            meter.Push(sample);

        Assert.Equal(0f, meter.Level, 3);
    }

    [Fact]
    public void EmptySample_KeepsLevel()
    {
        var meter = new AudioLevelMeter();
        meter.Push(new AudioSample(new[] { 1f, 1f, 1f, 1f }, 48000, 1));
        var before = meter.Level;

        var level = meter.Push(new AudioSample(Array.Empty<float>(), 48000, 1));

        Assert.Equal(before, level);
    }

    [Fact]
    public void Reset_ZerosLevel()
    {
        var meter = new AudioLevelMeter();
        meter.Push(new AudioSample(new[] { 1f, 1f, 1f, 1f }, 48000, 1));

        meter.Reset();

        Assert.Equal(0f, meter.Level);
    }

    [Fact]
    public void ToDisplay_IsLogarithmic()
    {
        Assert.Equal(0f, AudioLevelMeter.ToDisplay(0f));
        Assert.Equal(0f, AudioLevelMeter.ToDisplay(0.001f), 2);

        // -60 dBFS floor → 0, 0 dBFS → 1, and +10 dB amplification: a decade
        // (~-20 dB) is 2/3 up the bar.
        Assert.Equal(1f, AudioLevelMeter.ToDisplay(1f), 3);
        Assert.InRange(AudioLevelMeter.ToDisplay(0.0316f), 0.66f, 0.67f);
        Assert.InRange(AudioLevelMeter.ToDisplay(0.1f), 0.83f, 0.84f);
        Assert.InRange(AudioLevelMeter.ToDisplay(0.01f), 0.49f, 0.51f);
    }

    [Fact]
    public void ToDisplay_Pushes_Speech_Peaks_Into_Red_At_Maxed_Volume()
    {
        // ~0.2 linear RMS (-14 dBFS) is a loud speech peak; at maxed volume it
        // must land in the red zone (0.8+) and normal speech (~0.05, -26 dBFS)
        // in the yellow — the meter uses the whole bar proportionately.
        Assert.InRange(AudioLevelMeter.ToDisplay(0.2f), 0.92f, 0.95f);
        Assert.InRange(AudioLevelMeter.ToDisplay(0.05f), 0.72f, 0.75f);
    }

    [Fact]
    public void ToDisplay_ClampsAtFloor()
    {
        Assert.Equal(0f, AudioLevelMeter.ToDisplay(0.0001f));
        Assert.Equal(0f, AudioLevelMeter.ToDisplay(-1f));
    }
}

public class WaveToFloatTests
{
    private static byte[] FloatSamples(params float[] values)
    {
        var bytes = new byte[values.Length * 4];
        Buffer.BlockCopy(values, 0, bytes, 0, bytes.Length);
        return bytes;
    }

    [Fact]
    public void IeeeFloat32_PreservesValues()
    {
        var format = WaveFormat.CreateIeeeFloatWaveFormat(48000, 2);
        var samples = WaveToFloat.Convert(FloatSamples(0.25f, -0.5f, 1f), 12, format);

        Assert.Equal(3, samples.Length);
        Assert.Equal(0.25f, samples[0], 4);
        Assert.Equal(-0.5f, samples[1], 4);
        Assert.Equal(1f, samples[2], 4);
    }

    [Fact]
    public void Pcm16_NormalizesToUnitRange()
    {
        var format = WaveFormat.CreateCustomFormat(WaveFormatEncoding.Pcm, 48000, 1, 48000 * 2, 2, 16);
        var bytes = new byte[] { 0x00, 0x00, 0xFF, 0x7F, 0x00, 0x80 };
        var samples = WaveToFloat.Convert(bytes, 6, format);

        Assert.Equal(3, samples.Length);
        Assert.Equal(0f, samples[0], 4);
        Assert.Equal(1f, samples[1], 4);
        Assert.Equal(-1f, samples[2], 4);
    }

    [Fact]
    public void TruncatedTrailingBytes_AreIgnored()
    {
        var format = WaveFormat.CreateIeeeFloatWaveFormat(48000, 2);
        var bytes = new byte[] { 0, 0, 0x80, 0x3F, 1, 2, 3 }; // 1 float + 3 stray bytes

        var samples = WaveToFloat.Convert(bytes, bytes.Length, format);

        Assert.Single(samples);
        Assert.Equal(1f, samples[0], 4);
    }
}
