#include <windowsx.h>

#include <atomic>
#include <cstdlib>
#include <functional>
#include <future>
#include <iostream>
#include <thread>
#include <utility>

#include "mpv_player.h"

namespace mpv {

class MpvPlayerPropertyContractTestPeer {
 public:
  static void RegisterPendingPropertyWrite(MpvPlayer& player, MpvPlayer::StatusCallback callback) {
    player.pending_requests_.RegisterStatus(std::move(callback));
  }

  static void RegisterPendingPropertyRead(MpvPlayer& player, MpvPlayer::GetPropertyCallback callback) {
    player.pending_requests_.RegisterProperty(std::move(callback));
  }
  static void ConfigureInnerSubclass(MpvPlayer& player, HWND host, HWND target) {
    player.hwnd_ = host;
    player.forward_target_view_ = target;
  }

  static void EnsureInnerSubclass(MpvPlayer& player) { player.EnsureMpvInnerSubclassed(); }

  static void DetachInnerSubclass(MpvPlayer& player) { player.DetachMpvInnerSubclass(); }
  static const void* InnerSubclassIdentity(const MpvPlayer& player) { return player.inner_subclass_.get(); }

  static void ReleaseTestWindows(MpvPlayer& player) {
    player.DetachMpvInnerSubclass();
    player.hwnd_ = nullptr;
    player.forward_target_view_ = nullptr;
  }
};

namespace {

void Check(bool condition, const char* message) {
  if (!condition) {
    std::cerr << "mpv_player_property_contract_test: " << message << '\n';
    std::exit(1);
  }
}

std::atomic<int> g_forwarded_mouse_messages{0};
std::atomic<int> g_forwarded_touch_moves{0};
std::atomic<int> g_forwarded_mouse_down_messages{0};
std::atomic<int> g_forwarded_mouse_up_messages{0};
std::atomic<int> g_forwarded_pointer_messages{0};
std::atomic<LPARAM> g_forwarded_mouse_down_position{0};
std::atomic<LPARAM> g_forwarded_mouse_up_position{0};
std::atomic<WPARAM> g_forwarded_mouse_down_flags{0};
std::atomic<WPARAM> g_forwarded_mouse_up_flags{0};
constexpr UINT kBlockWindowThreadMessage = WM_APP + 0x0505;
std::atomic<HANDLE> g_block_entered{nullptr};
std::atomic<HANDLE> g_block_release{nullptr};
std::atomic<HANDLE> g_block_exited{nullptr};

// A WM_POINTER message cannot be handed to a window owned by another thread:
// the system drops a cross-thread send and refuses the post outright with
// ERROR_MESSAGE_SYNC_ONLY, because pointer data belongs to the queue of the
// thread that retrieved it. Driving mpv's window from the test thread therefore
// delivers nothing at all. This relays the send through the window's own thread,
// which is also where the real messages arrive in production.
constexpr UINT kDispatchOnOwnerThreadMessage = WM_APP + 0x0506;
struct OwnerThreadDispatch {
  HWND target;
  UINT message;
  WPARAM wparam;
  LPARAM lparam;
};
std::atomic<HWND> g_routing_view{nullptr};
std::atomic<int> g_routing_view_presses{0};
std::atomic<int> g_routing_child_presses{0};

LRESULT CALLBACK CountingWindowProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
  if (message == kBlockWindowThreadMessage) {
    const HANDLE entered = g_block_entered.load(std::memory_order_acquire);
    const HANDLE release = g_block_release.load(std::memory_order_acquire);
    const HANDLE exited = g_block_exited.load(std::memory_order_acquire);
    if (entered && release && exited) {
      ::SetEvent(entered);
      ::WaitForSingleObject(release, INFINITE);
      ::SetEvent(exited);
    }
    return 0;
  }
  if (message == kDispatchOnOwnerThreadMessage) {
    const auto* dispatch = reinterpret_cast<const OwnerThreadDispatch*>(lparam);
    return ::SendMessageW(dispatch->target, dispatch->message, dispatch->wparam, dispatch->lparam);
  }
  if (message == WM_MOUSEMOVE) {
    if ((wparam & MK_LBUTTON) != 0) {
      g_forwarded_touch_moves.fetch_add(1, std::memory_order_relaxed);
    } else {
      g_forwarded_mouse_messages.fetch_add(1, std::memory_order_relaxed);
    }
    return 0;
  }
  if (message == WM_LBUTTONDOWN) {
    g_forwarded_mouse_down_position.store(lparam, std::memory_order_relaxed);
    g_forwarded_mouse_down_flags.store(wparam, std::memory_order_relaxed);
    g_forwarded_mouse_down_messages.fetch_add(1, std::memory_order_relaxed);
    return 0;
  }
  if (message == WM_LBUTTONUP) {
    g_forwarded_mouse_up_position.store(lparam, std::memory_order_relaxed);
    g_forwarded_mouse_up_flags.store(wparam, std::memory_order_relaxed);
    g_forwarded_mouse_up_messages.fetch_add(1, std::memory_order_relaxed);
    return 0;
  }
  if (message >= WM_POINTERUPDATE && message <= WM_POINTERCAPTURECHANGED) {
    g_forwarded_pointer_messages.fetch_add(1, std::memory_order_relaxed);
    return 0;
  }
  return ::DefWindowProcW(hwnd, message, wparam, lparam);
}

// Sends |message| to |target| from the thread that owns |owner_window|, so a
// WM_POINTER message reaches the window procedure instead of being discarded as
// a cross-thread send. |owner_window| must be handled by CountingWindowProc and
// live on the same thread as |target|.
LRESULT SendFromOwnerThread(HWND owner_window, HWND target, UINT message, WPARAM wparam, LPARAM lparam) {
  OwnerThreadDispatch dispatch{target, message, wparam, lparam};
  return ::SendMessageW(owner_window, kDispatchOnOwnerThreadMessage, 0, reinterpret_cast<LPARAM>(&dispatch));
}

// Attributes a real button press to the window that received it. Installed on
// the stand-in view and on every window of the video subtree below it. The
// STATIC class answers WM_NCHITTEST with HTTRANSPARENT, which would take these
// windows out of the hit test for a reason other than the one under test, so
// every one of them claims its client area here.
LRESULT CALLBACK RoutingWindowProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
  if (message == WM_NCHITTEST) {
    return HTCLIENT;
  }
  if (message == WM_LBUTTONDOWN || message == WM_LBUTTONDBLCLK) {
    if (hwnd == g_routing_view.load(std::memory_order_acquire)) {
      g_routing_view_presses.fetch_add(1, std::memory_order_relaxed);
    } else {
      g_routing_child_presses.fetch_add(1, std::memory_order_relaxed);
    }
    return 0;
  }
  return ::DefWindowProcW(hwnd, message, wparam, lparam);
}

// Pumps this thread's queue until |predicate| holds or the budget runs out.
bool PumpUntil(const std::function<bool()>& predicate, DWORD timeout_ms) {
  const ULONGLONG deadline = ::GetTickCount64() + timeout_ms;
  for (;;) {
    MSG message;
    while (::PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
      ::TranslateMessage(&message);
      ::DispatchMessageW(&message);
    }
    if (predicate()) return true;
    if (::GetTickCount64() >= deadline) return false;
    ::Sleep(5);
  }
}

bool SendAbsoluteMouse(POINT screen_point, DWORD flags) {
  const LONG width = ::GetSystemMetrics(SM_CXSCREEN);
  const LONG height = ::GetSystemMetrics(SM_CYSCREEN);
  if (width < 2 || height < 2) return false;
  INPUT input = {};
  input.type = INPUT_MOUSE;
  input.mi.dx = static_cast<LONG>((static_cast<LONG64>(screen_point.x) * 65535) / (width - 1));
  input.mi.dy = static_cast<LONG>((static_cast<LONG64>(screen_point.y) * 65535) / (height - 1));
  input.mi.dwFlags = MOUSEEVENTF_ABSOLUTE | flags;
  return ::SendInput(1, &input, sizeof(input)) == 1;
}

bool InjectClick(POINT screen_point) {
  return SendAbsoluteMouse(screen_point, MOUSEEVENTF_MOVE) && SendAbsoluteMouse(screen_point, MOUSEEVENTF_LEFTDOWN) &&
         SendAbsoluteMouse(screen_point, MOUSEEVENTF_LEFTUP);
}

// A real touch contact at |screen_point|, for the touch half of the routing
// test. The contact is held across a few frames rather than sent as a bare
// down/up pair, because a contact that lifts in the same frame it landed is
// short enough for a consumer to discard as noise.
bool InjectTap(POINT screen_point) {
  POINTER_TOUCH_INFO contact = {};
  contact.pointerInfo.pointerType = PT_TOUCH;
  contact.pointerInfo.pointerId = 0;
  contact.pointerInfo.ptPixelLocation = screen_point;
  contact.touchFlags = TOUCH_FLAG_NONE;
  contact.touchMask = TOUCH_MASK_CONTACTAREA | TOUCH_MASK_ORIENTATION | TOUCH_MASK_PRESSURE;
  contact.rcContact.left = screen_point.x - 2;
  contact.rcContact.top = screen_point.y - 2;
  contact.rcContact.right = screen_point.x + 2;
  contact.rcContact.bottom = screen_point.y + 2;
  contact.orientation = 90;
  contact.pressure = 32000;

  contact.pointerInfo.pointerFlags = POINTER_FLAG_DOWN | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT;
  if (!::InjectTouchInput(1, &contact)) return false;
  contact.pointerInfo.pointerFlags = POINTER_FLAG_UPDATE | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT;
  for (int frame = 0; frame < 5; ++frame) {
    ::Sleep(20);
    if (!::InjectTouchInput(1, &contact)) return false;
  }
  contact.pointerInfo.pointerFlags = POINTER_FLAG_UP;
  return ::InjectTouchInput(1, &contact) != FALSE;
}

void TestUnavailablePropertyWriteFails() {
  MpvPlayer player;
  int callback_count = 0;
  int status = MPV_ERROR_SUCCESS;

  player.SetPropertyAsync("pause", "yes", [&](int error) {
    ++callback_count;
    status = error;
  });

  Check(callback_count == 1, "a property write without an mpv handle must complete exactly once");
  Check(status == MPV_ERROR_UNINITIALIZED, "a property write without an mpv handle must fail as uninitialized");
}

void TestPendingPropertyWriteFailsOnDispose() {
  MpvPlayer player;
  int callback_count = 0;
  int status = MPV_ERROR_SUCCESS;
  MpvPlayerPropertyContractTestPeer::RegisterPendingPropertyWrite(player, [&](int error) {
    ++callback_count;
    status = error;
  });

  player.Dispose();
  Check(callback_count == 1, "dispose must complete a pending property write exactly once");
  Check(status == MPV_ERROR_UNINITIALIZED, "dispose must cancel a pending property write as uninitialized");

  player.Dispose();
  Check(callback_count == 1, "repeated dispose must not complete a property write twice");
}

void TestPendingRequestTypesRemainDistinctOnDispose() {
  MpvPlayer player;
  int write_count = 0;
  int read_count = 0;
  std::string read_value = "unexpected";
  MpvPlayerPropertyContractTestPeer::RegisterPendingPropertyWrite(player, [&](int error) {
    Check(error < 0, "cancelled property write must receive an error");
    ++write_count;
  });
  MpvPlayerPropertyContractTestPeer::RegisterPendingPropertyRead(player, [&](int error, const std::string& value) {
    Check(error < 0, "cancelled property read must receive an error");
    ++read_count;
    read_value = value;
  });

  player.Dispose();
  Check(write_count == 1, "dispose must complete the typed write request exactly once");
  Check(read_count == 1, "dispose must complete the typed read request exactly once");
  Check(read_value.empty(), "cancelled property reads must not manufacture a value");
}

void TestInnerSubclassOwnershipIsSerializedAndDetached() {
  struct TestWindows {
    HWND target;
    HWND host;
    HWND inner;
    WNDPROC inner_original;
    DWORD owner_thread;
  };

  std::promise<TestWindows> windows_created;
  auto windows_future = windows_created.get_future();
  std::thread window_owner([&]() {
    HWND target =
        ::CreateWindowExW(0, L"STATIC", L"", WS_OVERLAPPED, 0, 0, 100, 100, nullptr, nullptr, nullptr, nullptr);
    HWND host = ::CreateWindowExW(0, L"STATIC", L"", WS_CHILD, 0, 0, 100, 100, target, nullptr, nullptr, nullptr);
    HWND inner = ::CreateWindowExW(0, L"STATIC", L"", WS_CHILD, 0, 0, 100, 100, host, nullptr, nullptr, nullptr);
    const auto target_original = reinterpret_cast<WNDPROC>(
        ::SetWindowLongPtrW(target, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(CountingWindowProc)));
    const auto inner_original = reinterpret_cast<WNDPROC>(::GetWindowLongPtrW(inner, GWLP_WNDPROC));
    windows_created.set_value(TestWindows{target, host, inner, inner_original, ::GetCurrentThreadId()});

    MSG message;
    while (::GetMessageW(&message, nullptr, 0, 0) > 0) {
      ::TranslateMessage(&message);
      ::DispatchMessageW(&message);
    }

    ::SetWindowLongPtrW(target, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(target_original));
    ::DestroyWindow(inner);
    ::DestroyWindow(host);
    ::DestroyWindow(target);
  });

  const TestWindows windows = windows_future.get();
  Check(windows.target && windows.host && windows.inner, "test windows must be created");
  MpvPlayer player;

  MpvPlayerPropertyContractTestPeer::ConfigureInnerSubclass(player, windows.host, windows.target);
  std::thread first([&]() { MpvPlayerPropertyContractTestPeer::EnsureInnerSubclass(player); });
  std::thread second([&]() { MpvPlayerPropertyContractTestPeer::EnsureInnerSubclass(player); });
  first.join();
  second.join();

  const auto installed = reinterpret_cast<WNDPROC>(::GetWindowLongPtrW(windows.inner, GWLP_WNDPROC));
  Check(installed && installed != windows.inner_original, "exactly one subclass procedure must be installed");
  ::SendMessageW(windows.inner, WM_NULL, 0, 0);

  ::SendMessageW(windows.inner, WM_MOUSEMOVE, 0, MAKELPARAM(4, 7));
  for (int attempt = 0; attempt < 100 && g_forwarded_mouse_messages.load(std::memory_order_relaxed) < 1; ++attempt) {
    ::Sleep(10);
  }
  Check(g_forwarded_mouse_messages.load(std::memory_order_relaxed) == 1, "active generation must forward mouse input");

  constexpr UINT32 kPrimaryPointerId = 7;
  constexpr UINT32 kSecondaryPointerId = 8;
  constexpr UINT32 kCancelledPointerId = 9;
  constexpr UINT32 kDetachedPointerId = 10;
  constexpr UINT32 kDestroyedPointerId = 11;
  constexpr UINT32 kPointerDownFlags = POINTER_MESSAGE_FLAG_NEW | POINTER_MESSAGE_FLAG_INRANGE |
                                       POINTER_MESSAGE_FLAG_INCONTACT | POINTER_MESSAGE_FLAG_FIRSTBUTTON |
                                       POINTER_MESSAGE_FLAG_PRIMARY;
  constexpr UINT32 kPointerMoveFlags = POINTER_MESSAGE_FLAG_INRANGE | POINTER_MESSAGE_FLAG_INCONTACT |
                                       POINTER_MESSAGE_FLAG_FIRSTBUTTON | POINTER_MESSAGE_FLAG_PRIMARY;
  constexpr UINT32 kPointerUpFlags = POINTER_MESSAGE_FLAG_INRANGE | POINTER_MESSAGE_FLAG_PRIMARY;

  POINT down_position = {14, 18};
  ::ClientToScreen(windows.inner, &down_position);
  POINT expected_down_position = down_position;
  ::ScreenToClient(windows.target, &expected_down_position);
  POINT up_position = {30, 36};
  ::ClientToScreen(windows.inner, &up_position);
  POINT expected_up_position = up_position;
  ::ScreenToClient(windows.target, &expected_up_position);

  SendFromOwnerThread(
      windows.target, windows.inner, WM_POINTERDOWN, MAKEWPARAM(kPrimaryPointerId, kPointerDownFlags),
      MAKELPARAM(down_position.x, down_position.y));
  SendFromOwnerThread(
      windows.target, windows.inner, WM_POINTERUPDATE, MAKEWPARAM(kPrimaryPointerId, kPointerMoveFlags),
      MAKELPARAM(up_position.x, up_position.y));
  SendFromOwnerThread(
      windows.target, windows.inner, WM_POINTERUP, MAKEWPARAM(kPrimaryPointerId, kPointerUpFlags),
      MAKELPARAM(up_position.x, up_position.y));
  for (int attempt = 0; attempt < 100 && g_forwarded_mouse_up_messages.load(std::memory_order_relaxed) < 1; ++attempt) {
    ::Sleep(10);
  }

  Check(g_forwarded_mouse_down_messages.load(std::memory_order_relaxed) == 1, "primary touch must press once");
  Check(g_forwarded_touch_moves.load(std::memory_order_relaxed) == 1, "primary touch movement must drag once");
  Check(g_forwarded_mouse_up_messages.load(std::memory_order_relaxed) == 1, "primary touch must release once");
  Check(
      g_forwarded_pointer_messages.load(std::memory_order_relaxed) == 0, "raw pointer messages must not reach Flutter");
  Check(
      g_forwarded_mouse_down_flags.load(std::memory_order_relaxed) == MK_LBUTTON,
      "touch down must hold the mouse button");
  Check(g_forwarded_mouse_up_flags.load(std::memory_order_relaxed) == 0, "touch up must release the mouse button");
  const LPARAM forwarded_down_position = g_forwarded_mouse_down_position.load(std::memory_order_relaxed);
  Check(
      GET_X_LPARAM(forwarded_down_position) == expected_down_position.x &&
          GET_Y_LPARAM(forwarded_down_position) == expected_down_position.y,
      "touch down must use Flutter-view client coordinates");
  const LPARAM forwarded_up_position = g_forwarded_mouse_up_position.load(std::memory_order_relaxed);
  Check(
      GET_X_LPARAM(forwarded_up_position) == expected_up_position.x &&
          GET_Y_LPARAM(forwarded_up_position) == expected_up_position.y,
      "touch up must use Flutter-view client coordinates");

  const WPARAM secondary_down = MAKEWPARAM(kSecondaryPointerId, kPointerDownFlags & ~POINTER_MESSAGE_FLAG_PRIMARY);
  const WPARAM secondary_up = MAKEWPARAM(kSecondaryPointerId, kPointerUpFlags & ~POINTER_MESSAGE_FLAG_PRIMARY);
  SendFromOwnerThread(
      windows.target, windows.inner, WM_POINTERDOWN, secondary_down, MAKELPARAM(down_position.x, down_position.y));
  SendFromOwnerThread(
      windows.target, windows.inner, WM_POINTERUP, secondary_up, MAKELPARAM(up_position.x, up_position.y));
  ::Sleep(30);
  Check(
      g_forwarded_mouse_down_messages.load(std::memory_order_relaxed) == 1 &&
          g_forwarded_mouse_up_messages.load(std::memory_order_relaxed) == 1,
      "secondary touch must not synthesize another click");

  SendFromOwnerThread(
      windows.target, windows.inner, WM_POINTERDOWN, MAKEWPARAM(kCancelledPointerId, kPointerDownFlags),
      MAKELPARAM(down_position.x, down_position.y));
  SendFromOwnerThread(
      windows.target, windows.inner, WM_POINTERCAPTURECHANGED, MAKEWPARAM(kCancelledPointerId, kPointerUpFlags),
      reinterpret_cast<LPARAM>(windows.host));
  for (int attempt = 0; attempt < 100 && g_forwarded_mouse_up_messages.load(std::memory_order_relaxed) < 2; ++attempt) {
    ::Sleep(10);
  }
  Check(
      g_forwarded_mouse_down_messages.load(std::memory_order_relaxed) == 2 &&
          g_forwarded_mouse_up_messages.load(std::memory_order_relaxed) == 2,
      "capture loss must release an active synthetic mouse press");

  SendFromOwnerThread(
      windows.target, windows.inner, WM_POINTERDOWN, MAKEWPARAM(kDetachedPointerId, kPointerDownFlags),
      MAKELPARAM(down_position.x, down_position.y));
  for (int attempt = 0; attempt < 100 && g_forwarded_mouse_down_messages.load(std::memory_order_relaxed) < 3;
       ++attempt) {
    ::Sleep(10);
  }

  MpvPlayerPropertyContractTestPeer::DetachInnerSubclass(player);
  Check(
      reinterpret_cast<WNDPROC>(::GetWindowLongPtrW(windows.inner, GWLP_WNDPROC)) == windows.inner_original,
      "detach must restore the original procedure before window destruction");
  for (int attempt = 0; attempt < 100 && g_forwarded_mouse_up_messages.load(std::memory_order_relaxed) < 3; ++attempt) {
    ::Sleep(10);
  }
  Check(
      g_forwarded_mouse_down_messages.load(std::memory_order_relaxed) == 3 &&
          g_forwarded_mouse_up_messages.load(std::memory_order_relaxed) == 3,
      "detach must release an active synthetic mouse press");

  SendFromOwnerThread(
      windows.target, windows.inner, WM_POINTERUP, MAKEWPARAM(kDetachedPointerId, kPointerUpFlags),
      MAKELPARAM(up_position.x, up_position.y));
  ::Sleep(30);
  Check(
      g_forwarded_mouse_up_messages.load(std::memory_order_relaxed) == 3,
      "a late pointer up after detach must not release twice");

  ::SendMessageW(windows.inner, WM_MOUSEMOVE, 0, MAKELPARAM(8, 9));
  ::Sleep(30);
  Check(
      g_forwarded_mouse_messages.load(std::memory_order_relaxed) == 1,
      "a callback after detaching the old generation must be ignored");

  MpvPlayerPropertyContractTestPeer::EnsureInnerSubclass(player);
  const auto replacement = reinterpret_cast<WNDPROC>(::GetWindowLongPtrW(windows.inner, GWLP_WNDPROC));
  Check(replacement && replacement != windows.inner_original, "a replacement generation must install cleanly");
  ::SendMessageW(windows.inner, WM_MOUSEMOVE, 0, MAKELPARAM(10, 11));
  for (int attempt = 0; attempt < 100 && g_forwarded_mouse_messages.load(std::memory_order_relaxed) < 2; ++attempt) {
    ::Sleep(10);
  }
  Check(
      g_forwarded_mouse_messages.load(std::memory_order_relaxed) == 2,
      "replacement generation must own forwarding after installation");

  MpvPlayerPropertyContractTestPeer::ReleaseTestWindows(player);
  Check(
      reinterpret_cast<WNDPROC>(::GetWindowLongPtrW(windows.inner, GWLP_WNDPROC)) == windows.inner_original,
      "replacement detach must restore the original procedure");

  MpvPlayerPropertyContractTestPeer::ConfigureInnerSubclass(player, windows.host, windows.target);
  MpvPlayerPropertyContractTestPeer::EnsureInnerSubclass(player);
  const auto destroy_generation = reinterpret_cast<WNDPROC>(::GetWindowLongPtrW(windows.inner, GWLP_WNDPROC));
  Check(
      destroy_generation && destroy_generation != windows.inner_original,
      "destroy test must install a fresh subclass generation");

  SendFromOwnerThread(
      windows.target, windows.inner, WM_POINTERDOWN, MAKEWPARAM(kDestroyedPointerId, kPointerDownFlags),
      MAKELPARAM(down_position.x, down_position.y));
  ::SendMessageW(windows.inner, WM_CLOSE, 0, 0);
  for (int attempt = 0; attempt < 100 && g_forwarded_mouse_up_messages.load(std::memory_order_relaxed) < 4; ++attempt) {
    ::Sleep(10);
  }
  Check(!::IsWindow(windows.inner), "destroy test must close the inner window");
  Check(
      g_forwarded_mouse_down_messages.load(std::memory_order_relaxed) == 4 &&
          g_forwarded_mouse_up_messages.load(std::memory_order_relaxed) == 4,
      "window destruction must release an active synthetic mouse press");
  MpvPlayerPropertyContractTestPeer::ReleaseTestWindows(player);

  ::PostThreadMessageW(windows.owner_thread, WM_QUIT, 0, 0);
  window_owner.join();
  g_forwarded_mouse_messages.store(0, std::memory_order_relaxed);
  g_forwarded_touch_moves.store(0, std::memory_order_relaxed);
  g_forwarded_mouse_down_messages.store(0, std::memory_order_relaxed);
  g_forwarded_mouse_up_messages.store(0, std::memory_order_relaxed);
  g_forwarded_pointer_messages.store(0, std::memory_order_relaxed);
}

void TestTimedOutSubclassDetachCanBeAdopted() {
  struct TestWindows {
    HWND target;
    HWND host;
    HWND inner;
    WNDPROC inner_original;
    DWORD owner_thread;
  };

  std::promise<TestWindows> windows_created;
  auto windows_future = windows_created.get_future();
  std::thread window_owner([&]() {
    HWND target =
        ::CreateWindowExW(0, L"STATIC", L"", WS_OVERLAPPED, 0, 0, 100, 100, nullptr, nullptr, nullptr, nullptr);
    HWND host = ::CreateWindowExW(0, L"STATIC", L"", WS_CHILD, 0, 0, 100, 100, target, nullptr, nullptr, nullptr);
    HWND inner = ::CreateWindowExW(0, L"STATIC", L"", WS_CHILD, 0, 0, 100, 100, host, nullptr, nullptr, nullptr);
    const auto target_original = reinterpret_cast<WNDPROC>(
        ::SetWindowLongPtrW(target, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(CountingWindowProc)));
    const auto inner_original = reinterpret_cast<WNDPROC>(::GetWindowLongPtrW(inner, GWLP_WNDPROC));
    windows_created.set_value(TestWindows{target, host, inner, inner_original, ::GetCurrentThreadId()});

    MSG message;
    while (::GetMessageW(&message, nullptr, 0, 0) > 0) {
      ::TranslateMessage(&message);
      ::DispatchMessageW(&message);
    }

    ::SetWindowLongPtrW(target, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(target_original));
    ::DestroyWindow(inner);
    ::DestroyWindow(host);
    ::DestroyWindow(target);
  });

  const TestWindows windows = windows_future.get();
  Check(windows.target && windows.host && windows.inner, "detach-timeout test windows must be created");
  const HANDLE block_entered = ::CreateEventW(nullptr, TRUE, FALSE, nullptr);
  const HANDLE block_release = ::CreateEventW(nullptr, TRUE, FALSE, nullptr);
  const HANDLE block_exited = ::CreateEventW(nullptr, TRUE, FALSE, nullptr);
  Check(block_entered && block_release && block_exited, "detach-timeout synchronization events must be created");
  g_block_entered.store(block_entered, std::memory_order_release);
  g_block_release.store(block_release, std::memory_order_release);
  g_block_exited.store(block_exited, std::memory_order_release);
  g_forwarded_mouse_messages.store(0, std::memory_order_relaxed);

  {
    MpvPlayer original;
    MpvPlayerPropertyContractTestPeer::ConfigureInnerSubclass(original, windows.host, windows.target);
    MpvPlayerPropertyContractTestPeer::EnsureInnerSubclass(original);
    Check(
        reinterpret_cast<WNDPROC>(::GetWindowLongPtrW(windows.inner, GWLP_WNDPROC)) != windows.inner_original,
        "the initial generation must be installed before forcing detach timeout");
    const void* retained_generation = MpvPlayerPropertyContractTestPeer::InnerSubclassIdentity(original);
    Check(retained_generation != nullptr, "the initial generation must have live state");

    Check(
        ::PostMessageW(windows.target, kBlockWindowThreadMessage, 0, 0) != FALSE,
        "the owner-thread blocking message must be posted");
    Check(
        ::WaitForSingleObject(block_entered, 1000) == WAIT_OBJECT_0,
        "the owner thread must enter the deterministic blocking message");

    MpvPlayerPropertyContractTestPeer::DetachInnerSubclass(original);

    MpvPlayer replacement;
    MpvPlayerPropertyContractTestPeer::ConfigureInnerSubclass(replacement, windows.host, windows.target);
    MpvPlayerPropertyContractTestPeer::EnsureInnerSubclass(replacement);
    Check(
        MpvPlayerPropertyContractTestPeer::InnerSubclassIdentity(replacement) == retained_generation,
        "replacement must atomically adopt the retained generation");
    Check(
        reinterpret_cast<WNDPROC>(::GetWindowLongPtrW(windows.inner, GWLP_WNDPROC)) != windows.inner_original,
        "replacement must adopt the retained installed generation without duplicate subclassing");

    ::SetEvent(block_release);
    Check(
        ::WaitForSingleObject(block_exited, 1000) == WAIT_OBJECT_0,
        "the owner thread must leave the deterministic blocking message");
    ::SendMessageW(windows.inner, WM_NULL, 0, 0);
    ::SendMessageW(windows.inner, WM_MOUSEMOVE, 0, MAKELPARAM(12, 13));
    for (int attempt = 0; attempt < 100 && g_forwarded_mouse_messages.load(std::memory_order_relaxed) < 1; ++attempt) {
      ::Sleep(10);
    }
    Check(
        g_forwarded_mouse_messages.load(std::memory_order_relaxed) == 1,
        "the adopted generation must resume input forwarding");

    MpvPlayerPropertyContractTestPeer::ReleaseTestWindows(replacement);
    Check(
        reinterpret_cast<WNDPROC>(::GetWindowLongPtrW(windows.inner, GWLP_WNDPROC)) == windows.inner_original,
        "adopted generation cleanup must eventually restore the original procedure");
  }

  g_block_entered.store(nullptr, std::memory_order_release);
  g_block_release.store(nullptr, std::memory_order_release);
  g_block_exited.store(nullptr, std::memory_order_release);
  ::CloseHandle(block_entered);
  ::CloseHandle(block_release);
  ::CloseHandle(block_exited);
  ::PostThreadMessageW(windows.owner_thread, WM_QUIT, 0, 0);
  window_owner.join();
  g_forwarded_mouse_messages.store(0, std::memory_order_relaxed);
}

void TestTimedOutSubclassInstallCannotOutliveItsState() {
  struct TestWindows {
    HWND target;
    HWND host;
    HWND inner;
    WNDPROC inner_original;
    DWORD owner_thread;
  };

  std::promise<TestWindows> windows_created;
  auto windows_future = windows_created.get_future();
  std::promise<void> begin_dispatch;
  auto begin_dispatch_future = begin_dispatch.get_future();
  std::thread window_owner([&]() {
    HWND target =
        ::CreateWindowExW(0, L"STATIC", L"", WS_OVERLAPPED, 0, 0, 100, 100, nullptr, nullptr, nullptr, nullptr);
    HWND host = ::CreateWindowExW(0, L"STATIC", L"", WS_CHILD, 0, 0, 100, 100, target, nullptr, nullptr, nullptr);
    HWND inner = ::CreateWindowExW(0, L"STATIC", L"", WS_CHILD, 0, 0, 100, 100, host, nullptr, nullptr, nullptr);
    const auto inner_original = reinterpret_cast<WNDPROC>(::GetWindowLongPtrW(inner, GWLP_WNDPROC));
    windows_created.set_value(TestWindows{target, host, inner, inner_original, ::GetCurrentThreadId()});

    // Keep the owning thread alive but unavailable long enough for
    // SendMessageTimeoutW to cancel the cross-thread ownership action.
    begin_dispatch_future.wait();
    MSG message;
    while (::GetMessageW(&message, nullptr, 0, 0) > 0) {
      ::TranslateMessage(&message);
      ::DispatchMessageW(&message);
    }

    ::DestroyWindow(inner);
    ::DestroyWindow(host);
    ::DestroyWindow(target);
  });

  const TestWindows windows = windows_future.get();
  Check(windows.target && windows.host && windows.inner, "timeout test windows must be created");
  {
    MpvPlayer player;
    MpvPlayerPropertyContractTestPeer::ConfigureInnerSubclass(player, windows.host, windows.target);
    MpvPlayerPropertyContractTestPeer::EnsureInnerSubclass(player);
    MpvPlayerPropertyContractTestPeer::ReleaseTestWindows(player);
  }

  // The action and its subclass reference data have now left caller scope.
  // Dispatching the timed-out message must neither install late nor touch the
  // destroyed caller state.
  begin_dispatch.set_value();
  ::SendMessageW(windows.inner, WM_NULL, 0, 0);
  Check(
      reinterpret_cast<WNDPROC>(::GetWindowLongPtrW(windows.inner, GWLP_WNDPROC)) == windows.inner_original,
      "a timed-out action must remain cancelled after the window thread resumes");

  ::PostThreadMessageW(windows.owner_thread, WM_QUIT, 0, 0);
  window_owner.join();
}

// The video child must never own a contact: mpv's window lives on mpv's own
// thread, where the cross-thread hit-test opt-outs (WS_EX_TRANSPARENT, an
// HTTRANSPARENT WM_NCHITTEST reply) do not apply. Disabling the host is what
// keeps mouse, touch, and pen on the Flutter view. The window mpv creates
// inside the host afterwards needs no state of its own — targeting skips the
// host without descending into it — so only the host's own state is asserted
// here (a disabled parent never clears its children's WS_DISABLED bit).
void TestDisabledVideoHostKeepsInputOnTheFlutterView() {
  HWND view = ::CreateWindowExW(0, L"STATIC", L"", WS_OVERLAPPED, 0, 0, 400, 400, nullptr, nullptr, nullptr, nullptr);
  Check(view != nullptr, "video host test needs a stand-in Flutter view window");
  RECT client = {};
  Check(::GetClientRect(view, &client) != FALSE, "the stand-in view must report a client area");
  Check(client.right > 2 && client.bottom > 2, "the stand-in view must have a usable client area");

  HWND host = ::CreateWindowExW(
      WS_EX_NOPARENTNOTIFY, L"STATIC", L"", kVideoHostWindowStyle, 0, 0, client.right, client.bottom, view, nullptr,
      nullptr, nullptr);
  Check(host != nullptr, "the video host window must be created");
  Check(!::IsWindowEnabled(host), "the video host must be created disabled");

  const POINT contact = {client.right / 2, client.bottom / 2};
  Check(
      ::ChildWindowFromPointEx(view, contact, CWP_SKIPDISABLED) == view,
      "input over the disabled video host must target the Flutter view");

  ::EnableWindow(host, TRUE);
  Check(
      ::ChildWindowFromPointEx(view, contact, CWP_SKIPDISABLED) == host,
      "the same contact must reach an enabled host, so the skip is the disabled state and not the geometry");

  ::DestroyWindow(host);
  ::DestroyWindow(view);
}

// The assertions above only prove that USER32's own child lookup honors
// CWP_SKIPDISABLED. What the video child actually depends on is the input hit
// test: a press over the disabled host has to reach the parent view, and must
// not be swallowed — swallowing is what the previous implementation reported
// when it tried disabling this subtree, and no relay can recover from it
// because mpv's window never sees the message either.
//
// A control press with the video subtree hidden runs first. If that one does
// not arrive, injection is unavailable here and the test skips. Once it has
// landed, silence from the press over the disabled host is a failure, not an
// environment problem.
void TestDisabledVideoHostRoutesRealPressesToParentView() {
  HWND view = ::CreateWindowExW(
      0, L"STATIC", L"", WS_OVERLAPPEDWINDOW | WS_VISIBLE, 100, 100, 400, 400, nullptr, nullptr, nullptr, nullptr);
  Check(view != nullptr, "input routing test needs a stand-in Flutter view window");
  RECT client = {};
  Check(::GetClientRect(view, &client) != FALSE, "the stand-in view must report a client area");
  Check(client.right > 2 && client.bottom > 2, "the stand-in view must have a usable client area");

  HWND host = ::CreateWindowExW(
      WS_EX_NOPARENTNOTIFY, L"STATIC", L"", kVideoHostWindowStyle | WS_VISIBLE, 0, 0, client.right, client.bottom, view,
      nullptr, nullptr, nullptr);
  Check(host != nullptr, "the video host window must be created");
  HWND inner = ::CreateWindowExW(
      0, L"STATIC", L"", WS_CHILD | WS_VISIBLE, 0, 0, client.right, client.bottom, host, nullptr, nullptr, nullptr);
  Check(inner != nullptr, "the mpv inner window stand-in must be created");

  g_routing_view.store(view, std::memory_order_release);
  g_routing_view_presses.store(0, std::memory_order_relaxed);
  g_routing_child_presses.store(0, std::memory_order_relaxed);
  const auto view_original =
      reinterpret_cast<WNDPROC>(::SetWindowLongPtrW(view, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(RoutingWindowProc)));
  const auto host_original =
      reinterpret_cast<WNDPROC>(::SetWindowLongPtrW(host, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(RoutingWindowProc)));
  const auto inner_original = reinterpret_cast<WNDPROC>(
      ::SetWindowLongPtrW(inner, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(RoutingWindowProc)));

  ::SetForegroundWindow(view);
  PumpUntil([]() { return false; }, 100);

  POINT contact = {client.right / 2, client.bottom / 2};
  ::ClientToScreen(view, &contact);
  POINT restore_cursor = {};
  const bool cursor_known = ::GetCursorPos(&restore_cursor) != FALSE;

  // Control: with the video subtree hidden, this press can only land on the
  // view. It establishes that injection reaches this process at all.
  ::ShowWindow(host, SW_HIDE);
  PumpUntil([]() { return false; }, 50);
  bool control_landed = false;
  if (::WindowFromPoint(contact) == view && InjectClick(contact)) {
    control_landed = PumpUntil([]() { return g_routing_view_presses.load(std::memory_order_relaxed) > 0; }, 2000);
  }

  if (!control_landed) {
    std::cout << "mpv_player_property_contract_test: skipped injected input routing (no usable desktop)\n";
  } else {
    g_routing_view_presses.store(0, std::memory_order_relaxed);
    g_routing_child_presses.store(0, std::memory_order_relaxed);
    ::ShowWindow(host, SW_SHOWNA);
    PumpUntil([]() { return false; }, 50);

    Check(
        ::WindowFromPoint(contact) == view,
        "the input hit test must skip the disabled video subtree and resolve to the Flutter view");
    Check(InjectClick(contact), "the control press injected, so the press over the video host must inject too");
    PumpUntil([]() { return g_routing_view_presses.load(std::memory_order_relaxed) > 0; }, 2000);

    Check(
        g_routing_child_presses.load(std::memory_order_relaxed) == 0,
        "a disabled video subtree must not receive the press itself");
    Check(
        g_routing_view_presses.load(std::memory_order_relaxed) == 1,
        "a press over the disabled video host must reach the parent Flutter view instead of being swallowed");

    // Touch is the contact kind issue #1556 was reported for, and it takes a
    // different path into the system than the mouse press above: the injection
    // below produces a real touch contact, so the system's own hit test picks
    // the target and promotes it to the legacy mouse stream the counters watch.
    // A machine without a digitizer can still inject, so this is not gated on
    // SM_DIGITIZER - only on the injection API being usable at all.
    if (!::InitializeTouchInjection(1, TOUCH_FEEDBACK_NONE)) {
      std::cout << "mpv_player_property_contract_test: skipped injected touch routing (injection unavailable)\n";
    } else {
      g_routing_view_presses.store(0, std::memory_order_relaxed);
      g_routing_child_presses.store(0, std::memory_order_relaxed);
      ::ShowWindow(host, SW_HIDE);
      PumpUntil([]() { return false; }, 50);

      // Same control as the mouse pass: with the video subtree hidden the tap
      // can only land on the view, which proves touch injection works here
      // before silence over the disabled host is treated as a failure.
      bool touch_control_landed = false;
      if (InjectTap(contact)) {
        touch_control_landed =
            PumpUntil([]() { return g_routing_view_presses.load(std::memory_order_relaxed) > 0; }, 2000);
      }

      if (!touch_control_landed) {
        std::cout << "mpv_player_property_contract_test: skipped injected touch routing (no usable desktop)\n";
      } else {
        g_routing_view_presses.store(0, std::memory_order_relaxed);
        g_routing_child_presses.store(0, std::memory_order_relaxed);
        ::ShowWindow(host, SW_SHOWNA);
        PumpUntil([]() { return false; }, 50);

        Check(InjectTap(contact), "the control tap injected, so the tap over the video host must inject too");
        PumpUntil([]() { return g_routing_view_presses.load(std::memory_order_relaxed) > 0; }, 2000);

        Check(
            g_routing_child_presses.load(std::memory_order_relaxed) == 0,
            "a disabled video subtree must not receive the touch itself");
        Check(
            g_routing_view_presses.load(std::memory_order_relaxed) == 1,
            "a touch over the disabled video host must reach the parent Flutter view instead of being swallowed");
      }
    }
  }

  if (cursor_known) ::SetCursorPos(restore_cursor.x, restore_cursor.y);
  ::SetWindowLongPtrW(inner, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(inner_original));
  ::SetWindowLongPtrW(host, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(host_original));
  ::SetWindowLongPtrW(view, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(view_original));
  g_routing_view.store(nullptr, std::memory_order_release);
  ::DestroyWindow(inner);
  ::DestroyWindow(host);
  ::DestroyWindow(view);
}

}  // namespace
}  // namespace mpv

int main() {
  mpv::TestUnavailablePropertyWriteFails();
  mpv::TestPendingPropertyWriteFailsOnDispose();
  mpv::TestPendingRequestTypesRemainDistinctOnDispose();
  mpv::TestInnerSubclassOwnershipIsSerializedAndDetached();
  mpv::TestTimedOutSubclassDetachCanBeAdopted();
  mpv::TestTimedOutSubclassInstallCannotOutliveItsState();
  mpv::TestDisabledVideoHostKeepsInputOnTheFlutterView();
  mpv::TestDisabledVideoHostRoutesRealPressesToParentView();
  std::cout << "mpv_player_property_contract_test: PASS\n";
  return 0;
}
