// Copyright 2017-2018 ccls Authors
// SPDX-License-Identifier: Apache-2.0

#include "test.hh"

#include "filesystem.hh"
#include "indexer.hh"
#include "pipeline.hh"
#include "platform.hh"
#include "sema_manager.hh"
#include "serializer.hh"
#include "utils.hh"

#include <llvm/ADT/StringRef.h>
#include <llvm/Config/llvm-config.h>

#include <rapidjson/document.h>
#include <rapidjson/prettywriter.h>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>

#include <fstream>
#include <stdio.h>
#include <stdlib.h>

// The 'diff' utility is available and we can use dprintf(3).
#if _POSIX_C_SOURCE >= 200809L
#include <sys/wait.h>
#include <unistd.h>
#endif

using namespace llvm;

extern bool gTestOutputMode;

namespace ccls {
std::string toString(const rapidjson::Document &document) {
  rapidjson::StringBuffer buffer;
  rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(buffer);
  writer.SetFormatOptions(rapidjson::PrettyFormatOptions::kFormatSingleLineArray);
  writer.SetIndent(' ', 2);

  buffer.Clear();
  document.Accept(writer);
  return buffer.GetString();
}

struct TextReplacer {
  struct Replacement {
    std::string from;
    std::string to;
  };

  std::vector<Replacement> replacements;

  std::string apply(const std::string &content) {
    std::string result = content;

    for (const Replacement &replacement : replacements) {
      while (true) {
        size_t idx = result.find(replacement.from);
        if (idx == std::string::npos)
          break;

        result.replace(result.begin() + idx, result.begin() + idx + replacement.from.size(), replacement.to);
      }
    }

    return result;
  }
};

void trimInPlace(std::string &s) {
  auto f = [](char c) { return !isspace(c); };
  s.erase(s.begin(), std::find_if(s.begin(), s.end(), f));
  s.erase(std::find_if(s.rbegin(), s.rend(), f).base(), s.end());
}

std::vector<std::string> splitString(const std::string &str, const std::string &delimiter) {
  // http://stackoverflow.com/a/13172514
  std::vector<std::string> strings;

  std::string::size_type pos = 0;
  std::string::size_type prev = 0;
  while ((pos = str.find(delimiter, prev)) != std::string::npos) {
    strings.push_back(str.substr(prev, pos - prev));
    prev = pos + 1;
  }

  // To get the last substring (or only, if delimiter is not found)
  strings.push_back(str.substr(prev));

  return strings;
}

void parseTestExpectation(const std::string &filename, const std::vector<std::string> &lines_with_endings,
                          TextReplacer *replacer, std::vector<std::string> *flags,
                          std::unordered_map<std::string, std::string> *output_sections) {
  // Scan for EXTRA_FLAGS:
  {
    bool in_output = false;
    for (StringRef line : lines_with_endings) {
      line = line.trim();

      if (line.startswith("EXTRA_FLAGS:")) {
        assert(!in_output && "multiple EXTRA_FLAGS sections");
        in_output = true;
        continue;
      }

      if (in_output && line.empty())
        break;

      if (in_output)
        flags->push_back(line.str());
    }
  }

  // Scan for OUTPUT:
  {
    std::string active_output_filename;
    std::string active_output_contents;

    bool in_output = false;
    for (StringRef line_with_ending : lines_with_endings) {
      if (line_with_ending.startswith("*/"))
        break;

      if (line_with_ending.startswith("OUTPUT:")) {
        // Terminate the previous output section if we found a new one.
        if (in_output) {
          (*output_sections)[active_output_filename] = active_output_contents;
        }

        // Try to tokenize OUTPUT: based one whitespace. If there is more than
        // one token assume it is a filename.
        SmallVector<StringRef, 2> tokens;
        line_with_ending.split(tokens, ' ');
        if (tokens.size() > 1) {
          active_output_filename = tokens[1].str();
        } else {
          active_output_filename = filename;
        }
        active_output_contents = "";

        in_output = true;
      } else if (in_output) {
        active_output_contents += line_with_ending;
        active_output_contents.push_back('\n');
      }
    }

    if (in_output)
      (*output_sections)[active_output_filename] = active_output_contents;
  }
}

void updateTestExpectation(const std::string &filename, const std::string &expectation, const std::string &actual) {
  // Read the entire file into a string.
  std::ifstream in(filename);
  std::string str;
  str.assign(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
  in.close();

  // Replace expectation
  auto it = str.find(expectation);
  assert(it != std::string::npos);
  str.replace(it, expectation.size(), actual);

  // Write it back out.
  writeToFile(filename, str);
}

void diffDocuments(std::string path, std::string path_section, rapidjson::Document &expected,
                   rapidjson::Document &actual) {
  std::string joined_actual_output = toString(actual);
  std::string joined_expected_output = toString(expected);
  printf("[FAILED] %s (section %s)\n", path.c_str(), path_section.c_str());

#if _POSIX_C_SOURCE >= 200809L
  char expected_file[] = "/tmp/ccls.expected.XXXXXX";
  char actual_file[] = "/tmp/ccls.actual.XXXXXX";
  int expected_fd = mkstemp(expected_file);
  int actual_fd = mkstemp(actual_file);
  dprintf(expected_fd, "%s\n", joined_expected_output.c_str());
  dprintf(actual_fd, "%s\n", joined_actual_output.c_str());
  close(expected_fd);
  close(actual_fd);
  pid_t child = fork();
  if (child == 0) {
    execlp("diff", "diff", "-U", "3", expected_file, actual_file, NULL);
    _Exit(127);
  } else {
    int status;
    waitpid(child, &status, 0);
    unlink(expected_file);
    unlink(actual_file);
    // 'diff' returns 0 or 1 if exitted normaly.
    if (WEXITSTATUS(status) <= 1)
      return;
  }
#endif
  std::vector<std::string> actual_output = splitString(joined_actual_output, "\n");
  std::vector<std::string> expected_output = splitString(joined_expected_output, "\n");

  printf("Expected output for %s (section %s)\n:%s\n", path.c_str(), path_section.c_str(),
         joined_expected_output.c_str());
  printf("Actual output for %s (section %s)\n:%s\n", path.c_str(), path_section.c_str(), joined_actual_output.c_str());
}

void verifySerializeToFrom(IndexFile *file) {
  std::string expected = file->toString();
  std::string serialized = ccls::serialize(SerializeFormat::Json, *file);
  std::unique_ptr<IndexFile> result =
      ccls::deserialize(SerializeFormat::Json, "--.cc", serialized, "<empty>", std::nullopt /*expected_version*/);
  std::string actual = result->toString();
  if (expected != actual) {
    fprintf(stderr, "Serialization failure\n");
    // assert(false);
  }
}

std::string findExpectedOutputForFilename(std::string filename,
                                          const std::unordered_map<std::string, std::string> &expected) {
  for (const auto &entry : expected) {
    if (StringRef(entry.first).endswith(filename))
      return entry.second;
  }

  fprintf(stderr, "Couldn't find expected output for %s\n", filename.c_str());
  getchar();
  getchar();
  return "{}";
}

IndexFile *findDbForPathEnding(const std::string &path, const std::vector<std::unique_ptr<IndexFile>> &dbs) {
  for (auto &db : dbs) {
    if (StringRef(db->path).endswith(path))
      return db.get();
  }
  return nullptr;
}

bool runIndexTests(const std::string &filter_path, bool enable_update) {
  gTestOutputMode = true;
  std::string version = LLVM_VERSION_STRING;

  // Index tests change based on the version of clang used.
  static const char kRequiredClangVersion[] = "22.0.0git";
  if (version != kRequiredClangVersion && version.find("svn") == std::string::npos) {
    fprintf(stderr,
            "Index tests must be run using clang version %s, ccls is running "
            "with %s\n",
            kRequiredClangVersion, version.c_str());
    return false;
  }

  bool success = true;
  bool update_all = false;
  // FIXME: show diagnostics in STL/headers when running tests. At the moment
  // this can be done by conRequestIdex index(1, 1);
  getFilesInFolder("index_tests", true /*recursive*/, true /*add_folder_to_path*/, [&](const std::string &path) {
    bool is_fail_allowed = false;

    if (path.find(filter_path) == std::string::npos)
      return;

    if (!filter_path.empty())
      printf("Running %s\n", path.c_str());

    // Parse expected output from the test, parse it into JSON document.
    std::vector<std::string> lines_with_endings;
    {
      std::ifstream fin(path);
      for (std::string line; std::getline(fin, line);)
        lines_with_endings.push_back(line);
    }
    TextReplacer text_replacer;
    std::vector<std::string> args{"-std=c++20", "-resource-dir=" + getDefaultResourceDirectory(), path};
    std::unordered_map<std::string, std::string> all_expected_output;
    parseTestExpectation(path, lines_with_endings, &text_replacer, &args, &all_expected_output);

    // Run test.
    g_config = new Config;
    VFS vfs;
    WorkingFiles wfiles;
    std::vector<const char *> cargs;
    for (auto &arg : args)
      cargs.push_back(arg.c_str());
    bool ok;
    auto result = ccls::idx::index(&wfiles, &vfs, "", path, cargs, {}, true, ok);

    for (const auto &entry : all_expected_output) {
      const std::string &expected_path = entry.first;
      std::string expected_output = text_replacer.apply(entry.second);

      // Get output from index operation.
      IndexFile *db = findDbForPathEnding(expected_path, result.indexes);
      std::string actual_output = "{}";
      if (db) {
        verifySerializeToFrom(db);
        actual_output = db->toString();
      }
      actual_output = text_replacer.apply(actual_output);

      // Compare output via rapidjson::Document to ignore any formatting
      // differences.
      rapidjson::Document actual;
      actual.Parse(actual_output.c_str());
      rapidjson::Document expected;
      expected.Parse(expected_output.c_str());

      if (actual == expected) {
        // std::cout << "[PASSED] " << path << std::endl;
      } else {
        if (!is_fail_allowed)
          success = false;
        diffDocuments(path, expected_path, expected, actual);
        puts("\n");
        if (enable_update) {
          printf("[Enter to continue - type u to update test, a to update "
                 "all]");
          char c = 'u';
          if (!update_all) {
            c = getchar();
            getchar();
          }

          if (c == 'a')
            update_all = true;

          if (update_all || c == 'u') {
            // Note: we use |entry.second| instead of |expected_output|
            // because
            // |expected_output| has had text replacements applied.
            updateTestExpectation(path, entry.second, toString(actual) + "\n");
          }
        }
      }
    }
  });

  return success;
}
} // namespace ccls
