//
// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2024
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#include "td/utils/tests.h"

#include "td/utils/crypto.h"
#include "td/utils/filesystem.h"
#include "td/utils/Parser.h"
#include "td/utils/PathView.h"
#include "td/utils/port/path.h"
#include "td/utils/port/Stat.h"
#include "td/utils/Random.h"
#include "td/utils/ScopeGuard.h"
#include "td/utils/SliceBuilder.h"
#include "td/utils/StringBuilder.h"
#include "td/utils/Time.h"

#include <map>

namespace td {

string rand_string(int from, int to, size_t len) {
  string res(len, '\0');
  for (auto &c : res) {
    c = static_cast<char>(Random::fast(from, to));
  }
  return res;
}

vector<string> rand_split(Slice str) {
  vector<string> res;
  size_t pos = 0;
  while (pos < str.size()) {
    size_t len;
    if (Random::fast_bool()) {
      len = Random::fast(1, 10);
    } else {
      len = Random::fast(100, 200);
    }
    res.push_back(str.substr(pos, len).str());
    pos += len;
  }
  return res;
}

struct TestInfo {
  string name;
  string result_hash;  // base64
};

StringBuilder &operator<<(StringBuilder &sb, const TestInfo &info) {
  // should I use JSON?
  CHECK(!info.name.empty());
  CHECK(!info.result_hash.empty());
  return sb << info.name << " " << info.result_hash << "\n";
}

class RegressionTesterImpl final : public RegressionTester {
 public:
  static void destroy(CSlice db_path) {
    unlink(db_path).ignore();
  }

  RegressionTesterImpl(string db_path, string db_cache_dir)
      : db_path_(std::move(db_path)), db_cache_dir_(std::move(db_cache_dir)) {
    load_db(db_path_).ignore();
    if (db_cache_dir_.empty()) {
      db_cache_dir_ = PathView(db_path_).without_extension().str() + ".cache/";
    }
    mkdir(db_cache_dir_).ensure();
  }

  Status verify_test(Slice name, Slice result) final {
#if TD_HAVE_OPENSSL
    auto hash = PSTRING() << format::as_hex_dump<0>(Slice(sha256(result)));
#else
    auto hash = to_string(crc64(result));
#endif
    TestInfo &old_test_info = tests_[name.str()];
    if (!old_test_info.result_hash.empty() && old_test_info.result_hash != hash) {
      auto wa_path = db_cache_dir_ + "WA";
      write_file(wa_path, result).ensure();
      return Status::Error(PSLICE() << "Test " << name << " changed: " << tag("expected", old_test_info.result_hash)
                                    << tag("received", hash));
    }
    auto result_cache_path = db_cache_dir_ + hash;
    if (stat(result_cache_path).is_error()) {
      write_file(result_cache_path, result).ensure();
    }
    if (!old_test_info.result_hash.empty()) {
      return Status::OK();
    }
    old_test_info.name = name.str();
    old_test_info.result_hash = hash;
    is_dirty_ = true;

    return Status::OK();
  }

  void save_db() final {
    if (!is_dirty_) {
      return;
    }
    SCOPE_EXIT {
      is_dirty_ = false;
    };
    string buf(2000000, ' ');
    StringBuilder sb(buf);
    save_db(sb);
    string new_db_path = db_path_ + ".new";
    write_file(new_db_path, sb.as_cslice()).ensure();
    rename(new_db_path, db_path_).ensure();
  }

  Slice magic() const {
    return Slice("abce");
  }

  void save_db(StringBuilder &sb) {
    sb << magic() << "\n";
    for (const auto &it : tests_) {
      sb << it.second;
    }
  }

  Status load_db(CSlice path) {
    TRY_RESULT(data, read_file(path));
    ConstParser parser(data.as_slice());
    auto db_magic = parser.read_word();
    if (db_magic != magic()) {
      return Status::Error(PSLICE() << "Wrong magic " << db_magic);
    }
    while (true) {
      TestInfo info;
      info.name = parser.read_word().str();
      if (info.name.empty()) {
        break;
      }
      info.result_hash = parser.read_word().str();
      tests_[info.name] = info;
    }
    return Status::OK();
  }

 private:
  string db_path_;
  string db_cache_dir_;
  bool is_dirty_{false};

  std::map<string, TestInfo> tests_;
};

void RegressionTester::destroy(CSlice path) {
  RegressionTesterImpl::destroy(path);
}

unique_ptr<RegressionTester> RegressionTester::create(string db_path, string db_cache_dir) {
  return td::make_unique<RegressionTesterImpl>(std::move(db_path), std::move(db_cache_dir));
}

TestsRunner &TestsRunner::get_default() {
  static TestsRunner default_runner;
  return default_runner;
}

void TestsRunner::add_test(string name, std::function<unique_ptr<Test>()> test) {
  for (auto &it : tests_) {
    if (it.first == name) {
      LOG(FATAL) << "Test name collision " << name;
    }
  }
  tests_.emplace_back(std::move(name), TestInfo{std::move(test), nullptr});
}

void TestsRunner::add_substr_filter(string str) {
  if (str[0] != '+' && str[0] != '-') {
    str = "+" + str;
  }
  substr_filters_.push_back(std::move(str));
}

void TestsRunner::set_offset(string str) {
  offset_ = std::move(str);
}

void TestsRunner::set_regression_tester(unique_ptr<RegressionTester> regression_tester) {
  regression_tester_ = std::move(regression_tester);
}

void TestsRunner::set_stress_flag(bool flag) {
  stress_flag_ = flag;
}

void TestsRunner::run_all() {
  while (run_all_step()) {
  }
}

bool TestsRunner::run_all_step() {
  Guard guard(this);
  if (state_.it == state_.end) {
    state_.end = tests_.size();
    state_.it = 0;
  }

  bool skip_tests = true;
  while (state_.it != state_.end) {
    auto &name = tests_[state_.it].first;
    auto &test = tests_[state_.it].second.test;
    if (!state_.is_running) {
      bool ok = true;
      for (const auto &filter : substr_filters_) {
        bool is_match = name.find(filter.substr(1)) != string::npos;
        if (is_match != (filter[0] == '+')) {
          ok = false;
          break;
        }
      }
      if (name.find(offset_) != string::npos) {
        skip_tests = false;
      }
      if (!ok || skip_tests) {
        ++state_.it;
        continue;
      }
      LOG(ERROR) << "Run test " << tag("name", name);
      state_.start = Time::now();
      state_.start_unadjusted = Time::now_unadjusted();
      state_.is_running = true;

      CHECK(!test);
      test = tests_[state_.it].second.creator();
    }

    if (test->step()) {
      break;
    }

    test = {};

    auto passed = Time::now() - state_.start;
    auto real_passed = Time::now_unadjusted() - state_.start_unadjusted;
    if (real_passed + 1e-1 > passed) {
      LOG(ERROR) << format::as_time(passed);
    } else {
      LOG(ERROR) << format::as_time(real_passed) << " adjusted [" << format::as_time(real_passed) << "]";
    }
    if (regression_tester_) {
      regression_tester_->save_db();
    }
    state_.is_running = false;
    ++state_.it;
  }

  auto ret = state_.it != state_.end;
  if (!ret) {
    state_ = State();
  }
  return ret || stress_flag_;
}

Slice TestsRunner::name() {
  CHECK(state_.is_running);
  return tests_[state_.it].first;
}

Status TestsRunner::verify(Slice data) {
  if (!regression_tester_) {
    LOG(INFO) << data;
    LOG(ERROR) << "Cannot verify and save <" << name() << "> answer. Use --regression <regression_db> option";
    return Status::OK();
  }
  return regression_tester_->verify_test(PSLICE() << name() << "_default", data);
}

}  // namespace td