/*
 * Copyright © 2020 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: Alan Griffiths <alan@octopull.co.uk>
 */

#include "pointer_constraints_unstable_v1.h"
#include "helpers.h"
#include "in_process_server.h"

#include <gmock/gmock.h>

#include <memory>

using testing::AnyNumber;
using testing::Eq;
using testing::Ne;
using testing::NotNull;

using namespace wlcs;

namespace
{
auto const any_width = 300;
auto const any_height = 300;
auto const nw_middle_x = any_width / 2;
auto const nw_middle_y = any_height / 2;
auto const se_middle_x = any_width + nw_middle_x;
auto const se_middle_y = any_height + nw_middle_y;

struct PointerConstraints : StartedInProcessServer
{
    // Create a client with two surface (ne & sw) and a cursor centered on the nw_surface
    Client client{the_server()};
    Surface se_surface{client.create_visible_surface(any_width, any_height)};
    Surface nw_surface{client.create_visible_surface(any_width, any_height)};
    wl_pointer* const pointer = client.the_pointer();

    Pointer cursor = the_server().create_pointer();

    ZwpPointerConstraintsV1 pointer_constraints{client};

    std::unique_ptr<ZwpLockedPointerV1> locked_ptr = nullptr;
    std::unique_ptr<ZwpConfinedPointerV1> confined_ptr = nullptr;

    void SetUp() override
    {
        StartedInProcessServer::SetUp();

        // Get the surface in a known position with the cursor over it
        the_server().move_surface_to(nw_surface, 0, 0);
        cursor.move_to(nw_middle_x, nw_middle_y);
        the_server().move_surface_to(se_surface, any_width, any_height);
    }

    void TearDown() override
    {
        locked_ptr.reset();
        confined_ptr.reset();
        client.roundtrip();
        StartedInProcessServer::TearDown();
    }

    void setup_locked_ptr_on(Surface& surface, zwp_pointer_constraints_v1_lifetime lifetime = ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ONESHOT)
    {
        locked_ptr = std::make_unique<ZwpLockedPointerV1>(pointer_constraints, surface, pointer, nullptr, lifetime);
    }

    void setup_confined_ptr_on(Surface& surface, zwp_pointer_constraints_v1_lifetime lifetime = ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ONESHOT)
    {
        confined_ptr = std::make_unique<ZwpConfinedPointerV1>(pointer_constraints, surface, pointer, nullptr, lifetime);
    }

    void setup_sync()
    {
        client.roundtrip();
        using namespace testing;
        if (locked_ptr) Mock::VerifyAndClearExpectations(locked_ptr.get());
        if (confined_ptr) Mock::VerifyAndClearExpectations(confined_ptr.get());
    }

    void select_se_window()
    {
        cursor.move_to(se_middle_x, se_middle_y);
        cursor.left_click();
        client.roundtrip();
    }

    void select_nw_window()
    {
        cursor.move_to(nw_middle_x, nw_middle_y);
        cursor.left_click();
        client.roundtrip();
    }
};
}

TEST_F(PointerConstraints, can_get_locked_pointer)
{
    setup_locked_ptr_on(nw_surface);

    EXPECT_THAT(*locked_ptr, NotNull());
}

TEST_F(PointerConstraints, locked_pointer_on_initially_focussed_surface_gets_locked_notification)
{
    setup_locked_ptr_on(nw_surface);

    EXPECT_CALL(*locked_ptr, locked()).Times(1);

    client.roundtrip();
}

TEST_F(PointerConstraints, locked_pointer_does_not_move)
{
    auto const initial_pointer_position = client.pointer_position();
    setup_locked_ptr_on(nw_surface);
    EXPECT_CALL(*locked_ptr, locked()).Times(AnyNumber());
    setup_sync();

    cursor.move_by(10, 10);
    client.roundtrip();

    EXPECT_THAT(client.pointer_position(), Eq(initial_pointer_position));
}

TEST_F(PointerConstraints, locked_pointer_on_initially_unfocussed_surface_gets_no_locked_notification)
{
    setup_locked_ptr_on(se_surface);

    EXPECT_CALL(*locked_ptr, locked()).Times(0);

    client.roundtrip();
}

TEST_F(PointerConstraints, when_surface_is_selected_locked_pointer_gets_locked_notification)
{
    setup_locked_ptr_on(se_surface);
    setup_sync();

    EXPECT_CALL(*locked_ptr, locked()).Times(1);

    select_se_window();
}

TEST_F(PointerConstraints, when_surface_is_unselected_locked_pointer_gets_unlocked_notification)
{
    setup_locked_ptr_on(nw_surface);
    EXPECT_CALL(*locked_ptr, locked()).Times(AnyNumber());
    setup_sync();

    EXPECT_CALL(*locked_ptr, unlocked()).Times(1);

    // A new surface will be given focus
    Surface a_new_surface{client.create_visible_surface(any_width, any_height)};
    client.roundtrip();
}

TEST_F(PointerConstraints, when_surface_is_reselected_persistent_locked_pointer_gets_notifications)
{
    setup_locked_ptr_on(nw_surface, ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT);
    EXPECT_CALL(*locked_ptr, locked()).Times(AnyNumber());
    setup_sync();

    for (auto i = 3; i-- != 0;)
    {
        {
            EXPECT_CALL(*locked_ptr, unlocked()).Times(1);
            // A new surface will be given focus
            Surface a_new_surface{client.create_visible_surface(any_width, any_height)};
            setup_sync();

            EXPECT_CALL(*locked_ptr, locked()).Times(1);
        }
        client.roundtrip();
    }
}

TEST_F(PointerConstraints, can_get_confined_pointer)
{
    setup_confined_ptr_on(nw_surface);
    EXPECT_THAT(*confined_ptr, NotNull());
}

TEST_F(PointerConstraints, confined_pointer_on_initially_focussed_surface_gets_confined_notification)
{
    setup_confined_ptr_on(nw_surface);

    EXPECT_CALL(*confined_ptr, confined()).Times(1);

    client.roundtrip();
}

TEST_F(PointerConstraints, confined_pointer_does_move)
{
    auto const initial_pointer_position = client.pointer_position();
    setup_confined_ptr_on(nw_surface);
    EXPECT_CALL(*confined_ptr, confined()).Times(AnyNumber());
    setup_sync();

    cursor.move_by(10, 10);
    client.roundtrip();

    EXPECT_THAT(client.pointer_position(), Ne(initial_pointer_position));
}

TEST_F(PointerConstraints, confined_pointer_movement_is_constrained)
{
    auto const top = wl_fixed_from_int(0);
    auto const left = wl_fixed_from_int(0);
    auto const bottom = wl_fixed_from_int(any_height-1);
    auto const right = wl_fixed_from_int(any_width-1);

    setup_confined_ptr_on(nw_surface);
    EXPECT_CALL(*confined_ptr, confined()).Times(AnyNumber());
    setup_sync();

    cursor.move_by(2*any_width, 2*any_height);
    client.roundtrip();

    EXPECT_THAT(client.pointer_position(), Eq(std::make_pair(bottom, right)));

    cursor.move_by(-2*any_width, -2*any_height);
    client.roundtrip();

    EXPECT_THAT(client.pointer_position(), Eq(std::make_pair(top, left)));

    cursor.move_by(-2*any_width, 2*any_height);
    client.roundtrip();

    EXPECT_THAT(client.pointer_position(), Eq(std::make_pair(top, right)));

    cursor.move_by(2*any_width, -2*any_height);
    client.roundtrip();

    EXPECT_THAT(client.pointer_position(), Eq(std::make_pair(bottom, left)));
}

TEST_F(PointerConstraints, confined_pointer_on_initially_unfocussed_surface_gets_no_confined_notification)
{
    setup_confined_ptr_on(se_surface);

    EXPECT_CALL(*confined_ptr, confined()).Times(0);

    client.roundtrip();
}

TEST_F(PointerConstraints, when_surface_is_selected_confined_pointer_gets_confined_notification)
{
    setup_confined_ptr_on(se_surface);
    setup_sync();

    EXPECT_CALL(*confined_ptr, confined()).Times(1);

    select_se_window();
}

TEST_F(PointerConstraints, when_surface_is_unselected_confined_pointer_gets_unconfined_notification)
{
    setup_confined_ptr_on(nw_surface);
    EXPECT_CALL(*confined_ptr, confined()).Times(AnyNumber());
    setup_sync();

    EXPECT_CALL(*confined_ptr, unconfined()).Times(1);

    // A new surface will be given focus
    Surface a_new_surface{client.create_visible_surface(any_width, any_height)};
    client.roundtrip();
}

TEST_F(PointerConstraints, when_surface_is_reselected_persistent_confined_pointer_gets_notifications)
{
    setup_confined_ptr_on(nw_surface, ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT);
    EXPECT_CALL(*confined_ptr, confined()).Times(AnyNumber());
    setup_sync();

    for (auto i = 3; i-- != 0;)
    {
        {
            EXPECT_CALL(*confined_ptr, unconfined()).Times(1);
            // A new surface will be given focus
            Surface a_new_surface{client.create_visible_surface(any_width, any_height)};
            setup_sync();

            EXPECT_CALL(*confined_ptr, confined()).Times(1);
        }
        client.roundtrip();
    }
}
