/*
 * Copyright (C) 2020 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"

#include <WebCore/CommonAtomStrings.h>
#include <WebCore/DocumentInlines.h>
#include <WebCore/HTMLBodyElement.h>
#include <WebCore/HTMLDivElement.h>
#include <WebCore/HTMLHtmlElement.h>
#include <WebCore/HTMLTextAreaElement.h>
#include <WebCore/Position.h>
#include <WebCore/Settings.h>
#include <WebCore/ShadowRoot.h>
#include <WebCore/SimpleRange.h>
#include <WebCore/TextControlInnerElements.h>
#include <WebCore/WebKitFontFamilyNames.h>

// FIXME(https://webkit.org/b/228175): Expose the functions tested here in WebKit internals object, then replace this test with one written in JavaScript.
// FIXME: When doing the above, don't forget to remove the many WEBCORE_EXPORT that were added so we could compile and link this test.

#define EXPECT_BOTH(a, b, forward, reversed) do { EXPECT_STREQ(string(documentOrder(a, b)), forward); EXPECT_STREQ(string(documentOrder(b, a)), reversed); } while (0)
#define EXPECT_EQUIVALENT(a, b) EXPECT_BOTH(a, b, "equivalent", "equivalent")
#define EXPECT_LESS(a, b) EXPECT_BOTH(a, b, "less", "greater")
#define EXPECT_UNORDERED(a, b) EXPECT_BOTH(a, b, "unordered", "unordered")

namespace TestWebKitAPI {

using namespace WebCore;

static Ref<Document> createDocument()
{
    HTMLNames::init();
    WebKitFontFamilyNames::init();
    initializeCommonAtomStrings();

    auto settings = Settings::create(nullptr);
    auto document = Document::create(settings.get(), aboutBlankURL());
    auto documentElement = HTMLHtmlElement::create(document);
    document->appendChild(documentElement);
    auto body = HTMLBodyElement::create(document);
    documentElement->appendChild(body);
    return document;
}

static constexpr const char* string(PartialOrdering ordering)
{
    if (is_lt(ordering))
        return "less";
    if (is_gt(ordering))
        return "greater";
    if (is_eq(ordering))
        return "equivalent";
    return "unordered";
}

static PartialOrdering operator-(PartialOrdering ordering)
{
    if (is_lt(ordering))
        return PartialOrdering::greater;
    if (is_gt(ordering))
        return PartialOrdering::less;
    return ordering;
}

static Vector<Position> allPositionTypes(Node* node, unsigned offset)
{
    Vector<Position> positions;
    bool canContainRangeEndpoint = !node || node->canContainRangeEndPoint();
    if (canContainRangeEndpoint)
        positions.append(makeContainerOffsetPosition(node, offset));
    if (!node && !offset) {
        positions.append({ nullptr, Position::PositionIsBeforeAnchor });
        positions.append({ nullptr, Position::PositionIsAfterAnchor });
    } else {
        if (canContainRangeEndpoint && !offset)
            positions.append({ node, Position::PositionIsBeforeChildren });
        if (canContainRangeEndpoint && offset == (node ? node->length() : 0))
            positions.append({ node, Position::PositionIsAfterChildren });
        if (node) {
            if (offset) {
                if (auto childBefore = node->traverseToChildAt(offset - 1))
                    positions.append({ childBefore, Position::PositionIsAfterAnchor });
            }
            if (auto childAfter = node->traverseToChildAt(offset))
                positions.append({ childAfter, Position::PositionIsBeforeAnchor });
        }
    }
    return positions;
}

static constexpr const char* typeStringSuffix(const Position& position)
{
    switch (position.anchorType()) {
    case Position::PositionIsOffsetInAnchor:
        return "";
    case Position::PositionIsBeforeChildren:
        return "[before-children]";
    case Position::PositionIsAfterChildren:
        return "[after-children]";
    case Position::PositionIsBeforeAnchor:
        return "[before]";
    case Position::PositionIsAfterAnchor:
        return "[after]";
    }
    return "[invalid]";
}

static String join(const Vector<String>& vector, ASCIILiteral separator)
{
    StringBuilder builder;
    bool needSeparator = false;
    for (auto& string : vector) {
        if (needSeparator)
            builder.append(separator);
        builder.append(string);
        needSeparator = true;
    }
    return builder.toString();
}

static CString allPositionTypeFailures(const Position& a, Node* nodeB, unsigned offsetB, PartialOrdering expectedResult)
{
    Vector<String> failures;
    for (auto& b : allPositionTypes(nodeB, offsetB)) {
        auto result = string(documentOrder(a, b));
        if (strcmp(result, string(expectedResult)))
            failures.append(makeString("order(b", typeStringSuffix(b), ")=", result, "<expected:", string(expectedResult), '>'));
        result = string(documentOrder(b, a));
        if (strcmp(result, string(-expectedResult)))
            failures.append(makeString("order(b", typeStringSuffix(b), ")=", result, "<expected:", string(-expectedResult), '>'));
    }
    return join(failures, ", "_s).utf8();
}

static CString allPositionTypeFailures(Node* nodeA, unsigned offsetA, Node* nodeB, unsigned offsetB, PartialOrdering expectedResult)
{
    Vector<String> failures;
    for (auto& a : allPositionTypes(nodeA, offsetA)) {
        for (auto& b : allPositionTypes(nodeB, offsetB)) {
            auto result = string(documentOrder(a, b));
            if (strcmp(result, string(expectedResult)))
                failures.append(makeString("order(a", typeStringSuffix(a), ",b", typeStringSuffix(b), ")=", result, "<expected:", string(expectedResult), '>'));
            result = string(documentOrder(b, a));
            if (strcmp(result, string(-expectedResult)))
                failures.append(makeString("order(b", typeStringSuffix(b), ",a", typeStringSuffix(a), ")=", result, "<expected:", string(-expectedResult), '>'));
        }
    }
    return join(failures, " | "_s).utf8();
}

static CString allPositionTypeFailures(Node& nodeA, unsigned offsetA, Node& nodeB, unsigned offsetB, PartialOrdering expectedResult)
{
    return allPositionTypeFailures(&nodeA, offsetA, &nodeB, offsetB, expectedResult);
}

static CString allPositionTypeFailures(Node* nodeA, unsigned offsetA, Node& nodeB, unsigned offsetB, PartialOrdering expectedResult)
{
    return allPositionTypeFailures(nodeA, offsetA, &nodeB, offsetB, expectedResult);
}

static CString allPositionTypeFailures(const Position& a, Node& nodeB, unsigned offsetB, PartialOrdering expectedResult)
{
    return allPositionTypeFailures(a, &nodeB, offsetB, expectedResult);
}

#define TEST_ALL_POSITION_TYPES(nodeA, offsetA, nodeB, offsetB, expectedResult) \
    EXPECT_STREQ(allPositionTypeFailures(nodeA, offsetA, nodeB, offsetB, PartialOrdering::expectedResult).data(), "")
#define TEST_ALL_POSITION_TYPES_B(positionA, nodeB, offsetB, expectedResult) \
    EXPECT_STREQ(allPositionTypeFailures(positionA, nodeB, offsetB, PartialOrdering::expectedResult).data(), "")

static Position makePositionBefore(Node& node)
{
    return { &node, Position::PositionIsBeforeAnchor };
}

static Position makePositionAfter(Node& node)
{
    return { &node, Position::PositionIsAfterAnchor };
}

TEST(DocumentOrder, Positions)
{
    auto document = createDocument();
    auto& body = *document->body();

    TEST_ALL_POSITION_TYPES(nullptr, 0, nullptr, 0, equivalent);
    TEST_ALL_POSITION_TYPES(nullptr, 1, nullptr, 0, equivalent);
    TEST_ALL_POSITION_TYPES(nullptr, 1, nullptr, 1, equivalent);

    TEST_ALL_POSITION_TYPES(nullptr, 0, document, 0, unordered);
    TEST_ALL_POSITION_TYPES(nullptr, 0, document, 1, unordered);

    TEST_ALL_POSITION_TYPES(document, 0, document, 0, equivalent);
    TEST_ALL_POSITION_TYPES(document, 0, document, 1, less);

    TEST_ALL_POSITION_TYPES(document, 0, body, 0, less);
    TEST_ALL_POSITION_TYPES(body, 0, document, 1, less);

    auto a = HTMLDivElement::create(document);
    body.appendChild(a);
    TEST_ALL_POSITION_TYPES(body, 0, a, 0, less);
    TEST_ALL_POSITION_TYPES(a, 0, body, 1, less);

    auto b = HTMLDivElement::create(document);
    body.appendChild(b);
    TEST_ALL_POSITION_TYPES(body, 0, b, 0, less);
    TEST_ALL_POSITION_TYPES(body, 1, b, 0, less);
    TEST_ALL_POSITION_TYPES(b, 0, body, 2, less);

    auto c = HTMLDivElement::create(document);
    b->appendChild(c);
    TEST_ALL_POSITION_TYPES(body, 0, c, 0, less);
    TEST_ALL_POSITION_TYPES(body, 1, c, 0, less);
    TEST_ALL_POSITION_TYPES(c, 0, body, 2, less);
    TEST_ALL_POSITION_TYPES(a, 0, c, 0, less);
    TEST_ALL_POSITION_TYPES(a, 1, c, 0, less);
    TEST_ALL_POSITION_TYPES(b, 0, c, 0, less);
    TEST_ALL_POSITION_TYPES(c, 0, b, 1, less);

    auto d = HTMLDivElement::create(document);
    a->appendChild(d);
    TEST_ALL_POSITION_TYPES(body, 0, d, 0, less);
    TEST_ALL_POSITION_TYPES(d, 0, body, 1, less);
    TEST_ALL_POSITION_TYPES(a, 0, d, 0, less);
    TEST_ALL_POSITION_TYPES(d, 0, a, 1, less);
    TEST_ALL_POSITION_TYPES(d, 0, b, 0, less);
    TEST_ALL_POSITION_TYPES(d, 0, b, 1, less);
    TEST_ALL_POSITION_TYPES(d, 0, c, 0, less);
    TEST_ALL_POSITION_TYPES(d, 0, c, 1, less);

    auto e = HTMLDivElement::create(document);
    EXPECT_UNORDERED(makePositionBefore(document), makePositionBefore(e));
    EXPECT_UNORDERED(makePositionBefore(document), makePositionAfter(e));
    EXPECT_UNORDERED(makePositionAfter(document), makePositionBefore(e));
    EXPECT_UNORDERED(makePositionAfter(document), makePositionAfter(e));

    auto f = HTMLDivElement::create(document);
    e->appendChild(f);
    TEST_ALL_POSITION_TYPES(body, 1, f, 0, unordered);

    auto g = HTMLTextAreaElement::create(document);
    c->appendChild(g);
    TEST_ALL_POSITION_TYPES(body, 1, g, 0, less);
    TEST_ALL_POSITION_TYPES(c, 0, c, 1, less);
    TEST_ALL_POSITION_TYPES(c, 1, c, 2, less);
    TEST_ALL_POSITION_TYPES(c, 0, c, 2, less);
    TEST_ALL_POSITION_TYPES(c, 0, g, 0, less);
    TEST_ALL_POSITION_TYPES(g, 0, c, 1, less);
    TEST_ALL_POSITION_TYPES(g, 0, c, 2, less);

    auto& h = *g->innerTextElement();
    TEST_ALL_POSITION_TYPES(body, 1, h, 0, less);
    TEST_ALL_POSITION_TYPES(c, 0, h, 0, less);
    TEST_ALL_POSITION_TYPES(h, 0, c, 1, less);
    TEST_ALL_POSITION_TYPES(h, 0, c, 2, less);

    EXPECT_EQUIVALENT(makePositionBefore(document), makePositionBefore(document));
    EXPECT_EQUIVALENT(makePositionAfter(document), makePositionAfter(document));
    EXPECT_LESS(makePositionBefore(document), makePositionAfter(document));

    TEST_ALL_POSITION_TYPES_B(makePositionBefore(document), document, 0, less);
    TEST_ALL_POSITION_TYPES_B(makePositionBefore(document), body, 0, less);
    TEST_ALL_POSITION_TYPES_B(makePositionBefore(document), document, 1, less);
    TEST_ALL_POSITION_TYPES_B(makePositionBefore(document), body, 1, less);
    TEST_ALL_POSITION_TYPES_B(makePositionBefore(document), h, 0, less);

    TEST_ALL_POSITION_TYPES_B(makePositionAfter(document), document, 0, greater);
    TEST_ALL_POSITION_TYPES_B(makePositionAfter(document), document, 1, greater);
    TEST_ALL_POSITION_TYPES_B(makePositionAfter(document), body, 0, greater);
    TEST_ALL_POSITION_TYPES_B(makePositionAfter(document), body, 1, greater);
    TEST_ALL_POSITION_TYPES_B(makePositionAfter(document), h, 0, greater);

    // FIXME: Add tests that cover shadow trees.
}

} // namespace TestWebKitAPI
