// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0

#include "diffeditorconstants.h"
#include "diffeditorcontroller.h"
#include "diffeditordocument.h"
#include "diffeditor.h"
#include "diffeditortr.h"
#include "inlinediff.h"

#include <coreplugin/actionmanager/actioncontainer.h>
#include <coreplugin/actionmanager/actionmanager.h>
#include <coreplugin/coreconstants.h>
#include <coreplugin/diffservice.h>
#include <coreplugin/editormanager/documentmodel.h>
#include <coreplugin/iversioncontrol.h>
#include <coreplugin/vcsmanager.h>
#include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/editormanager/ieditor.h>
#include <coreplugin/idocument.h>

#include <extensionsystem/iplugin.h>

#include <texteditor/textdocument.h>
#include <texteditor/texteditor.h>

#include <utils/async.h>
#include <utils/differ.h>
#include <utils/fileutils.h>
#include <utils/qtcassert.h>

#include <QAbstractButton>
#include <QAction>
#include <QMenu>
#include <QPointer>

using namespace Core;
using namespace TextEditor;
using namespace Utils;

namespace DiffEditor::Internal {

class ReloadInput {
public:
    std::array<QString, SideCount> text{};
    DiffFileInfoArray fileInfo{};
    FileData::FileOperation fileOperation = FileData::ChangeFile;
    bool binaryFiles = false;
};

class DiffFile
{
public:
    DiffFile(bool ignoreWhitespace, int contextLineCount)
        : m_contextLineCount(contextLineCount),
          m_ignoreWhitespace(ignoreWhitespace)
    {}

    void operator()(QPromise<FileData> &promise, const ReloadInput &reloadInput) const
    {
        if (reloadInput.text[LeftSide] == reloadInput.text[RightSide])
            return; // We show "No difference" in this case, regardless if it's binary or not

        Differ differ(QFuture<void>(promise.future()));

        FileData fileData;
        if (!reloadInput.binaryFiles) {
            const QList<Diff> diffList = Differ::cleanupSemantics(
                        differ.diff(reloadInput.text[LeftSide], reloadInput.text[RightSide]));

            QList<Diff> leftDiffList;
            QList<Diff> rightDiffList;
            Differ::splitDiffList(diffList, &leftDiffList, &rightDiffList);
            QList<Diff> outputLeftDiffList;
            QList<Diff> outputRightDiffList;

            if (m_ignoreWhitespace) {
                const QList<Diff> leftIntermediate
                        = Differ::moveWhitespaceIntoEqualities(leftDiffList);
                const QList<Diff> rightIntermediate
                        = Differ::moveWhitespaceIntoEqualities(rightDiffList);
                Differ::ignoreWhitespaceBetweenEqualities(leftIntermediate, rightIntermediate,
                                                          &outputLeftDiffList, &outputRightDiffList);
            } else {
                outputLeftDiffList = leftDiffList;
                outputRightDiffList = rightDiffList;
            }

            const ChunkData chunkData = DiffUtils::calculateOriginalData(
                        outputLeftDiffList, outputRightDiffList);
            fileData = DiffUtils::calculateContextData(chunkData, m_contextLineCount, 0);
        }
        fileData.fileInfo = reloadInput.fileInfo;
        fileData.fileOperation = reloadInput.fileOperation;
        fileData.binaryFiles = reloadInput.binaryFiles;
        promise.addResult(fileData);
    }

private:
    const int m_contextLineCount;
    const bool m_ignoreWhitespace;
};

class DiffFilesController : public DiffEditorController
{
public:
    using ReloadInputListGetter = std::function<QList<ReloadInput>()>;

    DiffFilesController(IDocument *document, const ReloadInputListGetter &getter);
};

DiffFilesController::DiffFilesController(IDocument *document, const ReloadInputListGetter &getter)
    : DiffEditorController(document)
{
    setDisplayName(Tr::tr("Diff"));
    using namespace QtTaskTree;

    struct StorageStruct
    {
        QList<ReloadInput> inputList;
        QList<std::optional<FileData>> resultList;
    };
    const Storage<StorageStruct> storage;

    const auto onSetup = [getter, storage] {
        StorageStruct *activeStorage = storage.activeStorage();
        activeStorage->inputList = getter();
        activeStorage->resultList.resize(activeStorage->inputList.size());
    };

    const UntilIterator iterator([storage](int iteration) {
        return iteration < storage->inputList.size();
    });

    const auto onDiffSetup = [this, storage, iterator](Async<FileData> &async) {
        async.setConcurrentCallData(DiffFile(ignoreWhitespace(), contextLineCount()),
                                    storage->inputList.at(iterator.iteration()));
    };
    const auto onDiffDone = [storage, iterator](const Async<FileData> &async) {
        if (async.isResultAvailable())
            storage->resultList[iterator.iteration()] = async.result();
    };

    const auto onDone = [this, storage] {
        QList<FileData> finalList;
        const QList<std::optional<FileData>> &results = storage->resultList;
        for (const std::optional<FileData> &result : results) {
            if (result.has_value())
                finalList.append(*result);
        }
        setDiffFiles(finalList);
    };

    const Group recipe = For (iterator) >> Do {
        parallelIdealThreadCountLimit,
        finishAllAndSuccess,
        storage,
        onGroupSetup(onSetup),
        AsyncTask<FileData>(onDiffSetup, onDiffDone, CallDoneFlag::OnSuccess),
        onGroupDone(onDone)
    };
    setReloadRecipe(recipe);
}

static QList<ReloadInput> reloadInputHelper(IDocument *doc, bool onlyIfModified = true)
{
    auto textDocument = qobject_cast<TextDocument *>(doc);
    if (!textDocument || (onlyIfModified && !textDocument->isModified()))
        return {};

    TextFileFormat format = textDocument->format();

    const FilePath filePath = textDocument->filePath();
    const TextFileFormat::ReadResult leftResult = format.readFile(filePath, format.encoding());

    const QString rightText = textDocument->plainText();

    ReloadInput reloadInput;
    reloadInput.text = {leftResult.content, rightText};
    reloadInput.fileInfo = {DiffFileInfo(filePath.path(), Tr::tr("Saved")),
                            DiffFileInfo(filePath.path(), Tr::tr("Modified"))};
    reloadInput.fileInfo[RightSide].patchBehaviour = DiffFileInfo::PatchEditor;
    reloadInput.binaryFiles = (leftResult.code == TextFileFormat::ReadEncodingError);

    if (leftResult.code == TextFileFormat::ReadIOError)
        reloadInput.fileOperation = FileData::NewFile;
    return {reloadInput};
}

static QList<ReloadInput> reloadInputHelper(const FilePath &filePath, bool onlyIfModified = true)
{
    return reloadInputHelper(DocumentModel::documentForFilePath(filePath), onlyIfModified);
}

static QList<ReloadInput> openFilesReloadInputList()
{
    QList<ReloadInput> result;
    const QList<IDocument *> openedDocuments = DocumentModel::openedDocuments();
    for (IDocument *doc : openedDocuments) {
        QTC_ASSERT(doc, continue);
        result << reloadInputHelper(doc);
    }
    return result;
}

static QList<ReloadInput> modifiedFilesReloadInputList(const FilePaths &filePaths)
{
    QList<ReloadInput> result;
    for (const FilePath &filePath : filePaths)
        result << reloadInputHelper(filePath, /*onlyIfModified=*/false);
    return result;
}

static QList<ReloadInput> externalFilesReloadInputList(const FilePath &leftFilePath,
                                                       const FilePath &rightFilePath)
{
    TextFileFormat format;
    format.setEncoding(EditorManager::defaultTextEncoding());

    const TextFileFormat::ReadResult leftResult = format.readFile(leftFilePath, format.encoding());
    const TextFileFormat::ReadResult rightResult = format.readFile(rightFilePath, format.encoding());

    ReloadInput reloadInput;
    reloadInput.text = {leftResult.content, rightResult.content};
    reloadInput.fileInfo[LeftSide].fileName = leftFilePath.path();
    reloadInput.fileInfo[RightSide].fileName = rightFilePath.path();
    reloadInput.binaryFiles = leftResult.code == TextFileFormat::ReadEncodingError
                           || rightResult.code == TextFileFormat::ReadEncodingError;

    const bool leftFileExists = leftResult.code != TextFileFormat::ReadIOError;
    const bool rightFileExists = rightResult.code != TextFileFormat::ReadIOError;
    if (!leftFileExists && rightFileExists)
        reloadInput.fileOperation = FileData::NewFile;
    else if (leftFileExists && !rightFileExists)
        reloadInput.fileOperation = FileData::DeleteFile;

    QList<ReloadInput> result;
    if (leftFileExists || rightFileExists)
        result << reloadInput;

    return result;
}

// Diffs the in-memory contents of two documents (which may have no file on
// disk, e.g. scratch editors). See QTCREATORBUG-33271.
static QList<ReloadInput> documentsReloadInputList(
    const QString &leftTitle, const QString &leftText,
    const QString &rightTitle, const QString &rightText)
{
    ReloadInput reloadInput;
    reloadInput.text = {leftText, rightText};
    reloadInput.fileInfo[LeftSide].fileName = leftTitle;
    reloadInput.fileInfo[RightSide].fileName = rightTitle;
    return {reloadInput};
}

static QString textOfDocument(IDocument *document)
{
    if (auto *textDocument = qobject_cast<TextDocument *>(document))
        return textDocument->plainText();
    return QString::fromUtf8(document->contents());
}

static QString titleOfDocument(IDocument *document)
{
    return document->filePath().isEmpty() ? document->displayName()
                                          : document->filePath().path();
}

/////////////////

static TextDocument *currentTextDocument()
{
    return qobject_cast<TextDocument *>(EditorManager::currentDocument());
}

class DiffEditorServiceImpl final : public QObject, public DiffService
{
    Q_OBJECT
    Q_INTERFACES(Core::DiffService)

public:
    void diffFiles(const FilePath &leftFilePath, const FilePath &rightFilePath) final;
    void diffModifiedFiles(const FilePaths &filePaths) final;
    void diffDocuments(IDocument *leftDocument, IDocument *rightDocument) final;
};

static void reload(const QString &vcsId, const QString &displayName,
                   const DiffFilesController::ReloadInputListGetter &getter)
{
    auto const document = qobject_cast<DiffEditorDocument *>(
        DiffEditorController::findOrCreateDocument(vcsId, displayName));
    if (!document)
        return;
    if (!DiffEditorController::controller(document))
        new DiffFilesController(document, getter);
    EditorManager::activateEditorForDocument(document);
    document->reload();
}

void DiffEditorServiceImpl::diffFiles(const FilePath &leftFilePath, const FilePath &rightFilePath)
{
    const QString documentId = Constants::DIFF_EDITOR_PLUGIN + QLatin1String(".DiffFiles.")
                               + leftFilePath.toUrlishString() + QLatin1Char('.')
                               + rightFilePath.toUrlishString();
    const QString title = Tr::tr("Diff Files");
    reload(documentId, title, [leftFilePath, rightFilePath] {
        return externalFilesReloadInputList(leftFilePath, rightFilePath);
    });
}

void DiffEditorServiceImpl::diffModifiedFiles(const FilePaths &filePaths)
{
    const QString documentId = Constants::DIFF_EDITOR_PLUGIN + QLatin1String(".DiffModifiedFiles");
    const QString title = Tr::tr("Diff Modified Files");
    reload(documentId, title, [filePaths] { return modifiedFilesReloadInputList(filePaths); });
}

void DiffEditorServiceImpl::diffDocuments(IDocument *leftDocument, IDocument *rightDocument)
{
    if (!leftDocument || !rightDocument)
        return;

    const QString documentId = Constants::DIFF_EDITOR_PLUGIN + QLatin1String(".DiffDocuments");
    const QPointer<IDocument> leftDoc(leftDocument);
    const QPointer<IDocument> rightDoc(rightDocument);
    // Snapshot at diff-creation time, used as a fallback once a document goes
    // away - pressing reload afterwards must not touch a destroyed IDocument.
    const QString leftInitialTitle = titleOfDocument(leftDocument);
    const QString leftInitialText = textOfDocument(leftDocument);
    const QString rightInitialTitle = titleOfDocument(rightDocument);
    const QString rightInitialText = textOfDocument(rightDocument);

    reload(documentId, Tr::tr("Diff"),
           [leftDoc, leftInitialTitle, leftInitialText,
            rightDoc, rightInitialTitle, rightInitialText] {
        const QString leftTitle = leftDoc ? titleOfDocument(leftDoc) : leftInitialTitle;
        const QString leftText = leftDoc ? textOfDocument(leftDoc) : leftInitialText;
        const QString rightTitle = rightDoc ? titleOfDocument(rightDoc) : rightInitialTitle;
        const QString rightText = rightDoc ? textOfDocument(rightDoc) : rightInitialText;
        return documentsReloadInputList(leftTitle, leftText, rightTitle, rightText);
    });
}

class DiffEditorPlugin final : public ExtensionSystem::IPlugin
{
    Q_OBJECT
    Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QtCreatorPlugin" FILE "DiffEditor.json")

private:
    void initialize() final;
    void updateDiffCurrentFileAction();
    void updateDiffOpenFilesAction();
    void diffCurrentFile();
    void diffOpenFiles();
    void diffExternalFiles();

    QAction *m_diffCurrentFileAction = nullptr;
    QAction *m_diffOpenFilesAction = nullptr;

    DiffEditorServiceImpl m_service;

#ifdef WITH_TESTS
private slots:
    void testMakePatch_data();
    void testMakePatch();
    void testReadPatch_data();
    void testReadPatch();
    void testFilterPatch_data();
    void testFilterPatch();
    void testDiffDocuments();
    void testInlineDiff();
    void testInlineDiffCollapse();
    void testInlineDiffCollapseAddedLines();
#endif // WITH_TESTS
};

void DiffEditorPlugin::initialize()
{
    setupDiffEditorFactory();

    //register actions
    ActionContainer *toolsContainer = ActionManager::actionContainer(Core::Constants::M_TOOLS);
    toolsContainer->insertGroup(Core::Constants::G_TOOLS_DEBUG, Constants::G_TOOLS_DIFF);
    ActionContainer *diffContainer = ActionManager::createMenu("Diff");
    diffContainer->menu()->setTitle(Tr::tr("&Diff"));
    toolsContainer->addMenu(diffContainer, Constants::G_TOOLS_DIFF);

    m_diffCurrentFileAction = new QAction(Tr::tr("Diff Current File"), this);
    Command *diffCurrentFileCommand = ActionManager::registerAction(m_diffCurrentFileAction, "DiffEditor.DiffCurrentFile");
    diffCurrentFileCommand->setDefaultKeySequence(QKeySequence(useMacShortcuts ? Tr::tr("Meta+H") : Tr::tr("Ctrl+H")));
    connect(m_diffCurrentFileAction, &QAction::triggered, this, &DiffEditorPlugin::diffCurrentFile);
    diffContainer->addAction(diffCurrentFileCommand);

    m_diffOpenFilesAction = new QAction(Tr::tr("Diff Open Files"), this);
    Command *diffOpenFilesCommand = ActionManager::registerAction(m_diffOpenFilesAction, "DiffEditor.DiffOpenFiles");
    diffOpenFilesCommand->setDefaultKeySequence(QKeySequence(useMacShortcuts ? Tr::tr("Meta+Shift+H") : Tr::tr("Ctrl+Shift+H")));
    connect(m_diffOpenFilesAction, &QAction::triggered, this, &DiffEditorPlugin::diffOpenFiles);
    diffContainer->addAction(diffOpenFilesCommand);

    QAction *diffExternalFilesAction = new QAction(Tr::tr("Diff External Files..."), this);
    Command *diffExternalFilesCommand = ActionManager::registerAction(diffExternalFilesAction, "DiffEditor.DiffExternalFiles");
    connect(diffExternalFilesAction, &QAction::triggered, this, &DiffEditorPlugin::diffExternalFiles);
    diffContainer->addAction(diffExternalFilesCommand);

    connect(EditorManager::instance(), &EditorManager::currentEditorChanged,
            this, &DiffEditorPlugin::updateDiffCurrentFileAction);
    connect(EditorManager::instance(), &EditorManager::currentDocumentStateChanged,
            this, &DiffEditorPlugin::updateDiffCurrentFileAction);
    connect(EditorManager::instance(), &EditorManager::editorOpened,
            this, &DiffEditorPlugin::updateDiffOpenFilesAction);
    connect(EditorManager::instance(), &EditorManager::editorsClosed,
            this, &DiffEditorPlugin::updateDiffOpenFilesAction);
    connect(EditorManager::instance(), &EditorManager::documentStateChanged,
            this, &DiffEditorPlugin::updateDiffOpenFilesAction);

    updateDiffCurrentFileAction();
    updateDiffOpenFilesAction();
}

void DiffEditorPlugin::updateDiffCurrentFileAction()
{
    auto textDocument = currentTextDocument();
    // files under version control can always be diffed, others only have
    // unsaved changes to show
    const bool enabled = textDocument
                         && (textDocument->isModified()
                             || (!textDocument->filePath().isEmpty()
                                 && Core::VcsManager::findVersionControlForDirectory(
                                     textDocument->filePath().parentDir())));
    m_diffCurrentFileAction->setEnabled(enabled);
}

void DiffEditorPlugin::updateDiffOpenFilesAction()
{
    const bool enabled = anyOf(DocumentModel::openedDocuments(), [](IDocument *doc) {
            QTC_ASSERT(doc, return false);
            return doc->isModified() && qobject_cast<TextDocument *>(doc);
        });
    m_diffOpenFilesAction->setEnabled(enabled);
}

void DiffEditorPlugin::diffCurrentFile()
{
    auto textDocument = currentTextDocument();
    if (!textDocument)
        return;

    const FilePath filePath = textDocument->filePath();
    if (filePath.isEmpty())
        return;

    // Files tracked by version control get the VCS diff, e.g. git's inline
    // diff against the index, which can also stage blocks of changes. Others
    // (including untracked files inside a checkout) are compared against
    // their saved contents.
    FilePath topLevel;
    Core::IVersionControl *versionControl
        = Core::VcsManager::findVersionControlForDirectory(filePath.parentDir(), &topLevel);
    if (versionControl
        && versionControl->managesFile(topLevel,
                                       filePath.relativeChildPath(topLevel).path())) {
        versionControl->vcsDiff(topLevel, filePath.relativeChildPath(topLevel));
        return;
    }

    // The inline diff editor requires a text editor for the document; custom
    // text based editors and too large documents get the classic diff view.
    IEditor *inlineEditor = nullptr;
    auto textEditor = qobject_cast<BaseTextEditor *>(EditorManager::currentEditor());
    if (textEditor && textEditor->editorWidget()
        && textEditor->document() == textDocument) {
        const TextDocumentPtr document = textEditor->editorWidget()->textDocumentPtr();
        InlineDiffBaseline baseline;
        baseline.id = "saved";
        baseline.displayName = Tr::tr("Saved");
        baseline.fetchText = [filePath, format = document->format()](
                                 const InlineDiffBaseline::TextCallback &callback) mutable {
            const TextFileFormat::ReadResult result = format.readFile(filePath,
                                                                      format.encoding());
            if (result.code == TextFileFormat::ReadSuccess)
                callback(result.content);
            else if (result.code == TextFileFormat::ReadIOError)
                callback(QString()); // file not saved yet, everything is added
            else
                callback(Utils::ResultError(
                    Tr::tr("Cannot read \"%1\".").arg(filePath.toUserOutput())));
        };
        inlineEditor = openInlineDiffEditor(
            document, baseline, Tr::tr("%1 (Modified vs Saved)").arg(filePath.fileName()));
    }
    if (!inlineEditor) {
        const QString documentId = Constants::DIFF_EDITOR_PLUGIN + QLatin1String(".Diff.")
                + filePath.toUrlishString();
        const QString title = Tr::tr("Diff \"%1\"").arg(filePath.toUserOutput());
        reload(documentId, title, [filePath] { return reloadInputHelper(filePath); });
    }
}

void DiffEditorPlugin::diffOpenFiles()
{
    const QString documentId = Constants::DIFF_EDITOR_PLUGIN + QLatin1String(".DiffOpenFiles");
    const QString title = Tr::tr("Diff Open Files");
    reload(documentId, title, openFilesReloadInputList);
}

void DiffEditorPlugin::diffExternalFiles()
{
    const FilePath filePath1 = FileUtils::getOpenFilePath(Tr::tr("Select First File for Diff"));
    if (filePath1.isEmpty())
        return;
    if (EditorManager::skipOpeningBigTextFile(filePath1))
        return;
    const FilePath filePath2 = FileUtils::getOpenFilePath(Tr::tr("Select Second File for Diff"));
    if (filePath2.isEmpty())
        return;
    if (EditorManager::skipOpeningBigTextFile(filePath2))
        return;

    const QString documentId = QLatin1String(Constants::DIFF_EDITOR_PLUGIN)
            + ".DiffExternalFiles." + filePath1.toUrlishString() + '.' + filePath2.toUrlishString();
    const QString title = Tr::tr("Diff \"%1\", \"%2\"").arg(filePath1.toUserOutput(), filePath2.toUserOutput());
    reload(documentId, title, [filePath1, filePath2] {
        return externalFilesReloadInputList(filePath1, filePath2);
    });
}

} // namespace DiffEditor::Internal

#ifdef WITH_TESTS

#include <QTest>
#include <QToolBar>

#include "diffutils.h"

Q_DECLARE_METATYPE(DiffEditor::ChunkData)
Q_DECLARE_METATYPE(DiffEditor::FileData)
Q_DECLARE_METATYPE(DiffEditor::ChunkSelection)

static inline QString _(const char *string) { return QString::fromLatin1(string); }

void DiffEditor::Internal::DiffEditorPlugin::testMakePatch_data()
{
    QTest::addColumn<ChunkData>("sourceChunk");
    QTest::addColumn<bool>("lastChunk");
    QTest::addColumn<QString>("patchText");

    const QString fileName = "a.txt";
    const QString header = "--- " + fileName + "\n+++ " + fileName + '\n';

    QList<RowData> rows;
    rows << RowData(_("ABCD"), TextLineData::Separator);
    rows << RowData(_("EFGH"));
    ChunkData chunk;
    chunk.rows = rows;
    QString patchText = header + "@@ -1,2 +1,1 @@\n"
                                 "-ABCD\n"
                                 " EFGH\n";
    QTest::newRow("Simple not a last chunk") << chunk
                            << false
                            << patchText;

    ///////////

    // chunk the same here
    patchText = header + "@@ -1,2 +1,1 @@\n"
                         "-ABCD\n"
                         " EFGH\n"
                         "\\ No newline at end of file\n";

    QTest::newRow("Simple last chunk") << chunk
                            << true
                            << patchText;

    ///////////

    rows.clear();
    rows << RowData(_("ABCD"));
    rows << RowData(_(""), TextLineData::Separator);
    chunk.rows = rows;
    patchText = header + "@@ -1,1 +1,1 @@\n"
                         "-ABCD\n"
                         "+ABCD\n"
                         "\\ No newline at end of file\n";

    QTest::newRow("EOL in last line removed") << chunk
                            << true
                            << patchText;

    ///////////

    // chunk the same here
    patchText = header + "@@ -1,2 +1,1 @@\n"
                         " ABCD\n"
                         "-\n";

    QTest::newRow("Last empty line removed") << chunk
                            << false
                            << patchText;

    ///////////

    rows.clear();
    rows << RowData(_("ABCD"));
    rows << RowData(_(""), TextLineData::Separator);
    rows << RowData(_(""), TextLineData::Separator);
    chunk.rows = rows;
    patchText = header + "@@ -1,2 +1,1 @@\n"
                         "-ABCD\n"
                         "-\n"
                         "+ABCD\n"
                         "\\ No newline at end of file\n";

    QTest::newRow("Two last EOLs removed") << chunk
                            << true
                            << patchText;

    ///////////

    rows.clear();
    rows << RowData(_("ABCD"));
    rows << RowData(TextLineData::Separator, _(""));
    chunk.rows = rows;
    patchText = header + "@@ -1,1 +1,1 @@\n"
                         "-ABCD\n"
                         "\\ No newline at end of file\n"
                         "+ABCD\n";

    QTest::newRow("EOL to last line added") << chunk
                            << true
                            << patchText;

    ///////////

    // chunk the same here
    patchText = header + "@@ -1,1 +1,2 @@\n"
                         " ABCD\n"
                         "+\n";

    QTest::newRow("Last empty line added") << chunk
                            << false
                            << patchText;

    ///////////

    rows.clear();
    rows << RowData(_("ABCD"), _("EFGH"));
    chunk.rows = rows;
    patchText = header + "@@ -1,1 +1,1 @@\n"
                         "-ABCD\n"
                         "+EFGH\n";

    QTest::newRow("Last line with a newline modified") << chunk
                            << false
                            << patchText;

    ///////////

    rows.clear();
    rows << RowData(_("ABCD"), _("EFGH"));
    rows << RowData(_(""));
    chunk.rows = rows;
    patchText = header + "@@ -1,2 +1,2 @@\n"
                         "-ABCD\n"
                         "+EFGH\n"
                         " \n";
    QTest::newRow("Not a last line with a newline modified") << chunk
                            << false
                            << patchText;

    ///////////

    rows.clear();
    rows << RowData(_("ABCD"), _("EFGH"));
    chunk.rows = rows;
    patchText = header + "@@ -1,1 +1,1 @@\n"
                         "-ABCD\n"
                         "\\ No newline at end of file\n"
                         "+EFGH\n"
                         "\\ No newline at end of file\n";

    QTest::newRow("Last line without a newline modified") << chunk
                            << true
                            << patchText;

    ///////////

    // chunk the same here
    patchText = header + "@@ -1,1 +1,1 @@\n"
                         "-ABCD\n"
                         "+EFGH\n";
    QTest::newRow("Not a last line without a newline modified") << chunk
                            << false
                            << patchText;

    ///////////

    rows.clear();
    rows << RowData(_("ABCD"), _("EFGH"));
    rows << RowData(_("IJKL"));
    chunk.rows = rows;
    patchText = header + "@@ -1,2 +1,2 @@\n"
                         "-ABCD\n"
                         "+EFGH\n"
                         " IJKL\n"
                         "\\ No newline at end of file\n";

    QTest::newRow("Last but one line modified, last line without a newline")
            << chunk
            << true
            << patchText;

    ///////////

    // chunk the same here
    patchText = header + "@@ -1,2 +1,2 @@\n"
                         "-ABCD\n"
                         "+EFGH\n"
                         " IJKL\n";

    QTest::newRow("Last but one line modified, last line with a newline")
            << chunk
            << false
            << patchText;

    ///////////

    rows.clear();
    rows << RowData(_("ABCD"));
    rows << RowData(TextLineData::Separator, _(""));
    rows << RowData(_(""), _("EFGH"));
    chunk.rows = rows;
    patchText = header + "@@ -1,1 +1,3 @@\n"
                         " ABCD\n"
                         "+\n"
                         "+EFGH\n"
                         "\\ No newline at end of file\n";

    QTest::newRow("Blank line followed by No newline")
            << chunk
            << true
            << patchText;
}

void DiffEditor::Internal::DiffEditorPlugin::testMakePatch()
{
    QFETCH(ChunkData, sourceChunk);
    QFETCH(bool, lastChunk);
    QFETCH(QString, patchText);

    const QString fileName = "a.txt";
    const QString result = DiffUtils::makePatch(sourceChunk, fileName, fileName, lastChunk);

    QCOMPARE(result, patchText);

    const std::optional<QList<FileData>> resultList = DiffUtils::readPatch(result);
    const bool ok = resultList.has_value();

    QVERIFY(ok);
    QCOMPARE(resultList->count(), 1);
    for (int i = 0; i < resultList->count(); i++) {
        const FileData &resultFileData = resultList->at(i);
        QCOMPARE(resultFileData.fileInfo[LeftSide].fileName, fileName);
        QCOMPARE(resultFileData.fileInfo[RightSide].fileName, fileName);
        QCOMPARE(resultFileData.chunks.count(), 1);
        for (int j = 0; j < resultFileData.chunks.count(); j++) {
            const ChunkData &resultChunkData = resultFileData.chunks.at(j);
            QCOMPARE(resultChunkData.startingLineNumber[LeftSide], sourceChunk.startingLineNumber[LeftSide]);
            QCOMPARE(resultChunkData.startingLineNumber[RightSide], sourceChunk.startingLineNumber[RightSide]);
            QCOMPARE(resultChunkData.contextChunk, sourceChunk.contextChunk);
            QCOMPARE(resultChunkData.rows.count(), sourceChunk.rows.count());
            for (int k = 0; k < sourceChunk.rows.count(); k++) {
                const RowData &sourceRowData = sourceChunk.rows.at(k);
                const RowData &resultRowData = resultChunkData.rows.at(k);
                QCOMPARE(resultRowData.equal, sourceRowData.equal);
                QCOMPARE(resultRowData.line[LeftSide].text, sourceRowData.line[LeftSide].text);
                QCOMPARE(resultRowData.line[LeftSide].textLineType, sourceRowData.line[LeftSide].textLineType);
                QCOMPARE(resultRowData.line[RightSide].text, sourceRowData.line[RightSide].text);
                QCOMPARE(resultRowData.line[RightSide].textLineType, sourceRowData.line[RightSide].textLineType);
            }
        }
    }
}

void DiffEditor::Internal::DiffEditorPlugin::testReadPatch_data()
{
    QTest::addColumn<QString>("sourcePatch");
    QTest::addColumn<QList<FileData>>("fileDataList");

    QString patch = "diff --git a/src/plugins/diffeditor/diffeditor.cpp b/src/plugins/diffeditor/diffeditor.cpp\n"
                    "index eab9e9b..082c135 100644\n"
                    "--- a/src/plugins/diffeditor/diffeditor.cpp\n"
                    "+++ b/src/plugins/diffeditor/diffeditor.cpp\n"
                    "@@ -187,9 +187,6 @@ void DiffEditor::ctor()\n"
                    "     m_controller = m_document->controller();\n"
                    "     m_guiController = new DiffEditorGuiController(m_controller, this);\n"
                    " \n"
                    "-//    m_sideBySideEditor->setDiffEditorGuiController(m_guiController);\n"
                    "-//    m_unifiedEditor->setDiffEditorGuiController(m_guiController);\n"
                    "-\n"
                    "     connect(m_controller, SIGNAL(cleared(QString)),\n"
                    "             this, SLOT(slotCleared(QString)));\n"
                    "     connect(m_controller, SIGNAL(diffContentsChanged(QList<DiffEditorController::DiffFilesContents>,QString)),\n"
                    "diff --git a/src/plugins/diffeditor/diffutils.cpp b/src/plugins/diffeditor/diffutils.cpp\n"
                    "index 2f641c9..f8ff795 100644\n"
                    "--- a/src/plugins/diffeditor/diffutils.cpp\n"
                    "+++ b/src/plugins/diffeditor/diffutils.cpp\n"
                    "@@ -464,5 +464,12 @@ QString DiffUtils::makePatch(const ChunkData &chunkData,\n"
                    "     return diffText;\n"
                    " }\n"
                    " \n"
                    "+FileData DiffUtils::makeFileData(const QString &patch)\n"
                    "+{\n"
                    "+    FileData fileData;\n"
                    "+\n"
                    "+    return fileData;\n"
                    "+}\n"
                    "+\n"
                    " } // namespace Internal\n"
                    " } // namespace DiffEditor\n"
                    "diff --git a/new b/new\n"
                    "new file mode 100644\n"
                    "index 0000000..257cc56\n"
                    "--- /dev/null\n"
                    "+++ b/new\n"
                    "@@ -0,0 +1 @@\n"
                    "+foo\n"
                    "diff --git a/deleted b/deleted\n"
                    "deleted file mode 100644\n"
                    "index 257cc56..0000000\n"
                    "--- a/deleted\n"
                    "+++ /dev/null\n"
                    "@@ -1 +0,0 @@\n"
                    "-foo\n"
                    "diff --git a/empty b/empty\n"
                    "new file mode 100644\n"
                    "index 0000000..e69de29\n"
                    "diff --git a/empty b/empty\n"
                    "deleted file mode 100644\n"
                    "index e69de29..0000000\n"
                    "diff --git a/file a.txt b/file b.txt\n"
                    "similarity index 99%\n"
                    "copy from file a.txt\n"
                    "copy to file b.txt\n"
                    "index 1234567..9876543\n"
                    "--- a/file a.txt\n"
                    "+++ b/file b.txt\n"
                    "@@ -20,3 +20,3 @@\n"
                    " A\n"
                    "-B\n"
                    "+C\n"
                    " D\n"
                    "diff --git a/file a.txt b/file b.txt\n"
                    "similarity index 99%\n"
                    "rename from file a.txt\n"
                    "rename to file b.txt\n"
                    "diff --git a/file.txt b/file.txt\n"
                    "old mode 100644\n"
                    "new mode 100755\n"
                    "index 1234567..9876543\n"
                    "--- a/file.txt\n"
                    "+++ b/file.txt\n"
                    "@@ -20,3 +20,3 @@\n"
                    " A\n"
                    "-B\n"
                    "+C\n"
                    " D\n"
                    ;

    FileData fileData1;
    fileData1.fileInfo = {DiffFileInfo("src/plugins/diffeditor/diffeditor.cpp", "eab9e9b"),
                          DiffFileInfo("src/plugins/diffeditor/diffeditor.cpp", "082c135")};
    ChunkData chunkData1;
    chunkData1.startingLineNumber = {186, 186};
    QList<RowData> rows1;
    rows1 << RowData(_("    m_controller = m_document->controller();"));
    rows1 << RowData(_("    m_guiController = new DiffEditorGuiController(m_controller, this);"));
    rows1 << RowData(_(""));
    rows1 << RowData(_("//    m_sideBySideEditor->setDiffEditorGuiController(m_guiController);"), TextLineData::Separator);
    rows1 << RowData(_("//    m_unifiedEditor->setDiffEditorGuiController(m_guiController);"), TextLineData::Separator);
    rows1 << RowData(_(""), TextLineData::Separator);
    rows1 << RowData(_("    connect(m_controller, SIGNAL(cleared(QString)),"));
    rows1 << RowData(_("            this, SLOT(slotCleared(QString)));"));
    rows1 << RowData(_("    connect(m_controller, SIGNAL(diffContentsChanged(QList<DiffEditorController::DiffFilesContents>,QString)),"));
    chunkData1.rows = rows1;
    fileData1.chunks << chunkData1;

    FileData fileData2;
    fileData2.fileInfo = {DiffFileInfo(_("src/plugins/diffeditor/diffutils.cpp"), _("2f641c9")),
                          DiffFileInfo(_("src/plugins/diffeditor/diffutils.cpp"), _("f8ff795"))};
    ChunkData chunkData2;
    chunkData2.startingLineNumber = {463, 463};
    QList<RowData> rows2;
    rows2 << RowData(_("    return diffText;"));
    rows2 << RowData(_("}"));
    rows2 << RowData(_(""));
    rows2 << RowData(TextLineData::Separator, _("FileData DiffUtils::makeFileData(const QString &patch)"));
    rows2 << RowData(TextLineData::Separator, _("{"));
    rows2 << RowData(TextLineData::Separator, _("    FileData fileData;"));
    rows2 << RowData(TextLineData::Separator, _(""));
    rows2 << RowData(TextLineData::Separator, _("    return fileData;"));
    rows2 << RowData(TextLineData::Separator, _("}"));
    rows2 << RowData(TextLineData::Separator, _(""));
    rows2 << RowData(_("} // namespace Internal"));
    rows2 << RowData(_("} // namespace DiffEditor"));
    chunkData2.rows = rows2;
    fileData2.chunks << chunkData2;

    FileData fileData3;
    fileData3.fileInfo = {DiffFileInfo("new", "0000000"), DiffFileInfo("new", "257cc56")};
    fileData3.fileOperation = FileData::NewFile;
    ChunkData chunkData3;
    chunkData3.startingLineNumber = {-1, 0};
    QList<RowData> rows3;
    rows3 << RowData(TextLineData::Separator, _("foo"));
    TextLineData textLineData3(TextLineData::TextLine);
    rows3 << RowData(TextLineData::Separator, textLineData3);
    chunkData3.rows = rows3;
    fileData3.chunks << chunkData3;

    FileData fileData4;
    fileData4.fileInfo = {DiffFileInfo("deleted", "257cc56"), DiffFileInfo("deleted", "0000000")};
    fileData4.fileOperation = FileData::DeleteFile;
    ChunkData chunkData4;
    chunkData4.startingLineNumber = {0, -1};
    QList<RowData> rows4;
    rows4 << RowData(_("foo"), TextLineData::Separator);
    TextLineData textLineData4(TextLineData::TextLine);
    rows4 << RowData(textLineData4, TextLineData::Separator);
    chunkData4.rows = rows4;
    fileData4.chunks << chunkData4;

    FileData fileData5;
    fileData5.fileInfo = {DiffFileInfo("empty", "0000000"), DiffFileInfo("empty", "e69de29")};
    fileData5.fileOperation = FileData::NewFile;

    FileData fileData6;
    fileData6.fileInfo = {DiffFileInfo("empty", "e69de29"), DiffFileInfo("empty", "0000000")};
    fileData6.fileOperation = FileData::DeleteFile;

    FileData fileData7;
    fileData7.fileInfo = {DiffFileInfo("file a.txt", "1234567"),
                          DiffFileInfo("file b.txt", "9876543")};
    fileData7.fileOperation = FileData::CopyFile;
    ChunkData chunkData7;
    chunkData7.startingLineNumber = {19, 19};
    QList<RowData> rows7;
    rows7 << RowData(_("A"));
    rows7 << RowData(_("B"), _("C"));
    rows7 << RowData(_("D"));
    chunkData7.rows = rows7;
    fileData7.chunks << chunkData7;

    FileData fileData8;
    fileData8.fileInfo = {DiffFileInfo("file a.txt"), DiffFileInfo("file b.txt")};
    fileData8.fileOperation = FileData::RenameFile;

    FileData fileData9;
    fileData9.fileInfo = {DiffFileInfo("file.txt", "1234567"), DiffFileInfo("file.txt", "9876543")};
    fileData9.chunks << chunkData7;
    QList<FileData> fileDataList1;
    fileDataList1 << fileData1 << fileData2 << fileData3 << fileData4 << fileData5
                  << fileData6 << fileData7 << fileData8 << fileData9;

    QTest::newRow("Git patch") << patch
                               << fileDataList1;

    //////////////

    patch = "diff --git a/file foo.txt b/file foo.txt\n"
            "index 1234567..9876543 100644\n"
            "--- a/file foo.txt\n"
            "+++ b/file foo.txt\n"
            "@@ -50,4 +50,5 @@ void DiffEditor::ctor()\n"
            " A\n"
            " B\n"
            " C\n"
            "+\n";

    fileData1.fileInfo = {DiffFileInfo("file foo.txt", "1234567"),
                          DiffFileInfo("file foo.txt", "9876543")};
    fileData1.fileOperation = FileData::ChangeFile;
    chunkData1.startingLineNumber = {49, 49};
    rows1.clear();
    rows1 << RowData(_("A"));
    rows1 << RowData(_("B"));
    rows1 << RowData(_("C"));
    rows1 << RowData(TextLineData::Separator, _(""));
    chunkData1.rows = rows1;
    fileData1.chunks.clear();
    fileData1.chunks << chunkData1;

    QList<FileData> fileDataList2;
    fileDataList2 << fileData1;

    QTest::newRow("Added line") << patch
                                << fileDataList2;

    //////////////

    patch = "diff --git a/file foo.txt b/file foo.txt\n"
            "index 1234567..9876543 100644\n"
            "--- a/file foo.txt\n"
            "+++ b/file foo.txt\n"
            "@@ -1,1 +1,1 @@\n"
            "-ABCD\n"
            "\\ No newline at end of file\n"
            "+ABCD\n";

    fileData1.fileInfo = {DiffFileInfo("file foo.txt", "1234567"),
                          DiffFileInfo("file foo.txt", "9876543")};
    fileData1.fileOperation = FileData::ChangeFile;
    chunkData1.startingLineNumber = {0, 0};
    rows1.clear();
    rows1 << RowData(_("ABCD"));
    rows1 << RowData(TextLineData::Separator, _(""));
    chunkData1.rows = rows1;
    fileData1.chunks.clear();
    fileData1.chunks << chunkData1;

    QList<FileData> fileDataList3;
    fileDataList3 << fileData1;

    QTest::newRow("Last newline added to a line without newline") << patch
                                << fileDataList3;

    patch = "diff --git a/difftest.txt b/difftest.txt\n"
            "index 1234567..9876543 100644\n"
            "--- a/difftest.txt\n"
            "+++ b/difftest.txt\n"
            "@@ -2,5 +2,5 @@ void func()\n"
            " A\n"
            " B\n"
            "-C\n"
            "+Z\n"
            " D\n"
            " \n"
            "@@ -9,2 +9,4 @@ void OtherFunc()\n"
            " \n"
            " D\n"
            "+E\n"
            "+F\n"
            ;

    fileData1.fileInfo = {DiffFileInfo("difftest.txt", "1234567"),
                          DiffFileInfo("difftest.txt", "9876543")};
    fileData1.fileOperation = FileData::ChangeFile;
    chunkData1.startingLineNumber = {1, 1};
    rows1.clear();
    rows1 << RowData(_("A"));
    rows1 << RowData(_("B"));
    rows1 << RowData(_("C"), _("Z"));
    rows1 << RowData(_("D"));
    rows1 << RowData(_(""));
    chunkData1.rows = rows1;

    chunkData2.startingLineNumber = {8, 8};
    rows2.clear();
    rows2 << RowData(_(""));
    rows2 << RowData(_("D"));
    rows2 << RowData(TextLineData::Separator, _("E"));
    rows2 << RowData(TextLineData::Separator, _("F"));
    chunkData2.rows = rows2;
    fileData1.chunks.clear();
    fileData1.chunks << chunkData1;
    fileData1.chunks << chunkData2;

    QList<FileData> fileDataList4;
    fileDataList4 << fileData1;

    QTest::newRow("2 chunks - first ends with blank line") << patch
                                << fileDataList4;

    //////////////

    patch = "diff --git a/file foo.txt b/file foo.txt\n"
            "index 1234567..9876543 100644\n"
            "--- a/file foo.txt\n"
            "+++ b/file foo.txt\n"
            "@@ -1,1 +1,3 @@ void DiffEditor::ctor()\n"
            " ABCD\n"
            "+\n"
            "+EFGH\n"
            "\\ No newline at end of file\n";

    fileData1.fileInfo = {DiffFileInfo("file foo.txt", "1234567"),
                          DiffFileInfo("file foo.txt", "9876543")};
    fileData1.fileOperation = FileData::ChangeFile;
    chunkData1.startingLineNumber = {0, 0};
    rows1.clear();
    rows1 << RowData(_("ABCD"));
    rows1 << RowData(TextLineData::Separator, _(""));
    rows1 << RowData(_(""), _("EFGH"));
    chunkData1.rows = rows1;
    fileData1.chunks.clear();
    fileData1.chunks << chunkData1;

    QList<FileData> fileDataList5;
    fileDataList5 << fileData1;

    QTest::newRow("Blank line followed by No newline") << patch
                                << fileDataList5;

    //////////////

    // Based on 953cdb97
    patch = "diff --git a/src/plugins/texteditor/basetextdocument.h b/src/plugins/texteditor/textdocument.h\n"
            "similarity index 100%\n"
            "rename from src/plugins/texteditor/basetextdocument.h\n"
            "rename to src/plugins/texteditor/textdocument.h\n"
            "diff --git a/src/plugins/texteditor/basetextdocumentlayout.cpp b/src/plugins/texteditor/textdocumentlayout.cpp\n"
            "similarity index 79%\n"
            "rename from src/plugins/texteditor/basetextdocumentlayout.cpp\n"
            "rename to src/plugins/texteditor/textdocumentlayout.cpp\n"
            "index 0121933..01cc3a0 100644\n"
            "--- a/src/plugins/texteditor/basetextdocumentlayout.cpp\n"
            "+++ b/src/plugins/texteditor/textdocumentlayout.cpp\n"
            "@@ -2,5 +2,5 @@ void func()\n"
            " A\n"
            " B\n"
            "-C\n"
            "+Z\n"
            " D\n"
            " \n"
            ;

    fileData1 = {};
    fileData1.fileInfo = {DiffFileInfo("src/plugins/texteditor/basetextdocument.h"),
                          DiffFileInfo("src/plugins/texteditor/textdocument.h")};
    fileData1.fileOperation = FileData::RenameFile;
    fileData2 = {};
    fileData2.fileInfo = {DiffFileInfo("src/plugins/texteditor/basetextdocumentlayout.cpp", "0121933"),
                          DiffFileInfo("src/plugins/texteditor/textdocumentlayout.cpp", "01cc3a0")};
    fileData2.fileOperation = FileData::RenameFile;
    chunkData2.startingLineNumber = {1, 1};
    rows2.clear();
    rows2 << RowData(_("A"));
    rows2 << RowData(_("B"));
    rows2 << RowData(_("C"), _("Z"));
    rows2 << RowData(_("D"));
    rows2 << RowData(_(""));
    chunkData2.rows = rows2;
    fileData2.chunks.clear();
    fileData2.chunks << chunkData2;

    QList<FileData> fileDataList6;
    fileDataList6 << fileData1 << fileData2;

    QTest::newRow("Multiple renames") << patch
                                      << fileDataList6;

    //////////////

    // Dirty submodule
    patch = "diff --git a/src/shared/qbs b/src/shared/qbs\n"
            "--- a/src/shared/qbs\n"
            "+++ b/src/shared/qbs\n"
            "@@ -1 +1 @@\n"
            "-Subproject commit eda76354077a427d692fee05479910de31040d3f\n"
            "+Subproject commit eda76354077a427d692fee05479910de31040d3f-dirty\n"
            ;
    fileData1 = {};
    fileData1.fileInfo = {DiffFileInfo("src/shared/qbs"), DiffFileInfo("src/shared/qbs")};
    chunkData1.startingLineNumber = {0, 0};
    rows1.clear();
    rows1 << RowData(_("Subproject commit eda76354077a427d692fee05479910de31040d3f"),
                     _("Subproject commit eda76354077a427d692fee05479910de31040d3f-dirty"));
    chunkData1.rows = rows1;
    fileData1.chunks.clear();
    fileData1.chunks <<  chunkData1;

    QList<FileData> fileDataList7;
    fileDataList7 << fileData1;
    QTest::newRow("Dirty submodule") << patch
                                     << fileDataList7;

    //////////////
    patch = "diff --git a/demos/arthurplugin/arthurplugin.pro b/demos/arthurplugin/arthurplugin.pro\n"
            "new file mode 100644\n"
            "index 0000000..c5132b4\n"
            "--- /dev/null\n"
            "+++ b/demos/arthurplugin/arthurplugin.pro\n"
            "@@ -0,0 +1 @@\n"
            "+XXX\n"
            "diff --git a/demos/arthurplugin/bg1.jpg b/demos/arthurplugin/bg1.jpg\n"
            "new file mode 100644\n"
            "index 0000000..dfc7cee\n"
            "Binary files /dev/null and b/demos/arthurplugin/bg1.jpg differ\n"
            "diff --git a/demos/arthurplugin/flower.jpg b/demos/arthurplugin/flower.jpg\n"
            "new file mode 100644\n"
            "index 0000000..f8e022c\n"
            "Binary files /dev/null and b/demos/arthurplugin/flower.jpg differ\n"
            ;

    fileData1 = {};
    fileData1.fileInfo = {DiffFileInfo("demos/arthurplugin/arthurplugin.pro", "0000000"),
                          DiffFileInfo("demos/arthurplugin/arthurplugin.pro", "c5132b4")};
    fileData1.fileOperation = FileData::NewFile;
    chunkData1 = {};
    chunkData1.startingLineNumber = {-1, 0};
    rows1.clear();
    rows1 << RowData(TextLineData::Separator, _("XXX"));
    rows1 << RowData(TextLineData::Separator, TextLineData(TextLineData::TextLine));
    chunkData1.rows = rows1;
    fileData1.chunks << chunkData1;
    fileData2 = {};
    fileData2.fileInfo = {DiffFileInfo("demos/arthurplugin/bg1.jpg", "0000000"),
                          DiffFileInfo("demos/arthurplugin/bg1.jpg", "dfc7cee")};
    fileData2.fileOperation = FileData::NewFile;
    fileData2.binaryFiles = true;
    fileData3 = {};
    fileData3.fileInfo = {DiffFileInfo("demos/arthurplugin/flower.jpg", "0000000"),
                          DiffFileInfo("demos/arthurplugin/flower.jpg", "f8e022c")};
    fileData3.fileOperation = FileData::NewFile;
    fileData3.binaryFiles = true;

    QList<FileData> fileDataList8;
    fileDataList8 << fileData1 << fileData2 << fileData3;

    QTest::newRow("Binary files") << patch
                                  << fileDataList8;

    //////////////
    patch = "diff --git a/script.sh b/script.sh\n"
            "old mode 100644\n"
            "new mode 100755\n"
            ;

    fileData1 = {};
    fileData1.fileInfo = {DiffFileInfo("script.sh"), DiffFileInfo("script.sh")};
    fileData1.fileOperation = FileData::ChangeMode;

    QList<FileData> fileDataList9;
    fileDataList9 << fileData1;

    QTest::newRow("Mode change") << patch << fileDataList9;

    //////////////
    patch = R"(diff --git a/old.sh b/new.sh
old mode 100644
new mode 100755
similarity index 100%
rename from old.sh
rename to new.sh
)"
            ;

    fileData1 = {};
    fileData1.fileInfo = {DiffFileInfo("old.sh"), DiffFileInfo("new.sh")};
    fileData1.fileOperation = FileData::RenameFile;

    QList<FileData> fileDataList10;
    fileDataList10 << fileData1;

    QTest::newRow("Mode change + rename") << patch << fileDataList10;

    //////////////

    // Subversion New
    patch = "Index: src/plugins/subversion/subversioneditor.cpp\n"
            "===================================================================\n"
            "--- src/plugins/subversion/subversioneditor.cpp\t(revision 0)\n"
            "+++ src/plugins/subversion/subversioneditor.cpp\t(revision 0)\n"
            "@@ -0,0 +125 @@\n\n";
    fileData1 = {};
    fileData1.fileInfo = {DiffFileInfo("src/plugins/subversion/subversioneditor.cpp"),
                          DiffFileInfo("src/plugins/subversion/subversioneditor.cpp")};
    chunkData1 = {};
    chunkData1.startingLineNumber = {-1, 124};
    fileData1.chunks << chunkData1;
    QList<FileData> fileDataList21;
    fileDataList21 << fileData1;
    QTest::newRow("Subversion New") << patch
                                    << fileDataList21;

    //////////////

    // Subversion Deleted
    patch = "Index: src/plugins/subversion/subversioneditor.cpp\n"
            "===================================================================\n"
            "--- src/plugins/subversion/subversioneditor.cpp\t(revision 42)\n"
            "+++ src/plugins/subversion/subversioneditor.cpp\t(working copy)\n"
            "@@ -1,125 +0,0 @@\n\n";
    fileData1 = {};
    fileData1.fileInfo = {DiffFileInfo("src/plugins/subversion/subversioneditor.cpp"),
                          DiffFileInfo("src/plugins/subversion/subversioneditor.cpp")};
    chunkData1 = {};
    chunkData1.startingLineNumber = {0, -1};
    fileData1.chunks << chunkData1;
    QList<FileData> fileDataList22;
    fileDataList22 << fileData1;
    QTest::newRow("Subversion Deleted") << patch
                                        << fileDataList22;

    //////////////

    // Subversion Normal
    patch = "Index: src/plugins/subversion/subversioneditor.cpp\n"
            "===================================================================\n"
            "--- src/plugins/subversion/subversioneditor.cpp\t(revision 42)\n"
            "+++ src/plugins/subversion/subversioneditor.cpp\t(working copy)\n"
            "@@ -120,7 +120,7 @@\n\n";
    fileData1 = {};
    fileData1.fileInfo = {DiffFileInfo("src/plugins/subversion/subversioneditor.cpp"),
                          DiffFileInfo("src/plugins/subversion/subversioneditor.cpp")};
    chunkData1 = {};
    chunkData1.startingLineNumber = {119, 119};
    fileData1.chunks << chunkData1;
    QList<FileData> fileDataList23;
    fileDataList23 << fileData1;
    QTest::newRow("Subversion Normal") << patch
                                       << fileDataList23;
}

void DiffEditor::Internal::DiffEditorPlugin::testReadPatch()
{
    QFETCH(QString, sourcePatch);
    QFETCH(QList<FileData>, fileDataList);

    const std::optional<QList<FileData>> result = DiffUtils::readPatch(sourcePatch);
    const bool ok = result.has_value();

    QVERIFY(ok);
    QCOMPARE(result->count(), fileDataList.count());
    for (int i = 0; i < fileDataList.count(); i++) {
        const FileData &origFileData = fileDataList.at(i);
        const FileData &resultFileData = result->at(i);
        QCOMPARE(resultFileData.fileInfo[LeftSide].fileName, origFileData.fileInfo[LeftSide].fileName);
        QCOMPARE(resultFileData.fileInfo[LeftSide].typeInfo, origFileData.fileInfo[LeftSide].typeInfo);
        QCOMPARE(resultFileData.fileInfo[RightSide].fileName, origFileData.fileInfo[RightSide].fileName);
        QCOMPARE(resultFileData.fileInfo[RightSide].typeInfo, origFileData.fileInfo[RightSide].typeInfo);
        QCOMPARE(resultFileData.chunks.count(), origFileData.chunks.count());
        QCOMPARE(resultFileData.fileOperation, origFileData.fileOperation);
        for (int j = 0; j < origFileData.chunks.count(); j++) {
            const ChunkData &origChunkData = origFileData.chunks.at(j);
            const ChunkData &resultChunkData = resultFileData.chunks.at(j);
            QCOMPARE(resultChunkData.startingLineNumber[LeftSide], origChunkData.startingLineNumber[LeftSide]);
            QCOMPARE(resultChunkData.startingLineNumber[RightSide], origChunkData.startingLineNumber[RightSide]);
            QCOMPARE(resultChunkData.contextChunk, origChunkData.contextChunk);
            QCOMPARE(resultChunkData.rows.count(), origChunkData.rows.count());
            for (int k = 0; k < origChunkData.rows.count(); k++) {
                const RowData &origRowData = origChunkData.rows.at(k);
                const RowData &resultRowData = resultChunkData.rows.at(k);
                QCOMPARE(resultRowData.equal, origRowData.equal);
                QCOMPARE(resultRowData.line[LeftSide].text, origRowData.line[LeftSide].text);
                QCOMPARE(resultRowData.line[LeftSide].textLineType, origRowData.line[LeftSide].textLineType);
                QCOMPARE(resultRowData.line[RightSide].text, origRowData.line[RightSide].text);
                QCOMPARE(resultRowData.line[RightSide].textLineType, origRowData.line[RightSide].textLineType);
            }
        }
    }
}

using ListOfStringPairs = QList<QPair<QString, QString>>;

void DiffEditor::Internal::DiffEditorPlugin::testFilterPatch_data()
{
    QTest::addColumn<ChunkData>("chunk");
    QTest::addColumn<ListOfStringPairs>("rows");
    QTest::addColumn<ChunkSelection>("selection");
    QTest::addColumn<PatchAction>("patchAction");

    auto createChunk = [] {
        ChunkData chunk;
        chunk.contextInfo = "void DiffEditor::ctor()";
        chunk.contextChunk = false;
        chunk.startingLineNumber = {49, 49};
        return chunk;
    };
    auto appendRow = [](ChunkData *chunk, const QString &left, const QString &right) {
        RowData row;
        row.equal = (left == right);
        row.line[LeftSide].text = left;
        row.line[LeftSide].textLineType = left.isEmpty() ? TextLineData::Separator : TextLineData::TextLine;
        row.line[RightSide].text = right;
        row.line[RightSide].textLineType = right.isEmpty() ? TextLineData::Separator : TextLineData::TextLine;
        chunk->rows.append(row);
    };
    ChunkData chunk;
    ListOfStringPairs rows;

    chunk = createChunk();
    appendRow(&chunk, "A", "A"); // 50
    appendRow(&chunk, "",  "B"); // 51 +
    appendRow(&chunk, "C", "C"); // 52
    rows = ListOfStringPairs {
        {"A", "A"},
        {"", "B"},
        {"C", "C"}
    };
    QTest::newRow("one added") << chunk << rows << ChunkSelection() << PatchAction::Apply;

    chunk = createChunk();
    appendRow(&chunk, "A", "A"); // 50
    appendRow(&chunk, "B", "");  // 51 -
    appendRow(&chunk, "C", "C"); // 52
    rows = ListOfStringPairs {
        {"A", "A"},
        {"B", ""},
        {"C", "C"}
    };
    QTest::newRow("one removed") << chunk << rows << ChunkSelection() << PatchAction::Apply;

    chunk = createChunk();
    appendRow(&chunk, "A", "A"); // 50
    appendRow(&chunk, "",  "B"); // 51
    appendRow(&chunk, "",  "C"); // 52 +
    appendRow(&chunk, "",  "D"); // 53 +
    appendRow(&chunk, "",  "E"); // 54
    appendRow(&chunk, "F", "F"); // 55
    rows = ListOfStringPairs {
        {"A", "A"},
        {"", "C"},
        {"", "D"},
        {"F", "F"}
    };
    QTest::newRow("stage selected added") << chunk << rows << ChunkSelection({2, 3}, {2, 3})
                                          << PatchAction::Apply;

    chunk = createChunk();
    appendRow(&chunk, "A", "A"); // 50
    appendRow(&chunk, "",  "B"); // 51 +
    appendRow(&chunk, "C", "D"); // 52
    appendRow(&chunk, "E", "E"); // 53
    rows = ListOfStringPairs {
        {"A", "A"},
        {"", "B"},
        {"C", "C"},
        {"E", "E"}
    };
    QTest::newRow("stage selected added keep changed") << chunk << rows << ChunkSelection({1}, {1})
                                                       << PatchAction::Apply;

    chunk = createChunk();
    appendRow(&chunk, "A", "A"); // 50
    appendRow(&chunk, "B", "");  // 51
    appendRow(&chunk, "C", "");  // 52 -
    appendRow(&chunk, "D", "");  // 53 -
    appendRow(&chunk, "E", "");  // 54
    appendRow(&chunk, "F", "F"); // 55
    rows = ListOfStringPairs {
        {"A", "A"},
        {"B", "B"},
        {"C", ""},
        {"D", ""},
        {"E", "E"},
        {"F", "F"}
    };
    QTest::newRow("stage selected removed") << chunk << rows << ChunkSelection({2, 3}, {2, 3})
                                            << PatchAction::Apply;

    chunk = createChunk();
    appendRow(&chunk, "A", "A"); // 50
    appendRow(&chunk, "B", "");  // 51
    appendRow(&chunk, "C", "");  // 52 -
    appendRow(&chunk, "",  "D"); // 53 +
    appendRow(&chunk, "",  "E"); // 54
    appendRow(&chunk, "F", "F"); // 55
    rows = ListOfStringPairs {
        {"A", "A"},
        {"B", "B"},
        {"C", ""},
        {"", "D"},
        {"F", "F"}
    };
    QTest::newRow("stage selected added/removed") << chunk << rows << ChunkSelection({2, 3}, {2, 3})
                                                  << PatchAction::Apply;

    chunk = createChunk();
    appendRow(&chunk, "A", "A"); // 50
    appendRow(&chunk, "B", "C"); // 51 -/+
    appendRow(&chunk, "D", "D"); // 52
    rows = ListOfStringPairs {
        {"A", "A"},
        {"B", "C"},
        {"D", "D"}
    };
    QTest::newRow("stage modified row") << chunk << rows << ChunkSelection({1}, {1})
                                        << PatchAction::Apply;

    chunk = createChunk();
    appendRow(&chunk, "A", "A"); // 50
    appendRow(&chunk, "B", "C"); // 51 -/+
    appendRow(&chunk, "D", "D"); // 52
    rows = ListOfStringPairs {
        {"A", "A"},
        {"B", "C"},
        {"D", "D"}
    };
    QTest::newRow("stage modified and unmodified rows") << chunk << rows
                  << ChunkSelection({0, 1, 2}, {0, 1, 2}) << PatchAction::Apply;

    chunk = createChunk();
    appendRow(&chunk, "A", "A"); // 50
    appendRow(&chunk, "B", "C"); // 51 -/+
    appendRow(&chunk, "D", "D"); // 52
    rows = ListOfStringPairs {
        {"A", "A"},
        {"B", "C"},
        {"D", "D"}
    };
    QTest::newRow("stage unmodified left rows") << chunk << rows << ChunkSelection({0, 1, 2}, {1})
                                                << PatchAction::Apply;

    chunk = createChunk();
    appendRow(&chunk, "A", "A"); // 50
    appendRow(&chunk, "B", "C"); // 51 -/+
    appendRow(&chunk, "D", "D"); // 52
    rows = ListOfStringPairs {
        {"A", "A"},
        {"B", "C"},
        {"D", "D"}
    };
    QTest::newRow("stage unmodified right rows") << chunk << rows << ChunkSelection({1}, {0, 1, 2})
                                                 << PatchAction::Apply;

    chunk = createChunk();
    appendRow(&chunk, "A", "A"); // 50
    appendRow(&chunk, "B", "C"); // 51 -/+
    appendRow(&chunk, "D", "D"); // 52
    rows = ListOfStringPairs {
        {"A", "A"},
        {"B", ""},
        {"D", "D"}
    };
    QTest::newRow("stage left only") << chunk << rows << ChunkSelection({1}, {})
                                     << PatchAction::Apply;

    chunk = createChunk();
    appendRow(&chunk, "A", "A"); // 50
    appendRow(&chunk, "B", "C"); // 51 -/+
    appendRow(&chunk, "D", "D"); // 52
    rows = ListOfStringPairs {
        {"A", "A"},
        {"B", "B"},
        {"", "C"},
        {"D", "D"}
    };
    QTest::newRow("stage right only") << chunk << rows << ChunkSelection({}, {1})
                                      << PatchAction::Apply;

    chunk = createChunk();
    appendRow(&chunk, "A", "A"); // 50
    appendRow(&chunk, "B", "C"); // 51 -/+
    appendRow(&chunk, "D", "D"); // 52
    rows = ListOfStringPairs {
        {"A", "A"},
        {"B", "C"},
        {"D", "D"}
    };
    QTest::newRow("stage modified row and revert") << chunk << rows << ChunkSelection({1}, {1})
                                                   << PatchAction::Revert;

    chunk = createChunk();
    appendRow(&chunk, "A", "A"); // 50
    appendRow(&chunk, "B", "C"); // 51 -/+
    appendRow(&chunk, "D", "D"); // 52
    rows = ListOfStringPairs {
        {"A", "A"},
        {"B", ""},
        {"C", "C"},
        {"D", "D"}
    };
    // symmetric to: "stage right only"
    QTest::newRow("stage left only and revert") << chunk << rows << ChunkSelection({1}, {})
                                                << PatchAction::Revert;

    chunk = createChunk();
    appendRow(&chunk, "A", "A"); // 50
    appendRow(&chunk, "B", "C"); // 51 -/+
    appendRow(&chunk, "D", "D"); // 52
    rows = ListOfStringPairs {
        {"A", "A"},
        {"", "C"},
        {"D", "D"}
    };
    // symmetric to: "stage left only"
    QTest::newRow("stage right only and revert") << chunk << rows << ChunkSelection({}, {1})
                                                 << PatchAction::Revert;
}

void DiffEditor::Internal::DiffEditorPlugin::testFilterPatch()
{
    QFETCH(ChunkData, chunk);
    QFETCH(ListOfStringPairs, rows);
    QFETCH(ChunkSelection, selection);
    QFETCH(PatchAction, patchAction);

    const ChunkData result = DiffEditorDocument::filterChunk(chunk, selection, patchAction);
    QCOMPARE(result.rows.size(), rows.size());
    for (int i = 0; i < rows.size(); ++i) {
        QCOMPARE(result.rows.at(i).line[LeftSide].text, rows.at(i).first);
        QCOMPARE(result.rows.at(i).line[RightSide].text, rows.at(i).second);
    }
}

void DiffEditor::Internal::DiffEditorPlugin::testDiffDocuments()
{
    // Two scratch documents without a file path, as produced by "Copy to New
    // Editor". diffDocuments() must open a diff of their in-memory contents,
    // even though there is nothing on disk to compare. See QTCREATORBUG-33271.
    QString leftTitle = "left";
    QString rightTitle = "right";
    IEditor *left = EditorManager::openEditorWithContents(
        Core::Constants::K_DEFAULT_TEXT_EDITOR_ID, &leftTitle, "a\nb\nc\n");
    IEditor *right = EditorManager::openEditorWithContents(
        Core::Constants::K_DEFAULT_TEXT_EDITOR_ID, &rightTitle, "a\nB\nc\n");
    QVERIFY(left && right);
    QVERIFY(left->document()->filePath().isEmpty());

    DiffService *service = DiffService::instance();
    QVERIFY(service);
    service->diffDocuments(left->document(), right->document());

    // A diff editor for the two documents must have been opened and activated.
    auto diffDocument = qobject_cast<DiffEditorDocument *>(EditorManager::currentDocument());
    QVERIFY(diffDocument);

    EditorManager::closeDocuments({left->document(), right->document(), diffDocument}, false);
}

#include "inlinediff.h"

#include <texteditor/displaysettings.h>
#include <texteditor/inlinediffdecorator.h>
#include <texteditor/textdocument.h>
#include <texteditor/texteditor.h>

#include <utils/environment.h>
#include <utils/plaintextedit/texteditorlayout.h>

void DiffEditor::Internal::DiffEditorPlugin::testInlineDiff()
{
    using namespace TextEditor;

    // baseline vs editor: line 2 modified, line 3 removed, a long line added.
    // The added line is long enough to word wrap, which the side by side view
    // has to account for when it aligns the two sides row by row.
    const QString baselineText = "one\ntwo\nthree\nfour\n";
    const QString longAddedLine = QString("five ").repeated(60).trimmed();
    const QString editorText = "one\ntwo changed\nfour\n" + longAddedLine + "\n";

    QTemporaryDir temporaryDir;
    QVERIFY(temporaryDir.isValid());
    const FilePath sourceFile
        = FilePath::fromString(temporaryDir.path()) / "testInlineDiff.txt";
    QVERIFY(sourceFile.writeFileContents(editorText.toUtf8()));
    IEditor *sourceEditor = EditorManager::openEditor(sourceFile);
    QVERIFY(sourceEditor);
    auto sourceTextEditor = qobject_cast<BaseTextEditor *>(sourceEditor);
    QVERIFY(sourceTextEditor);
    TextEditorWidget *sourceWidget = sourceTextEditor->editorWidget();
    QVERIFY(sourceWidget);
    const TextDocumentPtr sourceDocument = sourceWidget->textDocumentPtr();
    QVERIFY(sourceDocument);

    InlineDiffBaseline baseline;
    baseline.id = "test";
    baseline.displayName = "Test";
    baseline.fetchText = [baselineText](const InlineDiffBaseline::TextCallback &callback) {
        callback(baselineText);
    };
    QString stagedEditorText;
    baseline.stageHunk = [&stagedEditorText](const InlineDiffChunk &,
                                              const QString &editorText) {
        stagedEditorText = editorText;
    };

    const QString diffTitle = "testInlineDiff.txt (Working Tree)";
    IEditor *diffEditor = openInlineDiffEditor(sourceDocument, baseline, diffTitle);
    QVERIFY(diffEditor);
    QVERIFY(diffEditor != sourceEditor);
    QCOMPARE(diffEditor->document()->displayName(), diffTitle);
    // reopening reuses the existing inline diff editor
    QCOMPARE(openInlineDiffEditor(sourceDocument, baseline, diffTitle), diffEditor);

    setInlineDiffViewMode(diffEditor, InlineDiffViewMode::Inline);
    // the inline diff editor shares the text with the source document
    TextEditorWidget *diffWidget
        = Utils::findOrDefault(diffEditor->widget()->findChildren<TextEditorWidget *>(),
                               [&sourceDocument](TextEditorWidget *widget) {
        return widget->document() == sourceDocument->document();
    });
    QVERIFY(diffWidget);
    diffEditor->widget()->resize(800, 600);

    // added/changed line highlights arrive asynchronously
    QTRY_VERIFY(!inlineDiffChangedCharTexts(diffWidget).isEmpty());

    const auto ghostItemCount = [](TextEditorWidget *widget) {
        int count = 0;
        for (QTextBlock block = widget->document()->firstBlock(); block.isValid();
             block = block.next()) {
            count += widget->editorLayout()
                         ->layoutItemsForCategory(block, inlineDiffGhostCategory())
                         .size();
        }
        return count;
    };
    // one ghost row block for the modified "two" and the removed "three"
    QTRY_COMPARE(ghostItemCount(diffWidget), 1);

    // the ghost rows reserve additional height on their anchor block (line 2)
    const QTextBlock anchorBlock = diffWidget->document()->findBlockByNumber(1);
    QVERIFY(diffWidget->editorLayout()->additionalBlockHeight(anchorBlock, true) > 0);

    // the source editor stays free of decorations
    QCOMPARE(ghostItemCount(sourceWidget), 0);
    QVERIFY(inlineDiffChangedCharTexts(sourceWidget).isEmpty());

    const QString grabPath = Utils::qtcEnvironmentVariable("QTC_INLINE_DIFF_GRAB");
    if (!grabPath.isEmpty())
        diffWidget->grab().save(grabPath);

    // the side by side view shows the baseline in an additional read only
    // view and aligns the two sides row by row, so that mirrored pixel scroll
    // bars keep them in sync even when a line word wraps on only one side
    setInlineDiffViewMode(diffEditor, InlineDiffViewMode::SideBySide);
    QCOMPARE(inlineDiffViewMode(diffEditor), InlineDiffViewMode::SideBySide);
    const QList<TextEditorWidget *> sideBySideWidgets
        = diffEditor->widget()->findChildren<TextEditorWidget *>();
    QCOMPARE(sideBySideWidgets.size(), 2);
    TextEditorWidget *baselineWidget = sideBySideWidgets.first() == diffWidget
                                           ? sideBySideWidgets.last()
                                           : sideBySideWidgets.first();
    QVERIFY(baselineWidget->isReadOnly());
    QCOMPARE(baselineWidget->document()->toPlainText(), baselineText);
    QVERIFY(!baselineWidget->isHidden());
    // turn on word wrap and show the views at a width where the long added
    // line actually wraps - the alignment has to account for that
    for (TextEditorWidget *widget : {baselineWidget, diffWidget}) {
        DisplaySettingsData settings = widget->displaySettings();
        settings.m_textWrapping = true;
        widget->setDisplaySettings(settings);
    }
    diffEditor->widget()->resize(600, 600);
    diffEditor->widget()->show();
    QCoreApplication::processEvents();

    // Lay out all blocks so the alignment items measure real wrapped heights.
    const auto layOut = [](TextEditorWidget *widget) {
        TextEditorLayout *layout = widget->editorLayout();
        for (QTextBlock block = widget->document()->firstBlock(); block.isValid();
             block = block.next()) {
            layout->ensureBlockLayout(block);
        }
    };
    // pixel top of a line's text, including any spacer rows reserved above it
    const auto textTop = [&layOut](TextEditorWidget *widget, int lineIndex) {
        layOut(widget);
        TextEditorLayout *layout = widget->editorLayout();
        const QTextBlock block = widget->document()->findBlockByNumber(lineIndex);
        return layout->offsetForBlock(block) + layout->mainLayoutOffset(block);
    };
    // the added line wraps on the editor side; the alignment has to reserve a
    // matching multi line height on the baseline side
    QVERIFY(longAddedLine.size() > 100);
    const int lastLine = diffWidget->document()->blockCount() - 1;
    const auto hasWrappedLine = [&layOut](TextEditorWidget *widget) {
        layOut(widget);
        for (QTextBlock b = widget->document()->firstBlock(); b.isValid(); b = b.next()) {
            if (widget->editorLayout()->blockLineCount(b) > 1)
                return true;
        }
        return false;
    };
    QTRY_VERIFY(hasWrappedLine(diffWidget));
    // the trailing line follows the whole changed and wrapped region; it lines
    // up on both sides only if every row above has the same height on each side
    QCOMPARE(baselineWidget->document()->blockCount(),
             diffWidget->document()->blockCount());
    QTRY_COMPARE(textTop(baselineWidget, lastLine), textTop(diffWidget, lastLine));
    // full per-row alignment makes both sides the same total pixel height
    QCOMPARE(baselineWidget->editorLayout()->documentPixelHeight(),
             diffWidget->editorLayout()->documentPixelHeight());

    const QString sideGrabPath
        = Utils::qtcEnvironmentVariable("QTC_INLINE_DIFF_SIDE_GRAB");
    if (!sideGrabPath.isEmpty()) {
        diffEditor->widget()->resize(800, 300);
        diffEditor->widget()->grab().save(sideGrabPath);
    }

    // switching back restores the inline decorations and hides the baseline
    setInlineDiffViewMode(diffEditor, InlineDiffViewMode::Inline);
    QTRY_COMPARE(ghostItemCount(diffWidget), 1);
    QVERIFY(baselineWidget->isHidden());
    QCOMPARE(ghostItemCount(baselineWidget), 0);

    // editing the shared buffer recomputes the diff (debounced)
    QTextCursor cursor(sourceWidget->document());
    cursor.movePosition(QTextCursor::End);
    cursor.insertText("six\n");
    QCOMPARE(diffWidget->document()->lastBlock().previous().text(), "six");
    QTRY_VERIFY(Utils::anyOf(inlineDiffChangedCharTexts(diffWidget),
                             [](const QString &text) { return text.contains("six"); }));

    // saving through the inline diff editor's document saves the source file
    QVERIFY(diffEditor->document()->isModified());
    QVERIFY(!diffEditor->document()->isSaveAsNeeded());
    QVERIFY(EditorManager::saveDocument(diffEditor->document()));
    QVERIFY(!sourceDocument->isModified());
    QVERIFY(!diffEditor->document()->isModified());
    QVERIFY(sourceFile.fileContents().value_or(QByteArray()).contains("six"));

    // the hunk controls offer stage and revert buttons; staging hands the
    // hunk's editor lines to the baseline
    {
        QList<QAbstractButton *> buttons;
        QTRY_VERIFY((buttons = diffWidget->findChildren<QAbstractButton *>(),
                     buttons.size() >= 2));
        const QPointer<QAbstractButton> stageButton = buttons.first();
        stageButton->click(); // the stage button comes first
        QTRY_VERIFY(!stagedEditorText.isEmpty());
        QVERIFY(stagedEditorText.contains("two changed"));
        // clicking tears down the controls and schedules a debounced
        // recompute; wait for the stale buttons to go before reverting
        QTRY_VERIFY(stageButton.isNull());
    }

    // reverting all hunks through the floating controls restores the baseline
    for (int i = 0; i < 10 && sourceDocument->plainText() != baselineText; ++i) {
        QList<QAbstractButton *> buttons;
        QTRY_VERIFY((buttons = diffWidget->findChildren<QAbstractButton *>(),
                     !buttons.isEmpty()));
        // every second button is a revert button
        QVERIFY(buttons.size() % 2 == 0);
        const QPointer<QAbstractButton> revertButton = buttons.at(1);
        revertButton->click();
        // wait for the debounced recompute to tear down the stale controls,
        // so the next iteration picks up freshly rebuilt buttons
        QTRY_VERIFY(revertButton.isNull());
    }
    QCOMPARE(sourceDocument->plainText(), baselineText);

    // closing the source document also closes the inline diff editor
    const QPointer<QWidget> diffWidgetGuard = diffWidget;
    QVERIFY(EditorManager::closeDocuments({sourceDocument.data()}, false));
    QTRY_VERIFY(diffWidgetGuard.isNull());
}

void DiffEditor::Internal::DiffEditorPlugin::testInlineDiffCollapse()
{
    using namespace TextEditor;

    // a single change surrounded by many unchanged lines, so the unchanged
    // runs before and after it are long enough to collapse
    QStringList baselineLines;
    for (int i = 1; i <= 40; ++i)
        baselineLines << QString("line %1").arg(i);
    QStringList editorLines = baselineLines;
    editorLines[19] = "line 20 changed"; // 1-based line 20
    const QString baselineText = baselineLines.join('\n') + '\n';
    const QString editorText = editorLines.join('\n') + '\n';

    QTemporaryDir temporaryDir;
    QVERIFY(temporaryDir.isValid());
    const FilePath sourceFile
        = FilePath::fromString(temporaryDir.path()) / "testInlineDiffCollapse.txt";
    QVERIFY(sourceFile.writeFileContents(editorText.toUtf8()));
    IEditor *sourceEditor = EditorManager::openEditor(sourceFile);
    QVERIFY(sourceEditor);
    auto sourceTextEditor = qobject_cast<BaseTextEditor *>(sourceEditor);
    QVERIFY(sourceTextEditor);
    TextEditorWidget *sourceWidget = sourceTextEditor->editorWidget();
    QVERIFY(sourceWidget);
    const TextDocumentPtr sourceDocument = sourceWidget->textDocumentPtr();
    QVERIFY(sourceDocument);

    InlineDiffBaseline baseline;
    baseline.id = "test";
    baseline.displayName = "Test";
    baseline.fetchText = [baselineText](const InlineDiffBaseline::TextCallback &callback) {
        callback(baselineText);
    };

    IEditor *diffEditor = openInlineDiffEditor(sourceDocument, baseline,
                                               "testInlineDiffCollapse.txt");
    QVERIFY(diffEditor);
    setInlineDiffViewMode(diffEditor, InlineDiffViewMode::Inline);
    TextEditorWidget *diffWidget
        = Utils::findOrDefault(diffEditor->widget()->findChildren<TextEditorWidget *>(),
                               [&sourceDocument](TextEditorWidget *widget) {
        return widget->document() == sourceDocument->document();
    });
    QVERIFY(diffWidget);
    diffEditor->widget()->resize(800, 600);
    diffEditor->widget()->show();

    QTextDocument *doc = diffWidget->document();
    TextEditorLayout *layout = diffWidget->editorLayout();
    const auto visibleInEditor = [&](int line1) {
        return layout->isBlockVisibleInEditor(doc->findBlockByNumber(line1 - 1));
    };

    // collapsing is on by default: the unchanged lines away from the change
    // are hidden in the diff view's layout only
    QTRY_VERIFY(!visibleInEditor(1));
    QVERIFY(layout->hasEditorHiddenBlocks());
    // three context lines are kept around the change on line 20
    QVERIFY(!visibleInEditor(16));
    QVERIFY(visibleInEditor(17));
    QVERIFY(visibleInEditor(20));
    QVERIFY(visibleInEditor(23));
    QVERIFY(!visibleInEditor(24));
    // the last line stays visible as an anchor below the trailing run
    QVERIFY(visibleInEditor(doc->blockCount()));
    // hiding lines shrinks the laid out line count and the pixel height
    QVERIFY(layout->lineCount() < doc->blockCount());

    // a clickable placeholder row is floated over each collapsed run
    QTRY_COMPARE(diffWidget->viewport()
                     ->findChildren<QWidget *>("InlineDiffCollapsedRow").size(), 2);

    const QString grabPath = Utils::qtcEnvironmentVariable("QTC_INLINE_DIFF_COLLAPSE_GRAB");
    if (!grabPath.isEmpty()) {
        QCoreApplication::processEvents();
        diffWidget->grab().save(grabPath);
    }

    // the shared source editor is unaffected: it shows the full file
    TextEditorLayout *sourceLayout = sourceWidget->editorLayout();
    QVERIFY(!sourceLayout->hasEditorHiddenBlocks());
    QVERIFY(sourceLayout->isBlockVisibleInEditor(
        sourceWidget->document()->findBlockByNumber(0)));

    // turning the toggle off through the toolbar expands the whole file again
    auto toolBar = qobject_cast<QToolBar *>(diffEditor->toolBar());
    QVERIFY(toolBar);
    QAction *collapseAction = Utils::findOrDefault(toolBar->actions(), [](QAction *a) {
        return a->objectName() == "InlineDiffCollapseAction";
    });
    QVERIFY(collapseAction);
    QVERIFY(collapseAction->isChecked());
    collapseAction->setChecked(false);
    QTRY_VERIFY(!layout->hasEditorHiddenBlocks());
    QVERIFY(visibleInEditor(1));
    QCOMPARE(layout->lineCount(), doc->blockCount());

    // and back on collapses again
    collapseAction->setChecked(true);
    QTRY_VERIFY(layout->hasEditorHiddenBlocks());
    QVERIFY(!visibleInEditor(1));

    // the side by side view collapses both sides at their corresponding lines
    // and keeps them aligned row by row
    setInlineDiffViewMode(diffEditor, InlineDiffViewMode::SideBySide);
    const QList<TextEditorWidget *> sideWidgets
        = diffEditor->widget()->findChildren<TextEditorWidget *>();
    QCOMPARE(sideWidgets.size(), 2);
    TextEditorWidget *baselineWidget = sideWidgets.first() == diffWidget ? sideWidgets.last()
                                                                         : sideWidgets.first();
    QVERIFY(baselineWidget->isReadOnly());
    diffEditor->widget()->resize(1000, 600);
    diffEditor->widget()->show();
    QCoreApplication::processEvents();

    QTRY_VERIFY(baselineWidget->editorLayout()->hasEditorHiddenBlocks());
    QVERIFY(layout->hasEditorHiddenBlocks());
    // a placeholder row on each side
    QTRY_COMPARE(diffWidget->viewport()
                     ->findChildren<QWidget *>("InlineDiffCollapsedRow").size(), 2);
    QCOMPARE(baselineWidget->viewport()
                 ->findChildren<QWidget *>("InlineDiffCollapsedRow").size(), 2);
    // lay out all blocks so the pixel heights are real, then check both sides
    // ended up the same total height (the aligner stays in sync across the
    // collapsed runs)
    const auto layOut = [](TextEditorWidget *widget) {
        TextEditorLayout *l = widget->editorLayout();
        for (QTextBlock block = widget->document()->firstBlock(); block.isValid();
             block = block.next()) {
            l->ensureBlockLayout(block);
        }
    };
    layOut(diffWidget);
    layOut(baselineWidget);
    QTRY_COMPARE(baselineWidget->editorLayout()->documentPixelHeight(),
                 diffWidget->editorLayout()->documentPixelHeight());

    const QString sideGrabPath
        = Utils::qtcEnvironmentVariable("QTC_INLINE_DIFF_COLLAPSE_SIDE_GRAB");
    if (!sideGrabPath.isEmpty()) {
        QCoreApplication::processEvents();
        diffEditor->widget()->grab().save(sideGrabPath);
    }

    const QPointer<QWidget> diffWidgetGuard = diffWidget;
    QVERIFY(EditorManager::closeDocuments({sourceDocument.data()}, false));
    QTRY_VERIFY(diffWidgetGuard.isNull());
}

// A pure insertion diff ("only added lines"): the collapsible unchanged runs
// have different lengths on the two sides (the insertion shifts one side), so
// the side by side collapse must still hide them on both sides.
void DiffEditor::Internal::DiffEditorPlugin::testInlineDiffCollapseAddedLines()
{
    using namespace TextEditor;

    // baseline: 12 unchanged lines; editor: the same with 3 lines inserted
    // after line 5. This is a pure insertion, so the two sides produce a
    // different number of collapsible unchanged runs (the editor gets a run
    // both above and below the added block, the shorter baseline only one).
    // The collapse must still hide the unchanged lines on both sides, aligned.
    QStringList baselineLines;
    for (int i = 1; i <= 12; ++i)
        baselineLines << QString("line %1").arg(i);
    QStringList editorLines = baselineLines;
    for (int i = 0; i < 3; ++i)
        editorLines.insert(5, QString("added %1").arg(i + 1)); // after line 5
    const QString baselineText = baselineLines.join('\n') + '\n';
    const QString editorText = editorLines.join('\n') + '\n';

    QTemporaryDir temporaryDir;
    QVERIFY(temporaryDir.isValid());
    const FilePath sourceFile
        = FilePath::fromString(temporaryDir.path()) / "testInlineDiffCollapseAddedLines.txt";
    QVERIFY(sourceFile.writeFileContents(editorText.toUtf8()));
    IEditor *sourceEditor = EditorManager::openEditor(sourceFile);
    QVERIFY(sourceEditor);
    auto sourceTextEditor = qobject_cast<BaseTextEditor *>(sourceEditor);
    QVERIFY(sourceTextEditor);
    TextEditorWidget *sourceWidget = sourceTextEditor->editorWidget();
    QVERIFY(sourceWidget);
    const TextDocumentPtr sourceDocument = sourceWidget->textDocumentPtr();
    QVERIFY(sourceDocument);

    InlineDiffBaseline baseline;
    baseline.id = "test";
    baseline.displayName = "Test";
    baseline.fetchText = [baselineText](const InlineDiffBaseline::TextCallback &callback) {
        callback(baselineText);
    };

    IEditor *diffEditor = openInlineDiffEditor(sourceDocument, baseline,
                                               "testInlineDiffCollapseAddedLines.txt");
    QVERIFY(diffEditor);
    TextEditorWidget *diffWidget
        = Utils::findOrDefault(diffEditor->widget()->findChildren<TextEditorWidget *>(),
                               [&sourceDocument](TextEditorWidget *widget) {
        return widget->document() == sourceDocument->document();
    });
    QVERIFY(diffWidget);

    setInlineDiffViewMode(diffEditor, InlineDiffViewMode::SideBySide);
    const QList<TextEditorWidget *> sideWidgets
        = diffEditor->widget()->findChildren<TextEditorWidget *>();
    QCOMPARE(sideWidgets.size(), 2);
    TextEditorWidget *baselineWidget = sideWidgets.first() == diffWidget ? sideWidgets.last()
                                                                         : sideWidgets.first();
    QVERIFY(baselineWidget->isReadOnly());
    diffEditor->widget()->resize(1000, 600);
    diffEditor->widget()->show();
    QCoreApplication::processEvents();

    // both sides collapse their unchanged runs (a placeholder above and below
    // the inserted block on the editor side, matching runs on the baseline)
    QTRY_VERIFY(diffWidget->editorLayout()->hasEditorHiddenBlocks());
    QVERIFY(baselineWidget->editorLayout()->hasEditorHiddenBlocks());
    QCOMPARE(diffWidget->viewport()
                 ->findChildren<QWidget *>("InlineDiffCollapsedRow").size(), 2);
    QCOMPARE(baselineWidget->viewport()
                 ->findChildren<QWidget *>("InlineDiffCollapsedRow").size(), 2);

    const QPointer<QWidget> diffWidgetGuard = diffWidget;
    QVERIFY(EditorManager::closeDocuments({sourceDocument.data()}, false));
    QTRY_VERIFY(diffWidgetGuard.isNull());
}

#endif // WITH_TESTS

#include "diffeditorplugin.moc"
