/*
 * Copyright © 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/>.
 */

#include <miral/test_server.h>
#include "example_xdg_shell_v6.h"
#include "xdg-shell-unstable-v6_wrapper.h"

#include <mir/server.h>
#include <miral/internal_client.h>
#include <miral/wayland_extensions.h>

#include <wayland-client.h>

#include <memory>
#include <mutex>
#include <vector>

#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include <mir/abnormal_exit.h>

using namespace testing;

namespace mir::wayland
{
    extern struct wl_interface const zxdg_shell_v6_interface_data;
    extern struct wl_interface const zxdg_surface_v6_interface_data;
    extern struct wl_interface const zxdg_toplevel_v6_interface_data;
    extern struct wl_interface const wl_compositor_interface_data;
    extern struct wl_interface const wl_surface_interface_data;
}

namespace
{
class WaylandClient
{
public:

    void operator()(struct wl_display* display)
    {
        code(display);
    }

    void operator()(std::weak_ptr<mir::scene::Session> const& session)
    {
        std::lock_guard lock{mutex};
        session_ = session;
    }

    std::shared_ptr<mir::scene::Session> session() const
    {
        std::lock_guard lock{mutex};
        return session_.lock();
    }

    std::function<void (struct wl_display*)> code = [](auto){};

private:
    std::mutex mutable mutex;
    std::weak_ptr<mir::scene::Session> session_;
};

struct WaylandExtensions : miral::TestServer
{
    WaylandExtensions()
    {
        start_server_in_setup = false;
        add_server_init(launcher);
    }

    void run_as_client(std::function<void (struct wl_display*)>&& code)
    {
        bool client_run = false;
        std::condition_variable cv;
        std::mutex mutex;

        client.code = [&](struct wl_display* display)
            {
                {
                    std::lock_guard lock{mutex};
                    code(display);
                    client_run = true;
                }
                cv.notify_one();
            };

        std::unique_lock lock{mutex};
        launcher.launch(client);
        cv.wait(lock, [&]{ return client_run; });
    }

private:
    miral::InternalClientLauncher launcher;
    WaylandClient client;
};

template<typename Type>
auto make_scoped(Type* owned, void(*deleter)(Type*)) -> std::unique_ptr<Type, void(*)(Type*)>
{
    return {owned, deleter};
}

struct ClientGlobalEnumerator
{
    void run()
    {
        auto display = wl_display_connect(nullptr);
        auto const registry = make_scoped(wl_display_get_registry(display), &wl_registry_destroy);
        wl_registry_add_listener(registry.get(), &registry_listener, nullptr);
        wl_display_roundtrip(display);
        wl_display_roundtrip(display);
    }

    static void new_global(
        void* /*data*/,
        struct wl_registry* /*registry*/,
        uint32_t /*id*/,
        char const* interface,
        uint32_t /*version*/)
    {
        std::cout << interface << std::endl;
    }

    static void global_remove(
        void* /*data*/,
        struct wl_registry* /*registry*/,
        uint32_t /*name*/)
    {

    }

    static wl_registry_listener constexpr registry_listener = {
        new_global,
        global_remove
    };
};

wl_registry_listener constexpr ClientGlobalEnumerator::registry_listener;

/// Runs the client enumerator in a subprocess.
///
/// Mir will allow clients that originate from the compositor itself to access
/// any protocols that they like. However, we would like to test the case
/// where an external client asks the compositor for exstensions that it does
/// not have access to. To do this, we need to fork this process, record the
/// interfaces that come out of the child process, and return them to the
/// test code via a pipe.
///
/// \param server
/// \returns the list of interfaces that are available to the process
std::vector<std::string> run_client_enumerator(mir::Server& server)
{
    std::vector<std::string> interfaces;

    int pipefd[2];
    if (pipe(pipefd) == -1)
    {
        std::cerr << "run_client_enumerator: pipe failed" << std::endl;
        return {};
    }

    auto const wayland_display = server.wayland_display();

    auto const pid = fork();
    if (pid == 0)
    {
        close(pipefd[0]);
        dup2(pipefd[1], STDOUT_FILENO);
        close(pipefd[1]);

        setenv("WAYLAND_DISPLAY", wayland_display.value().c_str(),  true);

        ClientGlobalEnumerator enumerator;
        enumerator.run();
        _exit(0);
    }

    close(pipefd[1]);
    FILE *stream = fdopen(pipefd[0], "r");
    if (!stream)
    {
        std::cerr << "run_client_enumerator: fdopen failed" << std::endl;
        return {};
    }

    char line[256];
    while (fgets(line, sizeof(line), stream))
    {
        size_t const len = strlen(line);
        interfaces.emplace_back(line, line + len - 1);
    }
    fclose(stream);

    int status;
    waitpid(pid, &status, 0);
    return interfaces;
}

struct ClientXdgShellV6Creator
{
    ClientXdgShellV6Creator(std::function<void()>&& test)
        : test{std::move(test)}
    {
    }

    std::function<void()> test = []{};

    void operator()(wl_display* display)
    {
        auto const registry = make_scoped(wl_display_get_registry(display), &wl_registry_destroy);
        wl_registry_add_listener(registry.get(), &registry_listener, this);
        wl_display_roundtrip(display);
        wl_display_roundtrip(display);

        ASSERT_THAT(compositor, NotNull());
        ASSERT_THAT(shell, NotNull());

        wl_proxy* raw_surface = wl_proxy_marshal_constructor(
            reinterpret_cast<wl_proxy*>(compositor),
            0,
            &mir::wayland::wl_surface_interface_data,
            nullptr);
        auto const surface = make_scoped(reinterpret_cast<wl_surface*>(raw_surface), &wl_surface_destroy);

        wl_display_roundtrip(display);

        wl_proxy* xdg_surface = wl_proxy_marshal_constructor(
            shell, 2, &mir::wayland::zxdg_surface_v6_interface_data, NULL, surface.get());

        (void)wl_proxy_marshal_constructor(
            xdg_surface, 1, &mir::wayland::zxdg_toplevel_v6_interface_data);

        wl_surface_commit(surface.get());
        wl_display_roundtrip(display);

        test();
    }

    static void new_global(
        void* data,
        struct wl_registry* registry,
        uint32_t id,
        char const* interface,
        uint32_t version)
    {
        auto* self = static_cast<ClientXdgShellV6Creator*>(data);

        if (strcmp(interface, wl_compositor_interface.name) == 0)
        {
            self->compositor = static_cast<decltype(self->compositor)>(
                wl_registry_bind(registry, id, &mir::wayland::wl_compositor_interface_data, version));
        }
        else if (strcmp(interface, mir::wayland::zxdg_shell_v6_interface_data.name) == 0)
        {
            self->shell = static_cast<decltype(self->shell)>(
                wl_registry_bind(registry, id, &mir::wayland::zxdg_shell_v6_interface_data, version));
        }
    }

    static void global_remove(void*, struct wl_registry*, uint32_t) {}

    static wl_registry_listener constexpr registry_listener = {
        &new_global,
        &global_remove
    };

    wl_compositor* compositor = nullptr;
    wl_proxy* shell = nullptr;
};

wl_registry_listener constexpr ClientXdgShellV6Creator::registry_listener;
}

TEST_F(WaylandExtensions, client_connects)
{
    bool client_connected = false;
    start_server();

    run_as_client([&](auto) { client_connected = true; });

    EXPECT_THAT(client_connected, Eq(true));
}

TEST_F(WaylandExtensions, client_sees_default_extensions)
{

    start_server();

    auto const interfaces = run_client_enumerator(server());

    auto const available_extensions = miral::WaylandExtensions::recommended();

    // Some extension names do not include the *_manager like their globals's name does.
    // Fix by adding a non-manager version to the available list for each interface that contains "_manager".
    std::vector<std::string> available = interfaces;
    std::string const to_remove = "_manager";
    for (unsigned i = 0; i < available.size(); i++)
    {
        auto const pos = available[i].find(to_remove);
        if (pos != std::string::npos)
        {
            auto str = available[i];
            str.erase(pos, to_remove.size());
            available.push_back(str);
        }
    }

    for (auto const& extension : available_extensions)
    {
        EXPECT_THAT(available, Contains(Eq(extension)));
    }
}

TEST_F(WaylandExtensions, add_extension_adds_protocol_to_supported_enabled_extensions)
{
    miral::WaylandExtensions extensions;

    EXPECT_THAT(extensions.all_supported(), Not(Contains(Eq(mir::examples::xdg_shell_v6_extension().name))));

    extensions.add_extension(mir::examples::xdg_shell_v6_extension());

    EXPECT_THAT(extensions.all_supported(), Contains(Eq(mir::examples::xdg_shell_v6_extension().name)));
}

TEST_F(WaylandExtensions, add_extension_disabled_by_default_adds_protocol_to_supported_extensions_only)
{
    miral::WaylandExtensions extensions;
    extensions.add_extension_disabled_by_default(mir::examples::xdg_shell_v6_extension());

    add_server_init(extensions);
    start_server();

    auto const interfaces = run_client_enumerator(server());

    EXPECT_THAT(interfaces, Not(Contains(Eq(mir::examples::xdg_shell_v6_extension().name))));
}

TEST_F(WaylandExtensions, can_retrieve_application_for_client)
{
    miral::WaylandExtensions extensions;
    wl_client* client = nullptr;

    extensions.add_extension(mir::examples::xdg_shell_v6_extension([&](wl_client* c, auto){ client = c; }));

    add_server_init(extensions);
    start_server();

    run_as_client(ClientXdgShellV6Creator{[&]
        {
            EXPECT_THAT(client, NotNull());
            EXPECT_THAT(miral::application_for(client), NotNull());
        }});
}

TEST_F(WaylandExtensions, can_retrieve_application_for_surface)
{
    miral::WaylandExtensions extensions;

    wl_resource* surface = nullptr;
    extensions.add_extension(mir::examples::xdg_shell_v6_extension([&](auto, wl_resource* s){ surface = s; }));

    add_server_init(extensions);
    start_server();

    run_as_client(ClientXdgShellV6Creator{[&]
        {
            EXPECT_THAT(surface, NotNull());
            EXPECT_THAT(miral::application_for(surface), NotNull());
        }});
}

TEST_F(WaylandExtensions, some_extensions_are_supported)
{
    EXPECT_THAT(miral::WaylandExtensions::supported(), Not(IsEmpty()));
}

TEST_F(WaylandExtensions, some_extensions_are_recommended)
{
    EXPECT_THAT(miral::WaylandExtensions::recommended(), Not(IsEmpty()));
}

TEST_F(WaylandExtensions, recommeded_extensions_subset_of_supported_extensions)
{
    for (auto const& e : miral::WaylandExtensions::recommended())
    {
        EXPECT_THAT(miral::WaylandExtensions::supported(), Contains(e));
    }
}

TEST_F(WaylandExtensions, disable_can_remove_default_extensions)
{
    std::string const extension_to_remove{"xdg_wm_base"};
    miral::WaylandExtensions extensions;
    extensions.disable(extension_to_remove);

    add_server_init(extensions);
    start_server();

    auto const interfaces = run_client_enumerator(server());

    EXPECT_THAT(interfaces, Not(Contains(Eq(extension_to_remove))));
}

TEST_F(WaylandExtensions, enable_can_enable_non_standard_extensions)
{
    std::string const extension_to_enable{"zwlr_layer_shell_v1"};
    miral::WaylandExtensions extensions;
    extensions.enable(extension_to_enable);

    add_server_init(extensions);
    start_server();

    auto const interfaces = run_client_enumerator(server());

    EXPECT_THAT(interfaces, Contains(Eq(extension_to_enable)));
}

TEST_F(WaylandExtensions, enable_can_enable_bespoke_extension)
{
    miral::WaylandExtensions extensions;
    extensions.add_extension_disabled_by_default(mir::examples::xdg_shell_v6_extension());
    extensions.enable(mir::examples::xdg_shell_v6_extension().name);


    add_server_init(extensions);
    start_server();

    auto const interfaces = run_client_enumerator(server());

    EXPECT_THAT(interfaces, Contains(Eq(mir::examples::xdg_shell_v6_extension().name)));
}

TEST_F(WaylandExtensions, disable_can_disable_bespoke_extension)
{
    miral::WaylandExtensions extensions;
    extensions.add_extension(mir::examples::xdg_shell_v6_extension());
    extensions.disable(mir::examples::xdg_shell_v6_extension().name);


    add_server_init(extensions);
    start_server();

    auto const interfaces = run_client_enumerator(server());

    EXPECT_THAT(interfaces, Not(Contains(Eq(mir::examples::xdg_shell_v6_extension().name))));
}

TEST_F(WaylandExtensions, cannot_duplicate_existing_extension)
{
    miral::WaylandExtensions::Builder duplicate_extension{
        "zwlr_layer_shell_v1",
        [&](auto const&){ return std::shared_ptr<void>{};}};

    miral::WaylandExtensions extensions;


    EXPECT_THROW(
        extensions.add_extension(duplicate_extension),
        mir::AbnormalExit);

    EXPECT_THROW(
        extensions.add_extension_disabled_by_default(duplicate_extension),
        mir::AbnormalExit);
}

TEST_F(WaylandExtensions, cannot_duplicate_bespoke_extension)
{
    miral::WaylandExtensions extensions;


    extensions.add_extension(mir::examples::xdg_shell_v6_extension());

    EXPECT_THROW(
        extensions.add_extension(mir::examples::xdg_shell_v6_extension()),
        mir::AbnormalExit);

    EXPECT_THROW(
        extensions.add_extension_disabled_by_default(mir::examples::xdg_shell_v6_extension()),
        mir::AbnormalExit);
}

TEST_F(WaylandExtensions, wayland_extensions_option_sets_extensions)
{
    miral::WaylandExtensions extensions;


    add_to_environment("MIR_SERVER_WAYLAND_EXTENSIONS", "xdg_wm_base:zwlr_layer_shell_v1");

    add_server_init(extensions);
    start_server();

    auto const interfaces = run_client_enumerator(server());

    EXPECT_THAT(interfaces, Contains(Eq("xdg_wm_base")));
    EXPECT_THAT(interfaces, Contains(Eq("zwlr_layer_shell_v1")));
    EXPECT_THAT(interfaces, Not(Contains(Eq("wl_shell"))));
}

TEST_F(WaylandExtensions, add_extensions_option_adds_extensions)
{
    miral::WaylandExtensions extensions;


    add_to_environment("MIR_SERVER_ADD_WAYLAND_EXTENSIONS", "xdg_wm_base:zwlr_layer_shell_v1");

    add_server_init(extensions);
    start_server();

    auto const interfaces = run_client_enumerator(server());

    EXPECT_THAT(interfaces, Contains(Eq("wl_shell")));
    EXPECT_THAT(interfaces, Contains(Eq("xdg_wm_base")));
    EXPECT_THAT(interfaces, Contains(Eq("zwlr_layer_shell_v1")));
}

TEST_F(WaylandExtensions, drop_extensions_option_removes_extensions)
{
    miral::WaylandExtensions extensions;


    add_to_environment("MIR_SERVER_DROP_WAYLAND_EXTENSIONS", "xdg_wm_base:zwlr_layer_shell_v1");

    add_server_init(extensions);
    start_server();

    auto const interfaces = run_client_enumerator(server());

    EXPECT_THAT(interfaces, Contains(Eq("wl_shell")));
    EXPECT_THAT(interfaces, Not(Contains(Eq("xdg_wm_base"))));
    EXPECT_THAT(interfaces, Not(Contains(Eq("zwlr_layer_shell_v1"))));
}

TEST_F(WaylandExtensions, add_extensions_option_adds_extensions_if_name_is_not_global_name)
{
    miral::WaylandExtensions extensions;


    add_to_environment("MIR_SERVER_ADD_WAYLAND_EXTENSIONS", "zwp_input_method_v2:zwp_virtual_keyboard_v1");

    add_server_init(extensions);
    start_server();

    auto const interfaces = run_client_enumerator(server());

    EXPECT_THAT(interfaces, Contains(Eq("zwp_input_method_manager_v2")));
    EXPECT_THAT(interfaces, Contains(Eq("zwp_virtual_keyboard_manager_v1")));
}

TEST_F(WaylandExtensions, drop_extensions_option_overrides_enable)
{
    miral::WaylandExtensions extensions;
    extensions.enable("zwlr_layer_shell_v1");


    add_to_environment("MIR_SERVER_DROP_WAYLAND_EXTENSIONS", "zwlr_layer_shell_v1");

    add_server_init(extensions);
    start_server();

    auto const interfaces = run_client_enumerator(server());

    EXPECT_THAT(interfaces, Not(Contains(Eq("zwlr_layer_shell_v1"))));
}

TEST_F(WaylandExtensions, add_extensions_option_overrides_disable)
{
    miral::WaylandExtensions extensions;
    extensions.disable("zwlr_layer_shell_v1");


    add_to_environment("MIR_SERVER_ADD_WAYLAND_EXTENSIONS", "zwlr_layer_shell_v1");

    add_server_init(extensions);
    start_server();

    auto const interfaces = run_client_enumerator(server());

    EXPECT_THAT(interfaces, Contains(Eq("zwlr_layer_shell_v1")));
}

TEST_F(WaylandExtensions, conditionally_enable_given_correct_extension_name)
{
    miral::WaylandExtensions extensions;
    bool condition_called = false;
    extensions.conditionally_enable("zwlr_layer_shell_v1", [&](auto const& info)
        {
            EXPECT_THAT(info.name(), StrEq("zwlr_layer_shell_v1"));
            condition_called = true;
            return true;
        });

    add_server_init(extensions);
    start_server();

    run_client_enumerator(server());

    EXPECT_THAT(condition_called, Eq(true));
}

TEST_F(WaylandExtensions, conditionally_enable_not_given_user_preference_if_none_specified)
{
    miral::WaylandExtensions extensions;
    bool condition_called = false;
    extensions.conditionally_enable("zwlr_layer_shell_v1", [&](auto const& info)
        {
            EXPECT_THAT(info.user_preference(), Eq(std::nullopt));
            condition_called = true;
            return true;
        });

    add_server_init(extensions);
    start_server();
    run_client_enumerator(server());

    EXPECT_THAT(condition_called, Eq(true));
}

TEST_F(WaylandExtensions, conditionally_enable_given_user_preference_true_if_added)
{
    miral::WaylandExtensions extensions;
    bool condition_called = false;
    extensions.conditionally_enable("zwlr_layer_shell_v1", [&](auto const& info)
        {
            EXPECT_THAT(info.user_preference(), Eq(std::make_optional(true)));
            condition_called = true;
            return true;
        });
    add_to_environment("MIR_SERVER_ADD_WAYLAND_EXTENSIONS", "zwlr_layer_shell_v1");

    add_server_init(extensions);
    start_server();
    run_client_enumerator(server());

    EXPECT_THAT(condition_called, Eq(true));
}

TEST_F(WaylandExtensions, conditionally_enable_given_user_preference_false_if_dropped)
{
    miral::WaylandExtensions extensions;
    bool condition_called = false;
    extensions.conditionally_enable("zwlr_layer_shell_v1", [&](auto const& info)
        {
            EXPECT_THAT(info.user_preference(), Eq(std::make_optional(false)));
            condition_called = true;
            return true;
        });
    add_to_environment("MIR_SERVER_DROP_WAYLAND_EXTENSIONS", "zwlr_layer_shell_v1");

    add_server_init(extensions);
    start_server();
    run_client_enumerator(server());

    EXPECT_THAT(condition_called, Eq(true));
}

TEST_F(WaylandExtensions, conditionally_enable_given_user_preference_true_if_in_extensions_list)
{
    miral::WaylandExtensions extensions;
    bool condition_called = false;
    extensions.conditionally_enable("zwlr_layer_shell_v1", [&](auto const& info)
        {
            EXPECT_THAT(info.user_preference(), Eq(std::make_optional(true)));
            condition_called = true;
            return true;
        });
    add_to_environment("MIR_SERVER_WAYLAND_EXTENSIONS", "xdg_wm_base:zwlr_layer_shell_v1");

    add_server_init(extensions);
    start_server();
    run_client_enumerator(server());

    EXPECT_THAT(condition_called, Eq(true));
}

TEST_F(WaylandExtensions, conditionally_enable_given_user_preference_false_if_not_in_extensions_list)
{
    miral::WaylandExtensions extensions;
    bool condition_called = false;
    extensions.conditionally_enable("zwlr_layer_shell_v1", [&](auto const& info)
        {
            EXPECT_THAT(info.user_preference(), Eq(std::make_optional(false)));
            condition_called = true;
            return true;
        });
    add_to_environment("MIR_SERVER_WAYLAND_EXTENSIONS", "xdg_wm_base");

    add_server_init(extensions);
    start_server();
    run_client_enumerator(server());

    EXPECT_THAT(condition_called, Eq(true));
}

TEST_F(WaylandExtensions, conditionally_enable_can_enable_extension)
{
    miral::WaylandExtensions extensions;

    extensions.conditionally_enable("zwlr_layer_shell_v1", [&](auto const&)
        {
            return true;
        });

    add_server_init(extensions);
    start_server();
    auto const interfaces = run_client_enumerator(server());

    EXPECT_THAT(interfaces, Contains(Eq("zwlr_layer_shell_v1")));
}

TEST_F(WaylandExtensions, conditionally_enable_can_disable_extension)
{
    miral::WaylandExtensions extensions;

    extensions.conditionally_enable("zwlr_layer_shell_v1", [&](auto const&)
        {
            return false;
        });

    add_server_init(extensions);
    start_server();
    auto const interfaces = run_client_enumerator(server());

    EXPECT_THAT(interfaces, Not(Contains(Eq("zwlr_layer_shell_v1"))));
}

TEST_F(WaylandExtensions, conditionally_enable_can_enable_extension_disabled_by_user)
{
    miral::WaylandExtensions extensions;

    extensions.conditionally_enable("zwlr_layer_shell_v1", [&](auto const&)
        {
            return true;
        });

    add_to_environment("MIR_SERVER_DROP_WAYLAND_EXTENSIONS", "zwlr_layer_shell_v1");
    add_server_init(extensions);
    start_server();
    auto const interfaces = run_client_enumerator(server());

    EXPECT_THAT(interfaces, Contains(Eq("zwlr_layer_shell_v1")));
}

TEST_F(WaylandExtensions, conditionally_enable_can_disable_extension_enabled_by_user)
{
    miral::WaylandExtensions extensions;

    extensions.conditionally_enable("zwlr_layer_shell_v1", [&](auto const&)
        {
            return false;
        });

    add_to_environment("MIR_SERVER_ADD_WAYLAND_EXTENSIONS", "zwlr_layer_shell_v1");
    add_server_init(extensions);
    start_server();
    auto const interfaces = run_client_enumerator(server());

    EXPECT_THAT(interfaces, Not(Contains(Eq("zwlr_layer_shell_v1"))));
}
