/*
 * Copyright © 2018 Canonical Ltd.
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 3,
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Authored by: William Wold <william.wold@canonical.com>
 */

#include "helpers.h"
#include "in_process_server.h"
#include "xdg_shell_v6.h"

#include <gmock/gmock.h>

#include <vector>

using namespace testing;

struct AbstractInputDevice
{
    virtual void to_screen_position(int x, int y) = 0;
    virtual wl_surface* focused_window() = 0;
    virtual std::pair<int, int> position_on_window() = 0;
    virtual ~AbstractInputDevice() = default;
};

struct PointerInputDevice : AbstractInputDevice
{
    PointerInputDevice(wlcs::Server& server, wlcs::Client& client):
        client{client},
        pointer{server.create_pointer()}
    {
    }

    void to_screen_position(int x, int y) override
    {
        pointer.move_to(0, 0);
        pointer.move_to(x, y);
    }

    wl_surface* focused_window() override
    {
        return client.window_under_cursor();
    }

    std::pair<wl_fixed_t, wl_fixed_t> position_on_window() override
    {
        return client.pointer_position();
    }

    wlcs::Client& client;
    wlcs::Pointer pointer;
};

struct TouchInputDevice : AbstractInputDevice
{
    TouchInputDevice(wlcs::Server& server, wlcs::Client& client):
        client{client},
        touch{server.create_touch()}
    {
    }

    void to_screen_position(int x, int y) override
    {
        touch.up();
        touch.down_at(x, y);
    }

    wl_surface* focused_window() override
    {
        return client.touched_window();
    }

    std::pair<wl_fixed_t, wl_fixed_t> position_on_window() override
    {
        return client.touch_position();
    }

    wlcs::Client& client;
    wlcs::Touch touch;
};

struct SubsurfaceTestParams
{
    std::string name;
    std::function<std::unique_ptr<wlcs::Surface>(wlcs::InProcessServer& server,
                                                 wlcs::Client& client,
                                                 int x, int y,
                                                 int width, int height)> make_surface;
    std::function<std::unique_ptr<AbstractInputDevice>(wlcs::Server& server,
                                                       wlcs::Client& client)> make_input_device;
};

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

class SubsurfaceTest :
    public wlcs::StartedInProcessServer,
    public testing::WithParamInterface<SubsurfaceTestParams>
{
public:
    static int const surface_width = 200, surface_height = 300;
    static int const subsurface_width = 50, subsurface_height = 50;
    static int const surface_x = 20, surface_y = 30;

    SubsurfaceTest():
        client{the_server()},
        main_surface{std::move(*GetParam().make_surface(
            *this, client, surface_x, surface_y, surface_width, surface_height))},
        subsurface{wlcs::Subsurface::create_visible(main_surface, 0, 0, subsurface_width, subsurface_height)},
        input_device{GetParam().make_input_device(the_server(), client)}
    {
        client.roundtrip();
    }

    void move_subsurface_to(int x, int y)
    {
        wl_subsurface_set_position(subsurface, x, y);
        wl_surface_commit(main_surface);
        client.roundtrip();
    }

    wlcs::Client client{the_server()};
    wlcs::Surface main_surface;
    wlcs::Subsurface subsurface;
    std::unique_ptr<AbstractInputDevice> input_device;
};

class SubsurfaceMultilevelTest :
    public wlcs::StartedInProcessServer,
    public testing::WithParamInterface<SubsurfaceTestParams>
{
public:
    static int const surface_width = 200, surface_height = 300;
    static int const subsurface_width = 50, subsurface_height = 50;
    static int const surface_x = 20, surface_y = 30;

    SubsurfaceMultilevelTest():
        client{the_server()},
        main_surface{std::move(*GetParam().make_surface(
            *this, client, surface_x, surface_y, surface_width, surface_height))},
        parent_subsurface{wlcs::Subsurface::create_visible(main_surface, 0, 0, subsurface_width, subsurface_height)},
        child_subsurface{wlcs::Subsurface::create_visible(parent_subsurface, 0, 0, subsurface_width, subsurface_height)},
        input_device{GetParam().make_input_device(the_server(), client)}
    {
        client.roundtrip();
    }

    wlcs::Client client{the_server()};
    wlcs::Surface main_surface;
    wlcs::Subsurface parent_subsurface;
    wlcs::Subsurface child_subsurface;
    std::unique_ptr<AbstractInputDevice> input_device;
};

TEST_P(SubsurfaceTest, subsurface_has_correct_parent)
{
    EXPECT_THAT(&subsurface.parent(), Eq(&main_surface));
}

TEST_P(SubsurfaceTest, subsurface_gets_pointer_input)
{
    int const pointer_x = surface_x + 10, pointer_y = surface_y + 5;

    input_device->to_screen_position(pointer_x, pointer_y);
    client.roundtrip();

    EXPECT_THAT(input_device->focused_window(), Ne((wl_surface*)main_surface)) << "input fell through to main surface";
    EXPECT_THAT(input_device->focused_window(), Eq((wl_surface*)subsurface));
    EXPECT_THAT(input_device->position_on_window(),
                Eq(std::make_pair(
                    wl_fixed_from_int(pointer_x - surface_x),
                    wl_fixed_from_int(pointer_y - surface_y))));
}

TEST_P(SubsurfaceTest, pointer_input_correctly_offset_for_subsurface)
{
    int const pointer_x = surface_x + 13, pointer_y = surface_y + 24;
    int const subsurface_x = 8, subsurface_y = 17;

    move_subsurface_to(subsurface_x, subsurface_y);

    input_device->to_screen_position(pointer_x, pointer_y);
    client.roundtrip();

    EXPECT_THAT(input_device->focused_window(), Ne((wl_surface*)main_surface)) << "input fell through to main surface";
    EXPECT_THAT(input_device->focused_window(), Eq((wl_surface*)subsurface));
    EXPECT_THAT(input_device->position_on_window(),
                Eq(std::make_pair(
                    wl_fixed_from_int(pointer_x - surface_x - subsurface_x),
                    wl_fixed_from_int(pointer_y - surface_y - subsurface_y))));
}

TEST_P(SubsurfaceTest, sync_subsurface_moves_when_only_parent_committed)
{
    int const pointer_x = 30, pointer_y = 30;
    int const subsurface_x = 20, subsurface_y = 20;

    wl_subsurface_set_position(subsurface, subsurface_x, subsurface_y);
    // Position is applied when parent (main_surface) commits, so subsurface does not need to be committed
    wl_surface_commit(main_surface);
    client.roundtrip();

    input_device->to_screen_position(pointer_x + surface_x, pointer_y + surface_y);
    client.roundtrip();

    EXPECT_THAT(input_device->position_on_window(),
                Eq(std::make_pair(
                    wl_fixed_from_int(pointer_x - subsurface_x),
                    wl_fixed_from_int(pointer_y - subsurface_y))));

    EXPECT_THAT(input_device->position_on_window(),
                Ne(std::make_pair(
                    wl_fixed_from_int(pointer_x),
                    wl_fixed_from_int(pointer_y))))
        << "Subsurface did not move after parent commit";
}

TEST_P(SubsurfaceTest, desync_subsurface_moves_when_only_parent_committed)
{
    int const pointer_x = 30, pointer_y = 30;
    int const subsurface_x = 20, subsurface_y = 20;

    wl_subsurface_set_desync(subsurface);

    wl_subsurface_set_position(subsurface, subsurface_x, subsurface_y);
    // Position is applied when parent (main_surface) commits, so subsurface does not need to be committed
    wl_surface_commit(main_surface);
    client.roundtrip();

    input_device->to_screen_position(pointer_x + surface_x, pointer_y + surface_y);
    client.roundtrip();

    EXPECT_THAT(input_device->position_on_window(),
                Eq(std::make_pair(
                    wl_fixed_from_int(pointer_x - subsurface_x),
                    wl_fixed_from_int(pointer_y - subsurface_y))));

    EXPECT_THAT(input_device->position_on_window(),
                Ne(std::make_pair(
                    wl_fixed_from_int(pointer_x),
                    wl_fixed_from_int(pointer_y))))
        << "Subsurface did not move after parent commit";
}

TEST_P(SubsurfaceTest, subsurface_does_not_move_when_parent_not_committed)
{
    int const pointer_x = 30, pointer_y = 30;
    int const subsurface_x = 20, subsurface_y = 20;

    wl_subsurface_set_desync(subsurface);

    wl_subsurface_set_position(subsurface, subsurface_x, subsurface_y);
    wl_surface_commit(subsurface);
    // We don't call wl_surface_commit(main_surface), so position should not be applied
    client.roundtrip();

    input_device->to_screen_position(pointer_x + surface_x, pointer_y + surface_y);
    client.roundtrip();

    EXPECT_THAT(input_device->position_on_window(),
                Eq(std::make_pair(
                    wl_fixed_from_int(pointer_x),
                    wl_fixed_from_int(pointer_y))));

    EXPECT_THAT(input_device->position_on_window(),
                Ne(std::make_pair(
                    wl_fixed_from_int(pointer_x - subsurface_x),
                    wl_fixed_from_int(pointer_y - subsurface_y))))
        << "Subsurface moved to new location without parent being committed";
}

TEST_P(SubsurfaceTest, subsurface_extends_parent_input_region)
{
    int const pointer_x = surface_x - 5, pointer_y = surface_y + surface_height + 8;
    int const subsurface_x = -10, subsurface_y = surface_height - 10;

    move_subsurface_to(subsurface_x, subsurface_y);

    input_device->to_screen_position(pointer_x, pointer_y);
    client.roundtrip();

    EXPECT_THAT(input_device->focused_window(), Ne((wl_surface*)main_surface)) << "input fell through to main surface";
    EXPECT_THAT(input_device->focused_window(), Eq((wl_surface*)subsurface));
    EXPECT_THAT(input_device->position_on_window(),
                Eq(std::make_pair(
                    wl_fixed_from_int(pointer_x - surface_x - subsurface_x),
                    wl_fixed_from_int(pointer_y - surface_y - subsurface_y))));
}

TEST_P(SubsurfaceTest, input_falls_through_empty_subsurface_input_region)
{
    int const pointer_x = surface_x + 10, pointer_y = surface_y + 5;

    auto const wl_region = wl_compositor_create_region(client.compositor());
    wl_surface_set_input_region(subsurface, wl_region);
    wl_region_destroy(wl_region);
    wl_surface_commit(subsurface);
    wl_surface_commit(main_surface);
    client.roundtrip();

    input_device->to_screen_position(pointer_x, pointer_y);
    client.roundtrip();

    EXPECT_THAT(input_device->focused_window(), Ne((wl_surface*)subsurface)) << "input was incorrectly caught by subsurface";
    EXPECT_THAT(input_device->focused_window(), Eq((wl_surface*)main_surface));
    EXPECT_THAT(input_device->position_on_window(),
                Eq(std::make_pair(
                    wl_fixed_from_int(pointer_x - surface_x),
                    wl_fixed_from_int(pointer_y - surface_y))));
}

TEST_P(SubsurfaceTest, gets_input_over_surface_with_empty_region)
{
    int const pointer_x = surface_x + 32, pointer_y = surface_y + 21;

    auto const wl_region = wl_compositor_create_region(client.compositor());
    wl_surface_set_input_region(main_surface, wl_region);
    wl_region_destroy(wl_region);
    wl_surface_commit(main_surface);
    client.roundtrip();

    input_device->to_screen_position(pointer_x, pointer_y);
    client.roundtrip();

    EXPECT_THAT(input_device->focused_window(), Ne((wl_surface*)main_surface)) << "input fell through to main surface";
    EXPECT_THAT(input_device->focused_window(), Eq((wl_surface*)subsurface));
    EXPECT_THAT(input_device->position_on_window(),
                Eq(std::make_pair(
                    wl_fixed_from_int(pointer_x - surface_x),
                    wl_fixed_from_int(pointer_y - surface_y))));
}

TEST_P(SubsurfaceTest, one_subsurface_to_another_fallthrough)
{
    int const pointer_x_0 = 3, pointer_y_0 = 3;
    int const pointer_x_1 = 3, pointer_y_1 = 10;
    int const pointer_x_2 = 10, pointer_y_2 = 3;
    int const subsurface_x = 0, subsurface_y = 5;
    int const subsurface_top_x = 5, subsurface_top_y = 0;
    move_subsurface_to(subsurface_x, subsurface_y);
    auto subsurface_top{wlcs::Subsurface::create_visible(main_surface, subsurface_top_x, subsurface_top_y, subsurface_width, subsurface_height)};

    input_device->to_screen_position(pointer_x_0 + surface_x, pointer_y_0 + surface_y);
    client.roundtrip();

    ASSERT_THAT(input_device->focused_window(), Eq((wl_surface*)main_surface)) << "main surface not focused";
    EXPECT_THAT(input_device->position_on_window(),
                Eq(std::make_pair(
                    wl_fixed_from_int(pointer_x_0),
                    wl_fixed_from_int(pointer_y_0))));

    input_device->to_screen_position(pointer_x_1 + surface_x, pointer_y_1 + surface_y);
    client.roundtrip();

    ASSERT_THAT(input_device->focused_window(), Eq((wl_surface*)subsurface)) << "lower subsurface not focused";
    EXPECT_THAT(input_device->position_on_window(),
                Eq(std::make_pair(
                    wl_fixed_from_int(pointer_x_1 - subsurface_x),
                    wl_fixed_from_int(pointer_y_1 - subsurface_y))));

    input_device->to_screen_position(pointer_x_2 + surface_x, pointer_y_2 + surface_y);
    client.roundtrip();

    ASSERT_THAT(input_device->focused_window(), Eq((wl_surface*)subsurface_top)) << "upper subsurface not focused";
    EXPECT_THAT(input_device->position_on_window(),
                Eq(std::make_pair(
                    wl_fixed_from_int(pointer_x_2 - subsurface_top_x),
                    wl_fixed_from_int(pointer_y_2 - subsurface_top_y))));
}

TEST_P(SubsurfaceTest, place_below_simple)
{
    auto subsurface_moving_down{wlcs::Subsurface::create_visible(main_surface, 0, 0, subsurface_width, subsurface_height)};
    wl_subsurface_place_below(subsurface_moving_down, subsurface);
    wl_surface_commit(subsurface_moving_down);
    wl_surface_commit(subsurface);
    wl_surface_commit(main_surface);

    input_device->to_screen_position(5 + surface_x, 5 + surface_y);
    client.roundtrip();

    ASSERT_THAT(input_device->focused_window(), Ne((wl_surface*)subsurface_moving_down))
        << "subsurface.place_below() did not have an effect";

    ASSERT_THAT(input_device->focused_window(), Ne((wl_surface*)subsurface))
        << "wrong surface/subsurface on top";
}

TEST_P(SubsurfaceTest, place_above_simple)
{
    auto subsurface_being_covered{wlcs::Subsurface::create_visible(main_surface, 0, 0, subsurface_width, subsurface_height)};
    wl_subsurface_place_above(subsurface, subsurface_being_covered);
    wl_surface_commit(subsurface);
    wl_surface_commit(subsurface_being_covered);
    wl_surface_commit(main_surface);

    input_device->to_screen_position(5 + surface_x, 5 + surface_y);
    client.roundtrip();

    ASSERT_THAT(input_device->focused_window(), Ne((wl_surface*)subsurface_being_covered))
        << "subsurface.place_above() did not have an effect";

    ASSERT_THAT(input_device->focused_window(), Ne((wl_surface*)subsurface))
        << "wrong surface/subsurface on top";
}

TEST_P(SubsurfaceTest, subsurface_of_a_subsurface_handled)
{
    int const pointer_x_0 = 3, pointer_y_0 = 3;
    int const pointer_x_1 = 3, pointer_y_1 = 10;
    int const pointer_x_2 = 10, pointer_y_2 = 3;
    int const subsurface_x = 0, subsurface_y = 5;
    int const subsurface_top_x = 5, subsurface_top_y = -5;
    move_subsurface_to(subsurface_x, subsurface_y);
    auto subsurface_top{wlcs::Subsurface::create_visible(subsurface, subsurface_top_x, subsurface_top_y, subsurface_width, subsurface_height)};

    input_device->to_screen_position(pointer_x_0 + surface_x, pointer_y_0 + surface_y);
    client.roundtrip();

    ASSERT_THAT(input_device->focused_window(), Eq((wl_surface*)main_surface)) << "main surface not focused";
    EXPECT_THAT(input_device->position_on_window(),
                Eq(std::make_pair(
                    wl_fixed_from_int(pointer_x_0),
                    wl_fixed_from_int(pointer_y_0))));

    input_device->to_screen_position(pointer_x_1 + surface_x, pointer_y_1 + surface_y);
    client.roundtrip();

    ASSERT_THAT(input_device->focused_window(), Eq((wl_surface*)subsurface)) << "lower subsurface not focused";
    EXPECT_THAT(input_device->position_on_window(),
                Eq(std::make_pair(
                    wl_fixed_from_int(pointer_x_1 - subsurface_x),
                    wl_fixed_from_int(pointer_y_1 - subsurface_y))));

    input_device->to_screen_position(pointer_x_2 + surface_x, pointer_y_2 + surface_y);
    client.roundtrip();

    ASSERT_THAT(input_device->focused_window(), Eq((wl_surface*)subsurface_top)) << "subsurface of subsurface not focused";
    EXPECT_THAT(input_device->position_on_window(),
                Eq(std::make_pair(
                    wl_fixed_from_int(pointer_x_2 - subsurface_top_x - subsurface_x),
                    wl_fixed_from_int(pointer_y_2 - subsurface_top_y - subsurface_y))));
}

TEST_P(SubsurfaceTest, subsurface_moves_under_input_device_once)
{
    int const input_x = surface_x + 10, input_y = surface_y + 5;
    int const subsurface_x = -23, subsurface_y = -17;

    input_device->to_screen_position(input_x, input_y);
    client.roundtrip();

    ASSERT_THAT(input_device->focused_window(), Eq((wl_surface*)subsurface)) << "precondition failed";
    ASSERT_THAT(input_device->position_on_window(),
                Eq(std::make_pair(
                    wl_fixed_from_int(input_x - surface_x),
                    wl_fixed_from_int(input_y - surface_y)))) << "precondition failed";

    wl_subsurface_set_position(subsurface, subsurface_x, subsurface_y);
    wl_surface_commit(subsurface);
    wl_surface_commit(main_surface);
    client.roundtrip();

    EXPECT_THAT(input_device->focused_window(), Eq((wl_surface*)subsurface)) << "subsurface not focuesed";
    EXPECT_THAT(input_device->position_on_window(),
                Ne(std::make_pair(
                    wl_fixed_from_int(input_x - surface_x),
                    wl_fixed_from_int(input_y - surface_y)))) << "input device did not get new location";
    EXPECT_THAT(input_device->position_on_window(),
                Eq(std::make_pair(
                    wl_fixed_from_int(input_x - surface_x - subsurface_x),
                    wl_fixed_from_int(input_y - surface_y - subsurface_y)))) << "input device in wrong location";
}

TEST_P(SubsurfaceTest, subsurface_moves_under_input_device_twice)
{
    int const input_x = surface_x + 10, input_y = surface_y + 5;
    int const subsurface_x_0 = 4, subsurface_y_0 = 2;
    int const subsurface_x_1 = -23, subsurface_y_1 = -17;

    input_device->to_screen_position(input_x, input_y);
    wl_subsurface_set_position(subsurface, subsurface_x_0, subsurface_y_0);
    wl_surface_commit(subsurface);
    wl_surface_commit(main_surface);
    client.roundtrip();

    ASSERT_THAT(input_device->focused_window(), Eq((wl_surface*)subsurface)) << "precondition failed";
    ASSERT_THAT(input_device->position_on_window(),
                Eq(std::make_pair(
                    wl_fixed_from_int(input_x - surface_x - subsurface_x_0),
                    wl_fixed_from_int(input_y - surface_y - subsurface_y_0)))) << "precondition failed";

    wl_subsurface_set_position(subsurface, subsurface_x_1, subsurface_y_1);
    wl_surface_commit(subsurface);
    wl_surface_commit(main_surface);
    client.roundtrip();

    EXPECT_THAT(input_device->focused_window(), Eq((wl_surface*)subsurface)) << "subsurface not focuesed";
    EXPECT_THAT(input_device->position_on_window(),
                Ne(std::make_pair(
                    wl_fixed_from_int(input_x - surface_x - subsurface_x_0),
                    wl_fixed_from_int(input_y - surface_y - subsurface_y_0)))) << "input device did not get new location";
    EXPECT_THAT(input_device->position_on_window(),
                Eq(std::make_pair(
                    wl_fixed_from_int(input_x - surface_x - subsurface_x_1),
                    wl_fixed_from_int(input_y - surface_y - subsurface_y_1)))) << "input device in wrong location";
}

TEST_P(SubsurfaceTest, subsurface_moves_out_from_under_input_device)
{
    int const input_x = surface_x + 10, input_y = surface_y + 5;
    int const subsurface_x = input_x - surface_x + 10, subsurface_y = input_y - surface_y + 10;

    input_device->to_screen_position(input_x, input_y);
    client.roundtrip();

    ASSERT_THAT(input_device->focused_window(), Eq((wl_surface*)subsurface)) << "precondition failed";
    ASSERT_THAT(input_device->position_on_window(),
                Eq(std::make_pair(
                    wl_fixed_from_int(input_x - surface_x),
                    wl_fixed_from_int(input_y - surface_y)))) << "precondition failed";

    wl_subsurface_set_position(subsurface, subsurface_x, subsurface_y);
    wl_surface_commit(subsurface);
    wl_surface_commit(main_surface);
    client.roundtrip();

    EXPECT_THAT(input_device->focused_window(), Eq((wl_surface*)main_surface)) << "main surface not focuesed";
    EXPECT_THAT(input_device->position_on_window(),
                Eq(std::make_pair(
                    wl_fixed_from_int(input_x - surface_x),
                    wl_fixed_from_int(input_y - surface_y)))) << "input device in wrong location";
}

INSTANTIATE_TEST_SUITE_P(
    WlShellSubsurfaces,
    SubsurfaceTest,
    testing::Values(
        SubsurfaceTestParams{
            "wl_shell_surface",
            [](wlcs::InProcessServer& server, wlcs::Client& client, int x, int y, int width, int height)
                -> std::unique_ptr<wlcs::Surface>
                {
                    auto surface = client.create_wl_shell_surface(
                        width,
                        height);
                    server.the_server().move_surface_to(surface, x, y);
                    return std::make_unique<wlcs::Surface>(std::move(surface));
                },
            [](wlcs::Server& server, wlcs::Client& client) -> std::unique_ptr<AbstractInputDevice>
                {
                    return std::make_unique<PointerInputDevice>(server, client);
                }
        }
    ));

INSTANTIATE_TEST_SUITE_P(
    XdgShellV6Subsurfaces,
    SubsurfaceTest,
    testing::Values(
        SubsurfaceTestParams{
            "xdg_v6_surface",
            [](wlcs::InProcessServer& server, wlcs::Client& client, int x, int y, int width, int height)
                -> std::unique_ptr<wlcs::Surface>
                {
                    auto surface = client.create_xdg_shell_v6_surface(
                        width,
                        height);
                    server.the_server().move_surface_to(surface, x, y);
                    return std::make_unique<wlcs::Surface>(std::move(surface));
                },
            [](wlcs::Server& server, wlcs::Client& client) -> std::unique_ptr<AbstractInputDevice>
                {
                    return std::make_unique<PointerInputDevice>(server, client);
                }
        }
    ));

INSTANTIATE_TEST_SUITE_P(
    XdgShellStableSubsurfaces,
    SubsurfaceTest,
    testing::Values(
        SubsurfaceTestParams{
            "xdg_stable_surface",
            [](wlcs::InProcessServer& server, wlcs::Client& client, int x, int y, int width, int height)
                -> std::unique_ptr<wlcs::Surface>
                {
                    auto surface = client.create_xdg_shell_stable_surface(
                        width,
                        height);
                    server.the_server().move_surface_to(surface, x, y);
                    return std::make_unique<wlcs::Surface>(std::move(surface));
                },
            [](wlcs::Server& server, wlcs::Client& client) -> std::unique_ptr<AbstractInputDevice>
                {
                    return std::make_unique<PointerInputDevice>(server, client);
                }
        }
    ));

INSTANTIATE_TEST_SUITE_P(
    TouchInputSubsurfaces,
    SubsurfaceTest,
    testing::Values(
        SubsurfaceTestParams{
            "touch_input_subsurfaces",
            [](wlcs::InProcessServer& server, wlcs::Client& client, int x, int y, int width, int height)
                -> std::unique_ptr<wlcs::Surface>
                {
                    auto surface = client.create_xdg_shell_v6_surface(
                        width,
                        height);
                    server.the_server().move_surface_to(surface, x, y);
                    return std::make_unique<wlcs::Surface>(std::move(surface));
                },
            [](wlcs::Server& server, wlcs::Client& client) -> std::unique_ptr<AbstractInputDevice>
                {
                    return std::make_unique<TouchInputDevice>(server, client);
                }
        }
    ));

TEST_P(SubsurfaceMultilevelTest, subsurface_with_sync_parent_does_not_move_when_only_grandparent_committed)
{
    int const pointer_x = 30, pointer_y = 30;
    int const subsurface_x = 20, subsurface_y = 20;

    wl_subsurface_set_position(child_subsurface, subsurface_x, subsurface_y);
    // We don't call wl_surface_commit(parent_subsurface), so position should not be applied
    wl_surface_commit(main_surface);
    client.roundtrip();

    input_device->to_screen_position(pointer_x + surface_x, pointer_y + surface_y);
    client.roundtrip();

    EXPECT_THAT(input_device->position_on_window(),
                Eq(std::make_pair(
                    wl_fixed_from_int(pointer_x),
                    wl_fixed_from_int(pointer_y))));

    EXPECT_THAT(input_device->position_on_window(),
                Ne(std::make_pair(
                    wl_fixed_from_int(pointer_x - subsurface_x),
                    wl_fixed_from_int(pointer_y - subsurface_y))))
        << "Subsurface moved without parent being committed";
}

TEST_P(SubsurfaceMultilevelTest, subsurface_with_desync_parent_does_not_move_when_only_grandparent_committed)
{
    int const pointer_x = 30, pointer_y = 30;
    int const subsurface_x = 20, subsurface_y = 20;

    wl_subsurface_set_desync(parent_subsurface);

    wl_subsurface_set_position(child_subsurface, subsurface_x, subsurface_y);
    // We don't call wl_surface_commit(parent_subsurface), so position should not be applied
    wl_surface_commit(main_surface);
    client.roundtrip();

    input_device->to_screen_position(pointer_x + surface_x, pointer_y + surface_y);
    client.roundtrip();

    EXPECT_THAT(input_device->position_on_window(),
                Eq(std::make_pair(
                    wl_fixed_from_int(pointer_x),
                    wl_fixed_from_int(pointer_y))));

    EXPECT_THAT(input_device->position_on_window(),
                Ne(std::make_pair(
                    wl_fixed_from_int(pointer_x - subsurface_x),
                    wl_fixed_from_int(pointer_y - subsurface_y))))
        << "Subsurface moved without parent being committed";
}

TEST_P(SubsurfaceMultilevelTest, subsurface_with_sync_parent_does_not_move_when_only_parent_committed)
{
    int const pointer_x = 30, pointer_y = 30;
    int const subsurface_x = 20, subsurface_y = 20;

    wl_subsurface_set_position(child_subsurface, subsurface_x, subsurface_y);
    wl_surface_commit(parent_subsurface);
    // We don't call wl_surface_commit(main_surface), which should be required before position is applied because
    // parent_subsurface is sync
    client.roundtrip();

    input_device->to_screen_position(pointer_x + surface_x, pointer_y + surface_y);
    client.roundtrip();

    EXPECT_THAT(input_device->position_on_window(),
                Eq(std::make_pair(
                    wl_fixed_from_int(pointer_x),
                    wl_fixed_from_int(pointer_y))));

    EXPECT_THAT(input_device->position_on_window(),
                Ne(std::make_pair(
                    wl_fixed_from_int(pointer_x - subsurface_x),
                    wl_fixed_from_int(pointer_y - subsurface_y))))
        << "Subsurface of sync parent moved without grandparent being committed";
}

TEST_P(SubsurfaceMultilevelTest, subsurface_with_desync_parent_moves_when_only_parent_committed)
{
    int const pointer_x = 30, pointer_y = 30;
    int const subsurface_x = 20, subsurface_y = 20;

    wl_subsurface_set_desync(parent_subsurface);

    wl_subsurface_set_position(child_subsurface, subsurface_x, subsurface_y);
    wl_surface_commit(parent_subsurface);
    // wl_surface_commit(main_surface) should NOT be required for position to be applied because parent_subsurface is
    // desync
    client.roundtrip();

    input_device->to_screen_position(pointer_x + surface_x, pointer_y + surface_y);
    client.roundtrip();

    EXPECT_THAT(input_device->position_on_window(),
                Eq(std::make_pair(
                    wl_fixed_from_int(pointer_x - subsurface_x),
                    wl_fixed_from_int(pointer_y - subsurface_y))));

    EXPECT_THAT(input_device->position_on_window(),
                Ne(std::make_pair(
                    wl_fixed_from_int(pointer_x),
                    wl_fixed_from_int(pointer_y))))
        << "Did not move on desync parent commit";
}

TEST_P(SubsurfaceMultilevelTest, subsurface_does_not_move_when_grandparent_commit_is_before_sync_parent_commit)
{
    int const pointer_x = 30, pointer_y = 30;
    int const subsurface_x = 20, subsurface_y = 20;

    wl_subsurface_set_position(child_subsurface, subsurface_x, subsurface_y);
    wl_surface_commit(main_surface);
    wl_surface_commit(parent_subsurface);
    // Committing main_surface would need to happend AFTER parent_subsurface in order for position to be applied
    client.roundtrip();

    input_device->to_screen_position(pointer_x + surface_x, pointer_y + surface_y);
    client.roundtrip();

    EXPECT_THAT(input_device->position_on_window(),
                Eq(std::make_pair(
                    wl_fixed_from_int(pointer_x),
                    wl_fixed_from_int(pointer_y))));

    EXPECT_THAT(input_device->position_on_window(),
                Ne(std::make_pair(
                    wl_fixed_from_int(pointer_x - subsurface_x),
                    wl_fixed_from_int(pointer_y - subsurface_y))))
        << "Subsurface moved when hierarchy commits were in the wrong order";
}

TEST_P(SubsurfaceMultilevelTest, subsurface_moves_after_both_sync_parent_and_grandparent_commit)
{
    int const pointer_x = 30, pointer_y = 30;
    int const subsurface_x = 20, subsurface_y = 20;

    wl_subsurface_set_position(child_subsurface, subsurface_x, subsurface_y);
    wl_surface_commit(parent_subsurface);
    wl_surface_commit(main_surface);
    client.roundtrip();

    input_device->to_screen_position(pointer_x + surface_x, pointer_y + surface_y);
    client.roundtrip();

    EXPECT_THAT(input_device->position_on_window(),
                Eq(std::make_pair(
                    wl_fixed_from_int(pointer_x - subsurface_x),
                    wl_fixed_from_int(pointer_y - subsurface_y))));

    EXPECT_THAT(input_device->position_on_window(),
                Ne(std::make_pair(
                    wl_fixed_from_int(pointer_x),
                    wl_fixed_from_int(pointer_y))))
        << "Did not move after parent and grandparent both comitted";
}

TEST_P(SubsurfaceMultilevelTest, by_default_subsurface_is_sync)
{
    int const pointer_x = 30, pointer_y = 30;
    int const subsurface_x = 20, subsurface_y = 20;

    wl_subsurface_set_position(child_subsurface, subsurface_x, subsurface_x);
    wl_surface_commit(parent_subsurface);
    // Not calling wl_surface_commit(main_surface), so new position should not be applied
    client.roundtrip();

    input_device->to_screen_position(pointer_x + surface_x, pointer_y + surface_y);
    client.roundtrip();

    EXPECT_THAT(input_device->position_on_window(),
                Eq(std::make_pair(
                    wl_fixed_from_int(pointer_x),
                    wl_fixed_from_int(pointer_y))));

    EXPECT_THAT(input_device->position_on_window(),
                Ne(std::make_pair(
                    wl_fixed_from_int(pointer_x - subsurface_x),
                    wl_fixed_from_int(pointer_y - subsurface_y))))
        << "Subsurface moved without parent commit (it should have been 'sync' by default, but is acting as desync)";
}

TEST_P(SubsurfaceMultilevelTest, subsurface_can_be_set_to_sync)
{
    int const pointer_x = 30, pointer_y = 30;
    int const subsurface_x = 20, subsurface_y = 20;

    wl_subsurface_set_desync(child_subsurface);
    wl_subsurface_set_sync(child_subsurface);

    wl_subsurface_set_position(child_subsurface, subsurface_x, subsurface_x);
    wl_surface_commit(parent_subsurface);
    // Not calling wl_surface_commit(main_surface), so new position should not be applied
    client.roundtrip();

    input_device->to_screen_position(pointer_x + surface_x, pointer_y + surface_y);
    client.roundtrip();

    EXPECT_THAT(input_device->position_on_window(),
                Eq(std::make_pair(
                    wl_fixed_from_int(pointer_x),
                    wl_fixed_from_int(pointer_y))));

    EXPECT_THAT(input_device->position_on_window(),
                Ne(std::make_pair(
                    wl_fixed_from_int(pointer_x - subsurface_x),
                    wl_fixed_from_int(pointer_y - subsurface_y))))
        << "Subsurface moved without parent commit (it should have been 'sync' by default, but is acting as desync)";
}

INSTANTIATE_TEST_SUITE_P(
    WlShellSubsurfaces,
    SubsurfaceMultilevelTest,
    testing::Values(
        SubsurfaceTestParams{
            "wl_shell_surface",
            [](wlcs::InProcessServer& server, wlcs::Client& client, int x, int y, int width, int height)
                -> std::unique_ptr<wlcs::Surface>
                {
                    auto surface = client.create_wl_shell_surface(
                        width,
                        height);
                    server.the_server().move_surface_to(surface, x, y);
                    return std::make_unique<wlcs::Surface>(std::move(surface));
                },
            [](wlcs::Server& server, wlcs::Client& client) -> std::unique_ptr<AbstractInputDevice>
                {
                    return std::make_unique<PointerInputDevice>(server, client);
                }
        }
    ));

INSTANTIATE_TEST_SUITE_P(
    XdgShellV6Subsurfaces,
    SubsurfaceMultilevelTest,
    testing::Values(
        SubsurfaceTestParams{
            "xdg_v6_surface",
            [](wlcs::InProcessServer& server, wlcs::Client& client, int x, int y, int width, int height)
                -> std::unique_ptr<wlcs::Surface>
                {
                    auto surface = client.create_xdg_shell_v6_surface(
                        width,
                        height);
                    server.the_server().move_surface_to(surface, x, y);
                    return std::make_unique<wlcs::Surface>(std::move(surface));
                },
            [](wlcs::Server& server, wlcs::Client& client) -> std::unique_ptr<AbstractInputDevice>
                {
                    return std::make_unique<PointerInputDevice>(server, client);
                }
        }
    ));

INSTANTIATE_TEST_SUITE_P(
    XdgShellStableSubsurfaces,
    SubsurfaceMultilevelTest,
    testing::Values(
        SubsurfaceTestParams{
            "xdg_stable_surface",
            [](wlcs::InProcessServer& server, wlcs::Client& client, int x, int y, int width, int height)
                -> std::unique_ptr<wlcs::Surface>
                {
                    auto surface = client.create_xdg_shell_stable_surface(
                        width,
                        height);
                    server.the_server().move_surface_to(surface, x, y);
                    return std::make_unique<wlcs::Surface>(std::move(surface));
                },
            [](wlcs::Server& server, wlcs::Client& client) -> std::unique_ptr<AbstractInputDevice>
                {
                    return std::make_unique<PointerInputDevice>(server, client);
                }
        }
    ));

INSTANTIATE_TEST_SUITE_P(
    TouchInputSubsurfaces,
    SubsurfaceMultilevelTest,
    testing::Values(
        SubsurfaceTestParams{
            "touch_input_subsurfaces",
            [](wlcs::InProcessServer& server, wlcs::Client& client, int x, int y, int width, int height)
                -> std::unique_ptr<wlcs::Surface>
                {
                    auto surface = client.create_xdg_shell_v6_surface(
                        width,
                        height);
                    server.the_server().move_surface_to(surface, x, y);
                    return std::make_unique<wlcs::Surface>(std::move(surface));
                },
            [](wlcs::Server& server, wlcs::Client& client) -> std::unique_ptr<AbstractInputDevice>
                {
                    return std::make_unique<TouchInputDevice>(server, client);
                }
        }
    ));

// TODO: combinations of sync and desync at various levels of the tree
// TODO: "bad_surface" error
// TODO: seitch to the new surface/input method abstraction
