/** @file

    ts::file unit tests.

    @section license License

    Licensed to the Apache Software Foundation (ASF) under one
    or more contributor license agreements.  See the NOTICE file
    distributed with this work for additional information
    regarding copyright ownership.  The ASF licenses this file
    to you 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 <iostream>
#include <fstream> /* ofstream */

#include "tscore/ts_file.h"
#include "../../../tests/include/catch.hpp"

using ts::file::path;

// --------------------
TEST_CASE("ts_file", "[libts][ts_file]")
{
  path p1("/home");
  REQUIRE(p1.string() == "/home");
  auto p2 = p1 / "bob";
  REQUIRE(p2.string() == "/home/bob");
  p2 = p2 / "git/ats/";
  REQUIRE(p2.string() == "/home/bob/git/ats/");
  p2 /= "lib/ts";
  REQUIRE(p2.string() == "/home/bob/git/ats/lib/ts");
  p2 /= "/home/dave";
  REQUIRE(p2.string() == "/home/dave");
  path p3 = path("/home/dave") / "git/tools";
  REQUIRE(p3.string() == "/home/dave/git/tools");
}

TEST_CASE("ts_file_io", "[libts][ts_file_io]")
{
  path file("/etc/hosts");
  std::error_code ec;
  std::string content = ts::file::load(file, ec);
  REQUIRE(ec.value() == 0);
  REQUIRE(content.size() > 0);
  REQUIRE(content.find("localhost") != content.npos);

  // Check some file properties.
  REQUIRE(ts::file::is_readable(file) == true);
  auto fs = ts::file::status(file, ec);
  REQUIRE(ec.value() == 0);
  REQUIRE(ts::file::is_dir(fs) == false);
  REQUIRE(ts::file::is_regular_file(fs) == true);

  // Failure case.
  file    = "unit-tests/no_such_file.txt";
  content = ts::file::load(file, ec);
  REQUIRE(ec.value() == 2);
  REQUIRE(ts::file::is_readable(file) == false);
}

TEST_CASE("ts_file::path::parent_path", "[libts][fs_file]")
{
  CHECK(ts::file::path("/").parent_path() == path("/"));
  CHECK(ts::file::path("/absolute/path/file.txt").parent_path() == ts::file::path("/absolute/path"));
  CHECK(ts::file::path("/absolute/path/.").parent_path() == ts::file::path("/absolute/path"));

  CHECK(ts::file::path("relative/path/file.txt").parent_path() == ts::file::path("relative/path"));
  CHECK(ts::file::path("relative/path/.").parent_path() == ts::file::path("relative/path"));
  CHECK(ts::file::path(".").parent_path() == ts::file::path(""));
}

static std::string
setenvvar(const std::string &name, const std::string &value)
{
  std::string saved;
  if (nullptr != getenv(name.c_str())) {
    saved.assign(value);
  }

  if (!value.empty()) {
    setenv(name.c_str(), value.c_str(), 1);
  } else {
    unsetenv(name.c_str());
  }

  return saved;
}

TEST_CASE("ts_file::path::temp_directory_path", "[libts][fs_file]")
{
  // Clean all temp dir env variables.
  std::string s1 = setenvvar("TMPDIR", std::string());
  std::string s2 = setenvvar("TEMPDIR", std::string());
  std::string s3 = setenvvar("TMP", std::string());
  std::string s;

  // If nothing defined return "/tmp"
  CHECK(ts::file::temp_directory_path() == ts::file::path("/tmp"));

  // TMPDIR defined.
  s = setenvvar("TMPDIR", "/temp_dirname1");
  CHECK(ts::file::temp_directory_path() == ts::file::path("/temp_dirname1"));
  setenvvar("TMPDIR", s);

  // TEMPDIR
  s = setenvvar("TEMPDIR", "/temp_dirname");
  CHECK(ts::file::temp_directory_path() == ts::file::path("/temp_dirname"));
  // TMP defined, it should take precedence over TEMPDIR.
  s = setenvvar("TMP", "/temp_dirname1");
  CHECK(ts::file::temp_directory_path() == ts::file::path("/temp_dirname1"));
  // TMPDIR defined, it should take precedence over TMP.
  s = setenvvar("TMPDIR", "/temp_dirname2");
  CHECK(ts::file::temp_directory_path() == ts::file::path("/temp_dirname2"));
  setenvvar("TMPDIR", s);
  setenvvar("TMP", s);
  setenvvar("TEMPDIR", s);

  // Restore all temp dir env variables to their previous state.
  setenvvar("TMPDIR", s1);
  setenvvar("TEMPDIR", s2);
  setenvvar("TMP", s3);
}

TEST_CASE("ts_file::path::create_directories", "[libts][fs_file]")
{
  std::error_code ec;
  path tempdir = ts::file::temp_directory_path();

  CHECK_FALSE(ts::file::create_directories(path(), ec));
  CHECK(ec.value() == EINVAL);

  path testdir1 = tempdir / "dir1";
  CHECK(ts::file::create_directories(testdir1, ec));
  CHECK(ts::file::exists(testdir1));

  path testdir2 = testdir1 / "dir2";
  CHECK(ts::file::create_directories(testdir1, ec));
  CHECK(ts::file::exists(testdir1));

  // Cleanup
  CHECK(ts::file::remove(testdir1, ec));
  CHECK_FALSE(ts::file::exists(testdir1));
}

TEST_CASE("ts_file::path::remove", "[libts][fs_file]")
{
  std::error_code ec;
  path tempdir = ts::file::temp_directory_path();

  CHECK_FALSE(ts::file::remove(path(), ec));
  CHECK(ec.value() == EINVAL);

  path testdir1 = tempdir / "dir1";
  path testdir2 = testdir1 / "dir2";
  path file1    = testdir2 / "test.txt";

  // Simple creation and removal of a directory /tmp/dir1
  CHECK(ts::file::create_directories(testdir1, ec));
  CHECK(ts::file::exists(testdir1));
  CHECK(ts::file::remove(testdir1, ec));
  CHECK_FALSE(ts::file::exists(testdir1));

  // Create /tmp/dir1/dir2 and remove /tmp/dir1/dir2 => /tmp/dir1 should exist
  CHECK(ts::file::create_directories(testdir2, ec));
  CHECK(ts::file::remove(testdir2, ec));
  CHECK(ts::file::exists(testdir1));

  // Create a file, remove it, test if exists and then attempting to remove it again should fail.
  CHECK(ts::file::create_directories(testdir2, ec));
  std::ofstream file(file1.string());
  file << "Simple test file";
  file.close();
  CHECK(ts::file::exists(file1));
  CHECK(ts::file::remove(file1, ec));
  CHECK_FALSE(ts::file::exists(file1));
  CHECK_FALSE(ts::file::remove(file1, ec));

  // Clean up.
  CHECK(ts::file::remove(testdir1, ec));
  CHECK_FALSE(ts::file::exists(testdir1));
}

TEST_CASE("ts_file::path::canonical", "[libts][fs_file]")
{
  std::error_code ec;
  path tempdir    = ts::file::canonical(ts::file::temp_directory_path(), ec);
  path testdir1   = tempdir / "dir1";
  path testdir2   = testdir1 / "dir2";
  path testdir3   = testdir2 / "dir3";
  path unorthodox = testdir3 / path("..") / path("..") / "dir2";

  // Invalid empty path.
  CHECK(path() == ts::file::canonical(path(), ec));
  CHECK(ec.value() == EINVAL);

  // Fail if directory does not exist
  CHECK(path() == ts::file::canonical(unorthodox, ec));
  CHECK(ec.value() == ENOENT);

  // Create the dir3 and test again
  CHECK(create_directories(testdir3, ec));
  CHECK(ts::file::exists(testdir3));
  CHECK(ts::file::exists(testdir2));
  CHECK(ts::file::exists(testdir1));
  CHECK(ts::file::exists(unorthodox));
  CHECK(ts::file::canonical(unorthodox, ec) == testdir2);
  CHECK(ec.value() == 0);

  // Cleanup
  CHECK(ts::file::remove(testdir1, ec));
  CHECK_FALSE(ts::file::exists(testdir1));
}

TEST_CASE("ts_file::path::filename", "[libts][fs_file]")
{
  CHECK(ts::file::filename(path("/foo/bar.txt")) == path("bar.txt"));
  CHECK(ts::file::filename(path("/foo/.bar")) == path(".bar"));
  CHECK(ts::file::filename(path("/foo/bar")) == path("bar"));
  CHECK(ts::file::filename(path("/foo/bar/")) == path(""));
  CHECK(ts::file::filename(path("/foo/.")) == path("."));
  CHECK(ts::file::filename(path("/foo/..")) == path(".."));
  CHECK(ts::file::filename(path("/foo/../bar")) == path("bar"));
  CHECK(ts::file::filename(path("/foo/../bar/")) == path(""));
  CHECK(ts::file::filename(path(".")) == path("."));
  CHECK(ts::file::filename(path("..")) == path(".."));
  CHECK(ts::file::filename(path("/")) == path(""));
  CHECK(ts::file::filename(path("//host")) == path("host"));
}

TEST_CASE("ts_file::path::copy", "[libts][fs_file]")
{
  std::error_code ec;
  path tempdir  = ts::file::temp_directory_path();
  path testdir1 = tempdir / "dir1";
  path testdir2 = testdir1 / "dir2";
  path file1    = testdir2 / "test1.txt";
  path file2    = testdir2 / "test2.txt";

  // Invalid empty path, both to and from parameters.
  CHECK_FALSE(ts::file::copy(path(), path(), ec));
  CHECK(ec.value() == EINVAL);

  CHECK(ts::file::create_directories(testdir2, ec));
  std::ofstream file(file1.string());
  file << "Simple test file";
  file.close();
  CHECK(ts::file::exists(file1));

  // Invalid empty path, now from parameter is ok but to is empty
  CHECK_FALSE(ts::file::copy(file1, path(), ec));
  CHECK(ec.value() == EINVAL);

  // successful copy: "to" is directory
  CHECK(ts::file::copy(file1, testdir2, ec));
  CHECK(ec.value() == 0);

  // successful copy: "to" is file
  CHECK(ts::file::copy(file1, file2, ec));
  CHECK(ec.value() == 0);

  // Compare the content
  CHECK(ts::file::load(file1, ec) == ts::file::load(file2, ec));

  // Cleanup
  CHECK(ts::file::remove(testdir1, ec));
  CHECK_FALSE(ts::file::exists(testdir1));
}
