// Copyright 2018 The Bazel Authors. All rights reserved.
//
// 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 "src/tools/runfiles/runfiles.h"

#ifdef COMPILER_MSVC
#include <windows.h>
#endif  // COMPILER_MSVC

#include <fstream>
#include <memory>
#include <string>
#include <vector>

#include "gtest/gtest.h"
#include "src/main/cpp/util/file.h"

#define _T(x) #x
#define T(x) _T(x)
#define LINE() T(__LINE__)

namespace bazel {
namespace runfiles {
namespace {

using bazel::runfiles::testing::TestOnly_CreateRunfiles;
using bazel::runfiles::testing::TestOnly_IsAbsolute;
using std::cerr;
using std::endl;
using std::function;
using std::pair;
using std::string;
using std::unique_ptr;
using std::vector;

class RunfilesTest : public ::testing::Test {
 protected:
  // Create a temporary file that is deleted with the destructor.
  class MockFile {
   public:
    // Create an empty file with the given name under $TEST_TMPDIR.
    static MockFile* Create(const string& name);

    // Create a file with the given name and contents under $TEST_TMPDIR.
    // The method ensures to create all parent directories, so `name` is allowed
    // to contain directory components.
    static MockFile* Create(const string& name, const vector<string>& lines);

    ~MockFile();
    const string& Path() const { return path_; }

   private:
    MockFile(const string& path) : path_(path) {}
    MockFile(const MockFile&) = delete;
    MockFile(MockFile&&) = delete;
    MockFile& operator=(const MockFile&) = delete;
    MockFile& operator=(MockFile&&) = delete;

    const string path_;
  };

  static string GetTemp();

  static function<string(const string&)> kEnvWithTestSrcdir;
};

function<string(const string&)> RunfilesTest::kEnvWithTestSrcdir =
    [](const string& key) {
      if (key == "TEST_SRCDIR") {
        return string("always ignored");
      } else {
        return string();
      }
    };

string RunfilesTest::GetTemp() {
#ifdef COMPILER_MSVC
  DWORD size = ::GetEnvironmentVariableA("TEST_TMPDIR", NULL, 0);
  if (size == 0) {
    return std::move(string());  // unset or empty envvar
  }
  unique_ptr<char[]> value(new char[size]);
  ::GetEnvironmentVariableA("TEST_TMPDIR", value.get(), size);
  return std::move(string(value.get()));
#else
  char* result = getenv("TEST_TMPDIR");
  return result != NULL ? std::move(string(result)) : std::move(string());
#endif
}

RunfilesTest::MockFile* RunfilesTest::MockFile::Create(const string& name) {
  return Create(name, vector<string>());
}

RunfilesTest::MockFile* RunfilesTest::MockFile::Create(
    const string& name, const vector<string>& lines) {
  if (name.find("..") != string::npos || TestOnly_IsAbsolute(name)) {
    cerr << "WARNING: " << __FILE__ << "(" << __LINE__ << "): bad name: \""
         << name << "\"" << endl;
    return nullptr;
  }

  string tmp(std::move(RunfilesTest::GetTemp()));
  if (tmp.empty()) {
    cerr << "WARNING: " << __FILE__ << "(" << __LINE__
         << "): $TEST_TMPDIR is empty" << endl;
    return nullptr;
  }
  string path(tmp + "/" + name);
  string dirname = blaze_util::Dirname(path);
  if (!blaze_util::MakeDirectories(dirname, 0777)) {
    cerr << "WARNING: " << __FILE__ << "(" << __LINE__ << "): MakeDirectories("
         << dirname << ") failed" << endl;
    return nullptr;
  }

  auto stm = std::ofstream(path);
  for (auto i : lines) {
    stm << i << std::endl;
  }
  return new MockFile(path);
}

RunfilesTest::MockFile::~MockFile() { std::remove(path_.c_str()); }

TEST_F(RunfilesTest, CreatesManifestBasedRunfilesFromManifestNextToBinary) {
  unique_ptr<MockFile> mf(
      MockFile::Create("foo" LINE() ".runfiles_manifest", {"a/b c/d"}));
  EXPECT_TRUE(mf != nullptr);
  string argv0(mf->Path().substr(
      0, mf->Path().size() - string(".runfiles_manifest").size()));

  string error;
  unique_ptr<Runfiles> r(
      TestOnly_CreateRunfiles(argv0, kEnvWithTestSrcdir, &error));
  ASSERT_NE(r, nullptr);
  EXPECT_TRUE(error.empty());
  EXPECT_EQ(r->Rlocation("a/b"), "c/d");
  // We know it's manifest-based because it returns empty string for unknown
  // paths.
  EXPECT_EQ(r->Rlocation("unknown"), "");
}

TEST_F(RunfilesTest,
       CreatesManifestBasedRunfilesFromManifestInRunfilesDirectory) {
  unique_ptr<MockFile> mf(
      MockFile::Create("foo" LINE() ".runfiles/MANIFEST", {"a/b c/d"}));
  EXPECT_TRUE(mf != nullptr);
  string argv0(mf->Path().substr(
      0, mf->Path().size() - string(".runfiles/MANIFEST").size()));

  string error;
  unique_ptr<Runfiles> r(
      TestOnly_CreateRunfiles(argv0, kEnvWithTestSrcdir, &error));
  ASSERT_NE(r, nullptr);
  EXPECT_TRUE(error.empty());
  EXPECT_EQ(r->Rlocation("a/b"), "c/d");
  // We know it's manifest-based because it returns empty string for unknown
  // paths.
  EXPECT_EQ(r->Rlocation("unknown"), "");
}

TEST_F(RunfilesTest, CreatesManifestBasedRunfilesFromEnvvar) {
  unique_ptr<MockFile> mf(
      MockFile::Create("foo" LINE() ".runfiles_manifest", {"a/b c/d"}));
  EXPECT_TRUE(mf != nullptr);

  string error;
  unique_ptr<Runfiles> r(TestOnly_CreateRunfiles(
      "ignore-argv0",
      [&mf](const string& key) {
        if (key == "RUNFILES_MANIFEST_FILE") {
          return mf->Path();
        } else if (key == "RUNFILES_DIR") {
          return string("ignored when RUNFILES_MANIFEST_FILE has a value");
        } else if (key == "TEST_SRCDIR") {
          return string("always ignored");
        } else {
          return string();
        }
      },
      &error));
  ASSERT_NE(r, nullptr);
  EXPECT_TRUE(error.empty());
  EXPECT_EQ(r->Rlocation("a/b"), "c/d");
  // We know it's manifest-based because it returns empty string for unknown
  // paths.
  EXPECT_EQ(r->Rlocation("unknown"), "");
}

TEST_F(RunfilesTest, CannotCreateManifestBasedRunfilesDueToBadManifest) {
  unique_ptr<MockFile> mf(
      MockFile::Create("foo" LINE() ".runfiles_manifest", {"a b", "nospace"}));
  EXPECT_TRUE(mf != nullptr);

  string error;
  unique_ptr<Runfiles> r(Runfiles::CreateManifestBased(mf->Path(), &error));
  ASSERT_EQ(r, nullptr);
  EXPECT_NE(error.find("bad runfiles manifest entry"), string::npos);
  EXPECT_NE(error.find("line #2: \"nospace\""), string::npos);
}

TEST_F(RunfilesTest, ManifestBasedRunfilesRlocation) {
  unique_ptr<MockFile> mf(
      MockFile::Create("foo" LINE() ".runfiles_manifest", {"a/b c/d"}));
  EXPECT_TRUE(mf != nullptr);

  string error;
  unique_ptr<Runfiles> r(Runfiles::CreateManifestBased(mf->Path(), &error));
  ASSERT_NE(r, nullptr);
  EXPECT_TRUE(error.empty());
  EXPECT_EQ(r->Rlocation("a/b"), "c/d");
  EXPECT_EQ(r->Rlocation("c/d"), "");
  EXPECT_EQ(r->Rlocation(""), "");
  EXPECT_EQ(r->Rlocation("foo"), "");
  EXPECT_EQ(r->Rlocation("foo/"), "");
  EXPECT_EQ(r->Rlocation("foo/bar"), "");
  EXPECT_EQ(r->Rlocation("foo/.."), "");
  EXPECT_EQ(r->Rlocation("/Foo"), "/Foo");
  EXPECT_EQ(r->Rlocation("c:/Foo"), "c:/Foo");
  EXPECT_EQ(r->Rlocation("c:\\Foo"), "c:\\Foo");
}

TEST_F(RunfilesTest, DirectoryBasedRunfilesRlocation) {
  string error;
  unique_ptr<Runfiles> r(Runfiles::CreateDirectoryBased("whatever", &error));
  ASSERT_NE(r, nullptr);
  EXPECT_TRUE(error.empty());

  EXPECT_EQ(r->Rlocation("a/b"), "whatever/a/b");
  EXPECT_EQ(r->Rlocation("c/d"), "whatever/c/d");
  EXPECT_EQ(r->Rlocation(""), "");
  EXPECT_EQ(r->Rlocation("foo"), "whatever/foo");
  EXPECT_EQ(r->Rlocation("foo/"), "whatever/foo/");
  EXPECT_EQ(r->Rlocation("foo/bar"), "whatever/foo/bar");
  EXPECT_EQ(r->Rlocation("foo/.."), "");
  EXPECT_EQ(r->Rlocation("/Foo"), "/Foo");
  EXPECT_EQ(r->Rlocation("c:/Foo"), "c:/Foo");
  EXPECT_EQ(r->Rlocation("c:\\Foo"), "c:\\Foo");
}

TEST_F(RunfilesTest, ManifestBasedRunfilesEnvVars) {
  const vector<string> suffixes({"/MANIFEST", ".runfiles_manifest",
                                 "runfiles_manifest", ".runfiles", ".manifest",
                                 ".txt"});
  for (vector<string>::size_type i = 0; i < suffixes.size(); ++i) {
    unique_ptr<MockFile> mf(
        MockFile::Create(string("foo" LINE()) + suffixes[i]));
    EXPECT_TRUE(mf != nullptr) << " (suffix=\"" << suffixes[i] << "\")";

    string error;
    unique_ptr<Runfiles> r(Runfiles::CreateManifestBased(mf->Path(), &error));
    ASSERT_NE(r, nullptr) << " (suffix=\"" << suffixes[i] << "\")";
    EXPECT_TRUE(error.empty());

    // The object can compute the runfiles directory when i=0 and i=1, but not
    // when i>1 because the manifest file's name doesn't end in a well-known
    // way.
    const string expected_runfiles_dir(
        i < 2 ? mf->Path().substr(0, mf->Path().size() - 9 /* "_manifest" */)
              : "");
    vector<pair<string, string> > expected(
        {{"RUNFILES_MANIFEST_FILE", mf->Path()},
         {"JAVA_RUNFILES", expected_runfiles_dir}});
    EXPECT_EQ(r->EnvVars(), expected) << " (suffix=\"" << suffixes[i] << "\")";
  }
}

TEST_F(RunfilesTest, CreatesDirectoryBasedRunfilesFromDirectoryNextToBinary) {
  // We create a directory as a side-effect of creating a mock file.
  unique_ptr<MockFile> mf(
      MockFile::Create(string("foo" LINE() ".runfiles/dummy")));
  string argv0(mf->Path().substr(
      0, mf->Path().size() - string(".runfiles/dummy").size()));

  string error;
  unique_ptr<Runfiles> r(
      TestOnly_CreateRunfiles(argv0, kEnvWithTestSrcdir, &error));
  ASSERT_NE(r, nullptr);
  EXPECT_TRUE(error.empty());

  EXPECT_EQ(r->Rlocation("a/b"), argv0 + ".runfiles/a/b");
  // We know it's directory-based because it returns some result for unknown
  // paths.
  EXPECT_EQ(r->Rlocation("unknown"), argv0 + ".runfiles/unknown");
}

TEST_F(RunfilesTest, CreatesDirectoryBasedRunfilesFromEnvvar) {
  string error;
  unique_ptr<Runfiles> r(
      TestOnly_CreateRunfiles("ignore-argv0",
                              [](const string& key) {
                                if (key == "RUNFILES_DIR") {
                                  return string("runfiles/dir");
                                } else if (key == "TEST_SRCDIR") {
                                  return string("always ignored");
                                } else {
                                  return string();
                                }
                              },
                              &error));
  ASSERT_NE(r, nullptr);
  EXPECT_TRUE(error.empty());

  EXPECT_EQ(r->Rlocation("a/b"), "runfiles/dir/a/b");
  EXPECT_EQ(r->Rlocation("foo"), "runfiles/dir/foo");
  EXPECT_EQ(r->Rlocation("/Foo"), "/Foo");
  EXPECT_EQ(r->Rlocation("c:/Foo"), "c:/Foo");
  EXPECT_EQ(r->Rlocation("c:\\Foo"), "c:\\Foo");
}

TEST_F(RunfilesTest, DirectoryBasedRunfilesEnvVars) {
  string error;
  unique_ptr<Runfiles> r(
      Runfiles::CreateDirectoryBased("runfiles/dir", &error));
  ASSERT_NE(r, nullptr);
  EXPECT_TRUE(error.empty());

  vector<pair<string, string> > expected(
      {{"RUNFILES_DIR", "runfiles/dir"}, {"JAVA_RUNFILES", "runfiles/dir"}});
  EXPECT_EQ(r->EnvVars(), expected);
}

TEST_F(RunfilesTest, FailsToCreateManifestBasedBecauseManifestDoesNotExist) {
  string error;
  unique_ptr<Runfiles> r(
      Runfiles::CreateManifestBased("non-existent-file", &error));
  ASSERT_EQ(r, nullptr);
  EXPECT_NE(error.find("cannot open runfiles manifest"), string::npos);
}

TEST_F(RunfilesTest, FailsToCreateAnyRunfilesBecauseEnvvarsAreNotDefined) {
  unique_ptr<MockFile> mf(MockFile::Create(string("foo" LINE())));
  EXPECT_TRUE(mf != nullptr);

  string error;
  unique_ptr<Runfiles> r(
      TestOnly_CreateRunfiles("ignore-argv0",
                              [&mf](const string& key) {
                                if (key == "RUNFILES_MANIFEST_FILE") {
                                  return mf->Path();
                                } else if (key == "RUNFILES_DIR") {
                                  return string("whatever");
                                } else if (key == "TEST_SRCDIR") {
                                  return string("always ignored");
                                } else {
                                  return string();
                                }
                              },
                              &error));
  ASSERT_NE(r, nullptr);
  EXPECT_TRUE(error.empty());

  r.reset(TestOnly_CreateRunfiles("ignore-argv0",
                                  [](const string& key) {
                                    if (key == "RUNFILES_DIR") {
                                      return string("whatever");
                                    } else if (key == "TEST_SRCDIR") {
                                      return string("always ignored");
                                    } else {
                                      return string();
                                    }
                                  },
                                  &error));
  ASSERT_NE(r, nullptr);
  EXPECT_TRUE(error.empty());

  r.reset(TestOnly_CreateRunfiles("ignore-argv0", kEnvWithTestSrcdir, &error));
  ASSERT_EQ(r, nullptr);
  EXPECT_NE(error.find("cannot find runfiles"), string::npos);
}

TEST_F(RunfilesTest, MockFileTest) {
  {
    unique_ptr<MockFile> mf(MockFile::Create(string("foo" LINE() "/..")));
    EXPECT_TRUE(mf == nullptr);
  }

  {
    unique_ptr<MockFile> mf(MockFile::Create(string("/Foo" LINE())));
    EXPECT_TRUE(mf == nullptr);
  }

  {
    unique_ptr<MockFile> mf(MockFile::Create(string("C:/Foo" LINE())));
    EXPECT_TRUE(mf == nullptr);
  }

  string path;
  {
    unique_ptr<MockFile> mf(MockFile::Create(string("foo" LINE() "/bar1/qux")));
    EXPECT_TRUE(mf != nullptr);
    path = mf->Path();

    std::ifstream stm(path);
    EXPECT_TRUE(stm.good());
    string actual;
    stm >> actual;
    EXPECT_TRUE(actual.empty());
  }
  {
    std::ifstream stm(path);
    EXPECT_FALSE(stm.good());
  }

  {
    unique_ptr<MockFile> mf(
        MockFile::Create(string("foo" LINE() "/bar2/qux"), vector<string>()));
    EXPECT_TRUE(mf != nullptr);
    path = mf->Path();

    std::ifstream stm(path);
    EXPECT_TRUE(stm.good());
    string actual;
    stm >> actual;
    EXPECT_TRUE(actual.empty());
  }
  {
    std::ifstream stm(path);
    EXPECT_FALSE(stm.good());
  }

  {
    unique_ptr<MockFile> mf(
        MockFile::Create(string("foo" LINE() "/bar3/qux"),
                         {"hello world", "you are beautiful"}));
    EXPECT_TRUE(mf != nullptr);
    path = mf->Path();

    std::ifstream stm(path);
    EXPECT_TRUE(stm.good());
    string actual;
    std::getline(stm, actual);
    EXPECT_EQ("hello world", actual);
    std::getline(stm, actual);
    EXPECT_EQ("you are beautiful", actual);
    std::getline(stm, actual);
    EXPECT_EQ("", actual);
  }
  {
    std::ifstream stm(path);
    EXPECT_FALSE(stm.good());
  }
}

TEST_F(RunfilesTest, IsAbsolute) {
  EXPECT_FALSE(TestOnly_IsAbsolute("foo"));
  EXPECT_FALSE(TestOnly_IsAbsolute("foo/bar"));
  EXPECT_FALSE(TestOnly_IsAbsolute("\\foo"));
  EXPECT_TRUE(TestOnly_IsAbsolute("c:\\foo"));
  EXPECT_TRUE(TestOnly_IsAbsolute("c:/foo"));
  EXPECT_TRUE(TestOnly_IsAbsolute("/foo"));
  EXPECT_TRUE(TestOnly_IsAbsolute("x:\\foo"));
  EXPECT_FALSE(TestOnly_IsAbsolute("::\\foo"));
  EXPECT_FALSE(TestOnly_IsAbsolute("x\\foo"));
  EXPECT_FALSE(TestOnly_IsAbsolute("x:"));
  EXPECT_TRUE(TestOnly_IsAbsolute("x:\\"));
}

}  // namespace
}  // namespace runfiles
}  // namespace bazel
