// Verifies the pipeline's initial state, boost level clamping, gain curve
// shape, and start/stop lifecycle through an injected test audio device.
// This target still reports lower aggregate coverage than the pipeline logic
// itself because it links the default `WasapiAudioDevice` path. The remaining
// gap is the real COM/default-endpoint startup flow, which would require live
// hardware integration tests or deeper DI below `AudioDevice`. We do not take
// that extra step here because live hardware makes the suite environment-
// dependent, and deeper DI would push test scaffolding into the concrete
// WASAPI adapter.

#include "audio_pipeline.hpp"

#include <audioclient.h>

#include <chrono>
#include <cmath>
#include <future>
#include <memory>
#include <string>
#include <utility>

#include "audio_device.hpp"
#include "bass_boost_filter.hpp"
#include "gmock/gmock.h"
#include "gtest/gtest.h"

namespace {

using ::testing::_;
using ::testing::AllOf;
using ::testing::Each;
using ::testing::Ge;
using ::testing::Le;
using ::testing::Return;
using ::testing::ReturnRefOfCopy;

class MockAudioDevice final : public AudioDevice {
 public:
  MOCK_METHOD(AudioPipelineInterface::Status, Open, (), (override));

  MOCK_METHOD(AudioPipelineInterface::Status, StartStreams, (), (override));

  MOCK_METHOD(void, StopStreams, (), (override));

  MOCK_METHOD(void, Close, (), (override));

  MOCK_METHOD(CapturePacket, ReadNextPacket, (), (override));

  MOCK_METHOD(HRESULT, WriteRenderPacket, (std::span<const float> pcm),
              (override));

  MOCK_METHOD(bool, TryRecover, (HRESULT failure), (override));

  MOCK_METHOD(double, sample_rate, (), (const, override));

  MOCK_METHOD(const std::wstring&, endpoint_name, (), (const, override));
};

TEST(AudioPipelineTest, NotRunningBeforeStart) {
  AudioPipeline pipeline(std::make_unique<MockAudioDevice>());

  EXPECT_FALSE(pipeline.is_running());
}

TEST(AudioPipelineTest, DefaultConstructorStartsStopped) {
  AudioPipeline pipeline;

  EXPECT_FALSE(pipeline.is_running());
}

TEST(AudioPipelineTest, DefaultGainIsZero) {
  AudioPipeline pipeline(std::make_unique<MockAudioDevice>());

  EXPECT_NEAR(pipeline.gain_db(), 0.0, 1e-9);
}

TEST(AudioPipelineTest, EndpointNameEmptyBeforeInitialize) {
  AudioPipeline pipeline(std::make_unique<MockAudioDevice>());

  EXPECT_TRUE(pipeline.endpoint_name().empty());
}

TEST(AudioPipelineTest, MaxBoostSetsMaxGain) {
  AudioPipeline pipeline(std::make_unique<MockAudioDevice>());

  pipeline.SetBoostLevel(1.0);

  EXPECT_NEAR(pipeline.gain_db(), BassBoostFilter::kMaxGainDb, 1e-9);
}

TEST(AudioPipelineTest, FlatBoostSetsZeroGain) {
  AudioPipeline pipeline(std::make_unique<MockAudioDevice>());

  pipeline.SetBoostLevel(0.0);

  EXPECT_NEAR(pipeline.gain_db(), 0.0, 1e-9);
}

TEST(AudioPipelineTest, HalfBoostScalesGainBySqrt) {
  constexpr double kHalfLevel = 0.5;
  AudioPipeline pipeline(std::make_unique<MockAudioDevice>());

  pipeline.SetBoostLevel(kHalfLevel);

  EXPECT_NEAR(pipeline.gain_db(),
              BassBoostFilter::kMaxGainDb * std::sqrt(kHalfLevel), 1e-9);
}

TEST(AudioPipelineTest, BoostCurveIsConvexAtMidpoint) {
  constexpr double kHalfLevel = 0.5;
  AudioPipeline pipeline(std::make_unique<MockAudioDevice>());

  pipeline.SetBoostLevel(kHalfLevel);

  EXPECT_GT(pipeline.gain_db(), BassBoostFilter::kMaxGainDb * kHalfLevel);
}

TEST(AudioPipelineTest, BoostCurveIsConvexAtQuarterLevel) {
  constexpr double kQuarterLevel = 0.25;
  AudioPipeline pipeline(std::make_unique<MockAudioDevice>());

  pipeline.SetBoostLevel(kQuarterLevel);

  EXPECT_GT(pipeline.gain_db(), BassBoostFilter::kMaxGainDb * kQuarterLevel);
}

TEST(AudioPipelineTest, BoostLevelClampedAboveOne) {
  constexpr double kAboveMaxLevel = 2.0;
  AudioPipeline pipeline(std::make_unique<MockAudioDevice>());

  pipeline.SetBoostLevel(kAboveMaxLevel);

  EXPECT_NEAR(pipeline.gain_db(), BassBoostFilter::kMaxGainDb, 1e-9);
}

TEST(AudioPipelineTest, BoostLevelClampedBelowZero) {
  AudioPipeline pipeline(std::make_unique<MockAudioDevice>());

  pipeline.SetBoostLevel(-1.0);

  EXPECT_NEAR(pipeline.gain_db(), 0.0, 1e-9);
}

TEST(AudioPipelineTest, StopBeforeStartIsSafe) {
  auto device = std::make_unique<MockAudioDevice>();
  EXPECT_CALL(*device, StopStreams()).Times(0);
  // `Close()` is called once by `Stop()` and once by the destructor.
  EXPECT_CALL(*device, Close()).Times(2);
  AudioPipeline pipeline(std::move(device));

  pipeline.Stop();

  EXPECT_FALSE(pipeline.is_running());
}

TEST(AudioPipelineTest, RepeatedBoostLevelUpdatesReflectLatest) {
  AudioPipeline pipeline(std::make_unique<MockAudioDevice>());

  pipeline.SetBoostLevel(0.0);
  pipeline.SetBoostLevel(1.0);

  EXPECT_NEAR(pipeline.gain_db(), BassBoostFilter::kMaxGainDb, 1e-9);
}

TEST(AudioPipelineTest, GainFollowsSqrtAtThreeQuarterLevel) {
  constexpr double kThreeQuarterLevel = 0.75;
  AudioPipeline pipeline(std::make_unique<MockAudioDevice>());

  pipeline.SetBoostLevel(kThreeQuarterLevel);

  EXPECT_NEAR(pipeline.gain_db(),
              BassBoostFilter::kMaxGainDb * std::sqrt(kThreeQuarterLevel),
              1e-9);
}

TEST(AudioPipelineTest, DoubleStopIsSafe) {
  AudioPipeline pipeline(std::make_unique<MockAudioDevice>());

  pipeline.Stop();
  pipeline.Stop();

  EXPECT_FALSE(pipeline.is_running());
}

TEST(AudioPipelineTest, DestructorAfterStopIsSafe) {
  AudioPipeline pipeline(std::make_unique<MockAudioDevice>());
  pipeline.Stop();

  SUCCEED();
}

TEST(AudioPipelineTest, SetBoostLevelAfterStopStillUpdatesGain) {
  AudioPipeline pipeline(std::make_unique<MockAudioDevice>());

  pipeline.Stop();
  pipeline.SetBoostLevel(1.0);

  EXPECT_NEAR(pipeline.gain_db(), BassBoostFilter::kMaxGainDb, 1e-9);
}

TEST(AudioPipelineTest, GainFollowsSqrtAtTenPercentLevel) {
  constexpr double kTenPercentLevel = 0.1;
  AudioPipeline pipeline(std::make_unique<MockAudioDevice>());

  pipeline.SetBoostLevel(kTenPercentLevel);

  EXPECT_NEAR(pipeline.gain_db(),
              BassBoostFilter::kMaxGainDb * std::sqrt(kTenPercentLevel), 1e-9);
}

TEST(AudioPipelineTest, GainFollowsSqrtAtNinetyPercentLevel) {
  constexpr double kNinetyPercentLevel = 0.9;
  AudioPipeline pipeline(std::make_unique<MockAudioDevice>());

  pipeline.SetBoostLevel(kNinetyPercentLevel);

  EXPECT_NEAR(pipeline.gain_db(),
              BassBoostFilter::kMaxGainDb * std::sqrt(kNinetyPercentLevel),
              1e-9);
}

TEST(AudioPipelineTest, StartFailsWhenDeviceOpenFails) {
  auto device = std::make_unique<MockAudioDevice>();
  EXPECT_CALL(*device, Open())
      .WillOnce(Return(
          AudioPipelineInterface::Status::Error(E_FAIL, L"Test open error")));
  AudioPipeline pipeline(std::move(device));

  const AudioPipelineInterface::Status status = pipeline.Start();

  EXPECT_FALSE(status.ok());
  EXPECT_EQ(status.code, E_FAIL);
  EXPECT_EQ(status.error_message, L"Test open error");
  EXPECT_FALSE(pipeline.is_running());
}

TEST(AudioPipelineTest, StartSucceedsWithInjectedDevice) {
  auto device = std::make_unique<MockAudioDevice>();
  EXPECT_CALL(*device, Open())
      .WillOnce(Return(AudioPipelineInterface::Status::Ok()));
  // `Start()` always initializes `filter_` from `device.sample_rate()` before
  // it reports success. This test does not assert on DSP behavior, so a normal
  // 48 kHz rate keeps the setup realistic without adding extra expectations.
  constexpr double kSampleRateHz = 48000.0;
  EXPECT_CALL(*device, sample_rate()).WillRepeatedly(Return(kSampleRateHz));
  EXPECT_CALL(*device, endpoint_name())
      .WillRepeatedly(ReturnRefOfCopy(std::wstring(L"Configured Test Device")));
  EXPECT_CALL(*device, StartStreams())
      .WillOnce(Return(AudioPipelineInterface::Status::Ok()));
  EXPECT_CALL(*device, ReadNextPacket())
      .WillRepeatedly(Return(CapturePacket{}));
  AudioPipeline pipeline(std::move(device));

  const AudioPipelineInterface::Status status = pipeline.Start();

  EXPECT_TRUE(status.ok());
  EXPECT_TRUE(pipeline.is_running());
  EXPECT_EQ(pipeline.endpoint_name(), L"Configured Test Device");
}

TEST(AudioPipelineTest, StartStreamsFailureStopsPipeline) {
  auto device = std::make_unique<MockAudioDevice>();
  EXPECT_CALL(*device, Open())
      .WillOnce(Return(AudioPipelineInterface::Status::Ok()));
  constexpr double kSampleRateHz = 48000.0;
  EXPECT_CALL(*device, sample_rate()).WillRepeatedly(Return(kSampleRateHz));
  EXPECT_CALL(*device, endpoint_name())
      .WillRepeatedly(ReturnRefOfCopy(std::wstring(L"Test Device")));
  EXPECT_CALL(*device, StartStreams())
      .WillOnce(Return(
          AudioPipelineInterface::Status::Error(E_FAIL, L"Test start error")));
  AudioPipeline pipeline(std::move(device));
  ASSERT_TRUE(pipeline.Start().ok());

  pipeline.Stop();

  EXPECT_FALSE(pipeline.is_running());
}

TEST(AudioPipelineTest, StartWhileRunningReturnsOk) {
  auto device = std::make_unique<MockAudioDevice>();
  EXPECT_CALL(*device, Open())
      .WillOnce(Return(AudioPipelineInterface::Status::Ok()));
  constexpr double kSampleRateHz = 48000.0;
  EXPECT_CALL(*device, sample_rate()).WillRepeatedly(Return(kSampleRateHz));
  EXPECT_CALL(*device, endpoint_name())
      .WillRepeatedly(ReturnRefOfCopy(std::wstring(L"Test Device")));
  EXPECT_CALL(*device, StartStreams())
      .WillOnce(Return(AudioPipelineInterface::Status::Ok()));
  EXPECT_CALL(*device, ReadNextPacket())
      .WillRepeatedly(Return(CapturePacket{}));
  AudioPipeline pipeline(std::move(device));
  ASSERT_TRUE(pipeline.Start().ok());

  const AudioPipelineInterface::Status second_start = pipeline.Start();

  EXPECT_TRUE(second_start.ok());
  EXPECT_TRUE(pipeline.is_running());
}

TEST(AudioPipelineTest, BoostLevelUpdatesWhileRunning) {
  auto device = std::make_unique<MockAudioDevice>();
  EXPECT_CALL(*device, Open())
      .WillOnce(Return(AudioPipelineInterface::Status::Ok()));
  constexpr double kSampleRateHz = 48000.0;
  EXPECT_CALL(*device, sample_rate()).WillRepeatedly(Return(kSampleRateHz));
  EXPECT_CALL(*device, endpoint_name())
      .WillRepeatedly(ReturnRefOfCopy(std::wstring(L"Test Device")));
  EXPECT_CALL(*device, StartStreams())
      .WillOnce(Return(AudioPipelineInterface::Status::Ok()));
  EXPECT_CALL(*device, ReadNextPacket())
      .WillRepeatedly(Return(CapturePacket{}));
  AudioPipeline pipeline(std::move(device));
  ASSERT_TRUE(pipeline.Start().ok());

  pipeline.SetBoostLevel(1.0);

  EXPECT_NEAR(pipeline.gain_db(), BassBoostFilter::kMaxGainDb, 1e-9);
}

TEST(AudioPipelineTest, StopAfterStartCleansUpResources) {
  auto device = std::make_unique<MockAudioDevice>();
  EXPECT_CALL(*device, StopStreams()).Times(1);
  // `Close()` is called once by `Stop()` and once by the destructor.
  EXPECT_CALL(*device, Close()).Times(2);
  EXPECT_CALL(*device, Open())
      .WillOnce(Return(AudioPipelineInterface::Status::Ok()));
  constexpr double kSampleRateHz = 48000.0;
  EXPECT_CALL(*device, sample_rate()).WillRepeatedly(Return(kSampleRateHz));
  EXPECT_CALL(*device, endpoint_name())
      .WillRepeatedly(ReturnRefOfCopy(std::wstring(L"Configured Test Device")));
  EXPECT_CALL(*device, StartStreams())
      .WillOnce(Return(AudioPipelineInterface::Status::Ok()));
  EXPECT_CALL(*device, ReadNextPacket())
      .WillRepeatedly(Return(CapturePacket{}));
  AudioPipeline pipeline(std::move(device));

  ASSERT_TRUE(pipeline.Start().ok());
  ASSERT_TRUE(pipeline.is_running());

  pipeline.Stop();

  EXPECT_FALSE(pipeline.is_running());
  EXPECT_EQ(pipeline.endpoint_name(), L"Configured Test Device");
}

TEST(AudioPipelineTest, NonSilentPacketIsProcessedAndRendered) {
  auto device = std::make_unique<MockAudioDevice>();
  EXPECT_CALL(*device, Open())
      .WillOnce(Return(AudioPipelineInterface::Status::Ok()));
  constexpr double kSampleRateHz = 48000.0;
  EXPECT_CALL(*device, sample_rate()).WillRepeatedly(Return(kSampleRateHz));
  EXPECT_CALL(*device, endpoint_name())
      .WillRepeatedly(ReturnRefOfCopy(std::wstring(L"Test Device")));
  EXPECT_CALL(*device, StartStreams())
      .WillOnce(Return(AudioPipelineInterface::Status::Ok()));
  std::promise<void> queue_drained;
  std::future<void> queue_drained_future = queue_drained.get_future();
  int render_call_count = 0;
  constexpr float kSampleA = 0.5F;
  constexpr float kSampleB = 0.3F;
  EXPECT_CALL(*device, ReadNextPacket())
      .WillOnce(Return(CapturePacket{
          .samples = {kSampleA, -kSampleA, kSampleB, -kSampleB}, .frames = 2}))
      .WillOnce([&queue_drained] {
        queue_drained.set_value();
        return CapturePacket{};
      });
  EXPECT_CALL(*device, WriteRenderPacket(_))
      .WillOnce([&render_call_count](std::span<const float> /*pcm*/) {
        ++render_call_count;
        return S_OK;
      });
  AudioPipeline pipeline(std::move(device));
  pipeline.SetBoostLevel(1.0);

  ASSERT_TRUE(pipeline.Start().ok());
  ASSERT_EQ(queue_drained_future.wait_for(std::chrono::seconds(1)),
            std::future_status::ready);
  pipeline.Stop();

  EXPECT_GT(render_call_count, 0);
}

TEST(AudioPipelineTest, SilentPacketIsNotRendered) {
  auto device = std::make_unique<MockAudioDevice>();
  EXPECT_CALL(*device, Open())
      .WillOnce(Return(AudioPipelineInterface::Status::Ok()));
  constexpr double kSampleRateHz = 48000.0;
  EXPECT_CALL(*device, sample_rate()).WillRepeatedly(Return(kSampleRateHz));
  EXPECT_CALL(*device, endpoint_name())
      .WillRepeatedly(ReturnRefOfCopy(std::wstring(L"Test Device")));
  EXPECT_CALL(*device, StartStreams())
      .WillOnce(Return(AudioPipelineInterface::Status::Ok()));
  std::promise<void> queue_drained;
  std::future<void> queue_drained_future = queue_drained.get_future();
  constexpr float kSampleA = 0.5F;
  constexpr float kSampleB = 0.3F;
  EXPECT_CALL(*device, ReadNextPacket())
      .WillOnce(Return(
          CapturePacket{.samples = {kSampleA, -kSampleA, kSampleB, -kSampleB},
                        .frames = 2,
                        .silent = true}))
      .WillOnce([&queue_drained] {
        queue_drained.set_value();
        return CapturePacket{};
      });
  EXPECT_CALL(*device, WriteRenderPacket(_)).Times(0);
  AudioPipeline pipeline(std::move(device));

  ASSERT_TRUE(pipeline.Start().ok());
  ASSERT_EQ(queue_drained_future.wait_for(std::chrono::seconds(1)),
            std::future_status::ready);
  pipeline.Stop();
}

TEST(AudioPipelineTest, RenderedDeltaIsClamped) {
  auto device = std::make_unique<MockAudioDevice>();
  EXPECT_CALL(*device, Open())
      .WillOnce(Return(AudioPipelineInterface::Status::Ok()));
  constexpr double kSampleRateHz = 48000.0;
  EXPECT_CALL(*device, sample_rate()).WillRepeatedly(Return(kSampleRateHz));
  EXPECT_CALL(*device, endpoint_name())
      .WillRepeatedly(ReturnRefOfCopy(std::wstring(L"Test Device")));
  EXPECT_CALL(*device, StartStreams())
      .WillOnce(Return(AudioPipelineInterface::Status::Ok()));
  std::promise<void> queue_drained;
  std::future<void> queue_drained_future = queue_drained.get_future();
  std::vector<float> rendered_samples;
  constexpr float kSampleA = 0.5F;
  constexpr float kSampleB = 0.3F;
  EXPECT_CALL(*device, ReadNextPacket())
      .WillOnce(Return(CapturePacket{
          .samples = {kSampleA, -kSampleA, kSampleB, -kSampleB}, .frames = 2}))
      .WillOnce([&queue_drained] {
        queue_drained.set_value();
        return CapturePacket{};
      });
  EXPECT_CALL(*device, WriteRenderPacket(_))
      .WillOnce([&rendered_samples](std::span<const float> pcm) {
        rendered_samples.assign(pcm.begin(), pcm.end());
        return S_OK;
      });
  AudioPipeline pipeline(std::move(device));
  pipeline.SetBoostLevel(1.0);

  ASSERT_TRUE(pipeline.Start().ok());
  ASSERT_EQ(queue_drained_future.wait_for(std::chrono::seconds(1)),
            std::future_status::ready);
  pipeline.Stop();

  EXPECT_THAT(rendered_samples, Each(AllOf(Ge(-1.0F), Le(1.0F))));
}

TEST(AudioPipelineTest, FailedReadWithoutRecoveryStopsPipeline) {
  auto device = std::make_unique<MockAudioDevice>();
  EXPECT_CALL(*device, Open())
      .WillOnce(Return(AudioPipelineInterface::Status::Ok()));
  constexpr double kSampleRateHz = 48000.0;
  EXPECT_CALL(*device, sample_rate()).WillRepeatedly(Return(kSampleRateHz));
  EXPECT_CALL(*device, endpoint_name())
      .WillRepeatedly(ReturnRefOfCopy(std::wstring(L"Test Device")));
  EXPECT_CALL(*device, StartStreams())
      .WillOnce(Return(AudioPipelineInterface::Status::Ok()));
  std::promise<void> recover_attempted;
  std::future<void> recover_attempted_future = recover_attempted.get_future();
  EXPECT_CALL(*device, ReadNextPacket())
      .WillOnce(Return(CapturePacket{.status = E_FAIL}));
  EXPECT_CALL(*device, TryRecover(E_FAIL))
      .WillOnce([&recover_attempted](HRESULT /*failure*/) {
        recover_attempted.set_value();
        return false;
      });
  AudioPipeline pipeline(std::move(device));

  ASSERT_TRUE(pipeline.Start().ok());
  ASSERT_EQ(recover_attempted_future.wait_for(std::chrono::seconds(1)),
            std::future_status::ready);
  pipeline.Stop();

  EXPECT_FALSE(pipeline.is_running());
}

TEST(AudioPipelineTest, FailedRenderWithoutRecoveryStopsPipeline) {
  auto device = std::make_unique<MockAudioDevice>();
  EXPECT_CALL(*device, Open())
      .WillOnce(Return(AudioPipelineInterface::Status::Ok()));
  constexpr double kSampleRateHz = 48000.0;
  EXPECT_CALL(*device, sample_rate()).WillRepeatedly(Return(kSampleRateHz));
  EXPECT_CALL(*device, endpoint_name())
      .WillRepeatedly(ReturnRefOfCopy(std::wstring(L"Test Device")));
  EXPECT_CALL(*device, StartStreams())
      .WillOnce(Return(AudioPipelineInterface::Status::Ok()));
  std::promise<void> recover_attempted;
  std::future<void> recover_attempted_future = recover_attempted.get_future();
  constexpr float kSampleA = 0.5F;
  constexpr float kSampleB = 0.3F;
  EXPECT_CALL(*device, ReadNextPacket())
      .WillOnce(Return(CapturePacket{
          .samples = {kSampleA, -kSampleA, kSampleB, -kSampleB}, .frames = 2}));
  EXPECT_CALL(*device, WriteRenderPacket(_)).WillOnce(Return(E_FAIL));
  EXPECT_CALL(*device, TryRecover(E_FAIL))
      .WillOnce([&recover_attempted](HRESULT /*failure*/) {
        recover_attempted.set_value();
        return false;
      });
  AudioPipeline pipeline(std::move(device));

  ASSERT_TRUE(pipeline.Start().ok());
  ASSERT_EQ(recover_attempted_future.wait_for(std::chrono::seconds(1)),
            std::future_status::ready);
  pipeline.Stop();

  EXPECT_FALSE(pipeline.is_running());
}

TEST(AudioPipelineTest, RecoverableFailureRefreshesFilterSampleRate) {
  auto device = std::make_unique<MockAudioDevice>();
  EXPECT_CALL(*device, Open())
      .WillOnce(Return(AudioPipelineInterface::Status::Ok()));
  std::promise<void> sample_rate_refreshed;
  std::future<void> sample_rate_refreshed_future =
      sample_rate_refreshed.get_future();
  constexpr double kSampleRateHz = 48000.0;
  EXPECT_CALL(*device, sample_rate())
      .WillOnce(Return(kSampleRateHz))
      .WillOnce([&sample_rate_refreshed] {
        sample_rate_refreshed.set_value();
        constexpr double kNewSampleRateHz = 44100.0;
        return kNewSampleRateHz;
      });
  EXPECT_CALL(*device, endpoint_name())
      .WillRepeatedly(ReturnRefOfCopy(std::wstring(L"Test Device")));
  EXPECT_CALL(*device, StartStreams())
      .WillOnce(Return(AudioPipelineInterface::Status::Ok()));
  EXPECT_CALL(*device, ReadNextPacket())
      .WillOnce(Return(CapturePacket{.status = AUDCLNT_E_DEVICE_INVALIDATED}))
      .WillRepeatedly(Return(CapturePacket{}));
  EXPECT_CALL(*device, TryRecover(AUDCLNT_E_DEVICE_INVALIDATED))
      .WillOnce(Return(true));
  AudioPipeline pipeline(std::move(device));

  ASSERT_TRUE(pipeline.Start().ok());
  ASSERT_EQ(sample_rate_refreshed_future.wait_for(std::chrono::seconds(1)),
            std::future_status::ready);
  pipeline.Stop();

  EXPECT_FALSE(pipeline.is_running());
}

}  // namespace

Generated by OpenCppCoverage (Version: 0.9.9.0)