// Verifies `WasapiAudioDevice` behavior without real audio hardware by using
// constructor-injected clients plus mock COM clients.
// This target keeps lower coverage because the concrete adapter still owns the
// real default-endpoint acquisition and WASAPI client setup path. Covering
// more of that code would require live hardware integration tests or a deeper
// injected boundary below `AudioDevice`. We do not take that extra step here
// because live hardware makes the suite machine-dependent, and a deeper
// injected boundary would distort the production design just to raise
// coverage.

#include "wasapi_audio_device.hpp"

#include <array>
#include <cstring>
#include <utility>
#include <vector>

#include "gmock/gmock.h"
#include "gtest/gtest.h"

namespace {

using ::testing::_;
using ::testing::DoAll;
using ::testing::Return;
using ::testing::SetArgPointee;

// `MOCK_METHOD` plus this repository's `clang-format` config rewrites raw
// pointer parameters like `BYTE**` into harder-to-read forms such as
// `BYTE * *`. Keep the pointer spellings behind aliases so these mock
// signatures stay readable after the pre-commit formatter runs.
using MockByteBufferOut = BYTE**;
using MockUint32Out = UINT32*;
using MockWaveFormatOut = WAVEFORMATEX**;

class MockAudioClient final : public IAudioClient {
 public:
  MOCK_METHOD(HRESULT, QueryInterface, (REFIID riid, void** object),
              (Calltype(STDMETHODCALLTYPE), override));
  MOCK_METHOD(ULONG, AddRef, (), (Calltype(STDMETHODCALLTYPE), override));
  MOCK_METHOD(ULONG, Release, (), (Calltype(STDMETHODCALLTYPE), override));
  MOCK_METHOD(HRESULT, Initialize,
              (AUDCLNT_SHAREMODE share_mode, DWORD stream_flags,
               REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity,
               const WAVEFORMATEX* format, LPCGUID audio_session_guid),
              (Calltype(STDMETHODCALLTYPE), override));
  MOCK_METHOD(HRESULT, GetStreamLatency, (REFERENCE_TIME * latency),
              (Calltype(STDMETHODCALLTYPE), override));

  MOCK_METHOD(HRESULT, GetBufferSize, (MockUint32Out out_buffer_size),
              (Calltype(STDMETHODCALLTYPE), override));
  MOCK_METHOD(HRESULT, GetCurrentPadding, (MockUint32Out out_padding),
              (Calltype(STDMETHODCALLTYPE), override));
  MOCK_METHOD(HRESULT, IsFormatSupported,
              (AUDCLNT_SHAREMODE share_mode, const WAVEFORMATEX* format,
               WAVEFORMATEX** closest_match),
              (Calltype(STDMETHODCALLTYPE), override));
  MOCK_METHOD(HRESULT, GetMixFormat, (MockWaveFormatOut device_format),
              (Calltype(STDMETHODCALLTYPE), override));
  MOCK_METHOD(HRESULT, GetDevicePeriod,
              (REFERENCE_TIME * default_device_period,
               REFERENCE_TIME* minimum_device_period),
              (Calltype(STDMETHODCALLTYPE), override));
  MOCK_METHOD(HRESULT, Start, (), (Calltype(STDMETHODCALLTYPE), override));
  MOCK_METHOD(HRESULT, Stop, (), (Calltype(STDMETHODCALLTYPE), override));
  MOCK_METHOD(HRESULT, Reset, (), (Calltype(STDMETHODCALLTYPE), override));
  MOCK_METHOD(HRESULT, SetEventHandle, (HANDLE event_handle),
              (Calltype(STDMETHODCALLTYPE), override));
  MOCK_METHOD(HRESULT, GetService, (REFIID riid, void** service),
              (Calltype(STDMETHODCALLTYPE), override));
};

class MockAudioCaptureClient final : public IAudioCaptureClient {
 public:
  MOCK_METHOD(HRESULT, QueryInterface, (REFIID riid, void** object),
              (Calltype(STDMETHODCALLTYPE), override));
  MOCK_METHOD(ULONG, AddRef, (), (Calltype(STDMETHODCALLTYPE), override));
  MOCK_METHOD(ULONG, Release, (), (Calltype(STDMETHODCALLTYPE), override));
  MOCK_METHOD(HRESULT, GetBuffer,
              (MockByteBufferOut out_data, UINT32* out_frames, DWORD* out_flags,
               UINT64* device_position, UINT64* qpc_position),
              (Calltype(STDMETHODCALLTYPE), override));
  MOCK_METHOD(HRESULT, ReleaseBuffer, (UINT32 num_frames_read),
              (Calltype(STDMETHODCALLTYPE), override));
  MOCK_METHOD(HRESULT, GetNextPacketSize, (MockUint32Out out_packet_size),
              (Calltype(STDMETHODCALLTYPE), override));
};

class MockAudioRenderClient final : public IAudioRenderClient {
 public:
  MOCK_METHOD(HRESULT, QueryInterface, (REFIID riid, void** object),
              (Calltype(STDMETHODCALLTYPE), override));
  MOCK_METHOD(ULONG, AddRef, (), (Calltype(STDMETHODCALLTYPE), override));
  MOCK_METHOD(ULONG, Release, (), (Calltype(STDMETHODCALLTYPE), override));
  MOCK_METHOD(HRESULT, GetBuffer,
              (UINT32 num_frames_requested, BYTE** out_data),
              (Calltype(STDMETHODCALLTYPE), override));
  MOCK_METHOD(HRESULT, ReleaseBuffer,
              (UINT32 num_frames_written, DWORD release_flags),
              (Calltype(STDMETHODCALLTYPE), override));
};

// Returns a stereo 32-bit float `WAVEFORMATEX` describing the given sample
// rate. `WasapiAudioDevice` frees its format pointer with `CoTaskMemFree`, so
// the caller must copy the result into `CoTaskMemAlloc`-allocated memory;
// otherwise the device calls `CoTaskMemFree` on a non-COM pointer and the
// process crashes.
WAVEFORMATEX StereoFloatFormat(DWORD sample_rate_hz) {
  constexpr WORD kStereoChannelCount = 2;
  constexpr WORD kBitsPerSampleFloat = 32;
  constexpr WORD kStereoBlockAlign = 8;
  return {.wFormatTag = WAVE_FORMAT_IEEE_FLOAT,
          .nChannels = kStereoChannelCount,
          .nSamplesPerSec = sample_rate_hz,
          .nAvgBytesPerSec = sample_rate_hz * kStereoBlockAlign,
          .nBlockAlign = kStereoBlockAlign,
          .wBitsPerSample = kBitsPerSampleFloat,
          .cbSize = 0};
}

}  // namespace

TEST(WasapiAudioDeviceTest, OpenWithoutComInitializationFails) {
  WasapiAudioDevice device;

  const AudioPipelineInterface::Status status = device.Open();

  EXPECT_FALSE(status.ok());
  EXPECT_TRUE(FAILED(status.code));
  EXPECT_FALSE(status.error_message.empty());
  EXPECT_DOUBLE_EQ(device.sample_rate(), 0.0);
  EXPECT_TRUE(device.endpoint_name().empty());
}

TEST(WasapiAudioDeviceTest, StartStreamsBeforeOpenFails) {
  WasapiAudioDevice device;

  const AudioPipelineInterface::Status status = device.StartStreams();

  EXPECT_FALSE(status.ok());
  EXPECT_EQ(status.code, E_POINTER);
  EXPECT_FALSE(status.error_message.empty());
}

TEST(WasapiAudioDeviceTest, StartStreamsCaptureStartFailureReturnsError) {
  MockAudioClient capture_client;
  EXPECT_CALL(capture_client, Start()).WillOnce(Return(E_FAIL));
  EXPECT_CALL(capture_client, Stop()).WillOnce(Return(S_OK));

  MockAudioClient render_client;
  EXPECT_CALL(render_client, Start()).Times(0);
  EXPECT_CALL(render_client, Stop()).WillOnce(Return(S_OK));

  WasapiAudioDevice device(
      {.capture_client = &capture_client, .render_client = &render_client});

  const AudioPipelineInterface::Status status = device.StartStreams();

  EXPECT_FALSE(status.ok());
  EXPECT_EQ(status.code, E_FAIL);
  device.Close();
}

TEST(WasapiAudioDeviceTest, StartStreamsRenderStartFailureStopsCapture) {
  MockAudioClient capture_client;
  EXPECT_CALL(capture_client, Start()).WillOnce(Return(S_OK));
  // `Stop()` is called once by `StopStreams()` and once by `Close()`.
  EXPECT_CALL(capture_client, Stop()).Times(2).WillRepeatedly(Return(S_OK));

  MockAudioClient render_client;
  EXPECT_CALL(render_client, Start()).WillOnce(Return(E_FAIL));
  EXPECT_CALL(render_client, Stop()).WillOnce(Return(S_OK));

  WasapiAudioDevice device(
      {.capture_client = &capture_client, .render_client = &render_client});

  const AudioPipelineInterface::Status status = device.StartStreams();

  EXPECT_FALSE(status.ok());
  EXPECT_EQ(status.code, E_FAIL);
  device.Close();
}

TEST(WasapiAudioDeviceTest, StartStreamsStartsConfiguredClients) {
  MockAudioClient capture_client;
  EXPECT_CALL(capture_client, Start()).WillOnce(Return(S_OK));
  EXPECT_CALL(capture_client, Stop()).WillOnce(Return(S_OK));

  MockAudioClient render_client;
  EXPECT_CALL(render_client, Start()).WillOnce(Return(S_OK));
  EXPECT_CALL(render_client, Stop()).WillOnce(Return(S_OK));

  WasapiAudioDevice device(
      {.capture_client = &capture_client, .render_client = &render_client});

  const AudioPipelineInterface::Status status = device.StartStreams();

  EXPECT_TRUE(status.ok());
  device.Close();
}

TEST(WasapiAudioDeviceTest, StopStreamsStopsConfiguredClients) {
  MockAudioClient capture_client;
  // `Stop()` is called once by `StopStreams()` and once by `Close()`.
  EXPECT_CALL(capture_client, Stop()).Times(2).WillRepeatedly(Return(S_OK));

  MockAudioClient render_client;
  // `Stop()` is called once by `StopStreams()` and once by `Close()`.
  EXPECT_CALL(render_client, Stop()).Times(2).WillRepeatedly(Return(S_OK));

  WasapiAudioDevice device(
      {.capture_client = &capture_client, .render_client = &render_client});

  device.StopStreams();
  device.Close();
}

TEST(WasapiAudioDeviceTest, ReadNextPacketBeforeOpenReturnsPointerError) {
  WasapiAudioDevice device;

  const CapturePacket packet = device.ReadNextPacket();

  EXPECT_EQ(packet.status, E_POINTER);
  EXPECT_TRUE(packet.samples.empty());
  EXPECT_EQ(packet.frames, 0U);
  EXPECT_FALSE(packet.silent);
}

TEST(WasapiAudioDeviceTest, ReadNextPacketReturnsQueryError) {
  MockAudioCaptureClient capture_service;
  EXPECT_CALL(capture_service, GetNextPacketSize(_)).WillOnce(Return(E_FAIL));

  constexpr DWORD kSampleRateHz = 48000;
  const WAVEFORMATEX format = StereoFloatFormat(kSampleRateHz);
  auto* raw_format = static_cast<WAVEFORMATEX*>(CoTaskMemAlloc(sizeof(format)));
  ASSERT_NE(raw_format, nullptr);
  *raw_format = format;

  WasapiAudioDevice device(
      {.capture_service = &capture_service, .format = raw_format});

  const CapturePacket packet = device.ReadNextPacket();

  EXPECT_EQ(packet.status, E_FAIL);
  EXPECT_TRUE(packet.samples.empty());
  EXPECT_EQ(packet.frames, 0U);
}

TEST(WasapiAudioDeviceTest, ReadNextPacketReturnsEmptyWhenNoFramesPending) {
  MockAudioCaptureClient capture_service;
  EXPECT_CALL(capture_service, GetNextPacketSize(_))
      .WillOnce(DoAll(SetArgPointee<0>(0U), Return(S_OK)));

  constexpr DWORD kSampleRateHz = 48000;
  const WAVEFORMATEX format = StereoFloatFormat(kSampleRateHz);
  auto* raw_format = static_cast<WAVEFORMATEX*>(CoTaskMemAlloc(sizeof(format)));
  ASSERT_NE(raw_format, nullptr);
  *raw_format = format;

  WasapiAudioDevice device(
      {.capture_service = &capture_service, .format = raw_format});

  const CapturePacket packet = device.ReadNextPacket();

  EXPECT_EQ(packet.status, S_OK);
  EXPECT_TRUE(packet.samples.empty());
  EXPECT_EQ(packet.frames, 0U);
}

TEST(WasapiAudioDeviceTest, ReadNextPacketReturnsGetBufferError) {
  MockAudioCaptureClient capture_service;
  EXPECT_CALL(capture_service, GetNextPacketSize(_))
      .WillOnce(DoAll(SetArgPointee<0>(1U), Return(S_OK)));
  EXPECT_CALL(capture_service, GetBuffer(_, _, _, nullptr, nullptr))
      .WillOnce(Return(E_FAIL));

  constexpr DWORD kSampleRateHz = 48000;
  const WAVEFORMATEX format = StereoFloatFormat(kSampleRateHz);
  auto* raw_format = static_cast<WAVEFORMATEX*>(CoTaskMemAlloc(sizeof(format)));
  ASSERT_NE(raw_format, nullptr);
  *raw_format = format;

  WasapiAudioDevice device(
      {.capture_service = &capture_service, .format = raw_format});

  const CapturePacket packet = device.ReadNextPacket();

  EXPECT_EQ(packet.status, E_FAIL);
  EXPECT_TRUE(packet.samples.empty());
  EXPECT_EQ(packet.frames, 0U);
}

TEST(WasapiAudioDeviceTest, ReadNextPacketReturnsSilentPacket) {
  MockAudioCaptureClient capture_service;
  BYTE* capture_bytes = nullptr;
  EXPECT_CALL(capture_service, GetNextPacketSize(_))
      .WillOnce(DoAll(SetArgPointee<0>(1U), Return(S_OK)));
  EXPECT_CALL(capture_service, GetBuffer(_, _, _, nullptr, nullptr))
      .WillOnce(DoAll(SetArgPointee<0>(capture_bytes), SetArgPointee<1>(2U),
                      SetArgPointee<2>(AUDCLNT_BUFFERFLAGS_SILENT),
                      Return(S_OK)));
  EXPECT_CALL(capture_service, ReleaseBuffer(2U)).WillOnce(Return(S_OK));

  constexpr DWORD kSampleRateHz = 48000;
  const WAVEFORMATEX format = StereoFloatFormat(kSampleRateHz);
  auto* raw_format = static_cast<WAVEFORMATEX*>(CoTaskMemAlloc(sizeof(format)));
  ASSERT_NE(raw_format, nullptr);
  *raw_format = format;

  WasapiAudioDevice device(
      {.capture_service = &capture_service, .format = raw_format});

  const CapturePacket packet = device.ReadNextPacket();

  EXPECT_EQ(packet.status, S_OK);
  EXPECT_EQ(packet.frames, 2U);
  EXPECT_TRUE(packet.silent);
  EXPECT_TRUE(packet.samples.empty());
}

TEST(WasapiAudioDeviceTest, ReadNextPacketDecodesStereoFloatPacket) {
  MockAudioCaptureClient capture_service;
  const std::array<float, 4> samples = {0.25F, -0.25F, 0.5F, -0.5F};
  std::vector<BYTE> packet_bytes(sizeof(samples));
  std::memcpy(packet_bytes.data(), samples.data(), sizeof(samples));
  BYTE* capture_bytes = packet_bytes.data();
  EXPECT_CALL(capture_service, GetNextPacketSize(_))
      .WillOnce(DoAll(SetArgPointee<0>(1U), Return(S_OK)));
  EXPECT_CALL(capture_service, GetBuffer(_, _, _, nullptr, nullptr))
      .WillOnce(DoAll(SetArgPointee<0>(capture_bytes), SetArgPointee<1>(2U),
                      SetArgPointee<2>(0U), Return(S_OK)));
  EXPECT_CALL(capture_service, ReleaseBuffer(2U)).WillOnce(Return(S_OK));

  constexpr DWORD kSampleRateHz = 48000;
  const WAVEFORMATEX format = StereoFloatFormat(kSampleRateHz);
  auto* raw_format = static_cast<WAVEFORMATEX*>(CoTaskMemAlloc(sizeof(format)));
  ASSERT_NE(raw_format, nullptr);
  *raw_format = format;

  WasapiAudioDevice device(
      {.capture_service = &capture_service, .format = raw_format});

  const CapturePacket packet = device.ReadNextPacket();

  EXPECT_EQ(packet.status, S_OK);
  EXPECT_EQ(packet.frames, 2U);
  EXPECT_FALSE(packet.silent);
  EXPECT_EQ(packet.samples, (std::vector<float>{0.25F, -0.25F, 0.5F, -0.5F}));
}

TEST(WasapiAudioDeviceTest, WriteRenderPacketBeforeOpenReturnsPointerError) {
  WasapiAudioDevice device;

  const HRESULT status =
      device.WriteRenderPacket(std::vector<float>{0.25F, -0.25F});

  EXPECT_EQ(status, E_POINTER);
}

TEST(WasapiAudioDeviceTest, WriteRenderPacketWithNoFramesReturnsSuccess) {
  MockAudioClient render_client;
  EXPECT_CALL(render_client, Stop()).WillOnce(Return(S_OK));

  MockAudioRenderClient render_service;

  WasapiAudioDevice device(
      {.render_client = &render_client, .render_service = &render_service});

  const HRESULT status = device.WriteRenderPacket(std::vector<float>{});

  EXPECT_EQ(status, S_OK);
  device.Close();
}

TEST(WasapiAudioDeviceTest, WriteRenderPacketReturnsBufferSizeError) {
  MockAudioClient render_client;
  EXPECT_CALL(render_client, GetBufferSize(_)).WillOnce(Return(E_FAIL));
  EXPECT_CALL(render_client, Stop()).WillOnce(Return(S_OK));

  MockAudioRenderClient render_service;

  WasapiAudioDevice device(
      {.render_client = &render_client, .render_service = &render_service});

  const HRESULT status = device.WriteRenderPacket(
      std::array<float, 4>{0.25F, -0.25F, 0.5F, -0.5F});

  EXPECT_EQ(status, E_FAIL);
  device.Close();
}

TEST(WasapiAudioDeviceTest, WriteRenderPacketReturnsPaddingError) {
  MockAudioClient render_client;
  constexpr DWORD kBufferSize4Frames = 4U;
  EXPECT_CALL(render_client, GetBufferSize(_))
      .WillOnce(DoAll(SetArgPointee<0>(kBufferSize4Frames), Return(S_OK)));
  EXPECT_CALL(render_client, GetCurrentPadding(_)).WillOnce(Return(E_FAIL));
  EXPECT_CALL(render_client, Stop()).WillOnce(Return(S_OK));

  MockAudioRenderClient render_service;

  WasapiAudioDevice device(
      {.render_client = &render_client, .render_service = &render_service});

  const HRESULT status = device.WriteRenderPacket(
      std::array<float, 4>{0.25F, -0.25F, 0.5F, -0.5F});

  EXPECT_EQ(status, E_FAIL);
  device.Close();
}

TEST(WasapiAudioDeviceTest, WriteRenderPacketReturnsSFalseWhenBufferIsFull) {
  MockAudioClient render_client;
  EXPECT_CALL(render_client, GetBufferSize(_))
      .WillOnce(DoAll(SetArgPointee<0>(2U), Return(S_OK)));
  EXPECT_CALL(render_client, GetCurrentPadding(_))
      .WillOnce(DoAll(SetArgPointee<0>(1U), Return(S_OK)));
  EXPECT_CALL(render_client, Stop()).WillOnce(Return(S_OK));

  MockAudioRenderClient render_service;

  WasapiAudioDevice device(
      {.render_client = &render_client, .render_service = &render_service});

  const HRESULT status = device.WriteRenderPacket(
      std::array<float, 4>{0.25F, -0.25F, 0.5F, -0.5F});

  // `S_FALSE` signals success but no data was written because the render
  // buffer had no room for the requested frames.
  EXPECT_EQ(status, S_FALSE);
  device.Close();
}

TEST(WasapiAudioDeviceTest, WriteRenderPacketReturnsGetBufferError) {
  MockAudioClient render_client;
  constexpr DWORD kBufferSize4Frames = 4U;
  EXPECT_CALL(render_client, GetBufferSize(_))
      .WillOnce(DoAll(SetArgPointee<0>(kBufferSize4Frames), Return(S_OK)));
  EXPECT_CALL(render_client, GetCurrentPadding(_))
      .WillOnce(DoAll(SetArgPointee<0>(0U), Return(S_OK)));
  EXPECT_CALL(render_client, Stop()).WillOnce(Return(S_OK));

  MockAudioRenderClient render_service;
  EXPECT_CALL(render_service, GetBuffer(2U, _)).WillOnce(Return(E_FAIL));

  WasapiAudioDevice device(
      {.render_client = &render_client, .render_service = &render_service});

  const HRESULT status = device.WriteRenderPacket(
      std::array<float, 4>{0.25F, -0.25F, 0.5F, -0.5F});

  EXPECT_EQ(status, E_FAIL);
  device.Close();
}

TEST(WasapiAudioDeviceTest, WriteRenderPacketReturnsReleaseBufferError) {
  MockAudioClient render_client;
  constexpr DWORD kBufferSize4Frames = 4U;
  EXPECT_CALL(render_client, GetBufferSize(_))
      .WillOnce(DoAll(SetArgPointee<0>(kBufferSize4Frames), Return(S_OK)));
  EXPECT_CALL(render_client, GetCurrentPadding(_))
      .WillOnce(DoAll(SetArgPointee<0>(0U), Return(S_OK)));
  EXPECT_CALL(render_client, Stop()).WillOnce(Return(S_OK));

  MockAudioRenderClient render_service;
  std::vector<BYTE> render_buffer(4 * sizeof(float));
  BYTE* render_bytes = render_buffer.data();
  EXPECT_CALL(render_service, GetBuffer(2U, _))
      .WillOnce(DoAll(SetArgPointee<1>(render_bytes), Return(S_OK)));
  EXPECT_CALL(render_service, ReleaseBuffer(2U, 0U)).WillOnce(Return(E_FAIL));

  WasapiAudioDevice device(
      {.render_client = &render_client, .render_service = &render_service});

  const HRESULT status = device.WriteRenderPacket(
      std::array<float, 4>{0.25F, -0.25F, 0.5F, -0.5F});

  EXPECT_EQ(status, E_FAIL);
  device.Close();
}

TEST(WasapiAudioDeviceTest,
     WriteRenderPacketReturnsPointerErrorWhenBufferIsNull) {
  MockAudioClient render_client;
  constexpr DWORD kBufferSize4Frames = 4U;
  EXPECT_CALL(render_client, GetBufferSize(_))
      .WillOnce(DoAll(SetArgPointee<0>(kBufferSize4Frames), Return(S_OK)));
  EXPECT_CALL(render_client, GetCurrentPadding(_))
      .WillOnce(DoAll(SetArgPointee<0>(0U), Return(S_OK)));
  EXPECT_CALL(render_client, Stop()).WillOnce(Return(S_OK));

  MockAudioRenderClient render_service;
  // `GetBuffer` returns a null pointer, simulating a buffer that cannot be
  // written to. The device must still release the acquired buffer; the
  // `AUDCLNT_BUFFERFLAGS_SILENT` flag tells the endpoint to fill with silence
  // instead of reading from the (null) data pointer.
  BYTE* render_bytes = nullptr;
  EXPECT_CALL(render_service, GetBuffer(2U, _))
      .WillOnce(DoAll(SetArgPointee<1>(render_bytes), Return(S_OK)));
  EXPECT_CALL(render_service, ReleaseBuffer(2U, AUDCLNT_BUFFERFLAGS_SILENT))
      .WillOnce(Return(S_OK));

  WasapiAudioDevice device(
      {.render_client = &render_client, .render_service = &render_service});

  const HRESULT status = device.WriteRenderPacket(
      std::array<float, 4>{0.25F, -0.25F, 0.5F, -0.5F});

  EXPECT_EQ(status, E_POINTER);
  device.Close();
}

TEST(WasapiAudioDeviceTest, WriteRenderPacketCopiesSamplesIntoRenderBuffer) {
  MockAudioClient render_client;
  constexpr DWORD kBufferSize4Frames = 4U;
  EXPECT_CALL(render_client, GetBufferSize(_))
      .WillOnce(DoAll(SetArgPointee<0>(kBufferSize4Frames), Return(S_OK)));
  EXPECT_CALL(render_client, GetCurrentPadding(_))
      .WillOnce(DoAll(SetArgPointee<0>(0U), Return(S_OK)));
  EXPECT_CALL(render_client, Stop()).WillOnce(Return(S_OK));

  MockAudioRenderClient render_service;
  std::vector<BYTE> render_buffer(4 * sizeof(float));
  BYTE* render_bytes = render_buffer.data();
  EXPECT_CALL(render_service, GetBuffer(2U, _))
      .WillOnce(DoAll(SetArgPointee<1>(render_bytes), Return(S_OK)));
  EXPECT_CALL(render_service, ReleaseBuffer(2U, 0U)).WillOnce(Return(S_OK));

  WasapiAudioDevice device(
      {.render_client = &render_client, .render_service = &render_service});

  const std::vector<float> samples = {0.25F, -0.25F, 0.5F, -0.5F};
  const HRESULT status = device.WriteRenderPacket(samples);

  EXPECT_EQ(status, S_OK);
  EXPECT_EQ(0, std::memcmp(render_buffer.data(), samples.data(),
                           samples.size() * sizeof(float)));
  device.Close();
}

TEST(WasapiAudioDeviceTest, CloseBeforeOpenKeepsDefaultState) {
  WasapiAudioDevice device;

  device.Close();

  EXPECT_DOUBLE_EQ(device.sample_rate(), 0.0);
  EXPECT_TRUE(device.endpoint_name().empty());
}

TEST(WasapiAudioDeviceTest, SampleRateReturnsConfiguredFormatRate) {
  constexpr DWORD kAlternateSampleRateHz = 44100;
  const WAVEFORMATEX format = StereoFloatFormat(kAlternateSampleRateHz);
  auto* raw_format = static_cast<WAVEFORMATEX*>(CoTaskMemAlloc(sizeof(format)));
  ASSERT_NE(raw_format, nullptr);
  *raw_format = format;

  WasapiAudioDevice device({.format = raw_format});

  EXPECT_DOUBLE_EQ(device.sample_rate(),
                   static_cast<double>(kAlternateSampleRateHz));
}

TEST(WasapiAudioDeviceTest, EndpointNameReturnsConfiguredName) {
  WasapiAudioDevice device({.endpoint_name = L"Configured Endpoint"});

  EXPECT_EQ(device.endpoint_name(), L"Configured Endpoint");
}

TEST(WasapiAudioDeviceTest, CloseClearsLiveStateAndKeepsEndpointName) {
  MockAudioClient capture_client;
  EXPECT_CALL(capture_client, Stop()).WillOnce(Return(S_OK));

  MockAudioClient render_client;
  EXPECT_CALL(render_client, Stop()).WillOnce(Return(S_OK));

  MockAudioCaptureClient capture_service;

  MockAudioRenderClient render_service;

  constexpr DWORD kSampleRateHz = 48000;
  const WAVEFORMATEX format = StereoFloatFormat(kSampleRateHz);
  auto* raw_format = static_cast<WAVEFORMATEX*>(CoTaskMemAlloc(sizeof(format)));
  ASSERT_NE(raw_format, nullptr);
  *raw_format = format;

  WasapiAudioDevice device({.capture_client = &capture_client,
                            .render_client = &render_client,
                            .capture_service = &capture_service,
                            .render_service = &render_service,
                            .format = raw_format,
                            .endpoint_name = L"Configured Endpoint"});

  device.Close();

  EXPECT_DOUBLE_EQ(device.sample_rate(), 0.0);
  EXPECT_EQ(device.endpoint_name(), L"Configured Endpoint");
}

TEST(WasapiAudioDeviceTest, TryRecoverNonRecoverableFailureReturnsFalse) {
  WasapiAudioDevice device;

  EXPECT_FALSE(device.TryRecover(E_FAIL));
}

TEST(WasapiAudioDeviceTest,
     TryRecoverDeviceInvalidatedWithoutComInitializationReturnsFalse) {
  WasapiAudioDevice device;

  EXPECT_FALSE(device.TryRecover(AUDCLNT_E_DEVICE_INVALIDATED));
}

TEST(WasapiAudioDeviceTest,
     TryRecoverResourcesInvalidatedWithoutComInitializationReturnsFalse) {
  WasapiAudioDevice device;

  EXPECT_FALSE(device.TryRecover(AUDCLNT_E_RESOURCES_INVALIDATED));
}

TEST(WasapiAudioDeviceTest,
     TryRecoverServiceNotRunningWithoutComInitializationReturnsFalse) {
  WasapiAudioDevice device;

  EXPECT_FALSE(device.TryRecover(AUDCLNT_E_SERVICE_NOT_RUNNING));
}

TEST(WasapiAudioDeviceTest,
     TryRecoverRecoverableFailureStopsConfiguredClientsBeforeReturningFalse) {
  MockAudioClient capture_client;
  // `Stop()` is called once by `TryRecover()` and once by `Close()`.
  EXPECT_CALL(capture_client, Stop()).Times(2).WillRepeatedly(Return(S_OK));

  MockAudioClient render_client;
  // `Stop()` is called once by `TryRecover()` and once by `Close()`.
  EXPECT_CALL(render_client, Stop()).Times(2).WillRepeatedly(Return(S_OK));

  WasapiAudioDevice device(
      {.capture_client = &capture_client, .render_client = &render_client});

  EXPECT_FALSE(device.TryRecover(AUDCLNT_E_DEVICE_INVALIDATED));
  device.Close();
}

TEST(WasapiAudioDeviceTest, DoubleCloseIsSafe) {
  WasapiAudioDevice device;

  device.Close();
  device.Close();

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

Generated by OpenCppCoverage (Version: 0.9.9.0)