/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "gtest/gtest.h"

#include "MockWinWidget.h"
#include "mozilla/Preferences.h"
#include "mozilla/widget/WinEventObserver.h"
#include "mozilla/widget/WinWindowOcclusionTracker.h"
#include "nsThreadUtils.h"
#include "Units.h"
#include "WinUtils.h"

using namespace mozilla;
using namespace mozilla::widget;

#define PREF_DISPLAY_STATE \
  "widget.windows.window_occlusion_tracking_display_state.enabled"
#define PREF_SESSION_LOCK \
  "widget.windows.window_occlusion_tracking_session_lock.enabled"

class WinWindowOcclusionTrackerInteractiveTest : public ::testing::Test {
 protected:
  void SetUp() override {
    // Shut down WinWindowOcclusionTracker if it exists.
    // This could happen when WinWindowOcclusionTracker was initialized by other
    // gtest
    if (WinWindowOcclusionTracker::Get()) {
      WinWindowOcclusionTracker::ShutDown();
    }
    EXPECT_EQ(nullptr, WinWindowOcclusionTracker::Get());

    WinWindowOcclusionTracker::Ensure();
    EXPECT_NE(nullptr, WinWindowOcclusionTracker::Get());

    Preferences::SetBool(PREF_DISPLAY_STATE, true);
    Preferences::SetBool(PREF_SESSION_LOCK, true);
  }

  void TearDown() override {
    WinWindowOcclusionTracker::ShutDown();
    EXPECT_EQ(nullptr, WinWindowOcclusionTracker::Get());
  }

  void SetNativeWindowBounds(HWND aHWnd, const LayoutDeviceIntRect aBounds) {
    RECT wr;
    wr.left = aBounds.X();
    wr.top = aBounds.Y();
    wr.right = aBounds.XMost();
    wr.bottom = aBounds.YMost();

    ::AdjustWindowRectEx(&wr, ::GetWindowLong(aHWnd, GWL_STYLE), FALSE,
                         ::GetWindowLong(aHWnd, GWL_EXSTYLE));

    // Make sure to keep the window onscreen, as AdjustWindowRectEx() may have
    // moved part of it offscreen. But, if the original requested bounds are
    // offscreen, don't adjust the position.
    LayoutDeviceIntRect windowBounds(wr.left, wr.top, wr.right - wr.left,
                                     wr.bottom - wr.top);

    if (aBounds.X() >= 0) {
      windowBounds.x = std::max(0, windowBounds.X());
    }
    if (aBounds.Y() >= 0) {
      windowBounds.y = std::max(0, windowBounds.Y());
    }
    ::SetWindowPos(aHWnd, HWND_TOP, windowBounds.X(), windowBounds.Y(),
                   windowBounds.Width(), windowBounds.Height(),
                   SWP_NOREPOSITION);
    EXPECT_TRUE(::UpdateWindow(aHWnd));
  }

  void CreateNativeWindowWithBounds(LayoutDeviceIntRect aBounds) {
    mMockWinWidget = MockWinWidget::Create(
        WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN, /* aExStyle = */ 0, aBounds);
    EXPECT_NE(nullptr, mMockWinWidget.get());
    HWND hwnd = mMockWinWidget->GetWnd();
    SetNativeWindowBounds(hwnd, aBounds);
    HRGN region = ::CreateRectRgn(0, 0, 0, 0);
    EXPECT_NE(nullptr, region);
    if (::GetWindowRgn(hwnd, region) == COMPLEXREGION) {
      // On Windows 7, the newly created window has a complex region, which
      // means it will be ignored during the occlusion calculation. So, force
      // it to have a simple region so that we get test coverage on win 7.
      RECT boundingRect;
      EXPECT_TRUE(::GetWindowRect(hwnd, &boundingRect));
      HRGN rectangularRegion = ::CreateRectRgnIndirect(&boundingRect);
      EXPECT_NE(nullptr, rectangularRegion);
      ::SetWindowRgn(hwnd, rectangularRegion, /* bRedraw = */ TRUE);
      ::DeleteObject(rectangularRegion);
    }
    ::DeleteObject(region);

    ::ShowWindow(hwnd, SW_SHOWNORMAL);
    EXPECT_TRUE(UpdateWindow(hwnd));
  }

  RefPtr<MockWinWidget> CreateTrackedWindowWithBounds(
      LayoutDeviceIntRect aBounds) {
    RefPtr<MockWinWidget> window = MockWinWidget::Create(
        WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN, /* aExStyle */ 0, aBounds);
    EXPECT_NE(nullptr, window.get());
    HWND hwnd = window->GetWnd();
    ::ShowWindow(hwnd, SW_SHOWNORMAL);
    WinWindowOcclusionTracker::Get()->Enable(window, window->GetWnd());
    return window;
  }

  int GetNumVisibleRootWindows() {
    return WinWindowOcclusionTracker::Get()->mNumVisibleRootWindows;
  }

  void OnDisplayStateChanged(bool aDisplayOn) {
    WinWindowOcclusionTracker::Get()->OnDisplayStateChanged(aDisplayOn);
  }

  RefPtr<MockWinWidget> mMockWinWidget;
};

// Simple test completely covering a tracked window with a native window.
TEST_F(WinWindowOcclusionTrackerInteractiveTest, SimpleOcclusion) {
  RefPtr<MockWinWidget> window =
      CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
  window->SetExpectation(widget::OcclusionState::OCCLUDED);
  CreateNativeWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
  while (window->IsExpectingCall()) {
    WinWindowOcclusionTracker::Get()->TriggerCalculation();
    NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
  }
  EXPECT_FALSE(window->IsExpectingCall());
}

// Simple test partially covering a tracked window with a native window.
TEST_F(WinWindowOcclusionTrackerInteractiveTest, PartialOcclusion) {
  RefPtr<MockWinWidget> window =
      CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 200, 200));
  window->SetExpectation(widget::OcclusionState::VISIBLE);
  CreateNativeWindowWithBounds(LayoutDeviceIntRect(0, 0, 50, 50));
  while (window->IsExpectingCall()) {
    WinWindowOcclusionTracker::Get()->TriggerCalculation();
    NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
  }
  EXPECT_FALSE(window->IsExpectingCall());
}

// Simple test that a partly off screen tracked window, with the on screen part
// occluded by a native window, is considered occluded.
TEST_F(WinWindowOcclusionTrackerInteractiveTest, OffscreenOcclusion) {
  RefPtr<MockWinWidget> window =
      CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
  // Move the tracked window 50 pixels offscreen to the left.
  int screenLeft = ::GetSystemMetrics(SM_XVIRTUALSCREEN);
  ::SetWindowPos(window->GetWnd(), HWND_TOP, screenLeft - 50, 0, 100, 100,
                 SWP_NOZORDER | SWP_NOSIZE);

  // Create a native window that covers the onscreen part of the tracked window.
  CreateNativeWindowWithBounds(LayoutDeviceIntRect(screenLeft, 0, 50, 100));
  window->SetExpectation(widget::OcclusionState::OCCLUDED);
  while (window->IsExpectingCall()) {
    WinWindowOcclusionTracker::Get()->TriggerCalculation();
    NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
  }
  EXPECT_FALSE(window->IsExpectingCall());
}

// Simple test with a tracked window and native window that do not overlap.
TEST_F(WinWindowOcclusionTrackerInteractiveTest, SimpleVisible) {
  RefPtr<MockWinWidget> window =
      CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
  window->SetExpectation(widget::OcclusionState::VISIBLE);
  CreateNativeWindowWithBounds(LayoutDeviceIntRect(200, 0, 100, 100));
  while (window->IsExpectingCall()) {
    WinWindowOcclusionTracker::Get()->TriggerCalculation();
    WinWindowOcclusionTracker::Get()->TriggerCalculation();
    NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
  }
  EXPECT_FALSE(window->IsExpectingCall());
}

// Simple test with a minimized tracked window and native window.
TEST_F(WinWindowOcclusionTrackerInteractiveTest, SimpleHidden) {
  RefPtr<MockWinWidget> window =
      CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
  CreateNativeWindowWithBounds(LayoutDeviceIntRect(200, 0, 100, 100));
  // Iconify the tracked window and check that its occlusion state is HIDDEN.
  ::CloseWindow(window->GetWnd());
  window->SetExpectation(widget::OcclusionState::HIDDEN);
  while (window->IsExpectingCall()) {
    WinWindowOcclusionTracker::Get()->TriggerCalculation();
    NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
  }
  EXPECT_FALSE(window->IsExpectingCall());
}

// Test that minimizing and restoring a tracked window results in the occlusion
// tracker re-registering for win events and detecting that a native window
// occludes the tracked window.
TEST_F(WinWindowOcclusionTrackerInteractiveTest,
       OcclusionAfterVisibilityToggle) {
  RefPtr<MockWinWidget> window =
      CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
  window->SetExpectation(widget::OcclusionState::VISIBLE);
  while (window->IsExpectingCall()) {
    WinWindowOcclusionTracker::Get()->TriggerCalculation();
    NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
  }

  window->SetExpectation(widget::OcclusionState::HIDDEN);
  WinWindowOcclusionTracker::Get()->OnWindowVisibilityChanged(
      window, /* aVisible = */ false);

  // This makes the window iconic.
  ::CloseWindow(window->GetWnd());

  while (window->IsExpectingCall()) {
    WinWindowOcclusionTracker::Get()->TriggerCalculation();
    NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
  }

  // HIDDEN state is set synchronously by OnWindowVsiblityChanged notification,
  // before occlusion is calculated, so the above expectation will be met w/o an
  // occlusion calculation.
  // Loop until an occlusion calculation has run with no non-hidden windows.
  while (GetNumVisibleRootWindows() != 0) {
    // Need to pump events in order for UpdateOcclusionState to get called, and
    // update the number of non hidden windows. When that number is 0,
    // occlusion has been calculated with no visible tracked windows.
    NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
  }

  window->SetExpectation(widget::OcclusionState::VISIBLE);
  WinWindowOcclusionTracker::Get()->OnWindowVisibilityChanged(
      window, /* aVisible = */ true);
  // This opens the window made iconic above.
  ::OpenIcon(window->GetWnd());

  while (window->IsExpectingCall()) {
    WinWindowOcclusionTracker::Get()->TriggerCalculation();
    NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
  }

  // Open a native window that occludes the visible tracked window.
  window->SetExpectation(widget::OcclusionState::OCCLUDED);
  CreateNativeWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
  while (window->IsExpectingCall()) {
    WinWindowOcclusionTracker::Get()->TriggerCalculation();
    NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
  }
  EXPECT_FALSE(window->IsExpectingCall());
}

// Test that locking the screen causes visible windows to become occluded.
TEST_F(WinWindowOcclusionTrackerInteractiveTest, LockScreenVisibleOcclusion) {
  RefPtr<MockWinWidget> window =
      CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
  window->SetExpectation(widget::OcclusionState::VISIBLE);
  while (window->IsExpectingCall()) {
    WinWindowOcclusionTracker::Get()->TriggerCalculation();
    NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
  }

  window->SetExpectation(widget::OcclusionState::OCCLUDED);
  // Unfortunately, this relies on knowing that NativeWindowOcclusionTracker
  // uses WinEventWindow to listen for WM_WTSSESSION_CHANGE messages, but
  // actually locking the screen isn't feasible.
  DWORD currentSessionId = 0;
  ::ProcessIdToSessionId(::GetCurrentProcessId(), &currentSessionId);
  ::PostMessage(WinEventWindow::GetHwndForTestingOnly(), WM_WTSSESSION_CHANGE,
                WTS_SESSION_LOCK, currentSessionId);

  while (window->IsExpectingCall()) {
    WinWindowOcclusionTracker::Get()->TriggerCalculation();

    MSG msg;
    bool gotMessage = ::PeekMessageW(
        &msg, WinEventWindow::GetHwndForTestingOnly(), 0, 0, PM_REMOVE);
    if (gotMessage) {
      ::TranslateMessage(&msg);
      ::DispatchMessageW(&msg);
    }
    NS_ProcessNextEvent(nullptr, /* aMayWait = */ false);
    PR_Sleep(PR_INTERVAL_NO_WAIT);
  }
  EXPECT_FALSE(window->IsExpectingCall());
}

// Test that locking the screen leaves hidden windows as hidden.
TEST_F(WinWindowOcclusionTrackerInteractiveTest, LockScreenHiddenOcclusion) {
  RefPtr<MockWinWidget> window =
      CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
  // Iconify the tracked window and check that its occlusion state is HIDDEN.
  ::CloseWindow(window->GetWnd());
  window->SetExpectation(widget::OcclusionState::HIDDEN);
  while (window->IsExpectingCall()) {
    WinWindowOcclusionTracker::Get()->TriggerCalculation();
    NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
  }

  // Force the state to VISIBLE.
  window->NotifyOcclusionState(widget::OcclusionState::VISIBLE);

  window->SetExpectation(widget::OcclusionState::HIDDEN);

  // Unfortunately, this relies on knowing that NativeWindowOcclusionTracker
  // uses WinEventWindow to listen for WM_WTSSESSION_CHANGE messages, but
  // actually locking the screen isn't feasible.
  DWORD currentSessionId = 0;
  ::ProcessIdToSessionId(::GetCurrentProcessId(), &currentSessionId);
  PostMessage(WinEventWindow::GetHwndForTestingOnly(), WM_WTSSESSION_CHANGE,
              WTS_SESSION_LOCK, currentSessionId);

  while (window->IsExpectingCall()) {
    WinWindowOcclusionTracker::Get()->TriggerCalculation();

    MSG msg;
    bool gotMessage = ::PeekMessageW(
        &msg, WinEventWindow::GetHwndForTestingOnly(), 0, 0, PM_REMOVE);
    if (gotMessage) {
      ::TranslateMessage(&msg);
      ::DispatchMessageW(&msg);
    }
    NS_ProcessNextEvent(nullptr, /* aMayWait = */ false);
    PR_Sleep(PR_INTERVAL_NO_WAIT);
  }
  EXPECT_FALSE(window->IsExpectingCall());
}

// Test that locking the screen from a different session doesn't mark window
// as occluded.
TEST_F(WinWindowOcclusionTrackerInteractiveTest, LockScreenDifferentSession) {
  RefPtr<MockWinWidget> window =
      CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 200, 200));
  window->SetExpectation(widget::OcclusionState::VISIBLE);
  while (window->IsExpectingCall()) {
    WinWindowOcclusionTracker::Get()->TriggerCalculation();

    NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
  }

  // Force the state to OCCLUDED.
  window->NotifyOcclusionState(widget::OcclusionState::OCCLUDED);

  // Generate a session change lock screen with a session id that's not
  // currentSessionId.
  DWORD currentSessionId = 0;
  ::ProcessIdToSessionId(::GetCurrentProcessId(), &currentSessionId);
  ::PostMessage(WinEventWindow::GetHwndForTestingOnly(), WM_WTSSESSION_CHANGE,
                WTS_SESSION_LOCK, currentSessionId + 1);

  window->SetExpectation(widget::OcclusionState::VISIBLE);
  // Create a native window to trigger occlusion calculation.
  CreateNativeWindowWithBounds(LayoutDeviceIntRect(0, 0, 50, 50));
  while (window->IsExpectingCall()) {
    WinWindowOcclusionTracker::Get()->TriggerCalculation();

    MSG msg;
    bool gotMessage = ::PeekMessageW(
        &msg, WinEventWindow::GetHwndForTestingOnly(), 0, 0, PM_REMOVE);
    if (gotMessage) {
      ::TranslateMessage(&msg);
      ::DispatchMessageW(&msg);
    }
    NS_ProcessNextEvent(nullptr, /* aMayWait = */ false);
    PR_Sleep(PR_INTERVAL_NO_WAIT);
  }
  EXPECT_FALSE(window->IsExpectingCall());
}

// Test that display off & on power state notification causes visible windows to
// become occluded, then visible.
TEST_F(WinWindowOcclusionTrackerInteractiveTest, DisplayOnOffHandling) {
  RefPtr<MockWinWidget> window =
      CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
  window->SetExpectation(widget::OcclusionState::VISIBLE);
  while (window->IsExpectingCall()) {
    WinWindowOcclusionTracker::Get()->TriggerCalculation();

    NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
  }

  window->SetExpectation(widget::OcclusionState::OCCLUDED);

  // Turning display off and on isn't feasible, so send a notification.
  OnDisplayStateChanged(/* aDisplayOn */ false);
  while (window->IsExpectingCall()) {
    WinWindowOcclusionTracker::Get()->TriggerCalculation();

    NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
  }

  window->SetExpectation(widget::OcclusionState::VISIBLE);
  OnDisplayStateChanged(/* aDisplayOn */ true);
  while (window->IsExpectingCall()) {
    WinWindowOcclusionTracker::Get()->TriggerCalculation();

    NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
  }
  EXPECT_FALSE(window->IsExpectingCall());
}
