// Copyright 2021 DeepMind Technologies Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "test/fixture.h"

#include <array>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>  // NOLINT
#include <fstream>
#include <sstream>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <absl/base/attributes.h>
#include <absl/base/const_init.h>
#include <absl/base/thread_annotations.h>
#include <absl/strings/match.h>
#include <absl/strings/str_cat.h>
#include <absl/strings/str_join.h>
#include <absl/synchronization/mutex.h>
#include <mujoco/mjmodel.h>
#include <mujoco/mujoco.h>
#include "src/xml/xml_global.h"

namespace mujoco {
namespace {
using ::testing::_;
using ::testing::Not;
using ::testing::Return;
using ::testing::Truly;

// Returns true if the warning matches a known benign warning to ignore.
bool IsBenignWarning(const std::string& msg) {
  static const char* const kBenignWarnings[] = {
      "is not rigid and has no equality constraints",
      "Attach conflict",
  };
  for (const char* warning : kBenignWarnings) {
    if (absl::StrContains(msg, warning)) {
      return true;
    }
  }
  return false;
}
}  // namespace

thread_local MockWarningHandler* MockWarningHandler::active_handler = nullptr;

// Registers this handler as the active one.
MockWarningHandler::MockWarningHandler() {
  prev_ = active_handler;
  active_handler = this;

  // By default, ignore matches in the benign warnings list
  ON_CALL(*this, Warn(Truly(IsBenignWarning)))
      .WillByDefault([](const std::string&) {});

  // Fail on all other warnings
  ON_CALL(*this, Warn(Not(Truly(IsBenignWarning))))
      .WillByDefault([](const std::string& msg) {
        ADD_FAILURE() << "mju_user_warning: " << msg;
      });
}

// Restores the previously active warning handler.
MockWarningHandler::~MockWarningHandler() { active_handler = prev_; }

// Configures the mock warning handler to ignore all warnings (if empty) or
// expect at least one warning containing substring (if non-empty).
void MockWarningHandler::ExpectWarnings(std::string_view substring) {
  if (substring.empty()) {
    EXPECT_CALL(*this, Warn(_)).WillRepeatedly(Return());
  } else {
    EXPECT_CALL(*this, Warn(::testing::HasSubstr(std::string(substring))))
        .Times(::testing::AtLeast(1))
        .WillRepeatedly(Return());
  }
}

// Returns the active warning handler.
MockWarningHandler* MockWarningHandler::GetActive() { return active_handler; }

namespace {

using ::testing::NotNull;

ABSL_CONST_INIT static absl::Mutex handlers_mutex(absl::kConstInit);
static int guard_count ABSL_GUARDED_BY(handlers_mutex) = 0;
static mjfLogHandler prev_log_handler ABSL_GUARDED_BY(handlers_mutex) = nullptr;

void default_mj_log_handler(const mjLogMessage* msg) {
  std::string subject = msg->subject;
  if (msg->func) {
    subject = absl::StrCat(msg->func, ": ", msg->subject);
  }

  if (msg->level == mjLOG_ERROR) {
    // legacy fallback: some tests still install mju_user_error to intercept
    // errors with longjmp-based capture
    if (mju_user_error) {
      mju_user_error(subject.c_str());
    } else {
      FAIL() << "mju_user_error: " << subject;
    }
  } else if (msg->level == mjLOG_WARNING) {
    std::string full_msg = subject;
    if (msg->body) {
      absl::StrAppend(&full_msg, "\n", msg->body);
    }
    if (auto* handler = MockWarningHandler::GetActive()) {
      handler->Warn(full_msg);
    } else {
      ADD_FAILURE() << "Unexpected warning: " << full_msg;
    }
  }
}
}  // namespace

MujocoErrorTestGuard::MujocoErrorTestGuard() {
  absl::MutexLock lock(handlers_mutex);
  if (++guard_count == 1) {
    prev_log_handler = mju_setLogHandler(default_mj_log_handler);
  }
}

MujocoErrorTestGuard::~MujocoErrorTestGuard() {
  absl::MutexLock lock(handlers_mutex);
  if (--guard_count == 0) {
    mju_setLogHandler(prev_log_handler);
    prev_log_handler = nullptr;
  }
}

const std::string GetTestDataFilePath(std::string_view path) {  // NOLINT
  return std::string(path);
}

const std::string GetModelPath(std::string_view path) {  // NOLINT
  return absl::StrCat("../model/", path);
}

mjModel* LoadModelFromString(std::string_view xml, char* error,
                             int error_size, mjVFS* vfs) {
  if (error) {
    error[0] = '\0';
  }

  // This duplicates the logic in mj_loadXML, but allows us to use a string
  // directly rather than having to write the contents to a file. Most
  // importantly, we "save" the spec to global storage so that subsequent calls
  // to mj_saveLastXML will be done using the parsed mjSpec.
  mjSpec* spec = mj_parseXMLString(xml.data(), vfs, error, error_size);

  mjModel* model = nullptr;
  if (spec) {
    model = mj_compile(spec, vfs);

    if (error) {
      if (!model) {
        strncpy(error, mjs_getError(spec), error_size);
        error[error_size - 1] = '\0';
      } else {
        int num_warnings = mjs_numWarnings(spec);
        if (num_warnings > 0) {
          std::string all_warnings;
          for (int i = 0; i < num_warnings; ++i) {
            if (!all_warnings.empty()) {
              all_warnings += '\n';
            }
            all_warnings += mjs_getWarning(spec, i);
          }
          strncpy(error, all_warnings.c_str(), error_size);
          error[error_size - 1] = '\0';
        } else {
          error[0] = '\0';
        }
      }
    }
  }

  SetGlobalXmlSpec(spec);
  return model;
}

static void AssertModelNotNull(mjModel* model,
                                  const std::array<char, 1024>& error) {
  ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error.data();
}

mjModel* LoadModelFromPath(const char* model_path) {
  const std::string xml_path = GetModelPath(model_path);
  std::array<char, 1024> error;
  mjModel* model = mj_loadXML(
    xml_path.c_str(), nullptr, error.data(), error.size());
  AssertModelNotNull(model, error);
  return model;
}

std::string GetFileContents(const char* path) {
  std::ifstream ifs;
  ifs.open(path, std::ifstream::in);
  EXPECT_FALSE(ifs.fail());
  std::ostringstream sstream;
  sstream << ifs.rdbuf();
  return sstream.str();
}

std::string SaveAndReadXmlImpl(const mjModel* model, const mjSpec* spec) {
  constexpr int kMaxPathLen = 1024;
  std::string path_template =
      std::filesystem::temp_directory_path().append("tmp.XXXXXX").string();
  EXPECT_LT(path_template.size(), kMaxPathLen);

  char filepath[kMaxPathLen];
  mju_strncpy(filepath, path_template.c_str(), path_template.size() + 1);

#if defined(_POSIX_VERSION) && _POSIX_VERSION >= 200112L
  int fd = mkstemp(filepath);
  EXPECT_NE(fd, -1) << std::strerror(errno);
#elif defined(_WIN32)
  EXPECT_NE(_mktemp_s(filepath), EINVAL);
#endif

  if (spec) {
    mj_saveXML(spec, filepath, nullptr, 0);
  } else if (model) {
    mj_saveLastXML(filepath, model, nullptr, 0);
  }
  std::string contents = GetFileContents(filepath);

#if defined(_POSIX_VERSION) && _POSIX_VERSION >= 200112L
  close(fd);
#endif
  std::remove(filepath);

  return contents;
}

std::string SaveAndReadXml(const mjModel* model) {
  EXPECT_THAT(model, testing::NotNull());
  return SaveAndReadXmlImpl(model, nullptr);
}

std::string SaveAndReadXml(const mjSpec* spec) {
  EXPECT_THAT(spec, testing::NotNull());
  return SaveAndReadXmlImpl(nullptr, spec);
}

std::vector<mjtNum> GetCtrlNoise(const mjModel* m, int nsteps,
                                 mjtNum ctrlnoise) {
  std::vector<mjtNum> ctrl;
  for (int step=0; step < nsteps; step++) {
    for (int i = 0; i < m->nu; i++) {
      mjtNum center = 0.0;
      mjtNum radius = 1.0;
      mjtNum* range = m->actuator_ctrlrange + 2 * i;
      if (m->actuator_ctrllimited[i]) {
        center = (range[1] + range[0]) / 2;
        radius = (range[1] - range[0]) / 2;
      }
      radius *= ctrlnoise;
      ctrl.push_back(center + radius * (2 * mju_Halton(step, i+2) - 1));
    }
  }
  return ctrl;
}


MockFilesystem::MockFilesystem(std::string unit_test_name) {
  prefix_ = absl::StrCat("MjMock.", unit_test_name);
  dir_ = "/";
  if (mjp_getResourceProvider(prefix_.c_str()) != nullptr) {
    return;
  }

  mjpResourceProvider resourceProvider;
  mjp_defaultResourceProvider(&resourceProvider);
  resourceProvider.prefix = prefix_.c_str();
  resourceProvider.data = (void *) this;

  resourceProvider.open = +[](mjResource* resource) {
    MockFilesystem *fs = static_cast<MockFilesystem*>(resource->provider->data);
    std::string filename = fs->StripPrefix(resource->name);
    return fs->FileExists(filename) ? 1 : 0;
  };

  resourceProvider.read =+[](mjResource* resource, const void** buffer) {
    MockFilesystem *fs = static_cast<MockFilesystem*>(resource->provider->data);
    std::string filename = fs->StripPrefix(resource->name);
    return (int) fs->GetFile(filename, (const unsigned char**) buffer);
  };

  resourceProvider.close = +[](mjResource* resource) {};
  mjp_registerResourceProvider(&resourceProvider);
}

bool MockFilesystem::AddFile(std::string filename, const unsigned char* data,
                         std::size_t ndata) {
  std::string fullfilename = PathReduce(dir_, filename);
  auto [it, inserted] = filenames_.insert(fullfilename);
  if (inserted) {
    data_[fullfilename] = std::vector(data, data + ndata);
  }
  return inserted;
}

bool MockFilesystem::FileExists(const std::string& filename) {
  std::string fullfilename = PathReduce(dir_, filename);
  return filenames_.find(fullfilename) != filenames_.end();
}

std::size_t MockFilesystem::GetFile(const std::string& filename,
                                const unsigned char** buffer) const {
  std::string fullfilename = PathReduce(dir_, filename);
  auto it = data_.find(fullfilename);
  if (it == data_.end()) {
    return 0;
  }
  *buffer = it->second.data();
  return it->second.size();
}

void MockFilesystem::ChangeDirectory(std::string dir) {
  if (dir.empty()) {
    return;
  }

  dir_ = PathReduce(dir_, dir);
  if (dir_.back() != '/') {
    dir_ = absl::StrCat(dir_, "/");
  }
}

std::string MockFilesystem::FullPath(const std::string& path) const {
  return absl::StrCat(prefix_, ":", PathReduce(dir_, path));
}

std::string MockFilesystem::StripPrefix(const char* path) const {
  return &path[prefix_.size() + 1];
}

std::string MockFilesystem::PathReduce(const std::string& current_dir,
                                       const std::string& path) {
  std::stringstream stream;
  if (!path.empty() && path[0] != '/') {
    stream = std::stringstream(absl::StrCat(current_dir, path));
  } else {
    stream = std::stringstream(path);
  }

  std::string temp;
  std::vector<std::string> dirs;
  while (std::getline(stream, temp, '/')) {
    if (temp == ".." && !dirs.empty()) {
      dirs.pop_back();
      continue;
    }

    if (temp != "." && !temp.empty()) {
      dirs.push_back(temp);
    }
  }
  if (dirs.empty()) {
    return "/";
  }

  return absl::StrJoin(dirs, "/");
}

}  // namespace mujoco
