/**
 * @file tests/unit/platform/test_virtualhid_input.cpp
 * @brief Tests for shared libvirtualhid input helpers.
 */
#include "../../tests_common.h"

// standard includes
#include <array>
#include <chrono>
#include <cstdint>
#include <limits>
#include <memory>
#include <string>
#include <string_view>
#include <tuple>
#include <utility>

// local includes
#include "src/config.h"
#include "src/globals.h"
#include "src/platform/virtualhid_input.h"

using namespace std::chrono_literals;
using namespace std::literals;

namespace {

  /**
   * @brief Expected touchpad support for a configured gamepad.
   */
  struct gamepad_touchpad_case_t {
    std::string_view gamepad;  ///< Configured gamepad name.
    bool expected;  ///< Whether the configured gamepad supports touchpad input.
  };

  /**
   * @brief Parameterized fixture that restores the configured gamepad after each test.
   */
  class VirtualHidInputTest: public ::testing::TestWithParam<gamepad_touchpad_case_t> {
  protected:
    /**
     * @brief Preserve the configured gamepad.
     */
    void SetUp() override {
      original_gamepad = config::input.gamepad;
    }

    /**
     * @brief Restore the configured gamepad.
     */
    void TearDown() override {
      config::input.gamepad = std::move(original_gamepad);
    }

  private:
    std::string original_gamepad;  ///< Configured gamepad restored after each test.
  };

}  // namespace

TEST_P(VirtualHidInputTest, ReportsExpectedTouchpadSupport) {
  const auto &[gamepad, expected] = GetParam();
  config::input.gamepad = gamepad;
  EXPECT_EQ(platf::virtualhid::configured_gamepad_supports_touchpad(), expected) << gamepad;
}

INSTANTIATE_TEST_SUITE_P(
  ConfiguredGamepads,
  VirtualHidInputTest,
  ::testing::Values(
    gamepad_touchpad_case_t {"auto"sv, true},
    gamepad_touchpad_case_t {"generic"sv, false},
    gamepad_touchpad_case_t {"x360"sv, false},
    gamepad_touchpad_case_t {"xone"sv, false},
    gamepad_touchpad_case_t {"xseries"sv, false},
    gamepad_touchpad_case_t {"ds4"sv, true},
    gamepad_touchpad_case_t {"ds5"sv, true},
    gamepad_touchpad_case_t {"switch"sv, false}
  ),
  [](const ::testing::TestParamInfo<gamepad_touchpad_case_t> &info) {
    return std::string {info.param.gamepad};
  }
);

namespace {

  /**
   * @brief Fixture backed by libvirtualhid's observable in-memory devices.
   */
  class VirtualHidDeviceTest: public ::testing::Test {
  protected:
    /**
     * @brief Preserve input configuration and create fake virtual devices.
     */
    void SetUp() override {
      original_input = config::input;
      context_ = std::make_unique<platf::virtualhid::input_context_t>(lvh::BackendKind::fake);
      client_ = std::make_unique<platf::virtualhid::client_context_t>(*context_);
      const auto *test_info = ::testing::UnitTest::GetInstance()->current_test_info();
      const std::string queue_name = std::string {"virtualhid-input-test-"} + test_info->test_suite_name() + '-' + test_info->name();
      feedback_queue_ = mail::man->queue<platf::gamepad_feedback_msg_t>(queue_name);

      ASSERT_NE(context_->runtime, nullptr);
      ASSERT_NE(context_->keyboard, nullptr);
      ASSERT_NE(context_->mouse, nullptr);
      ASSERT_NE(client_->touch, nullptr);
      ASSERT_NE(client_->pen, nullptr);
    }

    /**
     * @brief Restore process-wide input configuration.
     */
    void TearDown() override {
      client_.reset();
      context_.reset();
      feedback_queue_.reset();
      config::input = std::move(original_input);
    }

    /**
     * @brief Allocate a fake gamepad and return its state adapter.
     *
     * @param profile Configured Sunshine profile name.
     * @param type Client-reported controller type.
     * @param capabilities Client-reported capability flags.
     * @param global_index Global Sunshine gamepad slot.
     * @param client_index Client-relative gamepad index.
     * @return Allocated adapter, or `nullptr` when allocation fails.
     */
    lvh::GamepadStateAdapter *allocate_gamepad(
      std::string_view profile,
      std::uint8_t type = LI_CTYPE_UNKNOWN,
      std::uint16_t capabilities = 0,
      int global_index = 0,
      std::uint8_t client_index = 3
    ) {
      config::input.gamepad = profile;
      const platf::gamepad_id_t id {global_index, client_index};
      const platf::gamepad_arrival_t metadata {type, capabilities, 0};
      if (platf::virtualhid::alloc_gamepad(*context_, id, metadata, feedback_queue_) != 0) {
        ADD_FAILURE() << "Failed to allocate fake gamepad profile " << profile;
        return nullptr;
      }

      return platf::virtualhid::gamepad_adapter_for_testing(*context_, global_index);
    }

    /**
     * @brief Access the fake global virtual HID context.
     *
     * @return Pointer to the fixture-owned global context.
     */
    platf::virtualhid::input_context_t *context() const {
      return context_.get();
    }

    /**
     * @brief Access the fake per-client virtual HID context.
     *
     * @return Pointer to the fixture-owned client context.
     */
    platf::virtualhid::client_context_t *client() const {
      return client_.get();
    }

    /**
     * @brief Access the gamepad feedback queue.
     *
     * @return Reference to the fixture-owned feedback queue.
     */
    platf::feedback_queue_t &feedback_queue() {
      return feedback_queue_;
    }

  private:
    config::input_t original_input;  ///< Input configuration restored after each test.
    std::unique_ptr<platf::virtualhid::input_context_t> context_;  ///< Shared fake virtual devices.
    std::unique_ptr<platf::virtualhid::client_context_t> client_;  ///< Per-client fake touch and pen devices.
    platf::feedback_queue_t feedback_queue_;  ///< Captured gamepad feedback messages.
  };

  /**
   * @brief Auto-selection scenario for a client-reported gamepad.
   */
  struct auto_profile_case_t {
    const char *name;  ///< Stable parameter name.
    std::uint8_t type;  ///< Client-reported controller type.
    std::uint16_t capabilities;  ///< Client-reported capability flags.
    lvh::GamepadProfileKind expected;  ///< Expected libvirtualhid profile.
  };

  /**
   * @brief Parameterized fixture for automatic gamepad profile selection.
   */
  class VirtualHidAutoProfileTest:
      public VirtualHidDeviceTest,
      public ::testing::WithParamInterface<auto_profile_case_t> {};

}  // namespace

TEST_F(VirtualHidDeviceTest, CreatesEveryDeviceWithFakeRuntime) {
  EXPECT_EQ(context()->runtime->backend_kind(), lvh::BackendKind::fake);
  EXPECT_EQ(context()->runtime->capabilities().backend_name, "fake");
  EXPECT_TRUE(context()->runtime->capabilities().supports_gamepad);
  EXPECT_TRUE(context()->keyboard->is_open());
  EXPECT_TRUE(context()->mouse->is_open());
  EXPECT_TRUE(client()->touch->is_open());
  EXPECT_TRUE(client()->pen->is_open());

  const auto runtime = platf::virtualhid::create_runtime(lvh::BackendKind::fake);
  ASSERT_NE(runtime, nullptr);
  EXPECT_EQ(runtime->backend_kind(), lvh::BackendKind::fake);
}

TEST_F(VirtualHidDeviceTest, ReportsStaticAndRuntimeGamepadChoices) {
  constexpr std::array expected_names {"auto"sv, "generic"sv, "x360"sv, "xone"sv, "xseries"sv, "ds4"sv, "ds5"sv, "switch"sv};

  const auto static_gamepads = platf::virtualhid::static_supported_gamepads();
  ASSERT_EQ(static_gamepads.size(), expected_names.size());
  EXPECT_TRUE(static_gamepads.front().is_enabled);
  for (std::size_t index = 0; index < expected_names.size(); ++index) {
    EXPECT_EQ(static_gamepads[index].name, expected_names[index]);
    EXPECT_TRUE(static_gamepads[index].reason_disabled.empty());
  }

  const auto available_gamepads = platf::virtualhid::supported_gamepads(context()->runtime.get());
  ASSERT_EQ(available_gamepads.size(), expected_names.size());
  for (const auto &gamepad : available_gamepads) {
    EXPECT_TRUE(gamepad.is_enabled) << gamepad.name;
    EXPECT_TRUE(gamepad.reason_disabled.empty()) << gamepad.name;
  }

  const auto no_runtime_gamepads = platf::virtualhid::supported_gamepads(nullptr, true);
  EXPECT_EQ(no_runtime_gamepads.size(), expected_names.size());
}

TEST_F(VirtualHidDeviceTest, RejectsUnavailableAndInvalidGamepadSlots) {
  const platf::gamepad_arrival_t metadata {LI_CTYPE_XBOX, 0, 0};
  const platf::gamepad_id_t valid_id {0, 0};

  platf::virtualhid::input_context_t no_runtime {lvh::BackendKind::fake};
  no_runtime.runtime.reset();
  EXPECT_EQ(platf::virtualhid::alloc_gamepad(no_runtime, valid_id, metadata, nullptr), -1);

  EXPECT_EQ(platf::virtualhid::alloc_gamepad(*context(), {-1, 0}, metadata, nullptr), -1);
  EXPECT_EQ(platf::virtualhid::alloc_gamepad(*context(), {static_cast<int>(context()->gamepads.size()), 0}, metadata, nullptr), -1);
  EXPECT_FALSE(platf::virtualhid::has_gamepad(*context(), -1));
  EXPECT_FALSE(platf::virtualhid::has_gamepad(*context(), static_cast<int>(context()->gamepads.size())));
  EXPECT_EQ(platf::virtualhid::gamepad_adapter_for_testing(*context(), 0), nullptr);
  EXPECT_EQ(platf::virtualhid::rebind_gamepad(*context(), valid_id, feedback_queue()), -1);
}

TEST_F(VirtualHidDeviceTest, AllocatesManualProfileAndTranslatesFullState) {
  const auto active_devices_before_gamepad = context()->runtime->active_device_count();
  config::input.ds4_back_as_touchpad_click = true;
  config::input.virtualhid_randomize_mac = false;
  const auto capabilities = static_cast<std::uint16_t>(LI_CCAP_ACCEL | LI_CCAP_GYRO | LI_CCAP_TOUCHPAD | LI_CCAP_RGB_LED | LI_CCAP_BATTERY_STATE);
  auto *adapter = allocate_gamepad("ds5"sv, LI_CTYPE_PS, capabilities, 1, 4);
  ASSERT_NE(adapter, nullptr);
  ASSERT_NE(adapter->gamepad(), nullptr);
  EXPECT_EQ(context()->runtime->active_device_count(), active_devices_before_gamepad + 1);

  EXPECT_EQ(adapter->gamepad()->profile().gamepad_kind, lvh::GamepadProfileKind::dualsense);
  EXPECT_TRUE(adapter->gamepad()->profile().name.starts_with("Sunshine "));
  const auto &metadata = adapter->gamepad()->metadata();
  EXPECT_EQ(metadata.global_index, 1);
  EXPECT_EQ(metadata.client_relative_index, 4);
  EXPECT_EQ(metadata.client_type, lvh::ClientControllerType::playstation);
  EXPECT_TRUE(metadata.has_motion_sensors);
  EXPECT_TRUE(metadata.has_touchpad);
  EXPECT_TRUE(metadata.has_rgb_led);
  EXPECT_TRUE(metadata.has_battery);
  EXPECT_EQ(metadata.stable_id, "02:00:00:00:00:01");

  const std::uint32_t button_flags = platf::DPAD_UP | platf::DPAD_DOWN | platf::DPAD_LEFT | platf::DPAD_RIGHT | platf::START | platf::BACK |
                                     platf::LEFT_STICK | platf::RIGHT_STICK | platf::LEFT_BUTTON | platf::RIGHT_BUTTON | platf::HOME |
                                     platf::A | platf::B | platf::X | platf::Y | platf::MISC_BUTTON;
  const platf::gamepad_state_t input_state {
    button_flags,
    128,
    255,
    std::numeric_limits<std::int16_t>::min(),
    std::numeric_limits<std::int16_t>::max(),
    -16384,
    0,
  };
  platf::virtualhid::gamepad_update(*context(), 1, input_state);

  const auto &state = adapter->state();
  using enum lvh::GamepadButton;
  for (const auto button : {dpad_up, dpad_down, dpad_left, dpad_right, start, back, left_stick, right_stick, left_shoulder, right_shoulder, guide, a, b, x, y, misc1, touchpad}) {
    EXPECT_TRUE(state.buttons.test(button));
  }
  EXPECT_FLOAT_EQ(state.left_stick.x, -1.0F);
  EXPECT_FLOAT_EQ(state.left_stick.y, 1.0F);
  EXPECT_FLOAT_EQ(state.right_stick.x, -0.5F);
  EXPECT_FLOAT_EQ(state.right_stick.y, 0.0F);
  EXPECT_FLOAT_EQ(state.left_trigger, 128.0F / 255.0F);
  EXPECT_FLOAT_EQ(state.right_trigger, 1.0F);

  config::input.ds4_back_as_touchpad_click = false;
  platf::virtualhid::gamepad_update(*context(), 1, {platf::BACK, 0, 0, 0, 0, 0, 0});
  EXPECT_FALSE(adapter->state().buttons.test(touchpad));

  platf::virtualhid::free_gamepad(*context(), 1);
  EXPECT_FALSE(platf::virtualhid::has_gamepad(*context(), 1));
  EXPECT_EQ(context()->runtime->active_device_count(), active_devices_before_gamepad);
  platf::virtualhid::free_gamepad(*context(), 1);
  platf::virtualhid::gamepad_update(*context(), 1, input_state);
}

TEST_P(VirtualHidAutoProfileTest, SelectsProfileFromClientMetadata) {
  const auto &test_case = GetParam();
  auto *adapter = allocate_gamepad("auto"sv, test_case.type, test_case.capabilities);
  ASSERT_NE(adapter, nullptr);
  ASSERT_NE(adapter->gamepad(), nullptr);
  EXPECT_EQ(adapter->gamepad()->profile().gamepad_kind, test_case.expected);
}

INSTANTIATE_TEST_SUITE_P(
  ClientMetadata,
  VirtualHidAutoProfileTest,
  ::testing::Values(
    auto_profile_case_t {"PlayStation", LI_CTYPE_PS, 0, lvh::GamepadProfileKind::dualsense},
    auto_profile_case_t {"Nintendo", LI_CTYPE_NINTENDO, 0, lvh::GamepadProfileKind::switch_pro},
    auto_profile_case_t {"Xbox", LI_CTYPE_XBOX, 0, lvh::GamepadProfileKind::xbox_series},
    auto_profile_case_t {"UnknownMotion", LI_CTYPE_UNKNOWN, LI_CCAP_ACCEL, lvh::GamepadProfileKind::dualsense},
    auto_profile_case_t {"UnknownTouchpad", LI_CTYPE_UNKNOWN, LI_CCAP_TOUCHPAD, lvh::GamepadProfileKind::dualsense},
    auto_profile_case_t {"UnknownDefault", LI_CTYPE_UNKNOWN, 0, lvh::GamepadProfileKind::xbox_series}
  ),
  [](const ::testing::TestParamInfo<auto_profile_case_t> &info) {
    return info.param.name;
  }
);

TEST_F(VirtualHidDeviceTest, FallsBackForUnknownManualProfileAndRandomizesPlayStationIdentity) {
  auto *fallback = allocate_gamepad("not-a-profile"sv, LI_CTYPE_UNKNOWN, 0, 0);
  ASSERT_NE(fallback, nullptr);
  EXPECT_EQ(fallback->gamepad()->profile().gamepad_kind, lvh::GamepadProfileKind::xbox_series);
  EXPECT_EQ(fallback->gamepad()->metadata().client_type, lvh::ClientControllerType::unknown);
  EXPECT_EQ(fallback->gamepad()->metadata().stable_id, "sunshine-gamepad-0");

  config::input.virtualhid_randomize_mac = true;
  auto *randomized = allocate_gamepad("ds4"sv, LI_CTYPE_PS, 0, 1);
  ASSERT_NE(randomized, nullptr);
  const auto &stable_id = randomized->gamepad()->metadata().stable_id;
  EXPECT_EQ(stable_id.size(), 17);
  EXPECT_TRUE(stable_id.starts_with("02:00:"));
}

TEST_F(VirtualHidDeviceTest, RoutesAndDeduplicatesGamepadFeedback) {
  auto *adapter = allocate_gamepad("xseries"sv, LI_CTYPE_XBOX);
  ASSERT_NE(adapter, nullptr);

  lvh::GamepadOutput output;
  output.kind = lvh::GamepadOutputKind::rumble;
  output.low_frequency_rumble = 100;
  output.high_frequency_rumble = 200;
  ASSERT_TRUE(adapter->dispatch_output(output).ok());
  auto feedback = feedback_queue()->pop(10ms);
  ASSERT_TRUE(feedback);
  EXPECT_EQ(feedback->type, platf::gamepad_feedback_e::rumble);
  EXPECT_EQ(feedback->id, 3);
  EXPECT_EQ(feedback->data.rumble.lowfreq, 100);
  EXPECT_EQ(feedback->data.rumble.highfreq, 200);

  ASSERT_TRUE(adapter->dispatch_output(output).ok());
  EXPECT_FALSE(feedback_queue()->pop(0ms));
  output.high_frequency_rumble = 201;
  ASSERT_TRUE(adapter->dispatch_output(output).ok());
  EXPECT_TRUE(feedback_queue()->pop(10ms));

  output.kind = lvh::GamepadOutputKind::trigger_rumble;
  output.left_trigger_rumble = 300;
  output.right_trigger_rumble = 400;
  ASSERT_TRUE(adapter->dispatch_output(output).ok());
  feedback = feedback_queue()->pop(10ms);
  ASSERT_TRUE(feedback);
  EXPECT_EQ(feedback->type, platf::gamepad_feedback_e::rumble_triggers);
  EXPECT_EQ(feedback->data.rumble_triggers.left_trigger, 300);
  EXPECT_EQ(feedback->data.rumble_triggers.right_trigger, 400);
  ASSERT_TRUE(adapter->dispatch_output(output).ok());
  EXPECT_FALSE(feedback_queue()->pop(0ms));
  ++output.right_trigger_rumble;
  ASSERT_TRUE(adapter->dispatch_output(output).ok());
  EXPECT_TRUE(feedback_queue()->pop(10ms));

  output.kind = lvh::GamepadOutputKind::rgb_led;
  output.red = 10;
  output.green = 20;
  output.blue = 30;
  ASSERT_TRUE(adapter->dispatch_output(output).ok());
  feedback = feedback_queue()->pop(10ms);
  ASSERT_TRUE(feedback);
  EXPECT_EQ(feedback->type, platf::gamepad_feedback_e::set_rgb_led);
  EXPECT_EQ(feedback->data.rgb_led.r, 10);
  EXPECT_EQ(feedback->data.rgb_led.g, 20);
  EXPECT_EQ(feedback->data.rgb_led.b, 30);
  ASSERT_TRUE(adapter->dispatch_output(output).ok());
  EXPECT_FALSE(feedback_queue()->pop(0ms));
  ++output.blue;
  ASSERT_TRUE(adapter->dispatch_output(output).ok());
  EXPECT_TRUE(feedback_queue()->pop(10ms));

  output.kind = lvh::GamepadOutputKind::adaptive_triggers;
  output.adaptive_trigger_flags = 5;
  output.left_trigger_effect_type = 6;
  output.right_trigger_effect_type = 7;
  output.left_trigger_effect = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
  output.right_trigger_effect = {9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
  ASSERT_TRUE(adapter->dispatch_output(output).ok());
  feedback = feedback_queue()->pop(10ms);
  ASSERT_TRUE(feedback);
  EXPECT_EQ(feedback->type, platf::gamepad_feedback_e::set_adaptive_triggers);
  EXPECT_EQ(feedback->data.adaptive_triggers.event_flags, 5);
  EXPECT_EQ(feedback->data.adaptive_triggers.type_left, 6);
  EXPECT_EQ(feedback->data.adaptive_triggers.type_right, 7);
  EXPECT_EQ(feedback->data.adaptive_triggers.left, output.left_trigger_effect);
  EXPECT_EQ(feedback->data.adaptive_triggers.right, output.right_trigger_effect);

  auto resumed_feedback = mail::man->queue<platf::gamepad_feedback_msg_t>("virtualhid-input-test-resumed-feedback");
  ASSERT_EQ(platf::virtualhid::rebind_gamepad(*context(), {0, 7}, resumed_feedback), 0);
  output.kind = lvh::GamepadOutputKind::rumble;
  output.low_frequency_rumble = 100;
  output.high_frequency_rumble = 201;
  ASSERT_TRUE(adapter->dispatch_output(output).ok());
  feedback = resumed_feedback->pop(10ms);
  ASSERT_TRUE(feedback);
  EXPECT_EQ(feedback->type, platf::gamepad_feedback_e::rumble);
  EXPECT_EQ(feedback->id, 7);
  EXPECT_FALSE(feedback_queue()->pop(0ms));

  output.kind = lvh::GamepadOutputKind::raw_report;
  ASSERT_TRUE(adapter->dispatch_output(output).ok());
  EXPECT_FALSE(feedback_queue()->pop(0ms));

  platf::virtualhid::free_gamepad(*context(), 0);
  const platf::gamepad_id_t id {0, 0};
  const platf::gamepad_arrival_t metadata {LI_CTYPE_XBOX, 0, 0};
  ASSERT_EQ(platf::virtualhid::alloc_gamepad(*context(), id, metadata, nullptr), 0);
  adapter = platf::virtualhid::gamepad_adapter_for_testing(*context(), 0);
  ASSERT_NE(adapter, nullptr);
  output.kind = lvh::GamepadOutputKind::rumble;
  ASSERT_TRUE(adapter->dispatch_output(output).ok());
}

TEST_F(VirtualHidDeviceTest, TranslatesGamepadTouchMotionAndBattery) {
  const auto capabilities = static_cast<std::uint16_t>(LI_CCAP_ACCEL | LI_CCAP_GYRO | LI_CCAP_TOUCHPAD | LI_CCAP_BATTERY_STATE);
  auto *adapter = allocate_gamepad("ds5"sv, LI_CTYPE_PS, capabilities);
  ASSERT_NE(adapter, nullptr);
  EXPECT_TRUE(feedback_queue()->pop(10ms));
  EXPECT_TRUE(feedback_queue()->pop(10ms));

  platf::gamepad_touch_t touch {{0, 3}, LI_TOUCH_EVENT_DOWN, 10, -0.25F, 1.25F, 1.0F};
  platf::virtualhid::gamepad_touch(*context(), touch);
  EXPECT_EQ(adapter->state().touchpad_contacts[0].id, 0);
  EXPECT_TRUE(adapter->state().touchpad_contacts[0].active);
  EXPECT_FLOAT_EQ(adapter->state().touchpad_contacts[0].x, 0.0F);
  EXPECT_FLOAT_EQ(adapter->state().touchpad_contacts[0].y, 1.0F);

  touch.eventType = LI_TOUCH_EVENT_MOVE;
  touch.pressure = 0.25F;
  touch.x = 0.4F;
  touch.y = 0.6F;
  platf::virtualhid::gamepad_touch(*context(), touch);
  EXPECT_FALSE(adapter->state().touchpad_contacts[0].active);
  EXPECT_FLOAT_EQ(adapter->state().touchpad_contacts[0].x, 0.4F);

  touch.eventType = LI_TOUCH_EVENT_DOWN;
  touch.pointerId = 11;
  platf::virtualhid::gamepad_touch(*context(), touch);
  const auto full_touch_submit_count = adapter->gamepad()->submit_count();
  touch.pointerId = 12;
  platf::virtualhid::gamepad_touch(*context(), touch);
  EXPECT_EQ(adapter->gamepad()->submit_count(), full_touch_submit_count);

  touch.eventType = LI_TOUCH_EVENT_MOVE;
  touch.pointerId = 99;
  platf::virtualhid::gamepad_touch(*context(), touch);
  EXPECT_EQ(adapter->gamepad()->submit_count(), full_touch_submit_count);
  touch.eventType = 0xFF;
  touch.pointerId = 10;
  platf::virtualhid::gamepad_touch(*context(), touch);
  EXPECT_EQ(adapter->gamepad()->submit_count(), full_touch_submit_count);

  touch.eventType = LI_TOUCH_EVENT_CANCEL;
  platf::virtualhid::gamepad_touch(*context(), touch);
  EXPECT_FALSE(adapter->state().touchpad_contacts[0].active);
  touch.eventType = LI_TOUCH_EVENT_UP;
  touch.pointerId = 11;
  platf::virtualhid::gamepad_touch(*context(), touch);
  EXPECT_FALSE(adapter->state().touchpad_contacts[1].active);

  touch.eventType = LI_TOUCH_EVENT_DOWN;
  touch.pointerId = 20;
  platf::virtualhid::gamepad_touch(*context(), touch);
  touch.pointerId = 21;
  platf::virtualhid::gamepad_touch(*context(), touch);
  touch.eventType = LI_TOUCH_EVENT_CANCEL_ALL;
  platf::virtualhid::gamepad_touch(*context(), touch);
  EXPECT_FALSE(adapter->state().touchpad_contacts[0].active);
  EXPECT_FALSE(adapter->state().touchpad_contacts[1].active);

  const platf::gamepad_motion_t acceleration {{0, 3}, LI_MOTION_TYPE_ACCEL, 1.0F, 2.0F, 3.0F};
  platf::virtualhid::gamepad_motion(*context(), acceleration);
  ASSERT_TRUE(adapter->state().acceleration);
  EXPECT_FLOAT_EQ(adapter->state().acceleration->x, 1.0F);
  const platf::gamepad_motion_t gyroscope {{0, 3}, LI_MOTION_TYPE_GYRO, 4.0F, 5.0F, 6.0F};
  platf::virtualhid::gamepad_motion(*context(), gyroscope);
  ASSERT_TRUE(adapter->state().gyroscope);
  EXPECT_FLOAT_EQ(adapter->state().gyroscope->z, 6.0F);
  const auto motion_submit_count = adapter->gamepad()->submit_count();
  platf::virtualhid::gamepad_motion(*context(), {{0, 3}, 0xFF, 0.0F, 0.0F, 0.0F});
  platf::virtualhid::gamepad_motion(*context(), {{7, 3}, LI_MOTION_TYPE_ACCEL, 0.0F, 0.0F, 0.0F});
  EXPECT_EQ(adapter->gamepad()->submit_count(), motion_submit_count);

  platf::virtualhid::gamepad_battery(*context(), {{0, 3}, LI_BATTERY_STATE_DISCHARGING, 75});
  ASSERT_TRUE(adapter->state().battery);
  EXPECT_EQ(adapter->state().battery->state, lvh::GamepadBatteryState::discharging);
  EXPECT_EQ(adapter->state().battery->percentage, 75);
  platf::virtualhid::gamepad_battery(*context(), {{0, 3}, LI_BATTERY_STATE_CHARGING, LI_BATTERY_PERCENTAGE_UNKNOWN});
  EXPECT_EQ(adapter->state().battery->state, lvh::GamepadBatteryState::charging);
  EXPECT_EQ(adapter->state().battery->percentage, 100);
  platf::virtualhid::gamepad_battery(*context(), {{0, 3}, LI_BATTERY_STATE_FULL, 120});
  EXPECT_EQ(adapter->state().battery->state, lvh::GamepadBatteryState::full);
  EXPECT_EQ(adapter->state().battery->percentage, 100);
  platf::virtualhid::gamepad_battery(*context(), {{0, 3}, LI_BATTERY_STATE_NOT_CHARGING, 50});
  EXPECT_EQ(adapter->state().battery->state, lvh::GamepadBatteryState::charging_error);
  platf::virtualhid::gamepad_battery(*context(), {{0, 3}, LI_BATTERY_STATE_UNKNOWN, 0});
  EXPECT_FALSE(adapter->state().battery);
  platf::virtualhid::gamepad_battery(*context(), {{0, 3}, LI_BATTERY_STATE_NOT_PRESENT, 0});
  EXPECT_FALSE(adapter->state().battery);
  platf::virtualhid::gamepad_battery(*context(), {{7, 3}, LI_BATTERY_STATE_FULL, 100});

  const auto unsupported_capabilities = static_cast<std::uint16_t>(LI_CCAP_ACCEL | LI_CCAP_TOUCHPAD | LI_CCAP_RGB_LED);
  auto *unsupported = allocate_gamepad("xseries"sv, LI_CTYPE_XBOX, unsupported_capabilities, 1);
  ASSERT_NE(unsupported, nullptr);
  const auto unsupported_count = unsupported->gamepad()->submit_count();
  platf::virtualhid::gamepad_touch(*context(), {{1, 3}, LI_TOUCH_EVENT_DOWN, 0, 0.5F, 0.5F, 1.0F});
  EXPECT_EQ(unsupported->gamepad()->submit_count(), unsupported_count);
}

TEST_F(VirtualHidDeviceTest, TranslatesMouseAndKeyboardInput) {
  platf::virtualhid::move_mouse(*context(), -4, 7);
  auto mouse_event = context()->mouse->last_submitted_event();
  EXPECT_EQ(mouse_event.kind, lvh::MouseEventKind::relative_motion);
  EXPECT_EQ(mouse_event.x, -4);
  EXPECT_EQ(mouse_event.y, 7);

  const platf::touch_port_t viewport {10, 20, 1920, 1080, 1920, 1080};
  platf::virtualhid::abs_mouse(*context(), viewport, 10.6F, 20.4F);
  mouse_event = context()->mouse->last_submitted_event();
  EXPECT_EQ(mouse_event.kind, lvh::MouseEventKind::absolute_motion);
  EXPECT_EQ(mouse_event.x, 11);
  EXPECT_EQ(mouse_event.y, 20);
  EXPECT_EQ(mouse_event.width, 1920);
  EXPECT_EQ(mouse_event.height, 1080);

  constexpr std::array button_cases {
    std::pair {BUTTON_LEFT, lvh::MouseButton::left},
    std::pair {BUTTON_MIDDLE, lvh::MouseButton::middle},
    std::pair {BUTTON_RIGHT, lvh::MouseButton::right},
    std::pair {BUTTON_X1, lvh::MouseButton::side},
    std::pair {BUTTON_X2, lvh::MouseButton::extra},
  };
  for (const auto &[button, expected] : button_cases) {
    platf::virtualhid::button_mouse(*context(), button, false);
    mouse_event = context()->mouse->last_submitted_event();
    EXPECT_EQ(mouse_event.kind, lvh::MouseEventKind::button);
    EXPECT_EQ(mouse_event.button, expected);
    EXPECT_TRUE(mouse_event.pressed);
    platf::virtualhid::button_mouse(*context(), button, true);
    EXPECT_FALSE(context()->mouse->last_submitted_event().pressed);
  }
  const auto button_submit_count = context()->mouse->submit_count();
  platf::virtualhid::button_mouse(*context(), 999, false);
  EXPECT_EQ(context()->mouse->submit_count(), button_submit_count);

  platf::virtualhid::scroll(*context(), 120);
  mouse_event = context()->mouse->last_submitted_event();
  EXPECT_EQ(mouse_event.kind, lvh::MouseEventKind::vertical_scroll);
  EXPECT_EQ(mouse_event.high_resolution_scroll, 120);
  platf::virtualhid::hscroll(*context(), -240);
  mouse_event = context()->mouse->last_submitted_event();
  EXPECT_EQ(mouse_event.kind, lvh::MouseEventKind::horizontal_scroll);
  EXPECT_EQ(mouse_event.high_resolution_scroll, -240);

  config::input.always_send_scancodes = true;
  platf::virtualhid::keyboard_update(*context(), 0x41, false, 0);
  auto keyboard_event = context()->keyboard->last_submitted_event();
  EXPECT_EQ(keyboard_event.key_code, 0x41);
  EXPECT_TRUE(keyboard_event.pressed);
#ifdef _WIN32
  EXPECT_TRUE(keyboard_event.uses_normalized_key_code);
  EXPECT_TRUE(keyboard_event.prefer_native_scan_code);
  platf::virtualhid::keyboard_update(*context(), 0x41, true, SS_KBE_FLAG_NON_NORMALIZED);
  keyboard_event = context()->keyboard->last_submitted_event();
  EXPECT_FALSE(keyboard_event.uses_normalized_key_code);
#else
  EXPECT_FALSE(keyboard_event.uses_normalized_key_code);
  EXPECT_FALSE(keyboard_event.prefer_native_scan_code);
  platf::virtualhid::keyboard_update(*context(), 0x41, true, 0xFF);
  keyboard_event = context()->keyboard->last_submitted_event();
#endif
  EXPECT_FALSE(keyboard_event.pressed);

  const auto keyboard_submit_count = context()->keyboard->submit_count();
  const std::string text = "Sunshine \u{2600}";
  platf::virtualhid::unicode(*context(), text.data(), static_cast<int>(text.size()));
  EXPECT_EQ(context()->keyboard->submit_count(), keyboard_submit_count + 1);
  platf::virtualhid::unicode(*context(), nullptr, 1);
  platf::virtualhid::unicode(*context(), text.data(), 0);
  EXPECT_EQ(context()->keyboard->submit_count(), keyboard_submit_count + 1);

  ASSERT_TRUE(context()->mouse->close().ok());
  ASSERT_TRUE(context()->keyboard->close().ok());
  platf::virtualhid::move_mouse(*context(), 1, 1);
  platf::virtualhid::keyboard_update(*context(), 0x41, false, 0);
  context()->mouse.reset();
  context()->keyboard.reset();
  platf::virtualhid::move_mouse(*context(), 1, 1);
  platf::virtualhid::abs_mouse(*context(), viewport, 1.0F, 1.0F);
  platf::virtualhid::button_mouse(*context(), BUTTON_LEFT, false);
  platf::virtualhid::scroll(*context(), 1);
  platf::virtualhid::hscroll(*context(), 1);
  platf::virtualhid::keyboard_update(*context(), 0x41, false, 0);
  platf::virtualhid::unicode(*context(), text.data(), static_cast<int>(text.size()));
}

TEST_F(VirtualHidDeviceTest, TranslatesTouchscreenLifecycleAndGeometry) {
  const platf::touch_port_t viewport {10, 20, 800, 600, 400, 300};
  platf::touch_input_t touch {LI_TOUCH_EVENT_DOWN, 100, 7, -0.1F, 1.1F, 1.2F, 15.0F, 10.0F};
  platf::virtualhid::touch_update(*client(), viewport, touch);
  auto contact = client()->touch->last_submitted_contact();
  EXPECT_EQ(contact.id, 7);
  EXPECT_FLOAT_EQ(contact.x, 0.0F);
  EXPECT_FLOAT_EQ(contact.y, 1.0F);
  EXPECT_FLOAT_EQ(contact.pressure, 1.0F);
  EXPECT_EQ(contact.orientation, 80);
  EXPECT_TRUE(contact.touching);
  EXPECT_EQ(contact.viewport.offset_x, 10);
  EXPECT_EQ(contact.viewport.offset_y, 20);
  EXPECT_EQ(contact.viewport.width, 800);
  EXPECT_EQ(contact.viewport.height, 600);
  EXPECT_FLOAT_EQ(contact.contact_major_axis, 15.0F);
  EXPECT_FLOAT_EQ(contact.contact_minor_axis, 10.0F);
  EXPECT_TRUE(client()->active_touches.contains(7));

  touch.eventType = LI_TOUCH_EVENT_HOVER;
  touch.rotation = 300;
  touch.pointerId = 8;
  touch.pressureOrDistance = 0.25F;
  platf::virtualhid::touch_update(*client(), viewport, touch);
  contact = client()->touch->last_submitted_contact();
  EXPECT_EQ(contact.orientation, -60);
  EXPECT_FALSE(contact.touching);
  EXPECT_TRUE(client()->active_touches.contains(8));

  touch.eventType = LI_TOUCH_EVENT_UP;
  platf::virtualhid::touch_update(*client(), viewport, touch);
  EXPECT_FALSE(client()->active_touches.contains(8));
  touch.eventType = LI_TOUCH_EVENT_CANCEL;
  touch.pointerId = 7;
  platf::virtualhid::touch_update(*client(), viewport, touch);
  EXPECT_FALSE(client()->active_touches.contains(7));

  touch.eventType = LI_TOUCH_EVENT_MOVE;
  touch.rotation = LI_ROT_UNKNOWN;
  touch.pointerId = 9;
  platf::virtualhid::touch_update(*client(), viewport, touch);
  EXPECT_EQ(client()->touch->last_submitted_contact().orientation, 0);
  touch.eventType = LI_TOUCH_EVENT_HOVER_LEAVE;
  platf::virtualhid::touch_update(*client(), viewport, touch);
  EXPECT_FALSE(client()->active_touches.contains(9));

  touch.eventType = LI_TOUCH_EVENT_DOWN;
  touch.pointerId = 10;
  platf::virtualhid::touch_update(*client(), viewport, touch);
  touch.pointerId = 11;
  platf::virtualhid::touch_update(*client(), viewport, touch);
  const auto before_cancel = client()->touch->submit_count();
  touch.eventType = LI_TOUCH_EVENT_CANCEL_ALL;
  platf::virtualhid::touch_update(*client(), viewport, touch);
  EXPECT_TRUE(client()->active_touches.empty());
  EXPECT_EQ(client()->touch->submit_count(), before_cancel + 2);

  const auto before_unknown = client()->touch->submit_count();
  touch.eventType = 0xFF;
  platf::virtualhid::touch_update(*client(), viewport, touch);
  EXPECT_EQ(client()->touch->submit_count(), before_unknown);
  ASSERT_TRUE(client()->touch->close().ok());
  touch.eventType = LI_TOUCH_EVENT_DOWN;
  platf::virtualhid::touch_update(*client(), viewport, touch);
  client()->touch.reset();
  platf::virtualhid::touch_update(*client(), viewport, touch);
}

TEST_F(VirtualHidDeviceTest, TranslatesPenButtonsToolsAndTransitions) {
  const platf::touch_port_t viewport {1, 2, 1000, 500, 1000, 500};
  platf::pen_input_t pen {
    LI_TOUCH_EVENT_DOWN,
    LI_TOOL_TYPE_PEN,
    static_cast<std::uint8_t>(LI_PEN_BUTTON_PRIMARY | LI_PEN_BUTTON_SECONDARY),
    45,
    450,
    -0.25F,
    1.25F,
    1.5F,
    0.0F,
    0.0F,
  };
  platf::virtualhid::pen_update(*client(), viewport, pen);
  auto state = client()->pen->last_submitted_tool();
  EXPECT_EQ(state.tool, lvh::PenToolType::pen);
  EXPECT_FLOAT_EQ(state.x, 0.0F);
  EXPECT_FLOAT_EQ(state.y, 1.0F);
  EXPECT_FLOAT_EQ(state.pressure, 1.0F);
  EXPECT_FLOAT_EQ(state.distance, -1.0F);
  EXPECT_NEAR(state.tilt_x, -45.0F, 0.001F);
  EXPECT_NEAR(state.tilt_y, 0.0F, 0.001F);
  EXPECT_EQ(state.transition, lvh::PointerTransition::update);
  EXPECT_EQ(state.viewport.offset_x, 1);
  EXPECT_EQ(state.viewport.offset_y, 2);
  EXPECT_TRUE(client()->pressed_pen_buttons.contains(lvh::PenButton::primary));
  EXPECT_TRUE(client()->pressed_pen_buttons.contains(lvh::PenButton::secondary));

  const auto before_unchanged_buttons = client()->pen->submit_count();
  platf::virtualhid::pen_update(*client(), viewport, pen);
  EXPECT_EQ(client()->pen->submit_count(), before_unchanged_buttons + 1);

  pen.eventType = LI_TOUCH_EVENT_HOVER;
  pen.toolType = LI_TOOL_TYPE_ERASER;
  pen.penButtons = LI_PEN_BUTTON_TERTIARY;
  pen.tilt = LI_TILT_UNKNOWN;
  pen.rotation = LI_ROT_UNKNOWN;
  pen.pressureOrDistance = 0.4F;
  platf::virtualhid::pen_update(*client(), viewport, pen);
  state = client()->pen->last_submitted_tool();
  EXPECT_EQ(state.tool, lvh::PenToolType::eraser);
  EXPECT_FLOAT_EQ(state.pressure, -1.0F);
  EXPECT_FLOAT_EQ(state.distance, 0.4F);
  EXPECT_FLOAT_EQ(state.tilt_x, 0.0F);
  EXPECT_FLOAT_EQ(state.tilt_y, 0.0F);
  EXPECT_TRUE(client()->pressed_pen_buttons.contains(lvh::PenButton::tertiary));
  EXPECT_FALSE(client()->pressed_pen_buttons.contains(lvh::PenButton::primary));

  pen.eventType = LI_TOUCH_EVENT_UP;
  pen.toolType = LI_TOOL_TYPE_UNKNOWN;
  pen.penButtons = 0;
  platf::virtualhid::pen_update(*client(), viewport, pen);
  EXPECT_EQ(client()->pen->last_submitted_tool().tool, lvh::PenToolType::unchanged);
  EXPECT_EQ(client()->pen->last_submitted_tool().transition, lvh::PointerTransition::release);

  pen.eventType = LI_TOUCH_EVENT_HOVER_LEAVE;
  platf::virtualhid::pen_update(*client(), viewport, pen);
  EXPECT_EQ(client()->pen->last_submitted_tool().transition, lvh::PointerTransition::leave);
  pen.eventType = LI_TOUCH_EVENT_CANCEL;
  platf::virtualhid::pen_update(*client(), viewport, pen);
  EXPECT_EQ(client()->pen->last_submitted_tool().transition, lvh::PointerTransition::cancel);

  pen.eventType = LI_TOUCH_EVENT_DOWN;
  pen.penButtons = LI_PEN_BUTTON_PRIMARY;
  platf::virtualhid::pen_update(*client(), viewport, pen);
  ASSERT_FALSE(client()->pressed_pen_buttons.empty());
  pen.eventType = LI_TOUCH_EVENT_CANCEL_ALL;
  platf::virtualhid::pen_update(*client(), viewport, pen);
  EXPECT_TRUE(client()->pressed_pen_buttons.empty());
  EXPECT_EQ(client()->pen->last_submitted_tool().transition, lvh::PointerTransition::cancel);

  ASSERT_TRUE(client()->pen->close().ok());
  pen.eventType = LI_TOUCH_EVENT_DOWN;
  platf::virtualhid::pen_update(*client(), viewport, pen);
  client()->pen.reset();
  platf::virtualhid::pen_update(*client(), viewport, pen);
}

TEST_F(VirtualHidDeviceTest, PlatformWrappersForwardToVirtualHidContext) {
  auto platform_input = platf::input();
  ASSERT_TRUE(platform_input);
  auto &platform_context = platf::virtualhid::get_input_context(platform_input);
  platform_context = platf::virtualhid::input_context_t {lvh::BackendKind::fake};
  ASSERT_NE(platform_context.mouse, nullptr);
  ASSERT_NE(platform_context.keyboard, nullptr);

  const platf::touch_port_t viewport {0, 0, 1280, 720, 1280, 720};
  platf::move_mouse(platform_input, 2, 3);
  EXPECT_EQ(platform_context.mouse->last_submitted_event().kind, lvh::MouseEventKind::relative_motion);
  platf::abs_mouse(platform_input, viewport, 40.0F, 50.0F);
  EXPECT_EQ(platform_context.mouse->last_submitted_event().kind, lvh::MouseEventKind::absolute_motion);
  platf::button_mouse(platform_input, BUTTON_LEFT, false);
  EXPECT_EQ(platform_context.mouse->last_submitted_event().kind, lvh::MouseEventKind::button);
  platf::scroll(platform_input, 120);
  EXPECT_EQ(platform_context.mouse->last_submitted_event().kind, lvh::MouseEventKind::vertical_scroll);
  platf::hscroll(platform_input, -120);
  EXPECT_EQ(platform_context.mouse->last_submitted_event().kind, lvh::MouseEventKind::horizontal_scroll);
  platf::keyboard_update(platform_input, 0x41, false, 0);
  EXPECT_EQ(platform_context.keyboard->last_submitted_event().key_code, 0x41);
  const std::string text = "wrapper";
  const auto keyboard_count = platform_context.keyboard->submit_count();
  platf::unicode(platform_input, text.data(), static_cast<int>(text.size()));
  EXPECT_EQ(platform_context.keyboard->submit_count(), keyboard_count + 1);

  config::input.gamepad = "ds5";
  const auto capabilities = static_cast<std::uint16_t>(LI_CCAP_ACCEL | LI_CCAP_GYRO | LI_CCAP_TOUCHPAD | LI_CCAP_BATTERY_STATE);
  const platf::gamepad_id_t gamepad_id {0, 2};
  const platf::gamepad_arrival_t gamepad_metadata {LI_CTYPE_PS, capabilities, 0};
  ASSERT_EQ(platf::alloc_gamepad(platform_input, gamepad_id, gamepad_metadata, feedback_queue()), 0);
  auto *adapter = platf::virtualhid::gamepad_adapter_for_testing(platform_context, 0);
  ASSERT_NE(adapter, nullptr);
  platf::gamepad_update(platform_input, 0, {platf::A, 255, 0, 0, 0, 0, 0});
  EXPECT_TRUE(adapter->state().buttons.test(lvh::GamepadButton::a));
  platf::gamepad_touch(platform_input, {{0, 2}, LI_TOUCH_EVENT_DOWN, 1, 0.25F, 0.5F, 1.0F});
  EXPECT_TRUE(adapter->state().touchpad_contacts[0].active);
  platf::gamepad_motion(platform_input, {{0, 2}, LI_MOTION_TYPE_ACCEL, 1.0F, 2.0F, 3.0F});
  EXPECT_TRUE(adapter->state().acceleration.has_value());
  platf::gamepad_battery(platform_input, {{0, 2}, LI_BATTERY_STATE_FULL, 100});
  EXPECT_TRUE(adapter->state().battery.has_value());

  const auto &supported = platf::supported_gamepads(std::addressof(platform_input));
  ASSERT_FALSE(supported.empty());
  EXPECT_TRUE(supported.front().is_enabled);
  EXPECT_FALSE(platf::supported_gamepads(nullptr).empty());
#ifdef __APPLE__
  EXPECT_EQ(platf::get_capabilities() & platf::platform_caps::controller_touch, 0U);
#else
  EXPECT_NE(platf::get_capabilities() & platf::platform_caps::controller_touch, 0U);
#endif

  platf::free_gamepad(platform_input, 0);
  EXPECT_FALSE(platf::virtualhid::has_gamepad(platform_context, 0));
  platf::free_gamepad(platform_input, 0);
  platf::gamepad_update(platform_input, 0, {});
  platf::gamepad_touch(platform_input, {{0, 2}, LI_TOUCH_EVENT_DOWN, 1, 0.25F, 0.5F, 1.0F});
  platf::gamepad_motion(platform_input, {{0, 2}, LI_MOTION_TYPE_ACCEL, 1.0F, 2.0F, 3.0F});
  platf::gamepad_battery(platform_input, {{0, 2}, LI_BATTERY_STATE_FULL, 100});

  auto platform_client = platf::allocate_client_input_context(platform_input);
  ASSERT_NE(platform_client, nullptr);
  auto &platform_client_context = platf::virtualhid::get_client_context(platform_client.get());
  ASSERT_NE(platform_client_context.touch, nullptr);
  ASSERT_NE(platform_client_context.pen, nullptr);
  platf::touch_update(platform_client.get(), viewport, {LI_TOUCH_EVENT_DOWN, 0, 1, 0.25F, 0.5F, 1.0F, 2.0F, 1.0F});
  EXPECT_EQ(platform_client_context.touch->last_submitted_contact().id, 1);
  platf::pen_update(platform_client.get(), viewport, {LI_TOUCH_EVENT_HOVER, LI_TOOL_TYPE_PEN, 0, LI_TILT_UNKNOWN, LI_ROT_UNKNOWN, 0.25F, 0.5F, 0.5F, 0.0F, 0.0F});
  EXPECT_EQ(platform_client_context.pen->last_submitted_tool().tool, lvh::PenToolType::pen);

  platform_context.runtime.reset();
  config::input.gamepad = "generic";
  EXPECT_EQ(platf::alloc_gamepad(platform_input, gamepad_id, gamepad_metadata, nullptr), -1);
}
