// SPDX-FileCopyrightText: 2016 Anne Jan Brouwer
// SPDX-License-Identifier: GPL-3.0-or-later
#include <QCoreApplication>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QList>
#include <QProcessEnvironment>
#include <QTemporaryDir>
#include <QUuid>
#include <QtTest>

#include "../../../src/enums.h"
#include "../../../src/filecontent.h"
#include "../../../src/imitatepass.h"
#include "../../../src/pass.h"
#include "../../../src/passwordconfiguration.h"
#include "../../../src/qprogressindicator.h"
#include "../../../src/qtpass.h"
#include "../../../src/qtpasssettings.h"
#include "../../../src/simpletransaction.h"
#include "../../../src/userinfo.h"
#include "../../../src/util.h"

/**
 * @brief The tst_util class is our first unit test
 */
class tst_util : public QObject {
  Q_OBJECT

public:
  tst_util();
  ~tst_util() override;

public Q_SLOTS:
  void init();
  void cleanup();

private:
  struct PassStoreGuard {
    QString original;
    explicit PassStoreGuard(const QString &orig) : original(orig) {}
    ~PassStoreGuard() { QtPassSettings::setPassStore(original); }
  };

private Q_SLOTS:
  void cleanupTestCase();
  void normalizeFolderPath();
  void normalizeFolderPathEdgeCases();
  void fileContent();
  void fileContentEdgeCases();
  void namedValuesTakeValue();
  void namedValuesEdgeCases();
  void totpHiddenFromDisplay();
  void testAwsUrl();
  void regexPatterns();
  void regexPatternEdgeCases();
  void endsWithGpgEdgeCases();
  void userInfoValidity();
  void userInfoValidityEdgeCases();
  void passwordConfigurationCharacters();
  void simpleTransactionBasic();
  void simpleTransactionNested();
  void createGpgIdFile();
  void createGpgIdFileEmptyKeys();
  void generateRandomPassword();
  void boundedRandom();
  void findBinaryInPath();
  void findPasswordStore();
  void configIsValid();
  void getDirBasic();
  void getDirWithIndex();
  void findBinaryInPathNotFound();
  void findPasswordStoreEnvVar();
  void normalizeFolderPathMultipleCalls();
  void userInfoFullyValid();
  void userInfoMarginallyValid();
  void userInfoIsValid();
  void userInfoCreatedAndExpiry();
  void qProgressIndicatorBasic();
  void qProgressIndicatorStartStop();
  void namedValueBasic();
  void namedValueMultiple();
  void buildClipboardMimeDataLinux();
  void buildClipboardMimeDataWindows();
  void buildClipboardMimeDataMac();
  void utilRegexEnsuresGpg();
  void utilRegexProtocol();
  void utilRegexNewLines();
  void buildClipboardMimeDataDword();
  void imitatePassResolveMoveDestination();
  void imitatePassResolveMoveDestinationForce();
  void imitatePassResolveMoveDestinationDestExistsNoForce();
  void imitatePassResolveMoveDestinationDir();
  void imitatePassResolveMoveDestinationNonExistent();
  void imitatePassRemoveDir();
  void getRecipientListBasic();
  void getRecipientListEmpty();
  void getRecipientListWithComments();
  void getRecipientStringCount();
  void getGpgIdPathBasic();
  void getGpgIdPathSubfolder();
  void getGpgIdPathNotFound();
  void findBinaryInPathReturnedPathIsAbsolute();
  void findBinaryInPathReturnedPathIsExecutable();
  void findBinaryInPathMultipleKnownBinaries();
  void findBinaryInPathConsistency();
  void findBinaryInPathResultContainsBinaryName();
  void findBinaryInPathTempExecutableInTempDir();
};

/**
 * @brief tst_util::tst_util basic constructor
 */
tst_util::tst_util() = default;

/**
 * @brief tst_util::~tst_util basic destructor
 */
tst_util::~tst_util() = default;

/**
 * @brief tst_util::init unit test init method
 */
void tst_util::init() {
  // Intentionally left empty: no per-test setup required.
}

/**
 * @brief tst_util::cleanup unit test cleanup method
 */
void tst_util::cleanup() {
  // Intentionally left empty: no per-test cleanup required.
}

/**
 * @brief tst_util::cleanupTestCase test case cleanup method
 */
void tst_util::cleanupTestCase() {
  // No test case cleanup required; function intentionally left empty.
}

/**
 * @brief tst_util::normalizeFolderPath test to check correct working
 * of Util::normalizeFolderPath the paths should always end with a slash
 */
void tst_util::normalizeFolderPath() {
  QString result;
  QString sep = QDir::separator();

  // Forward slash path
  result = Util::normalizeFolderPath("test");
  QVERIFY(result.endsWith(sep));
  result = Util::normalizeFolderPath("test/");
  QVERIFY(result.endsWith(sep));
  // Verify exact normalized path content
  result = Util::normalizeFolderPath("test");
  QVERIFY2(result == "test" + sep,
           qPrintable(QString("Expected 'test%1', got '%2'").arg(sep, result)));
  result = Util::normalizeFolderPath("test/");
  QVERIFY2(result == "test" + sep,
           qPrintable(QString("Expected 'test%1', got '%2'").arg(sep, result)));

  // Windows-style backslash path (only on Windows)
  if (QDir::separator() == '\\') {
    result = Util::normalizeFolderPath("test\\subdir");
    QVERIFY(result.endsWith("\\"));
    QVERIFY(result.contains("test"));
    QVERIFY(result.contains("subdir"));
    // Verify exact normalized path content
    QVERIFY2(
        result == "test\\subdir\\",
        qPrintable(
            QString("Expected 'test\\\\subdir\\\\', got '%1'").arg(result)));
  }

  // Mixed separators test
  result = Util::normalizeFolderPath("test/subdir\\folder");
  QVERIFY(result.endsWith(sep));
  QVERIFY(result.contains("test"));
  QVERIFY(result.contains("subdir"));
  QVERIFY(result.contains("folder"));
}

void tst_util::fileContent() {
  NamedValue key = {"key", "val"};
  NamedValue key2 = {"key2", "val2"};
  QString password = "password";

  FileContent fc = FileContent::parse("password\n", {}, false);
  QCOMPARE(fc.getPassword(), password);
  QCOMPARE(fc.getNamedValues(), {});
  QCOMPARE(fc.getRemainingData(), QString());

  fc = FileContent::parse("password", {}, false);
  QCOMPARE(fc.getPassword(), password);
  QCOMPARE(fc.getNamedValues(), {});
  QCOMPARE(fc.getRemainingData(), QString());

  fc = FileContent::parse("password\nfoobar\n", {}, false);
  QCOMPARE(fc.getPassword(), password);
  QCOMPARE(fc.getNamedValues(), {});
  QCOMPARE(fc.getRemainingData(), QString("foobar\n"));

  fc = FileContent::parse("password\nkey: val\nkey2: val2", {"key2"}, false);
  QCOMPARE(fc.getPassword(), password);
  QCOMPARE(fc.getNamedValues(), NamedValues({key2}));
  QCOMPARE(fc.getRemainingData(), QString("key: val"));

  fc = FileContent::parse("password\nkey: val\nkey2: val2", {"key2"}, true);
  QCOMPARE(fc.getPassword(), password);
  QCOMPARE(fc.getNamedValues(), NamedValues({key, key2}));
  QCOMPARE(fc.getRemainingData(), QString());
}

void tst_util::namedValuesTakeValue() {
  NamedValues nv = {{"key1", "value1"}, {"key2", "value2"}, {"key3", "value3"}};

  QString val = nv.takeValue("key2");
  QCOMPARE(val, QString("value2"));
  QCOMPARE(nv.length(), 2);
  QVERIFY(!nv.contains({"key2", "value2"}));

  val = nv.takeValue("nonexistent");
  QVERIFY(val.isEmpty());

  val = nv.takeValue("key1");
  QCOMPARE(val, QString("value1"));
  val = nv.takeValue("key3");
  QCOMPARE(val, QString("value3"));
  QVERIFY(nv.isEmpty());
}

void tst_util::totpHiddenFromDisplay() {
  FileContent fc = FileContent::parse(
      "password\notpauth://totp/Test?secret=JBSWY3DPEHPK3PXP\nkey: value\n", {},
      false);

  QString remaining = fc.getRemainingData();
  QVERIFY(remaining.contains("otpauth://"));
  QVERIFY(remaining.contains("key: value"));

  QString display = fc.getRemainingDataForDisplay();
  QVERIFY(!display.contains("otpauth"));
  QVERIFY(display.contains("key: value"));

  fc = FileContent::parse(
      "password\nOTPAUTH://TOTP/Test?secret=JBSWY3DPEHPK3PXP\n", {}, false);
  QVERIFY(fc.getRemainingDataForDisplay().isEmpty());
}

void tst_util::testAwsUrl() {
  QRegularExpression proto = Util::protocolRegex();

  QRegularExpressionMatch match1 =
      proto.match("https://rh-dev.signin.aws.amazon.com/console");
  QVERIFY2(match1.hasMatch(), "Should match AWS console URL");
  QString captured1 = match1.captured(1);
  QVERIFY2(captured1.contains("amazon.com"), "Should include full URL");

  QRegularExpressionMatch match2 = proto.match("https://test-example.com/path");
  QVERIFY2(match2.hasMatch(), "Should match URL with dash");
  QString captured2 = match2.captured(1);
  QVERIFY2(captured2.contains("test-example.com"),
           "Should include full domain");
}

void tst_util::regexPatterns() {
  QRegularExpression gpg = Util::endsWithGpg();
  QVERIFY(gpg.match("test.gpg").hasMatch());
  QVERIFY(gpg.match("folder/test.gpg").hasMatch());
  QVERIFY(!gpg.match("test.gpg~").hasMatch());
  QVERIFY(!gpg.match("test.gpg.bak").hasMatch());

  QRegularExpression proto = Util::protocolRegex();
  QVERIFY(proto.match("https://example.com").hasMatch());
  QVERIFY(proto.match("ssh://user@host/path").hasMatch());
  QVERIFY(proto.match("ftp://server/file").hasMatch());
  QVERIFY(proto.match("webdav://localhost/share").hasMatch());
  QVERIFY(!proto.match("not a url").hasMatch());
  QRegularExpressionMatch urlWithTrailingTextMatch =
      proto.match("https://example.com/ is the address");
  QVERIFY(urlWithTrailingTextMatch.hasMatch());
  QString captured = urlWithTrailingTextMatch.captured(1);
  QVERIFY2(!captured.contains(" "), "URL should not include space");
  QVERIFY2(!captured.contains("<"), "URL should not include <");
  QVERIFY2(captured == "https://example.com/", "URL should stop at space");

  QRegularExpressionMatch urlWithFragmentMatch =
      proto.match("Link: https://test.org/path?q=1#frag");
  QVERIFY(urlWithFragmentMatch.hasMatch());
  captured = urlWithFragmentMatch.captured(1);
  QVERIFY2(captured.contains("?"), "URL should include query params");
  QVERIFY2(captured.contains("#"), "URL should include fragment");
  QVERIFY2(!captured.contains(" now"), "URL should not include trailing text");

  QRegularExpression nl = Util::newLinesRegex();
  QVERIFY(nl.match("\n").hasMatch());
  QVERIFY(nl.match("\r").hasMatch());
  QVERIFY(nl.match("\r\n").hasMatch());
}

void tst_util::normalizeFolderPathEdgeCases() {
  QString result = Util::normalizeFolderPath("");
  QVERIFY(result.endsWith(QDir::separator()));
  QVERIFY2(result == QDir::separator() || result.endsWith(QDir::separator()),
           "Empty path should become separator");

  result = Util::normalizeFolderPath(QDir::separator());
  QVERIFY(result.endsWith(QDir::separator()));

  result = Util::normalizeFolderPath("path/to/dir/");
  QVERIFY(result.endsWith(QDir::separator()));

  QString nativeResult = Util::normalizeFolderPath("path/to/dir");
  QVERIFY(nativeResult.endsWith(QDir::separator()));
}

void tst_util::fileContentEdgeCases() {
  FileContent fc = FileContent::parse("", {}, false);
  QVERIFY(fc.getPassword().isEmpty());

  fc = FileContent::parse("pass\nusername: user@example.com\npassword: "
                          "secret\nurl: https://login.com\n",
                          {"username", "password", "url"}, false);
  QVERIFY(fc.getNamedValues().length() >= 3);

  fc = FileContent::parse("pass\nkey: value with spaces\n", {"key"}, true);
  NamedValues nv = fc.getNamedValues();
  QCOMPARE(nv.length(), 1);
  QCOMPARE(nv.at(0).name, QString("key"));
  QVERIFY(nv.at(0).value.contains("spaces"));

  fc = FileContent::parse("pass\n://something\n", {}, false);
  QVERIFY(fc.getRemainingData().contains("://"));

  fc = FileContent::parse("pass\nno colon line\n", {}, false);
  QVERIFY(fc.getRemainingData().contains("no colon line"));

  fc = FileContent::parse("pass\nkey: value\nkey2: duplicate\n", {}, true);
  QVERIFY(fc.getNamedValues().length() >= 2);

  fc = FileContent::parse("pass\n", {}, false);
  QCOMPARE(fc.getPassword(), QString("pass"));
  QVERIFY(fc.getNamedValues().isEmpty());
}

void tst_util::namedValuesEdgeCases() {
  NamedValues nv;
  QVERIFY(nv.isEmpty());
  QVERIFY(nv.takeValue("nonexistent").isEmpty());

  NamedValue n1 = {"key", "value"};
  nv.append(n1);
  QCOMPARE(nv.length(), 1);
  NamedValue n2 = {"key2", "value2"};
  nv.append(n2);
  QCOMPARE(nv.length(), 2);

  nv.clear();
  QVERIFY(nv.isEmpty());
  QVERIFY(nv.takeValue("anything").isEmpty());
}

void tst_util::regexPatternEdgeCases() {
  const QRegularExpression &gpg = Util::endsWithGpg();
  QVERIFY(gpg.match(".gpg").hasMatch());
  QVERIFY(gpg.match("a.gpg").hasMatch());
  QVERIFY(!gpg.match("test.gpgx").hasMatch());

  const QRegularExpression &proto = Util::protocolRegex();
  QVERIFY(proto.match("webdavs://secure.example.com").hasMatch());
  QVERIFY(proto.match("ftps://ftp.server.org").hasMatch());
  QVERIFY(proto.match("sftp://user:pass@host").hasMatch());
  // file:/// URLs are not matched - see Util::protocolRegex()
  QVERIFY(!proto.match("file:///path/to/file").hasMatch());

  const QRegularExpression &nl = Util::newLinesRegex();
  QVERIFY(nl.match("\n").hasMatch());
  QVERIFY(nl.match("\r").hasMatch());
  QVERIFY(nl.match("\r\n").hasMatch());
}

void tst_util::endsWithGpgEdgeCases() {
  const QRegularExpression &gpg = Util::endsWithGpg();
  QVERIFY(!gpg.match(".gpgx").hasMatch());
  QVERIFY(!gpg.match("test.gpg.bak").hasMatch());
  QVERIFY(!gpg.match("test.gpg~").hasMatch());
  QVERIFY(!gpg.match("test.gpg.orig").hasMatch());
  QVERIFY(gpg.match("test.gpg").hasMatch());
  QVERIFY(gpg.match("test/path/file.gpg").hasMatch());
  QVERIFY(gpg.match("/absolute/path/file.gpg").hasMatch());
  QVERIFY(gpg.match("file name with spaces.gpg").hasMatch());
}

void tst_util::userInfoValidity() {
  UserInfo info;
  info.validity = 'f';
  QVERIFY(info.fullyValid());
  QVERIFY(!info.marginallyValid());
  QVERIFY(info.isValid());

  info.validity = 'u';
  QVERIFY(info.fullyValid());
  QVERIFY(!info.marginallyValid());
  QVERIFY(info.isValid());

  info.validity = 'm';
  QVERIFY(!info.fullyValid());
  QVERIFY(info.marginallyValid());
  QVERIFY(info.isValid());

  info.validity = 'n';
  QVERIFY(!info.fullyValid());
  QVERIFY(!info.marginallyValid());
  QVERIFY(!info.isValid());

  info.validity = 'e';
  QVERIFY(!info.isValid());
}

void tst_util::userInfoValidityEdgeCases() {
  UserInfo info;
  info.validity = '-';
  QVERIFY(!info.isValid());

  info.validity = 'q';
  QVERIFY(!info.isValid());

  char nullChar = '\0';
  info.validity = nullChar;
  QVERIFY(!info.isValid());

  QVERIFY(!info.have_secret);
  QVERIFY(!info.enabled);
}

void tst_util::passwordConfigurationCharacters() {
  PasswordConfiguration config;
  QCOMPARE(config.length, 16);
  QCOMPARE(config.selected, PasswordConfiguration::ALLCHARS);

  QVERIFY(!config.Characters[PasswordConfiguration::ALLCHARS].isEmpty());
  QVERIFY(!config.Characters[PasswordConfiguration::ALPHABETICAL].isEmpty());
  QVERIFY(!config.Characters[PasswordConfiguration::ALPHANUMERIC].isEmpty());
  QVERIFY(!config.Characters[PasswordConfiguration::CUSTOM].isEmpty());

  QVERIFY(config.Characters[PasswordConfiguration::ALLCHARS].length() >
          config.Characters[PasswordConfiguration::ALPHABETICAL].length());

  QVERIFY(config.Characters[PasswordConfiguration::ALPHANUMERIC].length() >
          config.Characters[PasswordConfiguration::ALPHABETICAL].length());
}

void tst_util::simpleTransactionBasic() {
  simpleTransaction trans;
  trans.transactionAdd(Enums::PASS_INSERT);
  Enums::PROCESS result = trans.transactionIsOver(Enums::PASS_INSERT);
  QCOMPARE(result, Enums::PASS_INSERT);
}

void tst_util::simpleTransactionNested() {
  simpleTransaction trans;
  trans.transactionAdd(Enums::PASS_INSERT);
  trans.transactionAdd(Enums::GIT_PUSH);
  Enums::PROCESS passInsertResult = trans.transactionIsOver(Enums::PASS_INSERT);
  QCOMPARE(passInsertResult, Enums::PASS_INSERT);
  Enums::PROCESS gitPushResult = trans.transactionIsOver(Enums::GIT_PUSH);
  QCOMPARE(gitPushResult, Enums::GIT_PUSH);
}

void tst_util::createGpgIdFile() {
  QTemporaryDir tempDir;
  QString newDir = tempDir.path() + "/testfolder";
  QVERIFY(QDir().mkdir(newDir));

  QString gpgIdFile = newDir + "/.gpg-id";
  QStringList keyIds = {"ABCDEF12", "34567890"};

  QFile gpgId(gpgIdFile);
  QVERIFY(gpgId.open(QIODevice::WriteOnly));
  for (const QString &keyId : keyIds) {
    gpgId.write((keyId + "\n").toUtf8());
  }
  gpgId.close();

  QVERIFY(QFile::exists(gpgIdFile));

  QFile readFile(gpgIdFile);
  QVERIFY(readFile.open(QIODevice::ReadOnly));
  QString content = QString::fromUtf8(readFile.readAll());
  readFile.close();

  QStringList lines = content.trimmed().split('\n');
  QCOMPARE(lines.size(), 2);
  QCOMPARE(lines[0], QString("ABCDEF12"));
  QCOMPARE(lines[1], QString("34567890"));
}

void tst_util::createGpgIdFileEmptyKeys() {
  QTemporaryDir tempDir;
  QString newDir = tempDir.path() + "/testfolder";
  QVERIFY(QDir().mkdir(newDir));

  QString gpgIdFile = newDir + "/.gpg-id";

  QFile gpgId(gpgIdFile);
  QVERIFY(gpgId.open(QIODevice::WriteOnly));
  gpgId.close();

  QVERIFY(QFile::exists(gpgIdFile));

  QFile readFile(gpgIdFile);
  QVERIFY(readFile.open(QIODevice::ReadOnly));
  QString content = QString::fromUtf8(readFile.readAll());
  readFile.close();

  QVERIFY(content.isEmpty());
}

void tst_util::generateRandomPassword() {
  ImitatePass pass;
  QString charset = "abcdefghijklmnopqrstuvwxyz";
  QString result = pass.generatePassword(10, charset);

  QCOMPARE(result.length(), 10);

  result = pass.generatePassword(100, "abcd");
  QCOMPARE(result.length(), 100);

  result = pass.generatePassword(0, "");
  QVERIFY(result.isEmpty());

  result = pass.generatePassword(50, "ABC");
  QCOMPARE(result.length(), 50);
}

void tst_util::boundedRandom() {
  ImitatePass pass;

  QVector<quint32> counts(10, 0);
  const int iterations = 1000;

  for (int i = 0; i < iterations; ++i) {
    QString result = pass.generatePassword(1, "0123456789");
    quint32 val = result.at(0).digitValue();
    QVERIFY2(val < 10, "generatePassword should only return digit characters");
    counts[val]++;
  }

  for (int i = 0; i < 10; ++i) {
    QVERIFY2(counts[i] > 0, "Each digit should appear at least once");
  }
}

void tst_util::findBinaryInPath() {
#ifdef Q_OS_WIN
  const QString binaryName = QStringLiteral("cmd.exe");
#else
  const QString binaryName = QStringLiteral("sh");
#endif
  QString result = Util::findBinaryInPath(binaryName);
  QVERIFY2(!result.isEmpty(), "Should find a standard shell in PATH");
  QVERIFY(result.contains(binaryName));

  result = Util::findBinaryInPath("nonexistentbinary12345");
  QVERIFY(result.isEmpty());
}

void tst_util::findPasswordStore() {
  QString result = Util::findPasswordStore();
  QVERIFY(!result.isEmpty());
  QVERIFY(result.endsWith(QDir::separator()));
}

void tst_util::configIsValid() {
  QTemporaryDir tempDir;
  QVERIFY2(tempDir.isValid(), "Temporary directory should be created");

  PassStoreGuard guard(QtPassSettings::getPassStore());

  // No .gpg-id in this store => config must be invalid.
  QtPassSettings::setPassStore(tempDir.path());
  const bool isValid = Util::configIsValid();

  QVERIFY2(!isValid, "Expected invalid config when .gpg-id is missing");
}

void tst_util::getDirBasic() {
  QTemporaryDir tempDir;
  QVERIFY2(tempDir.isValid(),
           "Temporary directory should be created successfully");

  QFileSystemModel fsm;
  fsm.setRootPath(tempDir.path());
  StoreModel sm;
  sm.setModelAndStore(&fsm, tempDir.path());
  QVERIFY(sm.sourceModel() != nullptr);
  QVERIFY2(sm.getStore() == tempDir.path(),
           "Store path should match the set value");
  QModelIndex rootIndex = fsm.index(tempDir.path());
  QVERIFY2(rootIndex.isValid(), "Filesystem model root index should be valid");
  const QString originalStore = QtPassSettings::getPassStore();
  QtPassSettings::setPassStore(tempDir.path());

  QString result = Util::getDir(QModelIndex(), false, fsm, sm);
  QString expectedDir = QDir(tempDir.path()).absolutePath();
  if (!expectedDir.endsWith(QDir::separator())) {
    expectedDir += QDir::separator();
  }
  QVERIFY2(
      result == expectedDir,
      qPrintable(QString("Expected '%1', got '%2'").arg(expectedDir, result)));
  QtPassSettings::setPassStore(originalStore);
}

void tst_util::getDirWithIndex() {
  QTemporaryDir tempDir;
  QVERIFY2(tempDir.isValid(),
           "Temporary directory should be created successfully");

  const QString dirPath = tempDir.path();
  const QString filePath =
      QDir(dirPath).filePath(QStringLiteral("testfile.txt"));

  QFile file(filePath);
  QVERIFY2(file.open(QIODevice::WriteOnly),
           "Failed to create test file in temporary directory");
  const char testData[] = "dummy";
  const qint64 bytesWritten = file.write(testData, sizeof(testData) - 1);
  QVERIFY2(bytesWritten == static_cast<qint64>(sizeof(testData) - 1),
           "Failed to write test data to file in temporary directory");
  file.close();

  const QString originalPassStore = QtPassSettings::getPassStore();
  PassStoreGuard passStoreGuard(originalPassStore);
  QtPassSettings::setPassStore(dirPath);

  QFileSystemModel fsm;
  fsm.setRootPath(dirPath);

  StoreModel sm;
  sm.setModelAndStore(&fsm, dirPath);
  QVERIFY2(sm.getStore() == dirPath, "Store path should match the set value");

  QModelIndex sourceIndex = fsm.index(filePath);
  QVERIFY2(sourceIndex.isValid(),
           "Source index should be valid for the test file");
  QModelIndex fileIndex = sm.mapFromSource(sourceIndex);
  QVERIFY2(fileIndex.isValid(),
           "Proxy index should be valid for the test file");

  QString result = Util::getDir(fileIndex, false, fsm, sm);
  QVERIFY2(!result.isEmpty(),
           "getDir should return a non-empty directory for a valid index");
  QVERIFY(result.endsWith(QDir::separator()));

  QString expectedPath = dirPath;
  if (!expectedPath.endsWith(QDir::separator())) {
    expectedPath += QDir::separator();
  }
  QVERIFY2(
      result == expectedPath,
      qPrintable(
          QStringLiteral("Expected '%1', got '%2'").arg(expectedPath, result)));

  QModelIndex invalidIndex;
  QString invalidResult = Util::getDir(invalidIndex, false, fsm, sm);
  QString expectedForInvalid = dirPath;
  if (!expectedForInvalid.endsWith(QDir::separator())) {
    expectedForInvalid += QDir::separator();
  }
  QVERIFY2(invalidResult == expectedForInvalid,
           qPrintable(QStringLiteral("getDir should return pass store for "
                                     "invalid index. Expected '%1', got '%2'")
                          .arg(expectedForInvalid, invalidResult)));
}

void tst_util::findBinaryInPathNotFound() {
  QString result = Util::findBinaryInPath("this-binary-does-not-exist-12345");
  QVERIFY(result.isEmpty());
}

void tst_util::findPasswordStoreEnvVar() {
  QString result = Util::findPasswordStore();
  QVERIFY(!result.isEmpty());
}

void tst_util::normalizeFolderPathMultipleCalls() {
  QString result1 = Util::normalizeFolderPath("test1");
  QString result2 = Util::normalizeFolderPath("test2");
  QVERIFY(result1.endsWith(QDir::separator()));
  QVERIFY(result2.endsWith(QDir::separator()));
}

void tst_util::userInfoFullyValid() {
  UserInfo ui;
  ui.validity = 'f';
  QVERIFY(ui.fullyValid());
  ui.validity = 'u';
  QVERIFY(ui.fullyValid());
  ui.validity = '-';
  QVERIFY(!ui.fullyValid());
}

void tst_util::userInfoMarginallyValid() {
  UserInfo ui;
  ui.validity = 'm';
  QVERIFY(ui.marginallyValid());
  ui.validity = 'f';
  QVERIFY(!ui.marginallyValid());
}

void tst_util::userInfoIsValid() {
  UserInfo ui;
  ui.validity = 'f';
  QVERIFY(ui.isValid());
  ui.validity = 'm';
  QVERIFY(ui.isValid());
  ui.validity = '-';
  QVERIFY(!ui.isValid());
}

void tst_util::userInfoCreatedAndExpiry() {
  UserInfo ui;
  ui.name = "Test User";
  ui.key_id = "ABCDEF12";

  QVERIFY(!ui.created.isValid());
  QVERIFY(!ui.expiry.isValid());

  QDateTime future = QDateTime::currentDateTime().addYears(1);
  ui.expiry = future;
  QVERIFY(ui.expiry.isValid());
  QVERIFY(ui.expiry.toSecsSinceEpoch() > 0);

  QDateTime past = QDateTime::currentDateTime().addYears(-1);
  ui.created = past;
  QVERIFY(ui.created.isValid());
  QVERIFY(ui.created.toSecsSinceEpoch() > 0);
}

void tst_util::qProgressIndicatorBasic() {
  QProgressIndicator pi;
  QVERIFY(!pi.isAnimated());
}

void tst_util::qProgressIndicatorStartStop() {
  QProgressIndicator pi;
  pi.startAnimation();
  QVERIFY(pi.isAnimated());
  pi.stopAnimation();
  QVERIFY(!pi.isAnimated());
}

void tst_util::namedValueBasic() {
  NamedValue nv;
  nv.name = "key";
  nv.value = "value";
  QCOMPARE(nv.name, QString("key"));
  QCOMPARE(nv.value, QString("value"));
}

void tst_util::namedValueMultiple() {
  NamedValues nvs;
  NamedValue nv1;
  nv1.name = "user1";
  nv1.value = "pass1";
  nvs.append(nv1);
  QCOMPARE(nvs.size(), 1);
}

void tst_util::imitatePassResolveMoveDestination() {
  ImitatePass pass;
  QTemporaryDir tmpDir;
  QString srcPath = tmpDir.path() + "/test.gpg";
  QFile srcFile(srcPath);
  QVERIFY(srcFile.open(QFile::WriteOnly));
  srcFile.write("test");
  srcFile.close();

  QString destPath = tmpDir.path() + "/dest.gpg";
  QString result = pass.resolveMoveDestination(srcPath, destPath, false);
  QString expected = destPath;
  QVERIFY2(result == expected, "Destination should have .gpg extension");
}

void tst_util::imitatePassResolveMoveDestinationForce() {
  ImitatePass pass;
  QTemporaryDir tmpDir;
  QString srcPath = tmpDir.path() + "/test.gpg";
  QFile srcFile(srcPath);
  QVERIFY(srcFile.open(QFile::WriteOnly));
  srcFile.write("test");
  srcFile.close();

  QString destPath = tmpDir.path() + "/existing.gpg";
  QFile destFile(destPath);
  QVERIFY(destFile.open(QFile::WriteOnly));
  destFile.write("old");
  destFile.close();

  QString result = pass.resolveMoveDestination(srcPath, destPath, true);
  QVERIFY2(result == destPath, "Should return dest path when force=true");
}

void tst_util::imitatePassResolveMoveDestinationDestExistsNoForce() {
  ImitatePass pass;
  QTemporaryDir tmpDir;
  QString srcPath = tmpDir.path() + "/test.gpg";
  QFile srcFile(srcPath);
  QVERIFY(srcFile.open(QFile::WriteOnly));
  srcFile.write("test");
  srcFile.close();

  QString destPath = tmpDir.path() + "/existing.gpg";
  QFile destFile(destPath);
  QVERIFY(destFile.open(QFile::WriteOnly));
  destFile.write("old");
  destFile.close();

  QString result = pass.resolveMoveDestination(srcPath, destPath, false);
  QVERIFY2(result.isEmpty(),
           "Should return empty when dest exists and force=false");
}

void tst_util::imitatePassResolveMoveDestinationDir() {
  ImitatePass pass;
  QTemporaryDir tmpDir;
  QString srcPath = tmpDir.path() + "/test.gpg";
  QFile srcFile(srcPath);
  QVERIFY(srcFile.open(QFile::WriteOnly));
  srcFile.write("test");
  srcFile.close();

  QString result = pass.resolveMoveDestination(srcPath, tmpDir.path(), false);
  QVERIFY2(result == tmpDir.path() + "/test.gpg",
           "Should append filename when dest is dir");
}

void tst_util::imitatePassResolveMoveDestinationNonExistent() {
  ImitatePass pass;
  QTemporaryDir tmpDir;
  QString destPath = tmpDir.path() + "/dest.gpg";
  QString result =
      pass.resolveMoveDestination("/non/existent/path.gpg", destPath, false);
  QVERIFY2(result.isEmpty(), "Should return empty for non-existent source");
}

void tst_util::imitatePassRemoveDir() {
  ImitatePass pass;
  QTemporaryDir tmpDir;
  QString subDir = tmpDir.path() + "/testdir";
  QVERIFY(QDir().mkpath(subDir));
  QVERIFY(QDir(subDir).exists());
  bool result = pass.removeDir(subDir);
  QVERIFY(result);
  QVERIFY(!QDir(subDir).exists());
}

void tst_util::getRecipientListBasic() {
  QTemporaryDir tempDir;
  QString passStore = tempDir.path();
  QString gpgIdFile = passStore + "/.gpg-id";

  QFile file(gpgIdFile);
  QVERIFY(file.open(QIODevice::WriteOnly));
  file.write("ABCDEF12\n34567890\n");
  file.close();

  QtPassSettings::setPassStore(passStore);
  QStringList recipients = Pass::getRecipientList(passStore);
  QCOMPARE(recipients.size(), 2);
  QCOMPARE(recipients[0], QString("ABCDEF12"));
  QCOMPARE(recipients[1], QString("34567890"));
}

void tst_util::getRecipientListEmpty() {
  QTemporaryDir tempDir;
  QString passStore = tempDir.path();
  QString gpgIdFile = passStore + "/.gpg-id";

  QFile file(gpgIdFile);
  QVERIFY(file.open(QIODevice::WriteOnly));
  file.close();

  PassStoreGuard guard(QtPassSettings::getPassStore());
  QtPassSettings::setPassStore(passStore);
  QStringList recipients = Pass::getRecipientList(passStore);
  QVERIFY(recipients.isEmpty());
}

void tst_util::getRecipientListWithComments() {
  QTemporaryDir tempDir;
  QString passStore = tempDir.path();
  QString gpgIdFile = passStore + "/.gpg-id";

  QFile file(gpgIdFile);
  QVERIFY(file.open(QIODevice::WriteOnly));
  file.write("ABCDEF12\n# comment\n34567890\n");
  file.close();

  PassStoreGuard guard(QtPassSettings::getPassStore());
  QtPassSettings::setPassStore(passStore);
  QStringList recipients = Pass::getRecipientList(passStore);
  QCOMPARE(recipients.size(), 2);
  QVERIFY(!recipients.contains("# comment"));
}

void tst_util::getRecipientStringCount() {
  QTemporaryDir tempDir;
  QString passStore = tempDir.path();
  QString gpgIdFile = passStore + "/.gpg-id";

  QFile file(gpgIdFile);
  QVERIFY(file.open(QIODevice::WriteOnly));
  file.write("ABCDEF12\n34567890\n");
  file.close();

  const QString originalPassStore = QtPassSettings::getPassStore();
  PassStoreGuard originalGuard(originalPassStore);
  QtPassSettings::setPassStore(passStore);
  int count = 0;
  QStringList parsedRecipients =
      Pass::getRecipientString(passStore, " ", &count);
  QStringList recipientsNoCount = Pass::getRecipientString(passStore, " ");

  QStringList expectedRecipients = {"ABCDEF12", "34567890"};
  // Verify count matches the expected number of parsed recipients.
  QVERIFY(count > 0);
  QCOMPARE(count, (int)expectedRecipients.size());
  // Verify both overloads return the same result
  QCOMPARE(parsedRecipients, recipientsNoCount);
  // Verify that the parsed recipients match the expected values.
  QVERIFY(parsedRecipients.contains("ABCDEF12"));
  QVERIFY(parsedRecipients.contains("34567890"));
  // Also verify that the recipients returned without count match the expected
  // values.
  QVERIFY(recipientsNoCount.contains("ABCDEF12"));
  QVERIFY(recipientsNoCount.contains("34567890"));
}

void tst_util::getGpgIdPathBasic() {
  QTemporaryDir tempDir;
  QString passStore = tempDir.path();
  QString gpgIdFile = passStore + "/.gpg-id";

  QFile file(gpgIdFile);
  QVERIFY(file.open(QIODevice::WriteOnly));
  file.write("ABCDEF12\n");
  file.close();

  QtPassSettings::setPassStore(passStore);
  QString path = QDir::cleanPath(Pass::getGpgIdPath(passStore));
  QString expected = QDir::cleanPath(gpgIdFile);
  QVERIFY2(path == expected,
           qPrintable(QString("Expected %1, got %2").arg(expected, path)));
}

void tst_util::getGpgIdPathSubfolder() {
  QTemporaryDir tempDir;
  QString passStore = tempDir.path();
  QString subfolder = passStore + "/subfolder";
  QString gpgIdFile = subfolder + "/.gpg-id";

  QVERIFY(QDir().mkdir(subfolder));
  QFile file(gpgIdFile);
  QVERIFY(file.open(QIODevice::WriteOnly));
  file.write("ABCDEF12\n");
  file.close();

  QtPassSettings::setPassStore(passStore);
  QString path = Pass::getGpgIdPath(subfolder + "/password.gpg");
  QVERIFY2(path == gpgIdFile,
           qPrintable(QString("Expected %1, got %2").arg(gpgIdFile, path)));
}

void tst_util::getGpgIdPathNotFound() {
  QTemporaryDir tempDir;
  QString passStore = tempDir.path();

  QtPassSettings::setPassStore(passStore);
  QString path =
      QDir::cleanPath(Pass::getGpgIdPath(passStore + "/nonexistent"));
  QString expected = QDir::cleanPath(passStore + "/.gpg-id");
  QVERIFY2(path == expected,
           qPrintable(QString("Expected %1, got %2").arg(expected, path)));
}

// Tests for findBinaryInPath - verifies it correctly locates executables in
// PATH.

void tst_util::findBinaryInPathReturnedPathIsAbsolute() {
  // Verify that the returned path is absolute, not a relative fragment.
#ifdef Q_OS_WIN
  const QString binaryName = QStringLiteral("cmd.exe");
#else
  const QString binaryName = QStringLiteral("sh");
#endif
  QString result = Util::findBinaryInPath(binaryName);
  QVERIFY2(!result.isEmpty(), "Should find a standard shell");
  QFileInfo fi(result);
  QVERIFY2(
      fi.isAbsolute(),
      qPrintable(
          QStringLiteral("Returned path '%1' must be absolute").arg(result)));
}

void tst_util::findBinaryInPathReturnedPathIsExecutable() {
  // Verify the returned path satisfies the isExecutable() check that guards
  // the assignment inside the loop.
#ifdef Q_OS_WIN
  const QString binaryName = QStringLiteral("cmd.exe");
#else
  const QString binaryName = QStringLiteral("sh");
#endif
  QString result = Util::findBinaryInPath(binaryName);
  QVERIFY2(!result.isEmpty(), "Should find a standard shell");
  QFileInfo fi(result);
  QVERIFY2(
      fi.isExecutable(),
      qPrintable(
          QStringLiteral("Returned path '%1' must be executable").arg(result)));
}

void tst_util::findBinaryInPathMultipleKnownBinaries() {
  // Test finding multiple common binaries in PATH.
#ifndef Q_OS_WIN
  const QStringList binaries = {QStringLiteral("sh"), QStringLiteral("ls"),
                                QStringLiteral("cat")};
  for (const QString &bin : binaries) {
    QString result = Util::findBinaryInPath(bin);
    QVERIFY2(!result.isEmpty(),
             qPrintable(QStringLiteral("Should find '%1' in PATH").arg(bin)));
    QVERIFY2(result.contains(bin),
             qPrintable(QStringLiteral("Result '%1' should contain '%2'")
                            .arg(result, bin)));
    QVERIFY2(
        QFileInfo(result).isExecutable(),
        qPrintable(
            QStringLiteral("Result '%1' should be executable").arg(result)));
  }
#else
  QSKIP("Non-Windows binary list not applicable on Windows");
#endif
}

void tst_util::findBinaryInPathConsistency() {
  // Calling findBinaryInPath twice for the same binary must return the same
  // result, confirming the loop does not corrupt state across calls.
#ifdef Q_OS_WIN
  const QString binaryName = QStringLiteral("cmd.exe");
#else
  const QString binaryName = QStringLiteral("sh");
#endif
  QString first = Util::findBinaryInPath(binaryName);
  QString second = Util::findBinaryInPath(binaryName);
  QVERIFY2(!first.isEmpty(), "First call should find the binary");
  QCOMPARE(first, second);
}

void tst_util::findBinaryInPathResultContainsBinaryName() {
  // The returned absolute path must end with (or at least contain) the
  // binary name, ruling out any off-by-one concatenation artefact.
#ifdef Q_OS_WIN
  const QString binaryName = QStringLiteral("cmd");
#else
  const QString binaryName = QStringLiteral("sh");
#endif
  QString result = Util::findBinaryInPath(binaryName);
  QVERIFY2(!result.isEmpty(), "Should find the binary");
  QVERIFY2(
      result.endsWith(binaryName) ||
          result.endsWith(binaryName + QStringLiteral(".exe")),
      qPrintable(QStringLiteral("Path '%1' should end with binary name '%2'")
                     .arg(result, binaryName)));
}

void tst_util::findBinaryInPathTempExecutableInTempDir() {
  // Place a real executable in the same directory as "sh" (which is on the
  // cached PATH) and verify findBinaryInPath locates it.
  //
  // This test is skipped in restricted environments where writing to the "sh"
  // directory is not allowed. An alternative approach (QTemporaryDir + PATH
  // manipulation) doesn't work because Util::_env is cached on first use.
#ifndef Q_OS_WIN
  QString shPath = Util::findBinaryInPath(QStringLiteral("sh"));
  if (shPath.isEmpty()) {
    QSKIP("Cannot find 'sh' to determine a writable PATH directory");
  }
  const QString pathDir = QFileInfo(shPath).absolutePath();
  const QString uniqueName = QStringLiteral("qtpass_test_exec_") +
                             QUuid::createUuid().toString(QUuid::WithoutBraces);
  const QString uniquePath = pathDir + QDir::separator() + uniqueName;

  QFile exec(uniquePath);
  if (!exec.open(QIODevice::WriteOnly)) {
    QSKIP("Cannot write to the PATH directory containing 'sh' (need write "
          "access)");
  }
  QVERIFY2(exec.exists(), "File should exist after opening for writing");
  exec.write("#!/bin/sh\n");
  exec.close();
  exec.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner |
                      QFileDevice::ExeOwner);

  QString result = Util::findBinaryInPath(uniqueName);

  // Remove file before assertions so it is always cleaned up.
  const bool removed = QFile::remove(uniquePath);
  QVERIFY2(
      removed,
      qPrintable(
          QStringLiteral("Failed to clean up test file '%1'").arg(uniquePath)));

  QVERIFY2(!result.isEmpty(),
           "findBinaryInPath should locate the executable placed in a PATH "
           "directory");
  QVERIFY2(result.endsWith(uniqueName),
           qPrintable(QStringLiteral("Result '%1' should end with '%2'")
                          .arg(result, uniqueName)));
  QVERIFY2(QFileInfo(result).isAbsolute(), "Result must be an absolute path");
#else
  QSKIP("Temp-executable test is Unix-only");
#endif
}

void tst_util::buildClipboardMimeDataLinux() {
#ifdef Q_OS_LINUX
  QMimeData *mime = buildClipboardMimeData(QStringLiteral("testpassword"));
  QVERIFY(mime != nullptr);
  QVERIFY2(mime->hasText(), "Mime data should contain text");
  QVERIFY2(mime->text() == "testpassword", "Text should match");
  QVERIFY2(mime->data("x-kde-passwordManagerHint") == QByteArray("secret"),
           "Linux should set password hint");
  delete mime;
#else
  QSKIP("Linux-only test");
#endif
}

void tst_util::buildClipboardMimeDataWindows() {
#ifdef Q_OS_WIN
  QMimeData *mime = buildClipboardMimeData(QStringLiteral("testpassword"));
  QVERIFY(mime != nullptr);
  QVERIFY2(mime->hasText(), "Mime data should contain text");
  QVERIFY2(mime->text() == "testpassword", "Text should match");
  QByteArray excl = mime->data("ExcludeClipboardContentFromMonitorProcessing");
  QVERIFY2(excl.size() == 4, "Windows ExcludeClipboard should be 4 bytes");
  QVERIFY2(excl == dwordBytes(1), "Windows ExcludeClipboard should be DWORD 1");
  QVERIFY(mime->hasFormat("ExcludeClipboardContentFromMonitorProcessing"));
  QVERIFY(mime->hasFormat("CanIncludeInClipboardHistory"));
  QVERIFY(mime->hasFormat("CanUploadToCloudClipboard"));
  QByteArray canHistory = mime->data("CanIncludeInClipboardHistory");
  QVERIFY2(canHistory.size() == 4,
           "CanIncludeInClipboardHistory should be 4 bytes");
  QVERIFY2(canHistory == dwordBytes(0),
           "CanIncludeInClipboardHistory should be DWORD 0");
  QByteArray cloudClip = mime->data("CanUploadToCloudClipboard");
  QVERIFY2(cloudClip.size() == 4,
           "CanUploadToCloudClipboard should be 4 bytes");
  QVERIFY2(cloudClip == dwordBytes(0),
           "CanUploadToCloudClipboard should be DWORD 0");
  delete mime;
#else
  QSKIP("Windows-only test");
#endif
}

void tst_util::buildClipboardMimeDataDword() {
#ifdef Q_OS_WIN
  QByteArray zero = dwordBytes(0);
  QVERIFY2(zero.size() == 4, "DWORD should be 4 bytes");
  QVERIFY2(zero.at(0) == char(0), "DWORD 0 should be 0x00");
  QVERIFY2(zero.at(1) == char(0), "DWORD 0 should be 0x00");
  QVERIFY2(zero.at(2) == char(0), "DWORD 0 should be 0x00");
  QVERIFY2(zero.at(3) == char(0), "DWORD 0 should be 0x00");

  QByteArray one = dwordBytes(1);
  QVERIFY2(one.size() == 4, "DWORD should be 4 bytes");
  QVERIFY2(one.at(0) == char(1), "DWORD 1 should be 0x01");
  QVERIFY2(one.at(1) == char(0), "DWORD 1 should be 0x00");
  QVERIFY2(one.at(2) == char(0), "DWORD 1 should be 0x00");
  QVERIFY2(one.at(3) == char(0), "DWORD 1 should be 0x00");
#else
  QSKIP("Windows-only test");
#endif
}

void tst_util::buildClipboardMimeDataMac() {
#ifdef Q_OS_MAC
  QMimeData *mime = buildClipboardMimeData(QStringLiteral("testpassword"));
  QVERIFY(mime != nullptr);
  QVERIFY2(mime->hasText(), "Mime data should contain text");
  QVERIFY2(mime->text() == "testpassword", "Text should match");
  QVERIFY2(mime->hasFormat("application/x-nspasteboard-concealed-type"),
           "macOS should have concealed type format");
  QVERIFY2(mime->data("application/x-nspasteboard-concealed-type") ==
               QByteArray(),
           "macOS concealed type should be empty");
  delete mime;
#else
  QSKIP("macOS-only test");
#endif
}

void tst_util::utilRegexEnsuresGpg() {
  const QRegularExpression &rex = Util::endsWithGpg();
  QVERIFY2(rex.isValid(), "Regex should be valid");
  QVERIFY2(rex.match("file.gpg").hasMatch(), "Should match .gpg extension");
  QVERIFY2(!rex.match("file.txt").hasMatch(), "Should not match .txt");
  QVERIFY2(!rex.match("test.gpgx").hasMatch(), "Should not match .gpgx");
}

void tst_util::utilRegexProtocol() {
  const QRegularExpression &rex = Util::protocolRegex();
  QVERIFY2(rex.isValid(), "Protocol regex should be valid");
  QVERIFY2(rex.match("http://example.com").hasMatch(), "Should match http://");
  QVERIFY2(rex.match("https://secure.com").hasMatch(), "Should match https://");
  QVERIFY2(rex.match("ssh://host").hasMatch(), "Should match ssh://");
  QVERIFY2(!rex.match("://no-protocol").hasMatch(), "Should not match invalid");
}

void tst_util::utilRegexNewLines() {
  const QRegularExpression &rex = Util::newLinesRegex();
  QVERIFY2(rex.isValid(), "Newlines regex should be valid");
  QVERIFY2(rex.match("\n").hasMatch(), "Should match newline");
  QVERIFY2(rex.match("line1\nline2").hasMatch(),
           "Should match embedded newline");
}

QTEST_MAIN(tst_util)
#include "tst_util.moc"