/*
 * Copyright (C) 2012 Igalia S.L.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public License
 * along with this library; see the file COPYING.LIB.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

#include "config.h"

#include "LoadTrackingTest.h"
#include "WebKitTestServer.h"
#include "WebKitWebsitePolicies.h"
#include <WebCore/SoupVersioning.h>
#include <wtf/glib/GRefPtr.h>
#include <wtf/text/CString.h>

static WebKitTestServer* kServer;

class PolicyClientTest: public LoadTrackingTest {
public:
    MAKE_GLIB_TEST_FIXTURE(PolicyClientTest);

    enum PolicyDecisionResponse {
        Use,
        UseWithPolicy,
        Ignore,
        Download,
        None
    };

    static void testHandlerMessageReceivedCallback(WebKitUserContentManager* userContentManager, WebKitJavascriptResult* javascriptResult, PolicyClientTest* test)
    {
        GUniquePtr<char> valueString(WebViewTest::javascriptResultToCString(javascriptResult));
        if (g_str_equal(valueString.get(), "autoplayed"))
            test->m_autoplayed = true;
        else if (g_str_equal(valueString.get(), "did-not-play"))
            test->m_autoplayed = false;

        g_main_loop_quit(test->m_mainLoop);
    }

    static gboolean quitMainLoopLater(GMainLoop* loop)
    {
        g_main_loop_quit(loop);
        return FALSE;
    }

    static void respondToPolicyDecision(PolicyClientTest* test, WebKitPolicyDecision* decision)
    {
        switch (test->m_policyDecisionResponse) {
        case Use:
            webkit_policy_decision_use(decision);
            break;
        case UseWithPolicy:
            webkit_policy_decision_use_with_policies(decision, test->m_websitePolicies.get());
            break;
        case Ignore:
            webkit_policy_decision_ignore(decision);
            break;
        case Download:
            webkit_policy_decision_download(decision);
            break;
        case None:
            break;
        }

        if (test->m_haltMainLoopAfterMakingDecision)
            g_idle_add(reinterpret_cast<GSourceFunc>(quitMainLoopLater), test->m_mainLoop);
    }

    static gboolean respondToPolicyDecisionLater(PolicyClientTest* test)
    {
        respondToPolicyDecision(test, test->m_previousPolicyDecision.get());
        test->m_previousPolicyDecision = 0;
        return FALSE;
    }

    static gboolean decidePolicyCallback(WebKitWebView* webView, WebKitPolicyDecision* decision, WebKitPolicyDecisionType type, PolicyClientTest* test)
    {
        if (test->m_policyDecisionTypeFilter != type)
            return FALSE;

        test->m_previousPolicyDecision = decision;
        if (test->m_respondToPolicyDecisionAsynchronously) {
            g_idle_add(reinterpret_cast<GSourceFunc>(respondToPolicyDecisionLater), test);
            return TRUE;
        }

        respondToPolicyDecision(test, decision);

        // We return FALSE here to ensure that the default policy decision
        // handler doesn't override whatever we use here.
        return FALSE;
    }

    PolicyClientTest()
        : LoadTrackingTest()
    {
        g_signal_connect(m_webView, "decide-policy", G_CALLBACK(decidePolicyCallback), this);
        webkit_user_content_manager_register_script_message_handler(m_userContentManager.get(), "testHandler");
        g_signal_connect(m_userContentManager.get(), "script-message-received::testHandler", G_CALLBACK(testHandlerMessageReceivedCallback), this);
    }

    bool loadURIAndWaitForAutoPlayed(const char* uri, WebKitAutoplayPolicy policy)
    {
        m_autoplayed = std::nullopt;
        m_websitePolicies = adoptGRef(webkit_website_policies_new_with_policies("autoplay", policy, nullptr));
        m_policyDecisionResponse = PolicyClientTest::UseWithPolicy;

        loadURI(uri);
        // Run until the user content messages come back from the test HTML.
        g_main_loop_run(m_mainLoop);
        return m_autoplayed.value_or(false);
    }

    bool runJSAndWaitForAutoPlayed(const char* script)
    {
        runJavaScriptWithoutForcedUserGesturesAndWaitUntilFinished(script, nullptr);

        // Spin until autoplay status is reported, the run JS API can
        // complete its main loop before the promises in autoplay-check fire.
        while (!m_autoplayed.has_value())
            g_main_loop_run(m_mainLoop);

        return *m_autoplayed;
    }

    PolicyDecisionResponse m_policyDecisionResponse { None };
    int m_policyDecisionTypeFilter { 0 };
    bool m_respondToPolicyDecisionAsynchronously { false };
    bool m_haltMainLoopAfterMakingDecision { false };
    std::optional<bool> m_autoplayed;
    GRefPtr<WebKitPolicyDecision> m_previousPolicyDecision;
    GRefPtr<WebKitWebsitePolicies> m_websitePolicies;
};

static void testNavigationPolicy(PolicyClientTest* test, gconstpointer)
{
    test->m_policyDecisionTypeFilter = WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION;

    test->m_policyDecisionResponse = PolicyClientTest::Use;
    test->loadHtml("<html/>", "http://webkitgtk.org/");
    test->waitUntilLoadFinished();
    g_assert_cmpint(test->m_loadEvents.size(), ==, 3);

    // Ideally we'd like to have a more intensive test here, but it's still pretty tricky
    // to trigger different types of navigations with the GTK+ WebKit2 API.
    WebKitNavigationPolicyDecision* decision = WEBKIT_NAVIGATION_POLICY_DECISION(test->m_previousPolicyDecision.get());
    WebKitNavigationAction* navigationAction = webkit_navigation_policy_decision_get_navigation_action(decision);
    g_assert_cmpint(webkit_navigation_action_get_navigation_type(navigationAction), ==, WEBKIT_NAVIGATION_TYPE_OTHER);
    g_assert_cmpint(webkit_navigation_action_get_mouse_button(navigationAction), ==, 0);
    g_assert_cmpint(webkit_navigation_action_get_modifiers(navigationAction), ==, 0);
    g_assert_false(webkit_navigation_action_is_redirect(navigationAction));
    g_assert_null(webkit_navigation_policy_decision_get_frame_name(decision));
    WebKitURIRequest* request = webkit_navigation_action_get_request(navigationAction);
    g_assert_cmpstr(webkit_uri_request_get_uri(request), ==, "http://webkitgtk.org/");

    test->m_policyDecisionResponse = PolicyClientTest::Use;
    test->m_respondToPolicyDecisionAsynchronously = true;
    test->loadHtml("<html/>", "http://webkitgtk.org/");
    test->waitUntilLoadFinished();
    g_assert_cmpint(test->m_loadEvents.size(), ==, 3);

    test->m_policyDecisionResponse = PolicyClientTest::Use;
    test->m_respondToPolicyDecisionAsynchronously = false;
    test->loadURI(kServer->getURIForPath("/redirect").data());
    test->waitUntilLoadFinished();
    g_assert_cmpint(test->m_loadEvents.size(), ==, 4);

    decision = WEBKIT_NAVIGATION_POLICY_DECISION(test->m_previousPolicyDecision.get());
    navigationAction = webkit_navigation_policy_decision_get_navigation_action(decision);
    g_assert_true(webkit_navigation_action_is_redirect(navigationAction));
    g_assert_null(webkit_navigation_policy_decision_get_frame_name(decision));
    request = webkit_navigation_action_get_request(navigationAction);
    g_assert_cmpstr(webkit_uri_request_get_uri(request), ==, kServer->getURIForPath("/").data());

    // If we are waiting until load completion, it will never complete if we ignore the
    // navigation. So we tell the main loop to quit sometime later.
    test->m_policyDecisionResponse = PolicyClientTest::Ignore;
    test->m_respondToPolicyDecisionAsynchronously = false;
    test->m_haltMainLoopAfterMakingDecision = true;
    test->loadHtml("<html/>", "http://webkitgtk.org/");
    test->waitUntilLoadFinished();
    g_assert_cmpint(test->m_loadEvents.size(), ==, 0);

    test->m_policyDecisionResponse = PolicyClientTest::Ignore;
    test->loadHtml("<html/>", "http://webkitgtk.org/");
    test->waitUntilLoadFinished();
    g_assert_cmpint(test->m_loadEvents.size(), ==, 0);
}

static void testResponsePolicy(PolicyClientTest* test, gconstpointer)
{
    test->m_policyDecisionTypeFilter = WEBKIT_POLICY_DECISION_TYPE_RESPONSE;

    test->m_policyDecisionResponse = PolicyClientTest::Use;
    test->loadURI(kServer->getURIForPath("/").data());
    test->waitUntilLoadFinished();
    g_assert_cmpint(test->m_loadEvents.size(), ==, 3);
    g_assert_cmpint(test->m_loadEvents[0], ==, LoadTrackingTest::ProvisionalLoadStarted);
    g_assert_cmpint(test->m_loadEvents[1], ==, LoadTrackingTest::LoadCommitted);
    g_assert_cmpint(test->m_loadEvents[2], ==, LoadTrackingTest::LoadFinished);

    WebKitResponsePolicyDecision* decision = WEBKIT_RESPONSE_POLICY_DECISION(test->m_previousPolicyDecision.get());
    WebKitURIRequest* request = webkit_response_policy_decision_get_request(decision);
    g_assert_true(WEBKIT_IS_URI_REQUEST(request));
    ASSERT_CMP_CSTRING(webkit_uri_request_get_uri(request), ==, kServer->getURIForPath("/"));
    WebKitURIResponse* response = webkit_response_policy_decision_get_response(decision);
    g_assert_true(WEBKIT_IS_URI_RESPONSE(response));
    ASSERT_CMP_CSTRING(webkit_uri_response_get_uri(response), ==, kServer->getURIForPath("/"));
    g_assert_cmpint(webkit_web_view_can_show_mime_type(test->m_webView, webkit_uri_response_get_mime_type(response)), ==,
        webkit_response_policy_decision_is_mime_type_supported(decision));

    test->m_respondToPolicyDecisionAsynchronously = true;
    test->loadURI(kServer->getURIForPath("/").data());
    test->waitUntilLoadFinished();
    g_assert_cmpint(test->m_loadEvents.size(), ==, 3);
    g_assert_cmpint(test->m_loadEvents[0], ==, LoadTrackingTest::ProvisionalLoadStarted);
    g_assert_cmpint(test->m_loadEvents[1], ==, LoadTrackingTest::LoadCommitted);
    g_assert_cmpint(test->m_loadEvents[2], ==, LoadTrackingTest::LoadFinished);

    test->m_respondToPolicyDecisionAsynchronously = false;
    test->m_policyDecisionResponse = PolicyClientTest::Ignore;
    test->loadURI(kServer->getURIForPath("/").data());
    test->waitUntilLoadFinished();

    g_assert_cmpint(test->m_loadEvents.size(), ==, 3);
    g_assert_cmpint(test->m_loadEvents[0], ==, LoadTrackingTest::ProvisionalLoadStarted);
    g_assert_cmpint(test->m_loadEvents[1], ==, LoadTrackingTest::ProvisionalLoadFailed);
    g_assert_cmpint(test->m_loadEvents[2], ==, LoadTrackingTest::LoadFinished);
}

struct CreateCallbackData {
    bool triedToOpenWindow;
    GMainLoop* mainLoop;
};

static WebKitWebView* createCallback(WebKitWebView* webView, WebKitNavigationAction*, CreateCallbackData* data)
{
    data->triedToOpenWindow = true;
    g_main_loop_quit(data->mainLoop);
    return 0;
}

static void testNewWindowPolicy(PolicyClientTest* test, gconstpointer)
{
    static const char* windowOpeningHTML =
        "<html><body>"
        "    <a id=\"link\" href=\"http://www.google.com\" target=\"_blank\">Link</a>"
        "    <script>"
        "        var event = document.createEvent('MouseEvents');"
        "        event.initEvent('click', true, false);"
        "        document.getElementById('link').dispatchEvent(event);"
        "    </script>"
        "</body></html>";
    test->m_policyDecisionTypeFilter = WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION;
    webkit_settings_set_javascript_can_open_windows_automatically(webkit_web_view_get_settings(test->m_webView), TRUE);

    CreateCallbackData data;
    data.triedToOpenWindow = false;
    data.mainLoop = test->m_mainLoop;

    g_signal_connect(test->m_webView, "create", G_CALLBACK(createCallback), &data);
    test->m_policyDecisionResponse = PolicyClientTest::Use;
    test->loadHtml(windowOpeningHTML, "http://webkitgtk.org/");
    test->wait(1);
    g_assert_true(data.triedToOpenWindow);

    WebKitNavigationPolicyDecision* decision = WEBKIT_NAVIGATION_POLICY_DECISION(test->m_previousPolicyDecision.get());
    g_assert_cmpstr(webkit_navigation_policy_decision_get_frame_name(decision), ==, "_blank");

    // Using a short timeout is a bit ugly here, but it's hard to get around because if we block
    // the new window signal we cannot halt the main loop in the create callback. If we
    // halt the main loop in the policy decision, the create callback never executes.
    data.triedToOpenWindow = false;
    test->m_policyDecisionResponse = PolicyClientTest::Ignore;
    test->loadHtml(windowOpeningHTML, "http://webkitgtk.org/");
    test->wait(.2);
    g_assert_false(data.triedToOpenWindow);
}

#if USE(SOUP2)
static void serverCallback(SoupServer* server, SoupMessage* message, const char* path, GHashTable*, SoupClientContext*, gpointer)
#else
static void serverCallback(SoupServer* server, SoupServerMessage* message, const char* path, GHashTable*, gpointer)
#endif
{
    if (soup_server_message_get_method(message) != SOUP_METHOD_GET) {
        soup_server_message_set_status(message, SOUP_STATUS_NOT_IMPLEMENTED, nullptr);
        return;
    }

    if (g_str_equal(path, "/")) {
        static const char* responseString = "<html><body>Testing!</body></html>";
        soup_server_message_set_status(message, SOUP_STATUS_OK, nullptr);
        auto* responseBody = soup_server_message_get_response_body(message);
        soup_message_body_append(responseBody, SOUP_MEMORY_STATIC, responseString, strlen(responseString));
        soup_message_body_complete(responseBody);
    } else if (g_str_equal(path, "/redirect")) {
        soup_server_message_set_status(message, SOUP_STATUS_MOVED_PERMANENTLY, nullptr);
        soup_message_headers_append(soup_server_message_get_response_headers(message), "Location", "/");
    } else
        soup_server_message_set_status(message, SOUP_STATUS_NOT_FOUND, nullptr);
}

static void testAutoplayPolicy(PolicyClientTest* test, gconstpointer)
{
    // The web view must be realized for the video to start playback.
    test->showInWindow();

    test->m_policyDecisionTypeFilter = WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION;

    const char* resourceName = "autoplay-check.html";
    GUniquePtr<char> resourcePath(g_build_filename(Test::getResourcesDir(Test::WebKit2Resources).data(), resourceName, nullptr));
    GUniquePtr<char> resourceURL(g_filename_to_uri(resourcePath.get(), nullptr, nullptr));

    g_assert_false(test->loadURIAndWaitForAutoPlayed(resourceURL.get(), WEBKIT_AUTOPLAY_DENY));

    // Async test
    test->m_respondToPolicyDecisionAsynchronously = true;
    g_assert_false(test->loadURIAndWaitForAutoPlayed(resourceURL.get(), WEBKIT_AUTOPLAY_DENY));
    test->m_respondToPolicyDecisionAsynchronously = false;
    g_assert_true(test->loadURIAndWaitForAutoPlayed(resourceURL.get(), WEBKIT_AUTOPLAY_ALLOW));
    g_assert_false(test->loadURIAndWaitForAutoPlayed(resourceURL.get(), WEBKIT_AUTOPLAY_ALLOW_WITHOUT_SOUND));

    // Now check dynamically updating the loader policies
    g_assert_false(test->loadURIAndWaitForAutoPlayed(resourceURL.get(), WEBKIT_AUTOPLAY_DENY));
    // Check that JS can not side-step the active policy
    g_assert_false(test->runJSAndWaitForAutoPlayed("playVideo();"));

    // Silent audio track tests
    resourceName = "autoplay-no-audio-check.html";
    resourcePath.reset(g_build_filename(Test::getResourcesDir(Test::WebKit2Resources).data(), resourceName, nullptr));
    resourceURL.reset(g_filename_to_uri(resourcePath.get(), nullptr, nullptr));

    g_assert_false(test->loadURIAndWaitForAutoPlayed(resourceURL.get(), WEBKIT_AUTOPLAY_DENY));
    g_assert_true(test->loadURIAndWaitForAutoPlayed(resourceURL.get(), WEBKIT_AUTOPLAY_ALLOW));
    g_assert_true(test->loadURIAndWaitForAutoPlayed(resourceURL.get(), WEBKIT_AUTOPLAY_ALLOW_WITHOUT_SOUND));
}

void beforeAll()
{
    kServer = new WebKitTestServer();
    kServer->run(serverCallback);

    PolicyClientTest::add("WebKitPolicyClient", "navigation-policy", testNavigationPolicy);
    PolicyClientTest::add("WebKitPolicyClient", "response-policy", testResponsePolicy);
    PolicyClientTest::add("WebKitPolicyClient", "autoplay-policy", testAutoplayPolicy);
    // WARNING: This test must come last, it uses racey constructs that
    // interfere nondeterminisically with any test running after it.
    // https://bugs.webkit.org/show_bug.cgi?id=213190
    PolicyClientTest::add("WebKitPolicyClient", "new-window-policy", testNewWindowPolicy);
}

void afterAll()
{
    delete kServer;
}
