// Verifies window creation, slider range, slider-to-boost mapping, minimum
// size constraints, theme changes, paint cycles, and control color responses.

#include "main_window.hpp"

#include <commctrl.h>
#include <windows.h>

#include <string>

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

namespace {

using ::testing::DoubleNear;

class MockAudioPipeline final : public AudioPipelineInterface {
 public:
  [[nodiscard]] AudioPipelineInterface::Status Start() override { return {}; }
  MOCK_METHOD(void, Stop, (), (override));

  MOCK_METHOD(void, SetBoostLevel, (double level), (override));

  [[nodiscard]] double gain_db() const override { return 0.0; }

  [[nodiscard]] const std::wstring& endpoint_name() const override {
    return name_;
  }

 private:
  std::wstring name_ = L"Test Device";
};

TEST(MainWindowTest, DestroyWindowStopsAudioPipeline) {
  MockAudioPipeline pipeline;
  MainWindow window(&pipeline);

  ASSERT_TRUE(
      window.Create(GetModuleHandleW(/*lpModuleName=*/nullptr), SW_HIDE));
  ASSERT_NE(window.hwnd(), nullptr);
  ASSERT_NE(IsWindow(window.hwnd()), FALSE);

  EXPECT_CALL(pipeline, Stop).Times(1);
  EXPECT_NE(DestroyWindow(window.hwnd()), FALSE);

  // Drain the WM_QUIT that `DestroyWindow` posts so it does not leak into the
  // next test.
  MSG msg = {};
  PeekMessageW(&msg, /*hWnd=*/nullptr, WM_QUIT, WM_QUIT, PM_REMOVE);
}

TEST(MainWindowTest, SliderRangeIsValid) {
  MockAudioPipeline pipeline;
  MainWindow window(&pipeline);
  ASSERT_TRUE(
      window.Create(GetModuleHandleW(/*lpModuleName=*/nullptr), SW_HIDE));

  HWND slider = window.slider_hwnd();
  ASSERT_NE(slider, nullptr);

  const int slider_min = static_cast<int>(
      SendMessageW(slider, TBM_GETRANGEMIN, /*wParam=*/0, /*lParam=*/0));
  const int slider_max = static_cast<int>(
      SendMessageW(slider, TBM_GETRANGEMAX, /*wParam=*/0, /*lParam=*/0));

  EXPECT_LT(slider_min, slider_max);

  EXPECT_CALL(pipeline, Stop).Times(1);
  EXPECT_NE(DestroyWindow(window.hwnd()), FALSE);

  // Drain the WM_QUIT that `DestroyWindow` posts so it does not leak into the
  // next test.
  MSG msg = {};
  PeekMessageW(&msg, /*hWnd=*/nullptr, WM_QUIT, WM_QUIT, PM_REMOVE);
}

TEST(MainWindowTest, HScrollMessageUpdatesBoostLevel) {
  MockAudioPipeline pipeline;
  MainWindow window(&pipeline);
  ASSERT_TRUE(
      window.Create(GetModuleHandleW(/*lpModuleName=*/nullptr), SW_HIDE));

  HWND slider = window.slider_hwnd();
  ASSERT_NE(slider, nullptr);

  const int slider_min = static_cast<int>(
      SendMessageW(slider, TBM_GETRANGEMIN, /*wParam=*/0, /*lParam=*/0));
  const int slider_max = static_cast<int>(
      SendMessageW(slider, TBM_GETRANGEMAX, /*wParam=*/0, /*lParam=*/0));

  const int midpoint = (slider_min + slider_max) / 2;

  constexpr double kMidpointBoostLevel = 0.5;
  constexpr double kBoostTolerance = 1e-6;
  EXPECT_CALL(pipeline,
              SetBoostLevel(DoubleNear(kMidpointBoostLevel, kBoostTolerance)))
      .Times(1);
  // `TBM_SETPOS` updates the slider's internal position but does not fire the
  // parent notification, so a separate `WM_HSCROLL` is sent to simulate what
  // the OS does when the user drags the thumb. The slider HWND is passed as
  // lParam so the window handler can distinguish trackbar messages from
  // scrollbar messages.
  SendMessageW(slider, TBM_SETPOS, TRUE, midpoint);
  SendMessageW(window.hwnd(), WM_HSCROLL,
               MAKEWPARAM(TB_THUMBPOSITION, midpoint),
               reinterpret_cast<LPARAM>(slider));

  EXPECT_CALL(pipeline, Stop).Times(1);
  EXPECT_NE(DestroyWindow(window.hwnd()), FALSE);

  // Drain the WM_QUIT that `DestroyWindow` posts so it does not leak into the
  // next test.
  MSG msg = {};
  PeekMessageW(&msg, /*hWnd=*/nullptr, WM_QUIT, WM_QUIT, PM_REMOVE);
}

TEST(MainWindowTest, WindowHandleIsValid) {
  MockAudioPipeline pipeline;
  MainWindow window(&pipeline);
  ASSERT_TRUE(
      window.Create(GetModuleHandleW(/*lpModuleName=*/nullptr), SW_HIDE));

  EXPECT_NE(window.hwnd(), nullptr);
  EXPECT_NE(IsWindow(window.hwnd()), FALSE);

  EXPECT_CALL(pipeline, Stop).Times(1);
  EXPECT_NE(DestroyWindow(window.hwnd()), FALSE);

  // Drain the WM_QUIT that `DestroyWindow` posts so it does not leak into the
  // next test.
  MSG msg = {};
  PeekMessageW(&msg, /*hWnd=*/nullptr, WM_QUIT, WM_QUIT, PM_REMOVE);
}

TEST(MainWindowTest, SliderHandleIsValid) {
  MockAudioPipeline pipeline;
  MainWindow window(&pipeline);
  ASSERT_TRUE(
      window.Create(GetModuleHandleW(/*lpModuleName=*/nullptr), SW_HIDE));

  EXPECT_NE(window.slider_hwnd(), nullptr);
  EXPECT_NE(IsWindow(window.slider_hwnd()), FALSE);

  EXPECT_CALL(pipeline, Stop).Times(1);
  EXPECT_NE(DestroyWindow(window.hwnd()), FALSE);

  // Drain the WM_QUIT that `DestroyWindow` posts so it does not leak into the
  // next test.
  MSG msg = {};
  PeekMessageW(&msg, /*hWnd=*/nullptr, WM_QUIT, WM_QUIT, PM_REMOVE);
}

TEST(MainWindowTest, SliderAtMinSetsZeroBoost) {
  MockAudioPipeline pipeline;
  MainWindow window(&pipeline);
  ASSERT_TRUE(
      window.Create(GetModuleHandleW(/*lpModuleName=*/nullptr), SW_HIDE));

  HWND slider = window.slider_hwnd();
  ASSERT_NE(slider, nullptr);

  const int slider_min = static_cast<int>(
      SendMessageW(slider, TBM_GETRANGEMIN, /*wParam=*/0, /*lParam=*/0));

  constexpr double kBoostTolerance = 1e-6;
  EXPECT_CALL(pipeline, SetBoostLevel(DoubleNear(0.0, kBoostTolerance)))
      .Times(1);
  SendMessageW(slider, TBM_SETPOS, TRUE, slider_min);
  SendMessageW(window.hwnd(), WM_HSCROLL,
               MAKEWPARAM(TB_THUMBPOSITION, slider_min),
               reinterpret_cast<LPARAM>(slider));

  EXPECT_CALL(pipeline, Stop).Times(1);
  EXPECT_NE(DestroyWindow(window.hwnd()), FALSE);

  // Drain the WM_QUIT that `DestroyWindow` posts so it does not leak into the
  // next test.
  MSG msg = {};
  PeekMessageW(&msg, /*hWnd=*/nullptr, WM_QUIT, WM_QUIT, PM_REMOVE);
}

TEST(MainWindowTest, SliderAtMaxSetsMaxBoost) {
  MockAudioPipeline pipeline;
  MainWindow window(&pipeline);
  ASSERT_TRUE(
      window.Create(GetModuleHandleW(/*lpModuleName=*/nullptr), SW_HIDE));

  HWND slider = window.slider_hwnd();
  ASSERT_NE(slider, nullptr);

  const int slider_max = static_cast<int>(
      SendMessageW(slider, TBM_GETRANGEMAX, /*wParam=*/0, /*lParam=*/0));

  constexpr double kBoostTolerance = 1e-6;
  EXPECT_CALL(pipeline, SetBoostLevel(DoubleNear(1.0, kBoostTolerance)))
      .Times(1);
  SendMessageW(slider, TBM_SETPOS, TRUE, slider_max);
  SendMessageW(window.hwnd(), WM_HSCROLL,
               MAKEWPARAM(TB_THUMBPOSITION, slider_max),
               reinterpret_cast<LPARAM>(slider));

  EXPECT_CALL(pipeline, Stop).Times(1);
  EXPECT_NE(DestroyWindow(window.hwnd()), FALSE);

  // Drain the WM_QUIT that `DestroyWindow` posts so it does not leak into the
  // next test.
  MSG msg = {};
  PeekMessageW(&msg, /*hWnd=*/nullptr, WM_QUIT, WM_QUIT, PM_REMOVE);
}

TEST(MainWindowTest, GetMinMaxInfoEnforcesMinimumSize) {
  MockAudioPipeline pipeline;
  MainWindow window(&pipeline);
  ASSERT_TRUE(
      window.Create(GetModuleHandleW(/*lpModuleName=*/nullptr), SW_HIDE));

  MINMAXINFO info = {};
  SendMessageW(window.hwnd(), WM_GETMINMAXINFO, /*wParam=*/0,
               reinterpret_cast<LPARAM>(&info));

  EXPECT_GT(info.ptMinTrackSize.x, 0);
  EXPECT_GT(info.ptMinTrackSize.y, 0);

  EXPECT_CALL(pipeline, Stop).Times(1);
  EXPECT_NE(DestroyWindow(window.hwnd()), FALSE);

  // Drain the WM_QUIT that `DestroyWindow` posts so it does not leak into the
  // next test.
  MSG msg = {};
  PeekMessageW(&msg, /*hWnd=*/nullptr, WM_QUIT, WM_QUIT, PM_REMOVE);
}

TEST(MainWindowTest, EraseBkgndReturnsNonZero) {
  MockAudioPipeline pipeline;
  MainWindow window(&pipeline);
  ASSERT_TRUE(
      window.Create(GetModuleHandleW(/*lpModuleName=*/nullptr), SW_HIDE));

  HDC hdc = GetDC(window.hwnd());
  ASSERT_NE(hdc, nullptr);

  LRESULT result = SendMessageW(window.hwnd(), WM_ERASEBKGND,
                                reinterpret_cast<WPARAM>(hdc), /*lParam=*/0);
  ReleaseDC(window.hwnd(), hdc);

  // Non-zero means the window handled the erase; prevents flicker.
  EXPECT_NE(result, 0);

  EXPECT_CALL(pipeline, Stop).Times(1);
  EXPECT_NE(DestroyWindow(window.hwnd()), FALSE);

  // Drain the WM_QUIT that `DestroyWindow` posts so it does not leak into the
  // next test.
  MSG msg = {};
  PeekMessageW(&msg, /*hWnd=*/nullptr, WM_QUIT, WM_QUIT, PM_REMOVE);
}

TEST(MainWindowTest, HScrollFromNonSliderIsIgnored) {
  MockAudioPipeline pipeline;
  MainWindow window(&pipeline);
  ASSERT_TRUE(
      window.Create(GetModuleHandleW(/*lpModuleName=*/nullptr), SW_HIDE));

  // Send WM_HSCROLL with a null HWND (not the slider); should be ignored.
  EXPECT_CALL(pipeline, SetBoostLevel).Times(0);
  SendMessageW(window.hwnd(), WM_HSCROLL, MAKEWPARAM(TB_THUMBPOSITION, 500),
               /*lParam=*/0);

  EXPECT_CALL(pipeline, Stop).Times(1);
  EXPECT_NE(DestroyWindow(window.hwnd()), FALSE);

  // Drain the WM_QUIT that `DestroyWindow` posts so it does not leak into the
  // next test.
  MSG msg = {};
  PeekMessageW(&msg, /*hWnd=*/nullptr, WM_QUIT, WM_QUIT, PM_REMOVE);
}

TEST(MainWindowTest, SizeMessageUpdatesLayout) {
  MockAudioPipeline pipeline;
  MainWindow window(&pipeline);
  ASSERT_TRUE(
      window.Create(GetModuleHandleW(/*lpModuleName=*/nullptr), SW_HIDE));

  constexpr int kNewWidth = 800;
  constexpr int kNewHeight = 300;
  SendMessageW(window.hwnd(), WM_SIZE, SIZE_RESTORED,
               MAKELPARAM(kNewWidth, kNewHeight));

  // Window should still be valid after resize; layout was recomputed.
  EXPECT_NE(IsWindow(window.hwnd()), FALSE);

  EXPECT_CALL(pipeline, Stop).Times(1);
  EXPECT_NE(DestroyWindow(window.hwnd()), FALSE);

  // Drain the WM_QUIT that `DestroyWindow` posts so it does not leak into the
  // next test.
  MSG msg = {};
  PeekMessageW(&msg, /*hWnd=*/nullptr, WM_QUIT, WM_QUIT, PM_REMOVE);
}

TEST(MainWindowTest, ThemeChangedDoesNotCrash) {
  MockAudioPipeline pipeline;
  MainWindow window(&pipeline);
  ASSERT_TRUE(
      window.Create(GetModuleHandleW(/*lpModuleName=*/nullptr), SW_HIDE));

  SendMessageW(window.hwnd(), WM_THEMECHANGED, /*wParam=*/0, /*lParam=*/0);

  EXPECT_NE(IsWindow(window.hwnd()), FALSE);

  EXPECT_CALL(pipeline, Stop).Times(1);
  EXPECT_NE(DestroyWindow(window.hwnd()), FALSE);

  // Drain the WM_QUIT that `DestroyWindow` posts so it does not leak into the
  // next test.
  MSG msg = {};
  PeekMessageW(&msg, /*hWnd=*/nullptr, WM_QUIT, WM_QUIT, PM_REMOVE);
}

TEST(MainWindowTest, SettingChangeDoesNotCrash) {
  MockAudioPipeline pipeline;
  MainWindow window(&pipeline);
  ASSERT_TRUE(
      window.Create(GetModuleHandleW(/*lpModuleName=*/nullptr), SW_HIDE));

  SendMessageW(window.hwnd(), WM_SETTINGCHANGE, /*wParam=*/0, /*lParam=*/0);

  EXPECT_NE(IsWindow(window.hwnd()), FALSE);

  EXPECT_CALL(pipeline, Stop).Times(1);
  EXPECT_NE(DestroyWindow(window.hwnd()), FALSE);

  // Drain the WM_QUIT that `DestroyWindow` posts so it does not leak into the
  // next test.
  MSG msg = {};
  PeekMessageW(&msg, /*hWnd=*/nullptr, WM_QUIT, WM_QUIT, PM_REMOVE);
}

TEST(MainWindowTest, PaintMessageDoesNotCrash) {
  MockAudioPipeline pipeline;
  MainWindow window(&pipeline);
  ASSERT_TRUE(
      window.Create(GetModuleHandleW(/*lpModuleName=*/nullptr), SW_HIDE));

  // Invalidate and force a paint cycle.
  InvalidateRect(window.hwnd(), /*lpRect=*/nullptr, /*bErase=*/FALSE);
  SendMessageW(window.hwnd(), WM_PAINT, /*wParam=*/0, /*lParam=*/0);

  EXPECT_NE(IsWindow(window.hwnd()), FALSE);

  EXPECT_CALL(pipeline, Stop).Times(1);
  EXPECT_NE(DestroyWindow(window.hwnd()), FALSE);

  // Drain the WM_QUIT that `DestroyWindow` posts so it does not leak into the
  // next test.
  MSG msg = {};
  PeekMessageW(&msg, /*hWnd=*/nullptr, WM_QUIT, WM_QUIT, PM_REMOVE);
}

TEST(MainWindowTest, CtlColorStaticReturnsNonNullBrush) {
  MockAudioPipeline pipeline;
  MainWindow window(&pipeline);
  ASSERT_TRUE(
      window.Create(GetModuleHandleW(/*lpModuleName=*/nullptr), SW_HIDE));

  HDC hdc = GetDC(window.hwnd());
  ASSERT_NE(hdc, nullptr);

  LRESULT result = SendMessageW(window.hwnd(), WM_CTLCOLORSTATIC,
                                reinterpret_cast<WPARAM>(hdc),
                                reinterpret_cast<LPARAM>(window.slider_hwnd()));
  ReleaseDC(window.hwnd(), hdc);

  // The brush handle must be non-null for the slider to paint correctly.
  EXPECT_NE(result, 0);

  EXPECT_CALL(pipeline, Stop).Times(1);
  EXPECT_NE(DestroyWindow(window.hwnd()), FALSE);

  // Drain the WM_QUIT that `DestroyWindow` posts so it does not leak into the
  // next test.
  MSG msg = {};
  PeekMessageW(&msg, /*hWnd=*/nullptr, WM_QUIT, WM_QUIT, PM_REMOVE);
}

TEST(MainWindowTest, CtlColorScrollbarReturnsNonNullBrush) {
  MockAudioPipeline pipeline;
  MainWindow window(&pipeline);
  ASSERT_TRUE(
      window.Create(GetModuleHandleW(/*lpModuleName=*/nullptr), SW_HIDE));

  HDC hdc = GetDC(window.hwnd());
  ASSERT_NE(hdc, nullptr);

  LRESULT result = SendMessageW(window.hwnd(), WM_CTLCOLORSCROLLBAR,
                                reinterpret_cast<WPARAM>(hdc),
                                reinterpret_cast<LPARAM>(window.slider_hwnd()));
  ReleaseDC(window.hwnd(), hdc);

  EXPECT_NE(result, 0);

  EXPECT_CALL(pipeline, Stop).Times(1);
  EXPECT_NE(DestroyWindow(window.hwnd()), FALSE);

  // Drain the WM_QUIT that `DestroyWindow` posts so it does not leak into the
  // next test.
  MSG msg = {};
  PeekMessageW(&msg, /*hWnd=*/nullptr, WM_QUIT, WM_QUIT, PM_REMOVE);
}

}  // namespace

Generated by OpenCppCoverage (Version: 0.9.9.0)