/*
 * Copyright © 2019 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 "primary_selection.h"

#include "in_process_server.h"

#include <gmock/gmock.h>
#include <sys/socket.h>

using namespace wlcs;
using namespace testing;

namespace
{
char const any_mime_type[] = "AnyMimeType";
char const any_mime_data[] = "AnyMimeData";

struct SourceApp : Client
{
    // Can't use "using Client::Client;" because Xenial
    explicit SourceApp(Server& server) : Client{server} {}

    PrimarySelectionSource source{primary_selection_device_manager()};
    PrimarySelectionDevice device{primary_selection_device_manager(), seat()};

    void set_selection()
    {
        zwp_primary_selection_device_v1_set_selection(device, source, 0);
        roundtrip();
    }

    void offer(char const* mime_type)
    {
        zwp_primary_selection_source_v1_offer(source, mime_type);
        roundtrip();
    }
};

struct SinkApp : Client
{
    explicit SinkApp(Server& server) : Client{server} { roundtrip(); }

    PrimarySelectionDevice device{primary_selection_device_manager(), seat()};
};

struct PrimarySelection : StartedInProcessServer
{
    CheckInterfaceExpected must_be_first{the_server(), zwp_primary_selection_device_manager_v1_interface};
    SourceApp   source_app{the_server()};
    SinkApp     sink_app{the_server()};

    void TearDown() override
    {
        source_app.roundtrip();
        sink_app.roundtrip();
        StartedInProcessServer::TearDown();
    }
};

struct MockPrimarySelectionDeviceListener : PrimarySelectionDeviceListener
{
    using PrimarySelectionDeviceListener::PrimarySelectionDeviceListener;

    MOCK_METHOD2(data_offer, void(zwp_primary_selection_device_v1* device, zwp_primary_selection_offer_v1* offer));
    MOCK_METHOD2(selection, void(zwp_primary_selection_device_v1* device, zwp_primary_selection_offer_v1* offer));
};

struct MockPrimarySelectionOfferListener : PrimarySelectionOfferListener
{
    using PrimarySelectionOfferListener::PrimarySelectionOfferListener;

    MOCK_METHOD2(offer, void(zwp_primary_selection_offer_v1* offer, const char* mime_type));
};

struct MockPrimarySelectionSourceListener : PrimarySelectionSourceListener
{
    using PrimarySelectionSourceListener::PrimarySelectionSourceListener;

    MOCK_METHOD3(send, void(zwp_primary_selection_source_v1* source, const char* mime_type, int32_t fd));

    MOCK_METHOD1(cancelled, void(zwp_primary_selection_source_v1*));
};

struct StubPrimarySelectionDeviceListener : PrimarySelectionDeviceListener
{
    StubPrimarySelectionDeviceListener(
        zwp_primary_selection_device_v1* device,
        PrimarySelectionOfferListener& offer_listener) :
        PrimarySelectionDeviceListener{device},
        offer_listener{offer_listener}
    {
    }

    void data_offer(zwp_primary_selection_device_v1* device, zwp_primary_selection_offer_v1* offer) override
    {
        offer_listener.listen_to(offer);
        PrimarySelectionDeviceListener::data_offer(device, offer);
    }

    void selection(zwp_primary_selection_device_v1* device, zwp_primary_selection_offer_v1* offer) override
    {
        selected = offer;
        PrimarySelectionDeviceListener::selection(device, offer);
    }

    PrimarySelectionOfferListener& offer_listener;
    zwp_primary_selection_offer_v1* selected = nullptr;
};

struct StubPrimarySelectionSourceListener : PrimarySelectionSourceListener
{
    using PrimarySelectionSourceListener::PrimarySelectionSourceListener;

    void send(zwp_primary_selection_source_v1*, const char*, int32_t fd)
    {
        ASSERT_THAT(write(fd, any_mime_data, sizeof any_mime_data), Eq(ssize_t(sizeof any_mime_data)));
        close(fd);
    }
};

struct Pipe
{
    int source;
    int sink;

    Pipe() { socketpair(AF_LOCAL, SOCK_STREAM, 0, &source); }
    Pipe(Pipe const&) = delete;
    Pipe& operator=(Pipe const&) = delete;

    ~Pipe() { close(source); close(sink); }
};
}

TEST_F(PrimarySelection, source_can_offer)
{
    source_app.offer(any_mime_type);
    source_app.set_selection();
}

TEST_F(PrimarySelection, sink_can_listen)
{
    MockPrimarySelectionDeviceListener  device_listener{sink_app.device};
    MockPrimarySelectionOfferListener   offer_listener;

    InSequence seq;
    EXPECT_CALL(device_listener, data_offer(_, _))
        .WillOnce(Invoke([&](auto*, auto* id) { offer_listener.listen_to(id); }));

    EXPECT_CALL(offer_listener, offer(_, StrEq(any_mime_type)));

    EXPECT_CALL(device_listener, selection(_, _));

    source_app.offer(any_mime_type);
    source_app.set_selection();

    sink_app.roundtrip();
}

TEST_F(PrimarySelection, sink_can_request)
{
    PrimarySelectionOfferListener   offer_listener;
    StubPrimarySelectionDeviceListener  device_listener{sink_app.device, offer_listener};
    source_app.offer(any_mime_type);
    source_app.set_selection();
    sink_app.roundtrip();
    ASSERT_THAT(device_listener.selected, NotNull());

    Pipe pipe;
    zwp_primary_selection_offer_v1_receive(device_listener.selected, any_mime_type, pipe.source);
    sink_app.roundtrip();
}

TEST_F(PrimarySelection, source_sees_request)
{
    MockPrimarySelectionSourceListener  source_listener{source_app.source};
    PrimarySelectionOfferListener   offer_listener;
    StubPrimarySelectionDeviceListener  device_listener{sink_app.device, offer_listener};
    source_app.offer(any_mime_type);
    source_app.set_selection();
    sink_app.roundtrip();
    ASSERT_THAT(device_listener.selected, NotNull());

    EXPECT_CALL(source_listener, send(_, _, _))
        .Times(1)
        .WillRepeatedly(Invoke([&](auto*, auto*, int fd) { close(fd); }));

    Pipe pipe;
    zwp_primary_selection_offer_v1_receive(device_listener.selected, any_mime_type, pipe.source);
    sink_app.roundtrip();
    source_app.roundtrip();
}

TEST_F(PrimarySelection, source_can_supply_request)
{
    StubPrimarySelectionSourceListener source_listener{source_app.source};
    PrimarySelectionOfferListener   offer_listener;
    StubPrimarySelectionDeviceListener  device_listener{sink_app.device, offer_listener};
    source_app.offer(any_mime_type);
    source_app.set_selection();
    sink_app.roundtrip();
    ASSERT_THAT(device_listener.selected, NotNull());

    Pipe pipe;
    zwp_primary_selection_offer_v1_receive(device_listener.selected, any_mime_type, pipe.source);
    sink_app.roundtrip();
    source_app.roundtrip();

    char buffer[128];
    EXPECT_THAT(read(pipe.sink, buffer, sizeof buffer), Eq(ssize_t(sizeof any_mime_data)));
    EXPECT_THAT(buffer, StrEq(any_mime_data));
}