/*
 * Copyright © 2012 Intel Corporation
 * Copyright © 2013 Collabora, Ltd.
 * Copyright © 2018 Canonical Ltd.
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice (including the
 * next paragraph) shall be included in all copies or substantial
 * portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT.  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
 * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
 * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#include "helpers.h"
#include "in_process_server.h"
#include "xdg_shell_stable.h"
#include "xdg_shell_v6.h"
#include "layer_shell_v1.h"
#include "version_specifier.h"

#include <gmock/gmock.h>

#include <optional>

using namespace testing;

int const window_width = 400, window_height = 500;
int const popup_width = 60, popup_height = 40;

namespace
{
uint32_t anchor_stable_to_v6(xdg_positioner_anchor anchor)
{
    switch (anchor)
    {
        case XDG_POSITIONER_ANCHOR_NONE:
            return ZXDG_POSITIONER_V6_ANCHOR_NONE;
        case XDG_POSITIONER_ANCHOR_TOP:
            return ZXDG_POSITIONER_V6_ANCHOR_TOP;
        case XDG_POSITIONER_ANCHOR_BOTTOM:
            return ZXDG_POSITIONER_V6_ANCHOR_BOTTOM;
        case XDG_POSITIONER_ANCHOR_LEFT:
            return ZXDG_POSITIONER_V6_ANCHOR_LEFT;
        case XDG_POSITIONER_ANCHOR_RIGHT:
            return ZXDG_POSITIONER_V6_ANCHOR_RIGHT;
        case XDG_POSITIONER_ANCHOR_TOP_LEFT:
            return ZXDG_POSITIONER_V6_ANCHOR_TOP | ZXDG_POSITIONER_V6_ANCHOR_LEFT;
        case XDG_POSITIONER_ANCHOR_BOTTOM_LEFT:
            return ZXDG_POSITIONER_V6_ANCHOR_BOTTOM | ZXDG_POSITIONER_V6_ANCHOR_LEFT;
        case XDG_POSITIONER_ANCHOR_TOP_RIGHT:
            return ZXDG_POSITIONER_V6_ANCHOR_TOP | ZXDG_POSITIONER_V6_ANCHOR_RIGHT;
        case XDG_POSITIONER_ANCHOR_BOTTOM_RIGHT:
            return ZXDG_POSITIONER_V6_ANCHOR_BOTTOM | ZXDG_POSITIONER_V6_ANCHOR_RIGHT;
        default:
            return ZXDG_POSITIONER_V6_ANCHOR_NONE;
    }
}

uint32_t gravity_stable_to_v6(xdg_positioner_gravity gravity)
{
    switch (gravity)
    {
        case XDG_POSITIONER_GRAVITY_NONE:
            return ZXDG_POSITIONER_V6_GRAVITY_NONE;
        case XDG_POSITIONER_GRAVITY_TOP:
            return ZXDG_POSITIONER_V6_GRAVITY_TOP;
        case XDG_POSITIONER_GRAVITY_BOTTOM:
            return ZXDG_POSITIONER_V6_GRAVITY_BOTTOM;
        case XDG_POSITIONER_GRAVITY_LEFT:
            return ZXDG_POSITIONER_V6_GRAVITY_LEFT;
        case XDG_POSITIONER_GRAVITY_RIGHT:
            return ZXDG_POSITIONER_V6_GRAVITY_RIGHT;
        case XDG_POSITIONER_GRAVITY_TOP_LEFT:
            return ZXDG_POSITIONER_V6_GRAVITY_TOP | ZXDG_POSITIONER_V6_GRAVITY_LEFT;
        case XDG_POSITIONER_GRAVITY_BOTTOM_LEFT:
            return ZXDG_POSITIONER_V6_GRAVITY_BOTTOM | ZXDG_POSITIONER_V6_GRAVITY_LEFT;
        case XDG_POSITIONER_GRAVITY_TOP_RIGHT:
            return ZXDG_POSITIONER_V6_ANCHOR_TOP | ZXDG_POSITIONER_V6_ANCHOR_RIGHT;
        case XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT:
            return ZXDG_POSITIONER_V6_GRAVITY_BOTTOM | ZXDG_POSITIONER_V6_GRAVITY_RIGHT;
        default:
            return ZXDG_POSITIONER_V6_GRAVITY_NONE;
    }
}

uint32_t constraint_adjustment_stable_to_v6(xdg_positioner_constraint_adjustment ca)
{
    return ca; // the two enums have the same values
}

struct PositionerParams
{
    PositionerParams()
        : popup_size{popup_width, popup_height},
          anchor_rect{{0, 0}, {window_width, window_height}}
    {
    }

    auto with_size(int x, int y) -> PositionerParams& { popup_size = {x, y}; return *this; }
    auto with_anchor_rect(int x, int y, int w, int h) -> PositionerParams& { anchor_rect = {{x, y}, {w, h}}; return *this; }
    auto with_anchor(xdg_positioner_anchor value) -> PositionerParams& { anchor_stable = {value}; return *this; }
    auto with_gravity(xdg_positioner_gravity value) -> PositionerParams& { gravity_stable = {value}; return *this; }
    auto with_constraint_adjustment(uint32_t value) -> PositionerParams& { constraint_adjustment_stable = static_cast<xdg_positioner_constraint_adjustment>(value); return *this; }
    auto with_offset(int x, int y) -> PositionerParams& { offset = {{x, y}}; return *this; }
    auto with_reactive(bool enable = true) -> PositionerParams& { reactive = enable; return *this; }
    auto with_grab(bool enable = true) -> PositionerParams& { grab = enable; return *this; }

    std::pair<int, int> popup_size; // will default to XdgPopupStableTestBase::popup_(width|height) if nullopt
    std::pair<std::pair<int, int>, std::pair<int, int>> anchor_rect; // will default to the full window rect
    std::optional<xdg_positioner_anchor> anchor_stable;
    std::optional<xdg_positioner_gravity> gravity_stable;
    std::optional<xdg_positioner_constraint_adjustment> constraint_adjustment_stable;
    std::optional<std::pair<int, int>> offset;
    bool reactive{false};
    bool grab{false};
};

struct PositionerTestParams
{
    PositionerTestParams(std::string name, int expected_x, int expected_y, PositionerParams const& positioner)
        : name{name},
          expected_positon{expected_x, expected_y},
          expected_size{popup_width, popup_height},
          positioner{positioner},
          parent_position_func{std::nullopt}
    {
    }

    PositionerTestParams(
        std::string name,
        int expected_x, int expected_y,
        int expected_width, int expected_height,
        PositionerParams const& positioner,
        std::function<std::pair<int, int>(int, int)> parent_position_func)
        : name{name},
          expected_positon{expected_x, expected_y},
          expected_size{expected_width, expected_height},
          positioner{positioner},
          parent_position_func{std::move(parent_position_func)}
    {
    }

    std::string name;
    std::pair<int, int> expected_positon;
    std::pair<int, int> expected_size;
    PositionerParams positioner;
    /// parent_position_func is called with the size of the output
    std::optional<std::function<std::pair<int, int>(int output_width, int output_height)>> parent_position_func;
};

std::ostream& operator<<(std::ostream& out, PositionerTestParams const& param)
{
    return out << param.name;
}

class XdgPopupManagerBase
{
public:
    struct State
    {
        int x;
        int y;
        int width;
        int height;
    };

    static int const window_x, window_y;

    XdgPopupManagerBase(wlcs::Server& server, std::shared_ptr<wlcs::Client> client)
        : the_server{server},
          client{client},
          surface{*client},
          parent_position_{window_x, window_y}
    {
        surface.add_frame_callback([this](auto) { surface_rendered = true; });
    }

    virtual ~XdgPopupManagerBase() = default;

    void wait_for_frame_to_render()
    {
        surface.attach_buffer(window_width, window_height);
        surface_rendered = false;
        wl_surface_commit(surface);
        client->dispatch_until([this]() { return surface_rendered; });
        the_server.move_surface_to(surface, parent_position_.first, parent_position_.second);
    }

    void map_popup(PositionerParams const& params)
    {
        popup_surface.emplace(*client);
        setup_popup(params);
        wl_surface_commit(popup_surface.value());
        dispatch_until_popup_configure();
        popup_surface.value().attach_buffer(params.popup_size.first, params.popup_size.second);
        bool surface_rendered{false};
        popup_surface.value().add_frame_callback([&surface_rendered](auto) { surface_rendered = true; });
        wl_surface_commit(popup_surface.value());
        client->dispatch_until([&surface_rendered]() { return surface_rendered; });
    }

    void set_parent_position(
        std::function<std::pair<int, int>(int output_width, int output_height)> const& parent_position_func)
    {
        auto const output_size = client->output_state(0).mode_size.value();
        parent_position_ = parent_position_func(output_size.first, output_size.second);
        the_server.move_surface_to(surface, parent_position_.first, parent_position_.second);
        client->roundtrip();
    }

    auto parent_position() -> std::pair<int, int>
    {
        return parent_position_;
    }

    void unmap_popup()
    {
        clear_popup();
        popup_surface = std::nullopt;
    }

    virtual auto create_child_popup() -> std::unique_ptr<XdgPopupManagerBase> = 0;
    virtual void dispatch_until_popup_configure() = 0;

    MOCK_METHOD0(popup_done, void());

    wlcs::Server& the_server;

    std::shared_ptr<wlcs::Client> const client;
    wlcs::Surface surface;
    std::optional<wlcs::Surface> popup_surface;

    std::pair<int, int> parent_position_;
    std::optional<State> state;

protected:
    virtual void setup_popup(PositionerParams const& params) = 0;
    virtual void clear_popup() = 0;

    bool surface_rendered{true};
};

int const XdgPopupManagerBase::window_x = 500;
int const XdgPopupManagerBase::window_y = 500;

class XdgPopupStableManager : public XdgPopupManagerBase
{
public:
    XdgPopupStableManager(wlcs::Server& server)
        : XdgPopupManagerBase{server, std::make_shared<wlcs::Client>(server)},
          xdg_shell_surface{std::in_place, *client, surface},
          toplevel{std::in_place, xdg_shell_surface.value()},
          parent{&*xdg_shell_surface}
    {
        wait_for_frame_to_render();
    }

    XdgPopupStableManager(wlcs::Server& server, std::shared_ptr<wlcs::Client> client, wlcs::XdgSurfaceStable* parent)
        : XdgPopupManagerBase{server, client},
          parent{parent}
    {
    }

    void dispatch_until_popup_configure() override
    {
        client->dispatch_until(
            [prev_count = popup_surface_configure_count, &current_count = popup_surface_configure_count]()
            {
                return current_count > prev_count;
            });
    }

    static void setup_positioner(wlcs::XdgPositionerStable& positioner, PositionerParams const& param)
    {
        // size must always be set
        xdg_positioner_set_size(positioner, param.popup_size.first, param.popup_size.second);

        // anchor rect must always be set
        xdg_positioner_set_anchor_rect(
            positioner,
            param.anchor_rect.first.first,
            param.anchor_rect.first.second,
            param.anchor_rect.second.first,
            param.anchor_rect.second.second);

        if (param.anchor_stable)
            xdg_positioner_set_anchor(positioner,  param.anchor_stable.value());

        if (param.gravity_stable)
            xdg_positioner_set_gravity(positioner, param.gravity_stable.value());

        if (param.constraint_adjustment_stable)
            xdg_positioner_set_constraint_adjustment(positioner, param.constraint_adjustment_stable.value());

        if (param.offset)
            xdg_positioner_set_offset(positioner, param.offset.value().first, param.offset.value().second);

        if (param.reactive)
        {
            if (xdg_positioner_get_version(positioner) < XDG_POSITIONER_SET_REACTIVE_SINCE_VERSION)
            {
                BOOST_THROW_EXCEPTION(std::logic_error("XDG shell version does not support reactive popups"));
            }
            xdg_positioner_set_reactive(positioner);
        }
    }

    void setup_popup(PositionerParams const& param) override
    {
        wlcs::XdgPositionerStable positioner{*client};
        setup_positioner(positioner, param);

        popup_xdg_surface.emplace(*client, popup_surface.value());

        popup.emplace(popup_xdg_surface.value(), parent, positioner);
        if (param.grab)
        {
            if (!client->latest_serial())
            {
                BOOST_THROW_EXCEPTION(std::runtime_error("client does not have a serial"));
            }
            xdg_popup_grab(popup.value().popup, client->seat(), client->latest_serial().value());
        }

        ON_CALL(popup_xdg_surface.value(), configure).WillByDefault([&](auto serial)
            {
                xdg_surface_ack_configure(popup_xdg_surface.value(), serial);
                popup_surface_configure_count++;
            });
        ON_CALL(popup.value(), configure).WillByDefault([&](auto... args)
            {
                state = State{args...};
            });
        ON_CALL(popup.value(), done()).WillByDefault([this](){ popup_done(); });
    }

    void move_parent_using_pointer(std::pair<int, int> to)
    {
        auto pointer = the_server.create_pointer();
        pointer.move_to(parent_position_.first + 1, parent_position_.second + 1);
        pointer.left_button_down();
        client->roundtrip();
        if (client->window_under_cursor() != surface)
        {
            BOOST_THROW_EXCEPTION(std::runtime_error("surface not detected at expected position"));
        }
        xdg_toplevel_move(toplevel.value(), client->seat(), client->latest_serial().value());
        client->roundtrip();
        pointer.move_to(to.first + 1, to.second + 1);
        parent_position_ = to;
        pointer.left_button_up();
        client->roundtrip();
    }

    void clear_popup() override
    {
        popup = std::nullopt;
        popup_xdg_surface = std::nullopt;
    }

    auto create_child_popup() -> std::unique_ptr<XdgPopupManagerBase> override
    {
        return std::make_unique<XdgPopupStableManager>(the_server, client, &popup_xdg_surface.value());
    }

    std::optional<wlcs::XdgSurfaceStable> xdg_shell_surface;
    std::optional<wlcs::XdgToplevelStable> toplevel;
    wlcs::XdgSurfaceStable* const parent;

    std::optional<wlcs::XdgSurfaceStable> popup_xdg_surface;
    std::optional<wlcs::XdgPopupStable> popup;

    int popup_surface_configure_count{0};
};

class XdgPopupV6Manager : public XdgPopupManagerBase
{
public:
    XdgPopupV6Manager(wlcs::Server& server)
        : XdgPopupManagerBase{server, std::make_shared<wlcs::Client>(server)},
          xdg_shell_surface{std::in_place, *client, surface},
          toplevel{std::in_place, xdg_shell_surface.value()},
          parent{&*xdg_shell_surface}
    {
        wait_for_frame_to_render();
    }

    XdgPopupV6Manager(wlcs::Server& server, std::shared_ptr<wlcs::Client> client, wlcs::XdgSurfaceV6* parent)
        : XdgPopupManagerBase{server, client},
          parent{parent}
    {
    }

    void dispatch_until_popup_configure() override
    {
        client->dispatch_until(
            [prev_count = popup_surface_configure_count, &current_count = popup_surface_configure_count]()
            {
                return current_count > prev_count;
            });
    }

    void setup_popup(PositionerParams const& param) override
    {
        wlcs::XdgPositionerV6 positioner{*client};

        // size must always be set
        zxdg_positioner_v6_set_size(positioner, param.popup_size.first, param.popup_size.second);

        // anchor rect must always be set
        zxdg_positioner_v6_set_anchor_rect(
            positioner,
            param.anchor_rect.first.first,
            param.anchor_rect.first.second,
            param.anchor_rect.second.first,
            param.anchor_rect.second.second);

        if (param.anchor_stable)
        {
            uint32_t v6_anchor = anchor_stable_to_v6(param.anchor_stable.value());
            zxdg_positioner_v6_set_anchor(positioner, v6_anchor);
        }

        if (param.gravity_stable)
        {
            uint32_t v6_gravity = gravity_stable_to_v6(param.gravity_stable.value());
            zxdg_positioner_v6_set_gravity(positioner, v6_gravity);
        }

        if (param.constraint_adjustment_stable)
        {
            uint32_t v6_constraint_adjustment =
                constraint_adjustment_stable_to_v6(param.constraint_adjustment_stable.value());
            zxdg_positioner_v6_set_constraint_adjustment(positioner, v6_constraint_adjustment);
        }

        if (param.offset)
        {
            zxdg_positioner_v6_set_offset(positioner, param.offset.value().first, param.offset.value().second);
        }

        popup_xdg_surface.emplace(*client, popup_surface.value());
        popup.emplace(popup_xdg_surface.value(), *parent, positioner);
        if (param.grab)
        {
            if (!client->latest_serial())
            {
                BOOST_THROW_EXCEPTION(std::runtime_error("client does not have a serial"));
            }
            zxdg_popup_v6_grab(popup.value().popup, client->seat(), client->latest_serial().value());
        }

        ON_CALL(popup.value(), done).WillByDefault([this](){ popup_done(); });
        ON_CALL(popup_xdg_surface.value(), configure).WillByDefault([&](auto serial)
            {
                zxdg_surface_v6_ack_configure(popup_xdg_surface.value(), serial);
                popup_surface_configure_count++;
            });
        ON_CALL(popup.value(), configure).WillByDefault([this](auto... args)
            {
                state = State{args...};
            });
    }

    void clear_popup() override
    {
        popup = std::nullopt;
        popup_xdg_surface = std::nullopt;
    }

    auto create_child_popup() -> std::unique_ptr<XdgPopupManagerBase> override
    {
        return std::make_unique<XdgPopupV6Manager>(the_server, client, &popup_xdg_surface.value());
    }

    std::optional<wlcs::XdgSurfaceV6> xdg_shell_surface;
    std::optional<wlcs::XdgToplevelV6> toplevel;
    wlcs::XdgSurfaceV6* const parent;

    std::optional<wlcs::XdgSurfaceV6> popup_xdg_surface;
    std::optional<wlcs::XdgPopupV6> popup;

    int popup_surface_configure_count{0};
};

class LayerV1PopupManager : public XdgPopupManagerBase
{
public:
    LayerV1PopupManager(wlcs::Server& server)
        : XdgPopupManagerBase{server, std::make_shared<wlcs::Client>(server)},
          layer_surface{*client, surface}
    {
        {
            wlcs::Client client{server};
            auto const layer_shell = client.bind_if_supported<zwlr_layer_shell_v1>(
                wlcs::AtLeastVersion{ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_ON_DEMAND_SINCE_VERSION});
            client.roundtrip();
        }
        zwlr_layer_surface_v1_set_size(layer_surface, window_width, window_height);
        zwlr_layer_surface_v1_set_keyboard_interactivity(
            layer_surface,
            ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_ON_DEMAND);
        wait_for_frame_to_render();
    }

    void dispatch_until_popup_configure() override
    {
        client->dispatch_until(
            [prev_count = popup_surface_configure_count, &current_count = popup_surface_configure_count]()
            {
                return current_count > prev_count;
            });
    }

    void setup_popup(PositionerParams const& param) override
    {
        wlcs::XdgPositionerStable positioner{*client};

        // size must always be set
        xdg_positioner_set_size(positioner, param.popup_size.first, param.popup_size.second);

        // anchor rect must always be set
        xdg_positioner_set_anchor_rect(
            positioner,
            param.anchor_rect.first.first,
            param.anchor_rect.first.second,
            param.anchor_rect.second.first,
            param.anchor_rect.second.second);

        if (param.anchor_stable)
            xdg_positioner_set_anchor(positioner,  param.anchor_stable.value());

        if (param.gravity_stable)
            xdg_positioner_set_gravity(positioner, param.gravity_stable.value());

        if (param.constraint_adjustment_stable)
            xdg_positioner_set_constraint_adjustment(positioner, param.constraint_adjustment_stable.value());

        if (param.offset)
            xdg_positioner_set_offset(positioner, param.offset.value().first, param.offset.value().second);

        popup_xdg_surface.emplace(*client, popup_surface.value());
        popup.emplace(popup_xdg_surface.value(), std::nullopt, positioner);
        zwlr_layer_surface_v1_get_popup(layer_surface, popup.value());
        if (param.grab)
        {
            if (!client->latest_serial())
            {
                BOOST_THROW_EXCEPTION(std::runtime_error("client does not have a serial"));
            }
            xdg_popup_grab(popup.value().popup, client->seat(), client->latest_serial().value());
        }

        ON_CALL(popup_xdg_surface.value(), configure).WillByDefault([&](uint32_t serial)
            {
                xdg_surface_ack_configure(popup_xdg_surface.value(), serial);
                popup_surface_configure_count++;
            });
        ON_CALL(popup.value(), configure).WillByDefault([this](auto... args)
            {
                state = State{args...};
            });
        ON_CALL(popup.value(), done()).WillByDefault([this](){ popup_done(); });
    }

    void clear_popup() override
    {
        popup = std::nullopt;
        popup_xdg_surface = std::nullopt;
    }

    auto create_child_popup() -> std::unique_ptr<XdgPopupManagerBase> override
    {
        return std::make_unique<XdgPopupStableManager>(the_server, client, &popup_xdg_surface.value());
    }

    wlcs::LayerSurfaceV1 layer_surface;

    std::optional<wlcs::XdgSurfaceStable> popup_xdg_surface;
    std::optional<wlcs::XdgPopupStable> popup;

    int popup_surface_configure_count{0};
};

auto surface_actually_at_location(
    wlcs::Server& server,
    wlcs::Client& client,
    wl_surface* surface,
    std::pair<int, int> location) -> bool
{
    auto pointer = server.create_pointer();
    int offset_x = 1, offset_y = 1;
    if (location.first < 0)
    {
        offset_x -= location.first;
    }
    if (location.second < 0)
    {
        offset_y -= location.second;
    }
    pointer.move_to(location.first + offset_x, location.second + offset_y);
    client.roundtrip();
    return (
        client.window_under_cursor() == surface &&
        client.pointer_position() == std::make_pair(wl_fixed_from_int(offset_x), wl_fixed_from_int(offset_y)));
}

}

class XdgPopupPositionerTest:
    public wlcs::StartedInProcessServer,
    public testing::WithParamInterface<PositionerTestParams>
{
};

TEST_P(XdgPopupPositionerTest, xdg_shell_stable_popup_placed_correctly)
{
    auto manager = std::make_unique<XdgPopupStableManager>(the_server());
    auto const& param = GetParam();

    if (param.parent_position_func)
    {
        manager->set_parent_position(param.parent_position_func.value());
    }
    manager->map_popup(param.positioner);

    ASSERT_THAT(
        manager->state,
        Ne(std::nullopt)) << "popup configure event not sent";

    EXPECT_THAT(
        std::make_pair(manager->state.value().x, manager->state.value().y),
        Eq(param.expected_positon)) << "popup placed in incorrect position";

    EXPECT_THAT(
        std::make_pair(manager->state.value().width, manager->state.value().height),
        Eq(param.expected_size)) << "popup has incorrect size";

    EXPECT_THAT(
        surface_actually_at_location(
            the_server(),
            *manager->client,
            manager->popup_surface.value(),
            std::make_pair(
                manager->parent_position().first + param.expected_positon.first,
                manager->parent_position().second + param.expected_positon.second)),
        IsTrue());
}

TEST_P(XdgPopupPositionerTest, xdg_shell_unstable_v6_popup_placed_correctly)
{
    auto manager = std::make_unique<XdgPopupV6Manager>(the_server());
    auto const& param = GetParam();

    if (param.parent_position_func)
    {
        manager->set_parent_position(param.parent_position_func.value());
    }
    manager->map_popup(param.positioner);

    ASSERT_THAT(
        manager->state,
        Ne(std::nullopt)) << "popup configure event not sent";

    EXPECT_THAT(
        std::make_pair(manager->state.value().x, manager->state.value().y),
        Eq(param.expected_positon)) << "popup placed in incorrect position";

    EXPECT_THAT(
        std::make_pair(manager->state.value().width, manager->state.value().height),
        Eq(param.expected_size)) << "popup has incorrect size";

    EXPECT_THAT(
        surface_actually_at_location(
            the_server(),
            *manager->client,
            manager->popup_surface.value(),
            std::make_pair(
                manager->parent_position().first + param.expected_positon.first,
                manager->parent_position().second + param.expected_positon.second)),
        IsTrue());
}

TEST_P(XdgPopupPositionerTest, layer_shell_popup_placed_correctly)
{
    auto manager = std::make_unique<LayerV1PopupManager>(the_server());
    auto const& param = GetParam();

    if (param.parent_position_func)
    {
        manager->set_parent_position(param.parent_position_func.value());
    }
    manager->map_popup(param.positioner);

    ASSERT_THAT(
        manager->state,
        Ne(std::nullopt)) << "popup configure event not sent";


    EXPECT_THAT(
        std::make_pair(manager->state.value().x, manager->state.value().y),
        Eq(param.expected_positon)) << "popup placed in incorrect position";

    EXPECT_THAT(
        std::make_pair(manager->state.value().width, manager->state.value().height),
        Eq(param.expected_size)) << "popup has incorrect size";

    EXPECT_THAT(
        surface_actually_at_location(
            the_server(),
            *manager->client,
            manager->popup_surface.value(),
            std::make_pair(
                manager->parent_position().first + param.expected_positon.first,
                manager->parent_position().second + param.expected_positon.second)),
        IsTrue());
}

INSTANTIATE_TEST_SUITE_P(
    Default,
    XdgPopupPositionerTest,
    testing::Values(
        PositionerTestParams{"default values", (window_width - popup_width) / 2, (window_height - popup_height) / 2, PositionerParams()}
    ));

INSTANTIATE_TEST_SUITE_P(
    Anchor,
    XdgPopupPositionerTest,
    testing::Values(
        PositionerTestParams{"anchor left", -popup_width / 2, (window_height - popup_height) / 2,
            PositionerParams().with_anchor(XDG_POSITIONER_ANCHOR_LEFT)},

        PositionerTestParams{"anchor right", window_width - popup_width / 2, (window_height - popup_height) / 2,
            PositionerParams().with_anchor(XDG_POSITIONER_ANCHOR_RIGHT)},

        PositionerTestParams{"anchor top", (window_width - popup_width) / 2, -popup_height / 2,
            PositionerParams().with_anchor(XDG_POSITIONER_ANCHOR_TOP)},

        PositionerTestParams{"anchor bottom", (window_width - popup_width) / 2, window_height - popup_height / 2,
            PositionerParams().with_anchor(XDG_POSITIONER_ANCHOR_BOTTOM)},

        PositionerTestParams{"anchor top left", -popup_width / 2, -popup_height / 2,
            PositionerParams().with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT)},

        PositionerTestParams{"anchor top right", window_width - popup_width / 2, -popup_height / 2,
            PositionerParams().with_anchor(XDG_POSITIONER_ANCHOR_TOP_RIGHT)},

        PositionerTestParams{"anchor bottom left", -popup_width / 2, window_height - popup_height / 2,
            PositionerParams().with_anchor(XDG_POSITIONER_ANCHOR_BOTTOM_LEFT)},

        PositionerTestParams{"anchor bottom right", window_width - popup_width / 2, window_height - popup_height / 2,
            PositionerParams().with_anchor(XDG_POSITIONER_ANCHOR_BOTTOM_RIGHT)}
    ));

INSTANTIATE_TEST_SUITE_P(
    Gravity,
    XdgPopupPositionerTest,
    testing::Values(
        PositionerTestParams{"gravity none", (window_width - popup_width) / 2, (window_height - popup_height) / 2,
            PositionerParams().with_gravity(XDG_POSITIONER_GRAVITY_NONE)},

        PositionerTestParams{"gravity left", window_width / 2 - popup_width, (window_height - popup_height) / 2,
            PositionerParams().with_gravity(XDG_POSITIONER_GRAVITY_LEFT)},

        PositionerTestParams{"gravity right", window_width / 2, (window_height - popup_height) / 2,
            PositionerParams().with_gravity(XDG_POSITIONER_GRAVITY_RIGHT)},

        PositionerTestParams{"gravity top", (window_width - popup_width) / 2, window_height / 2 - popup_height,
            PositionerParams().with_gravity(XDG_POSITIONER_GRAVITY_TOP)},

        PositionerTestParams{"gravity bottom", (window_width - popup_width) / 2, window_height / 2,
            PositionerParams().with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM)},

        PositionerTestParams{"gravity top left", window_width / 2 - popup_width, window_height / 2 - popup_height,
            PositionerParams().with_gravity(XDG_POSITIONER_GRAVITY_TOP_LEFT)},

        PositionerTestParams{"gravity top right", window_width / 2, window_height / 2 - popup_height,
            PositionerParams().with_gravity(XDG_POSITIONER_GRAVITY_TOP_RIGHT)},

        PositionerTestParams{"gravity bottom left", window_width / 2 - popup_width, window_height / 2,
            PositionerParams().with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM_LEFT)},

        PositionerTestParams{"gravity bottom right", window_width / 2, window_height / 2,
            PositionerParams().with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT)}
    ));

INSTANTIATE_TEST_SUITE_P(
    AnchorRect,
    XdgPopupPositionerTest,
    testing::Values(
        PositionerTestParams{"explicit defaultPositionerParams anchor rect", (window_width - popup_width) / 2, (window_height - popup_height) / 2,
            PositionerParams().with_anchor_rect(0, 0, window_width, window_height)},

        PositionerTestParams{"upper left anchor rect", (window_width - 40 - popup_width) / 2, (window_height - 30 - popup_height) / 2,
            PositionerParams().with_anchor_rect(0, 0, window_width - 40, window_height - 30)},

        PositionerTestParams{"upper right anchor rect", (window_width + 40 - popup_width) / 2, (window_height - 30 - popup_height) / 2,
            PositionerParams().with_anchor_rect(40, 0, window_width - 40, window_height - 30)},

        PositionerTestParams{"lower left anchor rect", (window_width - 40 - popup_width) / 2, (window_height + 30 - popup_height) / 2,
            PositionerParams().with_anchor_rect(0, 30, window_width - 40, window_height - 30)},

        PositionerTestParams{"lower right anchor rect", (window_width + 40 - popup_width) / 2, (window_height + 30 - popup_height) / 2,
            PositionerParams().with_anchor_rect(40, 30, window_width - 40, window_height - 30)},

        PositionerTestParams{"offset anchor rect", (window_width - 40 - popup_width) / 2, (window_height - 80 - popup_height) / 2,
            PositionerParams().with_anchor_rect(20, 20, window_width - 80, window_height - 120)}
    ));

INSTANTIATE_TEST_SUITE_P(
    ConstraintAdjustmentNone,
    XdgPopupPositionerTest,
    testing::Values(
        PositionerTestParams{"middle of screen",
            -popup_width, -popup_height,
            popup_width, popup_height,
            PositionerParams()
                .with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT)
                .with_gravity(XDG_POSITIONER_GRAVITY_TOP_LEFT)
                .with_constraint_adjustment(XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_NONE),
            [](int width, int height){ return std::make_pair((width - window_width) / 2, (height - window_height) / 2); }},
        PositionerTestParams{"off top left edge",
            -popup_width, -popup_height,
            popup_width, popup_height,
            PositionerParams()
                .with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT)
                .with_gravity(XDG_POSITIONER_GRAVITY_TOP_LEFT)
                .with_constraint_adjustment(XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_NONE),
            [](int /*width*/, int /*height*/){ return std::make_pair(5, 5); }},
        PositionerTestParams{"off top right edge",
            window_width, -popup_height,
            popup_width, popup_height,
            PositionerParams()
                .with_anchor(XDG_POSITIONER_ANCHOR_TOP_RIGHT)
                .with_gravity(XDG_POSITIONER_GRAVITY_TOP_RIGHT)
                .with_constraint_adjustment(XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_NONE),
            [](int width, int /*height*/){ return std::make_pair(width - window_width - 5, 5); }},
        PositionerTestParams{"off bottom left edge",
            -popup_width, window_height,
            popup_width, popup_height,
            PositionerParams()
                .with_anchor(XDG_POSITIONER_ANCHOR_BOTTOM_LEFT)
                .with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM_LEFT)
                .with_constraint_adjustment(XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_NONE),
            [](int /*width*/, int height){ return std::make_pair(5, height - window_height - 5); }},
        PositionerTestParams{"off bottom right edge",
            window_width, window_height,
            popup_width, popup_height,
            PositionerParams()
                .with_anchor(XDG_POSITIONER_ANCHOR_BOTTOM_RIGHT)
                .with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT)
                .with_constraint_adjustment(XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_NONE),
            [](int width, int height){ return std::make_pair(width - window_width - 5, height - window_height - 5); }}
    ));

INSTANTIATE_TEST_SUITE_P(
    ConstraintAdjustmentSlide,
    XdgPopupPositionerTest,
    testing::Values(
        PositionerTestParams{"middle of screen",
            -popup_width, -popup_height,
            popup_width, popup_height,
            PositionerParams()
                .with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT)
                .with_gravity(XDG_POSITIONER_GRAVITY_TOP_LEFT)
                .with_constraint_adjustment(
                    XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y),
            [](int width, int height){ return std::make_pair((width - window_width) / 2, (height - window_height) / 2); }},
        PositionerTestParams{"off top left edge",
            -5, -5,
            popup_width, popup_height,
            PositionerParams()
                .with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT)
                .with_gravity(XDG_POSITIONER_GRAVITY_TOP_LEFT)
                .with_constraint_adjustment(
                    XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y),
            [](int /*width*/, int /*height*/){ return std::make_pair(5, 5); }},
        PositionerTestParams{"off top right edge",
            window_width - popup_width + 5, -5,
            popup_width, popup_height,
            PositionerParams()
                .with_anchor(XDG_POSITIONER_ANCHOR_TOP_RIGHT)
                .with_gravity(XDG_POSITIONER_GRAVITY_TOP_RIGHT)
                .with_constraint_adjustment(
                    XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y),
            [](int width, int /*height*/){ return std::make_pair(width - window_width - 5, 5); }},
        PositionerTestParams{"off bottom left edge", -5,
            window_height - popup_height + 5,
            popup_width, popup_height,
            PositionerParams()
                .with_anchor(XDG_POSITIONER_ANCHOR_BOTTOM_LEFT)
                .with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM_LEFT)
                .with_constraint_adjustment(
                    XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y),
            [](int /*width*/, int height){ return std::make_pair(5, height - window_height - 5); }},
        PositionerTestParams{"off bottom right edge",
            window_width - popup_width + 5, window_height - popup_height + 5,
            popup_width, popup_height,
            PositionerParams()
                .with_anchor(XDG_POSITIONER_ANCHOR_BOTTOM_RIGHT)
                .with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT)
                .with_constraint_adjustment(
                    XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y),
            [](int width, int height){ return std::make_pair(width - window_width - 5, height - window_height - 5); }}
    ));

INSTANTIATE_TEST_SUITE_P(
    ConstraintAdjustmentFlip,
    XdgPopupPositionerTest,
    testing::Values(
        PositionerTestParams{"middle of screen",
            -popup_width, -popup_height,
            popup_width, popup_height,
            PositionerParams()
                .with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT)
                .with_gravity(XDG_POSITIONER_GRAVITY_TOP_LEFT)
                .with_constraint_adjustment(
                    XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_Y),
            [](int width, int height){ return std::make_pair((width - window_width) / 2, (height - window_height) / 2); }},
        PositionerTestParams{"off top left edge",
            window_width, window_height,
            popup_width, popup_height,
            PositionerParams()
                .with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT)
                .with_gravity(XDG_POSITIONER_GRAVITY_TOP_LEFT)
                .with_constraint_adjustment(
                    XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_Y),
            [](int /*width*/, int /*height*/){ return std::make_pair(5, 5); }},
        PositionerTestParams{"off top right edge",
            -popup_width, window_height,
            popup_width, popup_height,
            PositionerParams()
                .with_anchor(XDG_POSITIONER_ANCHOR_TOP_RIGHT)
                .with_gravity(XDG_POSITIONER_GRAVITY_TOP_RIGHT)
                .with_constraint_adjustment(
                    XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_Y),
            [](int width, int /*height*/){ return std::make_pair(width - window_width - 5, 5); }},
        PositionerTestParams{"off bottom left edge",
            window_width, -popup_height,
            popup_width, popup_height,
            PositionerParams()
                .with_anchor(XDG_POSITIONER_ANCHOR_BOTTOM_LEFT)
                .with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM_LEFT)
                .with_constraint_adjustment(
                    XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_Y),
            [](int /*width*/, int height){ return std::make_pair(5, height - window_height - 5); }},
        PositionerTestParams{"off bottom right edge",
            -popup_width, -popup_height,
            popup_width, popup_height,
            PositionerParams()
                .with_anchor(XDG_POSITIONER_ANCHOR_BOTTOM_RIGHT)
                .with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT)
                .with_constraint_adjustment(
                    XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_Y),
            [](int width, int height){ return std::make_pair(width - window_width - 5, height - window_height - 5); }}
    ));

INSTANTIATE_TEST_SUITE_P(
    ConstraintAdjustmentResize,
    XdgPopupPositionerTest,
    testing::Values(
        PositionerTestParams{"middle of screen",
            -popup_width, -popup_height,
            popup_width, popup_height,
            PositionerParams()
                .with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT)
                .with_gravity(XDG_POSITIONER_GRAVITY_TOP_LEFT)
                .with_constraint_adjustment(
                    XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_Y),
            [](int width, int height){ return std::make_pair((width - window_width) / 2, (height - window_height) / 2); }},
        PositionerTestParams{"off top left edge",
            -5, -5,
            5, 5,
            PositionerParams()
                .with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT)
                .with_gravity(XDG_POSITIONER_GRAVITY_TOP_LEFT)
                .with_constraint_adjustment(
                    XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_Y),
            [](int /*width*/, int /*height*/){ return std::make_pair(5, 5); }},
        PositionerTestParams{"off top right edge",
            window_width, -5,
            5, 5,
            PositionerParams()
                .with_anchor(XDG_POSITIONER_ANCHOR_TOP_RIGHT)
                .with_gravity(XDG_POSITIONER_GRAVITY_TOP_RIGHT)
                .with_constraint_adjustment(
                    XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_Y),
            [](int width, int /*height*/){ return std::make_pair(width - window_width - 5, 5); }},
        PositionerTestParams{"off bottom left edge",
            -5, window_height,
            5, 5,
            PositionerParams()
                .with_anchor(XDG_POSITIONER_ANCHOR_BOTTOM_LEFT)
                .with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM_LEFT)
                .with_constraint_adjustment(
                    XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_Y),
            [](int /*width*/, int height){ return std::make_pair(5, height - window_height - 5); }},
        PositionerTestParams{"off bottom right edge",
            window_width, window_height,
            5, 5,
            PositionerParams()
                .with_anchor(XDG_POSITIONER_ANCHOR_BOTTOM_RIGHT)
                .with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT)
                .with_constraint_adjustment(
                    XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_Y),
            [](int width, int height){ return std::make_pair(width - window_width - 5, height - window_height - 5); }}
    ));

struct XdgPopupTestParam
{
    std::function<std::unique_ptr<XdgPopupManagerBase>(wlcs::InProcessServer* const)> build;
};

std::ostream& operator<<(std::ostream& out, XdgPopupTestParam const&) { return out; }

class XdgPopupTest:
    public wlcs::StartedInProcessServer,
    public testing::WithParamInterface<XdgPopupTestParam>
{
};

TEST_P(XdgPopupTest, pointer_focus_goes_to_popup)
{
    auto const& param = GetParam();
    auto manager = param.build(this);
    auto pointer = the_server().create_pointer();
    pointer.move_to(manager->window_x + 1, manager->window_y + 1);
    manager->client->roundtrip();

    EXPECT_THAT(manager->client->window_under_cursor(), Eq((wl_surface*)manager->surface));

    auto positioner = PositionerParams{}
        .with_size(30, 30)
        .with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT)
        .with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT);
    manager->map_popup(positioner);
    manager->client->roundtrip();

    pointer.move_to(manager->window_x + 2, manager->window_y + 1);
    manager->client->roundtrip();

    EXPECT_THAT(manager->client->window_under_cursor(), Eq((wl_surface*)manager->popup_surface.value()));
}

TEST_P(XdgPopupTest, popup_gives_up_pointer_focus_when_gone)
{
    auto const& param = GetParam();
    auto manager = param.build(this);
    auto pointer = the_server().create_pointer();

    auto positioner = PositionerParams{}
        .with_size(30, 30)
        .with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT)
        .with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT);
    manager->map_popup(positioner);
    manager->client->roundtrip();

    pointer.move_to(manager->window_x + 2, manager->window_y + 1);
    manager->client->roundtrip();

    EXPECT_THAT(manager->client->window_under_cursor(), Eq((wl_surface*)manager->popup_surface.value()));

    manager->unmap_popup();
    manager->client->roundtrip();
    pointer.move_to(manager->window_x + 3, manager->window_y + 1);
    manager->client->roundtrip();

    EXPECT_THAT(manager->client->window_under_cursor(), Eq((wl_surface*)manager->surface));
}

TEST_P(XdgPopupTest, grabbed_popup_gets_done_event_when_new_toplevel_created)
{
    auto const& param = GetParam();
    auto manager = param.build(this);
    auto pointer = the_server().create_pointer();

    // This is needed to get a serial, which will be used later on
    pointer.move_to(manager->window_x + 2, manager->window_y + 2);
    pointer.left_click();
    manager->client->roundtrip();

    auto positioner = PositionerParams{}
        .with_size(30, 30)
        .with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT)
        .with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT)
        .with_grab();
    manager->map_popup(positioner);
    manager->client->roundtrip();

    EXPECT_CALL(*manager, popup_done());

    manager->client->create_visible_surface(window_width, window_height);
}

TEST_P(XdgPopupTest, grabbed_popups_get_done_events_in_correct_order)
{
    auto const& param = GetParam();

    wlcs::Client background_client{the_server()};
    wlcs::Surface background_surface{background_client.create_visible_surface(10, 10)};
    the_server().move_surface_to(
        background_surface,
        XdgPopupManagerBase::window_x - 5,
        XdgPopupManagerBase::window_y - 5);

    auto top_popup_manager = param.build(this);
    auto pointer = the_server().create_pointer();

    // This is needed to get a serial, which will be used later on
    pointer.move_to(top_popup_manager->window_x + 2, top_popup_manager->window_y + 2);
    pointer.left_click();
    top_popup_manager->client->roundtrip();

    auto positioner = PositionerParams{}
        .with_anchor_rect(20, 20, 1, 1)
        .with_size(30, 30)
        .with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT)
        .with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT)
        .with_grab();
    top_popup_manager->map_popup(positioner);
    top_popup_manager->client->roundtrip();
    auto sub_popup_manager = top_popup_manager->create_child_popup();
    sub_popup_manager->map_popup(positioner);
    top_popup_manager->client->roundtrip();

    InSequence seq;
    EXPECT_CALL(*sub_popup_manager, popup_done());
    EXPECT_CALL(*top_popup_manager, popup_done());

    // Click on background surface so it is focused and grabbed popups are dismissed
    pointer.move_to(top_popup_manager->window_x - 2, top_popup_manager->window_y - 2);
    pointer.left_click();
    top_popup_manager->client->roundtrip();
}

TEST_P(XdgPopupTest, grabbed_popup_gets_keyboard_focus)
{
    auto const& param = GetParam();
    auto manager = param.build(this);
    auto pointer = the_server().create_pointer();

    // This is needed to get a serial, which will be used later on
    pointer.move_to(manager->window_x + 2, manager->window_y + 2);
    pointer.left_click();
    manager->client->roundtrip();

    auto positioner = PositionerParams{}
        .with_size(30, 30)
        .with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT)
        .with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT)
        .with_grab();
    manager->map_popup(positioner);
    manager->client->roundtrip();

    EXPECT_THAT(
        manager->client->keyboard_focused_window(),
        Eq(static_cast<wl_surface*>(manager->popup_surface.value())))
        << "grabbed popup not given keyboard focus";
}

TEST_P(XdgPopupTest, non_grabbed_popup_does_not_get_keyboard_focus)
{
    auto const& param = GetParam();
    auto manager = param.build(this);

    auto positioner = PositionerParams{}
        .with_size(30, 30)
        .with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT)
        .with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT);
    manager->map_popup(positioner);
    manager->client->roundtrip();

    EXPECT_THAT(
        manager->client->keyboard_focused_window(),
        Ne(static_cast<wl_surface*>(manager->popup_surface.value())))
        << "popup given keyboard focus";
    EXPECT_THAT(
        manager->client->keyboard_focused_window(),
        Eq(static_cast<wl_surface*>(manager->surface)));
}

TEST_P(XdgPopupTest, does_not_get_popup_done_event_before_button_press)
{
    auto const& param = GetParam();
    auto manager = param.build(this);
    auto pointer = the_server().create_pointer();

    // This is needed to get a serial, which will be used later on
    pointer.move_to(manager->window_x + 2, manager->window_y + 2);
    pointer.left_click();
    manager->client->roundtrip();

    auto positioner = PositionerParams{}
        .with_size(30, 30)
        .with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT)
        .with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT)
        .with_grab();
    manager->map_popup(positioner);
    manager->client->roundtrip();

    // This may or may not be sent, but a button press should not come in after it if it is sent
    bool got_popup_done = false;
    EXPECT_CALL(*manager, popup_done()).Times(AnyNumber()).WillRepeatedly([&]()
        {
            got_popup_done = true;
        });

    manager->client->add_pointer_button_notification([&](auto, auto, auto)
        {
            EXPECT_THAT(got_popup_done, IsFalse()) << "pointer button sent after popup done";
            return true;
        });

    pointer.move_to(manager->window_x + 32, manager->window_y + 32);
    pointer.left_click();
    manager->client->roundtrip();
}

TEST_F(XdgPopupTest, zero_size_anchor_rect_stable)
{
    auto manager = std::make_unique<XdgPopupStableManager>(the_server());

    auto positioner = PositionerParams{}
        .with_anchor_rect(window_width / 2, window_height / 2, 0, 0);

    manager->map_popup(positioner);
    manager->client->roundtrip();

    ASSERT_THAT(
        std::make_pair(manager->state.value().x, manager->state.value().y),
        Eq(std::make_pair(
            (window_width - popup_width) / 2,
            (window_height - popup_height) / 2))) << "popup placed in incorrect position";
}

// regression test for https://github.com/MirServer/mir/issues/836
TEST_P(XdgPopupTest, popup_configure_is_valid)
{
    auto const& param = GetParam();
    auto manager = param.build(this);

    auto positioner = PositionerParams{}
        .with_size(30, 30)
        .with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT)
        .with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT);
    manager->map_popup(positioner);
    manager->client->roundtrip();

    ASSERT_THAT(manager->state, Ne(std::nullopt));
    EXPECT_THAT(manager->state.value().width, Gt(0));
    EXPECT_THAT(manager->state.value().height, Gt(0));
}

TEST_F(XdgPopupTest, when_parent_surface_is_moved_a_reactive_popup_is_moved)
{
    XdgPopupStableManager manager{the_server()};

    manager.client->bind_if_supported<xdg_wm_base>(wlcs::AtLeastVersion{XDG_POSITIONER_SET_REACTIVE_SINCE_VERSION});

    auto positioner = PositionerParams{}
        .with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT)
        .with_gravity(XDG_POSITIONER_GRAVITY_TOP_LEFT)
        .with_constraint_adjustment(
            XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y)
        .with_reactive();

    manager.map_popup(positioner);
    // repositioned should not be called unless reposition is called
    EXPECT_CALL(manager.popup.value(), repositioned).Times(0);
    manager.client->roundtrip();

    ASSERT_THAT(
        manager.state,
        Ne(std::nullopt)) << "popup configure event not sent";

    ASSERT_THAT(
        std::make_pair(manager.state.value().x, manager.state.value().y),
        Eq(std::make_pair(-popup_width, -popup_height))) << "popup initially placed in incorrect position";

    manager.move_parent_using_pointer({5, 5});

    ASSERT_THAT(
        std::make_pair(manager.state.value().x, manager.state.value().y),
        Ne(std::make_pair(-popup_width, -popup_height))) << "reactive popup was not moved";

    EXPECT_THAT(
        std::make_pair(manager.state.value().x, manager.state.value().y),
        Eq(std::make_pair(-5, -5))) << "reactive popup placed in incorrect position";

    EXPECT_THAT(
        surface_actually_at_location(
            the_server(),
            *manager.client,
            manager.popup_surface.value(),
            std::make_pair(0, 0)),
        IsTrue());
}

TEST_F(XdgPopupTest, when_parent_surface_is_moved_a_nonreactive_popup_is_not_moved)
{
    XdgPopupStableManager manager{the_server()};

    manager.client->bind_if_supported<xdg_wm_base>(wlcs::AtLeastVersion{XDG_POSITIONER_SET_REACTIVE_SINCE_VERSION});

    auto positioner = PositionerParams{}
        .with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT)
        .with_gravity(XDG_POSITIONER_GRAVITY_TOP_LEFT)
        .with_constraint_adjustment(
            XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y);

    manager.map_popup(positioner);
    // repositioned should not be called unless reposition is called
    EXPECT_CALL(manager.popup.value(), repositioned).Times(0);
    manager.client->roundtrip();

    ASSERT_THAT(
        manager.state,
        Ne(std::nullopt)) << "popup configure event not sent";

    ASSERT_THAT(
        std::make_pair(manager.state.value().x, manager.state.value().y),
        Eq(std::make_pair(-popup_width, -popup_height))) << "popup initially placed in incorrect position";

    manager.move_parent_using_pointer({5, 5});

    EXPECT_THAT(
        std::make_pair(manager.state.value().x, manager.state.value().y),
        Eq(std::make_pair(-popup_width, -popup_height))) << "nonreactive popup was moved";

    EXPECT_THAT(
        surface_actually_at_location(
            the_server(),
            *manager.client,
            manager.popup_surface.value(),
            std::make_pair(5 - popup_width, 5 - popup_height)),
        IsTrue());
}

TEST_F(XdgPopupTest, popup_can_be_repositioned)
{
    XdgPopupStableManager manager{the_server()};

    manager.client->bind_if_supported<xdg_wm_base>(wlcs::AtLeastVersion{XDG_POPUP_REPOSITION_SINCE_VERSION});

    auto positioner_a = PositionerParams{}
        .with_anchor(XDG_POSITIONER_ANCHOR_TOP_LEFT)
        .with_gravity(XDG_POSITIONER_GRAVITY_TOP_LEFT);

    manager.map_popup(positioner_a);
    EXPECT_CALL(manager.popup.value(), repositioned).Times(0);
    manager.client->roundtrip();

    auto positioner_b = PositionerParams{}
        .with_anchor(XDG_POSITIONER_ANCHOR_BOTTOM_RIGHT)
        .with_gravity(XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT);
    wlcs::XdgPositionerStable xdg_positioner{*manager.client};
    XdgPopupStableManager::setup_positioner(xdg_positioner, positioner_b);
    xdg_popup_reposition(manager.popup.value(), xdg_positioner, 1);
    EXPECT_CALL(manager.popup.value(), repositioned(1));
    manager.client->roundtrip();

    ASSERT_THAT(
        manager.state,
        Ne(std::nullopt)) << "popup configure event not sent";

    EXPECT_THAT(
        std::make_pair(manager.state.value().x, manager.state.value().y),
        Eq(std::make_pair(window_width, window_height)));

    EXPECT_THAT(
        surface_actually_at_location(
            the_server(),
            *manager.client,
            manager.popup_surface.value(),
            std::make_pair(
                manager.parent_position().first + window_width,
                manager.parent_position().second + window_height)),
        IsTrue());
}

INSTANTIATE_TEST_SUITE_P(
    XdgPopupStable,
    XdgPopupTest,
    testing::Values(XdgPopupTestParam{
        [](wlcs::InProcessServer* const server)
        {
            return std::make_unique<XdgPopupStableManager>(server->the_server());
        }}));

INSTANTIATE_TEST_SUITE_P(
    XdgPopupUnstableV6,
    XdgPopupTest,
    testing::Values(XdgPopupTestParam{
        [](wlcs::InProcessServer* const server)
        {
            return std::make_unique<XdgPopupV6Manager>(server->the_server());
        }}));

INSTANTIATE_TEST_SUITE_P(
    LayerShellPopup,
    XdgPopupTest,
    testing::Values(XdgPopupTestParam{
        [](wlcs::InProcessServer* const server)
        {
            return std::make_unique<LayerV1PopupManager>(server->the_server());
        }}));

// TODO: test that positioner is always overlapping or adjacent to parent
// TODO: test that positioner is copied immediately after use
// TODO: test that error is raised when incomplete positioner is used (positioner without size and anchor rect set)
// TODO: test set_size
// TODO: test that set_window_geometry affects anchor rect
// TODO: test set_offset
// TODO: test that a zero size anchor rect fails on v6
