/*
 * Copyright (C) 2017-2021 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"

#if ENABLE(APPLICATION_MANIFEST)

#include <JavaScriptCore/InitializeThreading.h>
#include <WebCore/ApplicationManifestParser.h>
#include <wtf/RunLoop.h>

using namespace WebCore;

namespace WebCore {
static inline std::ostream& operator<<(std::ostream& os, const ApplicationManifest::Display& display)
{
    switch (display) {
    case ApplicationManifest::Display::Browser:
        return os << "ApplicationManifest::Display::Browser";
    case ApplicationManifest::Display::MinimalUI:
        return os << "ApplicationManifest::Display::MinimalUI";
    case ApplicationManifest::Display::Standalone:
        return os << "ApplicationManifest::Display::Standalone";
    case ApplicationManifest::Display::Fullscreen:
        return os << "ApplicationManifest::Display::Fullscreen";
    }
}
} // namespace WebCore

class ApplicationManifestParserTest : public testing::Test {
public:
    URL m_manifestURL;
    URL m_documentURL;

    virtual void SetUp()
    {
        JSC::initialize();
        WTF::initializeMainThread();

        m_manifestURL = URL { "https://example.com/manifest.json"_s };
        m_documentURL = URL { "https://example.com/"_s };
    }

    ApplicationManifest parseString(const String& data)
    {
        return ApplicationManifestParser::parse(data, m_manifestURL, m_documentURL);
    }

    ApplicationManifest parseTopLevelProperty(const String& key, const String& value)
    {
        String manifestContent = "{ \"" + key + "\" : " + value + " }";
        return parseString(manifestContent);
    }

    ApplicationManifest parseIconFirstTopLevelProperty(const String& key, const String& value)
    {
        String manifestContent = "{ \"icons\": [{\"" + key + "\": " + value + ", \"src\": \"icon/example.png\" }]}";
        return parseString(manifestContent);
    }

    ApplicationManifest parseIconFirstTopLevelPropertyForSrc(const String& key, const String& value)
    {
        String manifestContent = "{ \"icons\": [{\"" + key + "\": " + value + " }]}";
        return parseString(manifestContent);
    }

    void testStartURL(const String& rawJSON, const String& expectedValue)
    {
        testStartURL(rawJSON, { { }, expectedValue });
    }

    void testStartURL(const String& rawJSON, const URL& expectedValue)
    {
        auto manifest = parseTopLevelProperty("start_url"_s, rawJSON);
        auto value = manifest.startURL;
        EXPECT_STREQ(expectedValue.string().utf8().data(), value.string().utf8().data());
    }

    void testDisplay(const String& rawJSON, ApplicationManifest::Display expectedValue)
    {
        auto manifest = parseTopLevelProperty("display"_s, rawJSON);
        auto value = manifest.display;
        EXPECT_EQ(expectedValue, value);
    }

    void testName(const String& rawJSON, const String& expectedValue)
    {
        auto manifest = parseTopLevelProperty("name"_s, rawJSON);
        auto value = manifest.name;
        EXPECT_STREQ(expectedValue.utf8().data(), value.utf8().data());
    }

    void testDescription(const String& rawJSON, const String& expectedValue)
    {
        auto manifest = parseTopLevelProperty("description"_s, rawJSON);
        auto value = manifest.description;
        EXPECT_STREQ(expectedValue.utf8().data(), value.utf8().data());
    }

    void testShortName(const String& rawJSON, const String& expectedValue)
    {
        auto manifest = parseTopLevelProperty("short_name"_s, rawJSON);
        auto value = manifest.shortName;
        EXPECT_STREQ(expectedValue.utf8().data(), value.utf8().data());
    }

    void testScope(const String& rawJSON, const String& startURL, const String& expectedValue)
    {
        String manifestContent = "{ \"scope\" : " + rawJSON + ", \"start_url\" : \"" + startURL + "\" }";
        auto manifest = parseString(manifestContent);
        auto value = manifest.scope;
        EXPECT_STREQ(expectedValue.utf8().data(), value.string().utf8().data());
    }

    void testScope(const String& rawJSON, const String& expectedValue)
    {
        testScope(rawJSON, String(), expectedValue);
    }

    void testThemeColor(const String& rawJSON, const Color& expectedValue)
    {
        auto manifest = parseTopLevelProperty("theme_color"_s, rawJSON);
        auto value = manifest.themeColor;
        EXPECT_EQ(expectedValue, value);
    }

    void testIconsSrc(const String& rawJSON, const URL& expectedValue)
    {
        auto manifest = parseIconFirstTopLevelPropertyForSrc("src"_s, rawJSON);
        auto value = manifest.icons[0].src;
        EXPECT_STREQ(expectedValue.string().utf8().data(), value.string().utf8().data());
    }

    void testIconsType(const String &rawJSON, const String& expectedValue)
    {
        auto manifest = parseIconFirstTopLevelProperty("type"_s, rawJSON);
        auto value = manifest.icons[0].type;
        EXPECT_STREQ(expectedValue.utf8().data(), value.utf8().data());
    }

    void testIconsSizes(const String &rawJSON, size_t expectedCount, size_t testIndex, const String& expectedValue)
    {
        auto manifest = parseIconFirstTopLevelProperty("sizes"_s, rawJSON);
        auto value = manifest.icons[0].sizes;
        EXPECT_EQ(expectedCount, value.size());
        EXPECT_TRUE(testIndex < value.size());
        EXPECT_STREQ(expectedValue.utf8().data(), value[testIndex].utf8().data());
    }

    void testIconsPurposes(const String &rawJSON, OptionSet<ApplicationManifest::Icon::Purpose> expectedValues)
    {
        auto manifest = parseIconFirstTopLevelProperty("purpose"_s, rawJSON);
        auto value = manifest.icons[0].purposes;
        EXPECT_EQ(expectedValues, value);
    }

};

static void assertManifestHasDefaultValues(const URL& manifestURL, const URL& documentURL, const ApplicationManifest& manifest)
{
    EXPECT_TRUE(manifest.name.isNull());
    EXPECT_TRUE(manifest.shortName.isNull());
    EXPECT_TRUE(manifest.description.isNull());
    EXPECT_STREQ("https://example.com/", manifest.scope.string().utf8().data());
    EXPECT_STREQ(documentURL.string().utf8().data(), manifest.startURL.string().utf8().data());
}

TEST_F(ApplicationManifestParserTest, DefaultManifest)
{
    assertManifestHasDefaultValues(m_manifestURL, m_documentURL, parseString(String()));
    assertManifestHasDefaultValues(m_manifestURL, m_documentURL, parseString(""_s));
    assertManifestHasDefaultValues(m_manifestURL, m_documentURL, parseString("{ }"_s));
    assertManifestHasDefaultValues(m_manifestURL, m_documentURL, parseString("This is 100% not JSON."_s));
}

TEST_F(ApplicationManifestParserTest, StartURL)
{
    m_documentURL = URL { "https://example.com/home"_s };
    m_manifestURL = URL { "https://example.com/manifest.json"_s };

    testStartURL("123"_s, m_documentURL);
    testStartURL("null"_s, m_documentURL);
    testStartURL("true"_s, m_documentURL);
    testStartURL("{ }"_s, m_documentURL);
    testStartURL("[ ]"_s, m_documentURL);
    testStartURL("[ \"http://example.com/somepage\" ]"_s, m_documentURL);
    testStartURL("\"\""_s, m_documentURL);
    testStartURL("\"http:?\""_s, m_documentURL);

    testStartURL("\"appstartpage\""_s, "https://example.com/appstartpage"_s);
    testStartURL("\"a/b/cdefg\""_s, "https://example.com/a/b/cdefg"_s);

    m_documentURL = URL { "https://example.com/subfolder/home"_s };
    m_manifestURL = URL { "https://example.com/resources/manifest.json"_s };

    testStartURL("\"resource-relative-to-manifest-url\""_s, "https://example.com/resources/resource-relative-to-manifest-url"_s);
    testStartURL("\"http://different-page.com/12/34\""_s, m_documentURL);

    m_documentURL = URL { "https://example.com/home"_s };
    m_manifestURL = URL { "https://other-domain.com/manifiest.json"_s };

    testStartURL("\"resource_on_other_domain\""_s, m_documentURL);
    testStartURL("\"http://example.com/scheme-does-not-match-document\""_s, m_documentURL);
    testStartURL("\"https://example.com:123/port-does-not-match-document"_s, m_documentURL);
    testStartURL("\"https://example.com/page2\""_s, "https://example.com/page2"_s);
    testStartURL("\"//example.com/page2\""_s, "https://example.com/page2"_s);

    m_documentURL = URL { "https://example.com/a"_s };
    m_manifestURL = URL { "https://example.com/z/manifest.json"_s };

    testStartURL("\"b/c\""_s, "https://example.com/z/b/c"_s);
    testStartURL("\"/b/c\""_s, "https://example.com/b/c"_s);
    testStartURL("\"?query\""_s, "https://example.com/z/manifest.json?query"_s);

    m_documentURL = URL { "https://example.com/dir1/dir2/page1"_s };
    m_manifestURL = URL { "https://example.com/dir3/manifest.json"_s };

    testStartURL("\"../page2\""_s, "https://example.com/page2"_s);
}

TEST_F(ApplicationManifestParserTest, Display)
{
    testDisplay("123"_s, ApplicationManifest::Display::Browser);
    testDisplay("null"_s, ApplicationManifest::Display::Browser);
    testDisplay("true"_s, ApplicationManifest::Display::Browser);
    testDisplay("{ }"_s, ApplicationManifest::Display::Browser);
    testDisplay("[ ]"_s, ApplicationManifest::Display::Browser);
    testDisplay("\"\""_s, ApplicationManifest::Display::Browser);
    testDisplay("\"garbage string\""_s, ApplicationManifest::Display::Browser);

    testDisplay("\"browser\""_s, ApplicationManifest::Display::Browser);
    testDisplay("\"standalone\""_s, ApplicationManifest::Display::Standalone);
    testDisplay("\"minimal-ui\""_s, ApplicationManifest::Display::MinimalUI);
    testDisplay("\"fullscreen\""_s, ApplicationManifest::Display::Fullscreen);
    testDisplay("\"\t\nMINIMAL-UI \""_s, ApplicationManifest::Display::MinimalUI);
}

TEST_F(ApplicationManifestParserTest, Name)
{
    testName("123"_s, String());
    testName("null"_s, String());
    testName("true"_s, String());
    testName("{ }"_s, String());
    testName("[ ]"_s, String());
    testName("\"\""_s, emptyString());
    testName("\"example\""_s, "example"_s);
    testName("\"\\t Hello\\nWorld\\t \""_s, "Hello\nWorld"_s);
}

TEST_F(ApplicationManifestParserTest, Description)
{
    testDescription("123"_s, String());
    testDescription("null"_s, String());
    testDescription("true"_s, String());
    testDescription("{ }"_s, String());
    testDescription("[ ]"_s, String());
    testDescription("\"\""_s, emptyString());
    testDescription("\"example\""_s, "example"_s);
    testDescription("\"\\t Hello\\nWorld\\t \""_s, "Hello\nWorld"_s);
}

TEST_F(ApplicationManifestParserTest, ShortName)
{
    testShortName("123"_s, String());
    testShortName("null"_s, String());
    testShortName("true"_s, String());
    testShortName("{ }"_s, String());
    testShortName("[ ]"_s, String());
    testShortName("\"\""_s, ""_s);
    testShortName("\"example\""_s, "example"_s);
    testShortName("\"\\t Hello\\nWorld\\t \""_s, "Hello\nWorld"_s);
}

TEST_F(ApplicationManifestParserTest, Scope)
{
    // If the scope is not a string or not a valid URL, return the default scope (the parent path of the start URL).
    m_documentURL = URL { "https://example.com/a/page?queryParam=value#fragment"_s };
    m_manifestURL = URL { "https://example.com/manifest.json"_s };
    testScope("123"_s, "https://example.com/a/"_s);
    testScope("null"_s, "https://example.com/a/"_s);
    testScope("true"_s, "https://example.com/a/"_s);
    testScope("{ }"_s, "https://example.com/a/"_s);
    testScope("[ ]"_s, "https://example.com/a/"_s);
    testScope("\"\""_s, "https://example.com/a/"_s);
    testScope("\"http:?\""_s, "https://example.com/a/"_s);

    m_documentURL = URL { "https://example.com/a/pageEndingWithSlash/"_s };
    testScope("null"_s, "https://example.com/a/pageEndingWithSlash/"_s);

    // If scope URL is not same origin as document URL, return the default scope.
    m_documentURL = URL { "https://example.com/home"_s };
    m_manifestURL = URL { "https://other-site.com/manifest.json"_s };
    testScope("\"https://other-site.com/some-scope\""_s, "https://example.com/"_s);

    m_documentURL = URL { "https://example.com/app/home"_s };
    m_manifestURL = URL { "https://example.com/app/manifest.json"_s };

    // If start URL is not within scope of scope URL, return the default scope.
    testScope("\"https://example.com/subdirectory\""_s, "https://example.com/app/"_s);
    testScope("\"https://example.com/app\""_s, "https://example.com/app"_s);
    testScope("\"https://example.com/APP\""_s, "https://example.com/app/"_s);
    testScope("\"https://example.com/a\""_s, "https://example.com/a"_s);

    m_documentURL = URL { "https://example.com/a/b/c/index"_s };
    m_manifestURL = URL { "https://example.com/a/manifest.json"_s };

    testScope("\"./b/c/index\""_s, "https://example.com/a/b/c/index"_s);
    testScope("\"b/somewhere-else/../c\""_s, "https://example.com/a/b/c"_s);
    testScope("\"b\""_s, "https://example.com/a/b"_s);
    testScope("\"b/\""_s, "https://example.com/a/b/"_s);

    m_documentURL = URL { "https://example.com/documents/home"_s };
    m_manifestURL = URL { "https://example.com/resources/manifest.json"_s };

    // It's fine if the document URL or manifest URL aren't within the application scope - only the start URL needs to be.
    testScope("\"https://example.com/other\""_s, "https://example.com/other/start-url"_s, "https://example.com/other"_s);
}

TEST_F(ApplicationManifestParserTest, ThemeColor)
{
    testThemeColor("123"_s, Color());
    testThemeColor("null"_s, Color());
    testThemeColor("true"_s, Color());
    testThemeColor("{ }"_s, Color());
    testThemeColor("[ ]"_s, Color());
    testThemeColor("\"\""_s, Color());
    testThemeColor("\"garbage string\""_s, Color());

    testThemeColor("\"red\""_s, Color::red);
    testThemeColor("\"#f00\""_s, Color::red);
    testThemeColor("\"#ff0000\""_s, Color::red);
    testThemeColor("\"#ff0000ff\""_s, Color::red);
    testThemeColor("\"rgb(255, 0, 0)\""_s, Color::red);
    testThemeColor("\"rgba(255, 0, 0, 1)\""_s, Color::red);
    testThemeColor("\"hsl(0, 100%, 50%)\""_s, Color::red);
    testThemeColor("\"hsla(0, 100%, 50%, 1)\""_s, Color::red);
}

TEST_F(ApplicationManifestParserTest, Whitespace)
{
    auto manifest = parseString("  { \"name\": \"PASS\" }\n"_s);

    EXPECT_STREQ("PASS", manifest.name.utf8().data());
}

TEST_F(ApplicationManifestParserTest, Icons)
{
    URL srcURL = URL { "https://example.com/icon.jpg"_s };
    testIconsSrc("\"icon.jpg\""_s, srcURL);
    testIconsType("\"image/webp\""_s, "image/webp"_s);
    testIconsSizes("\"256x256\""_s, 1, 0, "256x256"_s);
    testIconsSizes("\"72x72 96x96\""_s, 2, 0, "72x72"_s);
    testIconsSizes("\"72x72 96x96\""_s, 2, 1, "96x96"_s);

    OptionSet<ApplicationManifest::Icon::Purpose> purposeAny { ApplicationManifest::Icon::Purpose::Any };
    OptionSet<ApplicationManifest::Icon::Purpose> purposeMonochrome { ApplicationManifest::Icon::Purpose::Monochrome };
    OptionSet<ApplicationManifest::Icon::Purpose> purposeMaskable { ApplicationManifest::Icon::Purpose::Maskable };

    testIconsPurposes("\"monochrome\""_s, purposeMonochrome);
    testIconsPurposes("\"maskable\""_s, purposeMaskable);
    testIconsPurposes("\"any\""_s, purposeAny);
    testIconsPurposes("\"\tMONOCHROME\""_s, purposeMonochrome);

    testIconsPurposes("123"_s, purposeAny);
    testIconsPurposes("null"_s, purposeAny);
    testIconsPurposes("true"_s, purposeAny);
    testIconsPurposes("{ }"_s, purposeAny);
    testIconsPurposes("[ ]"_s, purposeAny);

    OptionSet<ApplicationManifest::Icon::Purpose> purposeMonochromeAny { ApplicationManifest::Icon::Purpose::Monochrome, ApplicationManifest::Icon::Purpose::Any };

    testIconsPurposes("\"monochrome any\""_s, purposeMonochromeAny);

}

#endif
