#include <QtTest/QTest>
#include <QtCore/QTemporaryFile>

#include "Outline.h"
#include "PDFDoc.h"
#include "PDFDocFactory.h"

class TestInternalOutline : public QObject
{
    Q_OBJECT
public:
    explicit TestInternalOutline(QObject *parent = nullptr) : QObject(parent) { }
private Q_SLOTS:
    static void testCreateOutline();
    static void testSetOutline();
    static void testInsertChild();
    static void testRemoveChild();
    static void testSetTitleAndSetPageDest();
};

void TestInternalOutline::testCreateOutline()
{
    QTemporaryFile tempFile;
    QVERIFY(tempFile.open());
    tempFile.close();

    const std::string tempFileName = tempFile.fileName().toStdString();

    std::unique_ptr<PDFDoc> doc = PDFDocFactory().createPDFDoc(GooString(TESTDATADIR "/unittestcases/truetype.pdf"));
    QVERIFY(doc.get());

    // ensure the file has no existing outline
    Outline *outline = doc->getOutline();
    QVERIFY(outline != nullptr);
    const auto *outlineItems = outline->getItems();
    QVERIFY(outlineItems == nullptr);

    // create an empty outline and save the file
    outline->setOutline({});
    outlineItems = outline->getItems();
    // no items will result in a nullptr rather than a 0 length list
    QVERIFY(outlineItems == nullptr);
    doc->saveAs(tempFileName);

    /******************************************************/

    const GooString gooTempFileName { tempFileName };
    doc = PDFDocFactory().createPDFDoc(gooTempFileName);
    QVERIFY(doc.get());

    // ensure the re-opened file has an outline with no items
    outline = doc->getOutline();
    QVERIFY(outline != nullptr);
    outlineItems = outline->getItems();
    QVERIFY(outlineItems == nullptr);
}

static std::string getTitle(const OutlineItem *item)
{
    const std::vector<Unicode> &u = item->getTitle();
    std::string s;
    for (const auto &c : u) {
        s.append(1, static_cast<char>(c));
    }
    return s;
}

void TestInternalOutline::testSetOutline()
{
    QTemporaryFile tempFile;
    QVERIFY(tempFile.open());
    tempFile.close();

    const std::string tempFileName = tempFile.fileName().toStdString();

    std::unique_ptr<PDFDoc> doc = PDFDocFactory().createPDFDoc(GooString(TESTDATADIR "/unittestcases/truetype.pdf"));
    QVERIFY(doc.get());

    // ensure the file has no existing outline
    Outline *outline = doc->getOutline();
    QVERIFY(outline != nullptr);
    const auto *outlineItems = outline->getItems();
    QVERIFY(outlineItems == nullptr);

    // create an outline and save the file
    outline->setOutline({ { .title = "1",
                            .destPageNum = 1,
                            .children = { { .title = "1.1", .destPageNum = 1, .children = {} },
                                          { .title = "1.2", .destPageNum = 2, .children = {} },
                                          { .title = "1.3",
                                            .destPageNum = 3,
                                            .children = { { .title = "1.3.1", .destPageNum = 1, .children = {} },
                                                          { .title = "1.3.2", .destPageNum = 2, .children = {} },
                                                          { .title = "1.3.3", .destPageNum = 3, .children = {} },
                                                          { .title = "1.3.4", .destPageNum = 4, .children = {} } } },
                                          { .title = "1.4", .destPageNum = 4, .children = {} } } },
                          { .title = "2", .destPageNum = 2, .children = {} },
                          { .title = "3", .destPageNum = 3, .children = {} },
                          { .title = "4", .destPageNum = 4, .children = {} } });
    outlineItems = outline->getItems();
    QVERIFY(outlineItems != nullptr);
    doc->saveAs(tempFileName);
    outline = nullptr;

    /******************************************************/

    const GooString gooTempFileName { tempFileName };
    doc = PDFDocFactory().createPDFDoc(gooTempFileName);
    QVERIFY(doc.get());

    // ensure the re-opened file has an outline
    outline = doc->getOutline();
    QVERIFY(outline != nullptr);
    outlineItems = outline->getItems();

    QVERIFY(outlineItems != nullptr);
    QVERIFY(outlineItems->size() == 4);

    OutlineItem *item = outlineItems->at(0);
    QVERIFY(item != nullptr);

    // c_str() is used so QCOMPARE prints string correctly on disagree
    QCOMPARE(getTitle(item).c_str(), "1");
    item = outlineItems->at(1);
    QVERIFY(item != nullptr);
    QCOMPARE(getTitle(item).c_str(), "2");
    item = outlineItems->at(2);
    QVERIFY(item != nullptr);
    QCOMPARE(getTitle(item).c_str(), "3");
    item = outlineItems->at(3);
    QVERIFY(item != nullptr);
    QCOMPARE(getTitle(item).c_str(), "4");

    outlineItems = outlineItems->at(0)->getKids();
    QVERIFY(outlineItems != nullptr);
    item = outlineItems->at(0);
    QVERIFY(item != nullptr);
    QCOMPARE(getTitle(item).c_str(), "1.1");
    item = outlineItems->at(1);
    QVERIFY(item != nullptr);
    QCOMPARE(getTitle(item).c_str(), "1.2");
    item = outlineItems->at(2);
    QVERIFY(item != nullptr);
    QCOMPARE(getTitle(item).c_str(), "1.3");
    item = outlineItems->at(3);
    QVERIFY(item != nullptr);
    QCOMPARE(getTitle(item).c_str(), "1.4");

    outlineItems = outlineItems->at(2)->getKids();
    QVERIFY(outlineItems != nullptr);

    item = outlineItems->at(0);
    QVERIFY(item != nullptr);
    QCOMPARE(getTitle(item).c_str(), "1.3.1");
    item = outlineItems->at(1);
    QVERIFY(item != nullptr);
    QCOMPARE(getTitle(item).c_str(), "1.3.2");
    item = outlineItems->at(2);
    QVERIFY(item != nullptr);
    QCOMPARE(getTitle(item).c_str(), "1.3.3");
    item = outlineItems->at(3);
    QVERIFY(item != nullptr);
    QCOMPARE(getTitle(item).c_str(), "1.3.4");
}

void TestInternalOutline::testInsertChild()
{
    QTemporaryFile tempFile;
    QVERIFY(tempFile.open());
    tempFile.close();
    QTemporaryFile tempFile2;
    QVERIFY(tempFile2.open());
    tempFile2.close();

    const std::string tempFileName = tempFile.fileName().toStdString();
    const std::string tempFileName2 = tempFile2.fileName().toStdString();

    std::unique_ptr<PDFDoc> doc = PDFDocFactory().createPDFDoc(GooString(TESTDATADIR "/unittestcases/truetype.pdf"));
    QVERIFY(doc.get());

    // ensure the file has no existing outline
    Outline *outline = doc->getOutline();
    QVERIFY(outline != nullptr);
    const auto *outlineItems = outline->getItems();
    QVERIFY(outlineItems == nullptr);

    // create an outline and save the file
    outline->setOutline({});
    doc->saveAs(tempFileName);
    outline = nullptr;

    /******************************************************/

    const GooString gooTempFileName { tempFileName };
    doc = PDFDocFactory().createPDFDoc(gooTempFileName);
    QVERIFY(doc.get());

    // ensure the re-opened file has an outline with no items
    outline = doc->getOutline();
    QVERIFY(outline != nullptr);
    // nullptr for 0-length
    QVERIFY(outline->getItems() == nullptr);

    // insert first one to empty
    outline->insertChild("2", 1, 0);
    // insert at the end
    outline->insertChild("3", 1, 1);
    // insert at the start
    outline->insertChild("1", 1, 0);

    // add an item to "2"
    outlineItems = outline->getItems();
    QVERIFY(outlineItems != nullptr);
    QVERIFY(outlineItems->at(1));
    outlineItems->at(1)->insertChild("2.1", 2, 0);
    outlineItems->at(1)->insertChild("2.2", 2, 1);
    outlineItems->at(1)->insertChild("2.4", 2, 2);

    outlineItems->at(1)->insertChild("2.3", 2, 2);

    // save the file
    doc->saveAs(tempFileName2);
    outline = nullptr;

    /******************************************************/

    const GooString gooTempFileName2 { tempFileName2 };
    doc = PDFDocFactory().createPDFDoc(gooTempFileName2);
    QVERIFY(doc.get());

    // ensure the re-opened file has an outline
    outline = doc->getOutline();
    QVERIFY(outline != nullptr);

    outlineItems = outline->getItems();

    QVERIFY(outlineItems != nullptr);
    QVERIFY(outlineItems->size() == 3);

    OutlineItem *item = outlineItems->at(0);
    QVERIFY(item != nullptr);

    // c_str() is used so QCOMPARE prints string correctly on disagree
    QCOMPARE(getTitle(item).c_str(), "1");
    item = outlineItems->at(1);
    QVERIFY(item != nullptr);
    QCOMPARE(getTitle(item).c_str(), "2");
    item = outlineItems->at(2);
    QVERIFY(item != nullptr);
    QCOMPARE(getTitle(item).c_str(), "3");

    outlineItems = outlineItems->at(1)->getKids();
    item = outlineItems->at(0);
    QVERIFY(item != nullptr);
    QCOMPARE(getTitle(item).c_str(), "2.1");
    item = outlineItems->at(1);
    QVERIFY(item != nullptr);
    QCOMPARE(getTitle(item).c_str(), "2.2");
    item = outlineItems->at(2);
    QVERIFY(item != nullptr);
    QCOMPARE(getTitle(item).c_str(), "2.3");
    item = outlineItems->at(3);
    QVERIFY(item != nullptr);
    QCOMPARE(getTitle(item).c_str(), "2.4");
}

void TestInternalOutline::testRemoveChild()
{
    QTemporaryFile tempFile;
    QVERIFY(tempFile.open());
    tempFile.close();

    QTemporaryFile tempFile2;
    QVERIFY(tempFile2.open());
    tempFile2.close();

    const std::string tempFileName = tempFile.fileName().toStdString();
    const std::string tempFileName2 = tempFile2.fileName().toStdString();

    std::unique_ptr<PDFDoc> doc = PDFDocFactory().createPDFDoc(GooString(TESTDATADIR "/unittestcases/truetype.pdf"));
    QVERIFY(doc.get());

    // ensure the file has no existing outline
    Outline *outline = doc->getOutline();
    QVERIFY(outline != nullptr);
    const auto *outlineItems = outline->getItems();
    QVERIFY(outlineItems == nullptr);

    // create an outline and save the file
    outline->setOutline(
            { { .title = "1",
                .destPageNum = 1,
                .children = { { .title = "1.1", .destPageNum = 1, .children = {} },
                              { .title = "1.2", .destPageNum = 2, .children = {} },
                              { .title = "1.3",
                                .destPageNum = 3,
                                .children = { { .title = "1.3.1", .destPageNum = 1, .children = {} },
                                              { .title = "1.3.2", .destPageNum = 2, .children = {} },
                                              { .title = "1.3.3", .destPageNum = 3, .children = {} },
                                              { .title = "1.3.4", .destPageNum = 4, .children = {} } } },
                              { .title = "1.4", .destPageNum = 4, .children = {} } } },
              { .title = "2", .destPageNum = 2, .children = { { .title = "2.1", .destPageNum = 1, .children = {} } } },
              { .title = "3", .destPageNum = 3, .children = { { .title = "3.1", .destPageNum = 1, .children = {} }, { .title = "3.2", .destPageNum = 2, .children = { { .title = "3.2.1", .destPageNum = 1, .children = {} } } } } },
              { .title = "4", .destPageNum = 4, .children = {} } });
    outlineItems = outline->getItems();
    QVERIFY(outlineItems != nullptr);
    doc->saveAs(tempFileName);
    outline = nullptr;

    /******************************************************/

    const GooString gooTempFileName { tempFileName };
    doc = PDFDocFactory().createPDFDoc(gooTempFileName);
    QVERIFY(doc.get());

    outline = doc->getOutline();
    QVERIFY(outline != nullptr);

    // remove "3"
    outline->removeChild(2);
    // remove "1.3.1"
    outline->getItems()->at(0)->getKids()->at(2)->removeChild(0);
    // remove "1.3.4"
    outline->getItems()->at(0)->getKids()->at(2)->removeChild(2);
    // remove "2.1"
    outline->getItems()->at(1)->removeChild(0);

    // save the file
    doc->saveAs(tempFileName2);
    outline = nullptr;

    /******************************************************/

    const GooString gooTempFileName2 { tempFileName2 };
    doc = PDFDocFactory().createPDFDoc(gooTempFileName2);
    QVERIFY(doc.get());

    // ensure the re-opened file has an outline
    outline = doc->getOutline();
    QVERIFY(outline != nullptr);

    outlineItems = outline->getItems();

    QVERIFY(outlineItems != nullptr);
    QVERIFY(outlineItems->size() == 3);

    OutlineItem *item = outlineItems->at(0);
    QVERIFY(item != nullptr);

    // c_str() is used so QCOMPARE prints string correctly on disagree
    QCOMPARE(getTitle(item).c_str(), "1");
    item = outlineItems->at(1);
    QVERIFY(item != nullptr);
    QCOMPARE(getTitle(item).c_str(), "2");
    item = outlineItems->at(2);
    QVERIFY(item != nullptr);
    QCOMPARE(getTitle(item).c_str(), "4");

    outlineItems = outlineItems->at(0)->getKids();
    outlineItems = outlineItems->at(2)->getKids();
    item = outlineItems->at(0);
    QVERIFY(item != nullptr);
    QCOMPARE(getTitle(item).c_str(), "1.3.2");
    item = outlineItems->at(1);
    QVERIFY(item != nullptr);
    QCOMPARE(getTitle(item).c_str(), "1.3.3");

    // verify "2.1" is removed, lst length 0 is returned as a nullptr
    QVERIFY(outline->getItems()->at(1)->getKids() == nullptr);
}

void TestInternalOutline::testSetTitleAndSetPageDest()
{
    QTemporaryFile tempFile;
    QVERIFY(tempFile.open());
    tempFile.close();

    QTemporaryFile tempFile2;
    QVERIFY(tempFile2.open());
    tempFile2.close();

    const std::string tempFileName = tempFile.fileName().toStdString();
    const std::string tempFileName2 = tempFile2.fileName().toStdString();

    std::unique_ptr<PDFDoc> doc = PDFDocFactory().createPDFDoc(GooString(TESTDATADIR "/unittestcases/truetype.pdf"));
    QVERIFY(doc.get());

    // ensure the file has no existing outline
    Outline *outline = doc->getOutline();
    QVERIFY(outline != nullptr);
    const auto *outlineItems = outline->getItems();
    QVERIFY(outlineItems == nullptr);

    // create an outline and save the file
    outline->setOutline(
            { { .title = "1",
                .destPageNum = 1,
                .children = { { .title = "1.1", .destPageNum = 1, .children = {} },
                              { .title = "1.2", .destPageNum = 2, .children = {} },
                              { .title = "1.3",
                                .destPageNum = 3,
                                .children = { { .title = "1.3.1", .destPageNum = 1, .children = {} },
                                              { .title = "1.3.2", .destPageNum = 2, .children = {} },
                                              { .title = "1.3.3", .destPageNum = 3, .children = {} },
                                              { .title = "1.3.4", .destPageNum = 4, .children = {} } } },
                              { .title = "1.4", .destPageNum = 4, .children = {} } } },
              { .title = "2", .destPageNum = 2, .children = { { .title = "2.1", .destPageNum = 1, .children = {} } } },
              { .title = "3", .destPageNum = 3, .children = { { .title = "3.1", .destPageNum = 1, .children = {} }, { .title = "3.2", .destPageNum = 2, .children = { { .title = "3.2.1", .destPageNum = 1, .children = {} } } } } },
              { .title = "4", .destPageNum = 4, .children = {} } });
    outlineItems = outline->getItems();
    QVERIFY(outlineItems != nullptr);
    doc->saveAs(tempFileName);

    outline = nullptr;

    /******************************************************/

    const GooString gooTempFileName { tempFileName };
    doc = PDFDocFactory().createPDFDoc(gooTempFileName);
    QVERIFY(doc.get());

    outline = doc->getOutline();
    QVERIFY(outline != nullptr);

    // change "1.3.1"
    OutlineItem *item = outline->getItems()->at(0)->getKids()->at(2)->getKids()->at(0);
    QCOMPARE(getTitle(item).c_str(), "1.3.1");

    item->setTitle("Changed to a different title");

    item = outline->getItems()->at(2);
    {
        const LinkAction *action = item->getAction();
        QVERIFY(action->getKind() == actionGoTo);
        const auto *gotoAction = dynamic_cast<const LinkGoTo *>(action);
        const LinkDest *dest = gotoAction->getDest();
        QVERIFY(dest->isPageRef() == false);
        QCOMPARE(dest->getPageNum(), 3);

        item->setPageDest(1);
    }

    // save the file
    doc->saveAs(tempFileName2);
    outline = nullptr;
    item = nullptr;

    /******************************************************/

    const GooString gooTempFileName2 { tempFileName2 };
    doc = PDFDocFactory().createPDFDoc(gooTempFileName2);
    QVERIFY(doc.get());

    outline = doc->getOutline();
    QVERIFY(outline != nullptr);

    item = outline->getItems()->at(0)->getKids()->at(2)->getKids()->at(0);
    QCOMPARE(getTitle(item).c_str(), "Changed to a different title");
    {
        item = outline->getItems()->at(2);
        const LinkAction *action = item->getAction();
        QVERIFY(action->getKind() == actionGoTo);
        const auto *gotoAction = dynamic_cast<const LinkGoTo *>(action);
        const LinkDest *dest = gotoAction->getDest();
        QVERIFY(dest->isPageRef() == false);
        QCOMPARE(dest->getPageNum(), 1);
    }
}

QTEST_GUILESS_MAIN(TestInternalOutline)
#include "check_internal_outline.moc"
