// Author: Derek Barnett

#include <pbbam/../../src/FileUtils.h>

#include <cctype>
#include <chrono>
#include <cstdio>
#include <cstdlib>

#include <string>
#include <tuple>
#include <vector>

#include <gtest/gtest.h>

#include <boost/algorithm/string.hpp>

#include <pbbam/../../src/TimeUtils.h>

#include "PbbamTestData.h"

using namespace PacBio;
using namespace PacBio::BAM;

TEST(BAM_FileUtils, can_determines_if_file_exists)
{
    EXPECT_FALSE(FileUtils::Exists("does_not_exist.txt"));

    const std::string tmp = PbbamTestsConfig::GeneratedData_Dir + "/pbbam_exists_check.tmp";
    const std::string cmd = "touch " + tmp;
    ASSERT_EQ(0, system(cmd.c_str()));
    EXPECT_TRUE(FileUtils::Exists(tmp));
}

TEST(BAM_FileUtils, can_determines_last_modified_time)
{
    // a little tricky to check without going a full 'mock' filesystem route, but we can approximate
    //
    // also, I can't seem to get better than second resolution (on OSX 10.9/clang at least, st_mtimespec.tv_nsec is always zero)

    const auto then = TimeUtils::CurrentTime();
    const auto thenDuration = then.time_since_epoch();
    const auto thenSeconds = std::chrono::duration_cast<std::chrono::seconds>(thenDuration).count();

    const std::string tmp = PbbamTestsConfig::GeneratedData_Dir + "/pbbam_lastmod_check.tmp";
    const std::string rmCmd = "rm " + tmp;
    const std::string touchCmd = "touch  " + tmp;
    const auto ret = system(rmCmd.c_str());
    std::ignore = ret;
    ASSERT_EQ(0, system(touchCmd.c_str()));

    const auto stamp = FileUtils::LastModified(tmp);
    const auto stampDuration = stamp.time_since_epoch();
    const auto stampSeconds =
        std::chrono::duration_cast<std::chrono::seconds>(stampDuration).count();

    const int skew = 3600;  // allow 1 hour of clock-skew
    EXPECT_LE(thenSeconds, stampSeconds + skew);
}

TEST(BAM_FileUtils, can_resolve_file_paths)
{
    const std::string testFrom = "/path/to/myDir";

    // "raw" filenames - no URI scheme

    const std::string absolutePath = "/absolute/path/to/file.txt";
    const std::string relativePath = "../relative/path/to/file.txt";
    const std::string noPathFn = "file.txt";

    const std::string resolvedAbsolutePath = FileUtils::ResolvedFilePath(absolutePath, testFrom);
    const std::string resolvedRelativePath = FileUtils::ResolvedFilePath(relativePath, testFrom);
    const std::string resolvedNoPath = FileUtils::ResolvedFilePath(noPathFn, testFrom);
    const std::string resolvedAbsolutePath_defaultFrom = FileUtils::ResolvedFilePath(absolutePath);
    const std::string resolvedRelativePath_defaultFrom = FileUtils::ResolvedFilePath(relativePath);
    const std::string resolvedNoPath_defaultFrom = FileUtils::ResolvedFilePath(noPathFn);

    EXPECT_EQ("/absolute/path/to/file.txt", resolvedAbsolutePath);
    EXPECT_EQ("/path/to/myDir/../relative/path/to/file.txt", resolvedRelativePath);
    EXPECT_EQ("/path/to/myDir/file.txt", resolvedNoPath);

    EXPECT_EQ("/absolute/path/to/file.txt", resolvedAbsolutePath_defaultFrom);
    EXPECT_EQ("./../relative/path/to/file.txt", resolvedRelativePath_defaultFrom);
    EXPECT_EQ("./file.txt", resolvedNoPath_defaultFrom);

    // filenames with URI scheme ("file://")

    const std::string absoluteSchemeFn = "file:///absolute/path/to/file.txt";
    const std::string relativeSchemeFn = "file://../relative/path/to/file.txt";
    const std::string noPathSchemeFn = "file://file.txt";

    const std::string resolvedAbsoluteSchemePath =
        FileUtils::ResolvedFilePath(absoluteSchemeFn, testFrom);
    const std::string resolvedRelativeSchemePath =
        FileUtils::ResolvedFilePath(relativeSchemeFn, testFrom);
    const std::string resolvedNoPathSchemeFn =
        FileUtils::ResolvedFilePath(noPathSchemeFn, testFrom);
    const std::string resolvedAbsoluteSchemePath_defaultFrom =
        FileUtils::ResolvedFilePath(absoluteSchemeFn);
    const std::string resolvedRelativeSchemePath_defaultFrom =
        FileUtils::ResolvedFilePath(relativeSchemeFn);
    const std::string resolvedNoPathSchemeFn_defaultFrom =
        FileUtils::ResolvedFilePath(noPathSchemeFn);

    EXPECT_EQ("/absolute/path/to/file.txt", resolvedAbsoluteSchemePath);
    EXPECT_EQ("/path/to/myDir/../relative/path/to/file.txt", resolvedRelativeSchemePath);
    EXPECT_EQ("/path/to/myDir/file.txt", resolvedNoPathSchemeFn);

    EXPECT_EQ("/absolute/path/to/file.txt", resolvedAbsoluteSchemePath_defaultFrom);
    EXPECT_EQ("./../relative/path/to/file.txt", resolvedRelativeSchemePath_defaultFrom);
    EXPECT_EQ("./file.txt", resolvedNoPathSchemeFn_defaultFrom);
}

TEST(BAM_FileUtils, can_determines_file_size)
{
    const std::string tmp = PbbamTestsConfig::GeneratedData_Dir + "/pbbam_empty_file.tmp";
    const std::string cmd = "touch " + tmp;
    ASSERT_EQ(0, system(cmd.c_str()));
    EXPECT_EQ(0, FileUtils::Size(tmp));

    EXPECT_THROW(FileUtils::Size("does_not_exist.txt"), std::runtime_error);
}

// ####################################################################################################
// The code below is part of a simple check whether or not a (Windows-only) file path is absolute.
//
// NOTE: (and this is admittedly brittle for maintenance, but) the internal methods used are literally
// copied here for direct driving. There's likely a better way going forward, than the manual copy/paste.
// But in the absence of a similar runtime environment to build in & test against, while
// the motivating behavior is blocking other work, this lets me get the fix in their hands ASAP and still
// have some test code poking it beforehand. -DB
//
namespace test_windows {

std::string removeFileUriScheme(const std::string& uri)
{
    assert(!uri.empty());

    std::string schemeLess{uri};
    const std::string fileScheme = "file://";
    const auto schemeFound = schemeLess.find(fileScheme);
    if (schemeFound != std::string::npos) {
        if (schemeFound != 0) throw std::runtime_error{"Malformed URI: scheme not at beginning"};
        schemeLess = schemeLess.substr(fileScheme.size());
    }
    return schemeLess;
}

std::string removeDiskName(const std::string& filePath)
{
    if (filePath.size() >= 2) {
        const char firstChar = filePath.at(0);
        if ((isalpha(firstChar) != 0) && (filePath.at(1) == ':')) return filePath.substr(2);
    }
    return filePath;
}

const char native_pathSeparator = '\\';

bool native_pathIsAbsolute(const std::string& filePath)
{
    assert(!filePath.empty());

    // if starts with single slash or double slash [cases 1,3]
    if (boost::algorithm::starts_with(filePath, "\\")) return true;

    // if starts with single or double-dots -> not absolute [case 4 + ".\file.txt"]
    if (boost::algorithm::starts_with(filePath, ".")) return false;

    // if starts with drive name and colon ("C:\foo\bar.txt")
    if (filePath.size() >= 2) {
        const char firstChar = filePath.at(0);
        if ((isalpha(firstChar) != 0) && (filePath.at(1) == ':')) {
            return native_pathIsAbsolute(removeDiskName(filePath));
        }
    }

    // otherwise, likely relative
    return false;
}

std::string native_resolvedFilePath(const std::string& filePath, const std::string& from)
{
    // strip file:// scheme if present
    auto schemeLess = removeFileUriScheme(filePath);

    // if empty or already absolute path, just return it
    // upfront empty check simplifies further parsing logic
    if (schemeLess.empty() || native_pathIsAbsolute(schemeLess)) {
        return schemeLess;
    }

    // else make relative from the provided 'from' directory
    //
    // first pop disk name, then any leading single-dot '.'
    //
    // since we're prepending the 'from' directory, we can remove
    // any leading './' form our file path. this may just mean that
    // we pop it off to add it right back (when from == '.'), but this
    // keeps it consistent with other 'from' parent directories
    //
    schemeLess = removeDiskName(schemeLess);

    const bool thisDirAtStart = (schemeLess.find(".") == 0);
    if (thisDirAtStart) {
        if (schemeLess.find(native_pathSeparator) == 1) schemeLess = schemeLess.substr(2);
    }
    return from + native_pathSeparator + schemeLess;
}

}  // namespace test_windows

TEST(BAM_FileUtils, handles_windows_file_paths)
{
    {  // remove disk name

        // "C:\tmp.txt"
        const std::string f1{"C:\\tmp.txt"};
        EXPECT_EQ("\\tmp.txt", test_windows::removeDiskName(f1));

        // "C:tmp.txt"
        const std::string f2{"C:tmp.txt"};
        EXPECT_EQ("tmp.txt", test_windows::removeDiskName(f2));

        // "\tmp.txt"
        const std::string f3{"\\tmp.txt"};
        EXPECT_EQ(f3, test_windows::removeDiskName(f3));

        // "tmp.txt"
        const std::string f4{"tmp.txt"};
        EXPECT_EQ(f4, test_windows::removeDiskName(f4));
    }

    {  // isAbsolute ?

        // "\\server\path\to\tmp.txt"
        EXPECT_TRUE(test_windows::native_pathIsAbsolute("\\\\server\\path\\to\tmp.txt"));

        // "..\tmp.txt"
        EXPECT_FALSE(test_windows::native_pathIsAbsolute("..\\tmp.txt"));

        // ".\tmp.txt"
        EXPECT_FALSE(test_windows::native_pathIsAbsolute(".\\tmp.txt"));

        // "C:\path\to\tmp.txt"
        EXPECT_TRUE(test_windows::native_pathIsAbsolute("C:\\path\\to\\tmp.txt"));

        // "C:..\path\to\tmp.txt"
        EXPECT_FALSE(test_windows::native_pathIsAbsolute("C:..\\path\\to\\tmp.txt"));
    }

    {  // resolve file path

        const std::string myRootDir{R"(C:\path\to\myRootDir)"};

        // "\\server\path\to\tmp.txt"
        const std::string fn1{R"(\\server\path\to\tmp.txt)"};
        const std::string& fn1_expected{fn1};
        EXPECT_EQ(fn1_expected, test_windows::native_resolvedFilePath(fn1, myRootDir));

        // "..\tmp.txt"
        const std::string fn2{R"(..\tmp.txt)"};
        const std::string fn2_expected{R"(C:\path\to\myRootDir\..\tmp.txt)"};
        EXPECT_EQ(fn2_expected, test_windows::native_resolvedFilePath(fn2, myRootDir));

        // ".\tmp.txt"
        const std::string fn3{R"(.\tmp.txt)"};
        const std::string fn3_expected{R"(C:\path\to\myRootDir\tmp.txt)"};
        EXPECT_EQ(fn3_expected, test_windows::native_resolvedFilePath(fn3, myRootDir));

        // "C:\path\to\tmp.txt"
        const std::string fn4{R"(C:\path\to\tmp.txt)"};
        const std::string& fn4_expected{fn4};
        EXPECT_EQ(fn4_expected, test_windows::native_resolvedFilePath(fn4, myRootDir));

        // "C:..\path\to\tmp.txt"
        const std::string fn5{R"(C:..\path\to\tmp.txt)"};
        const std::string fn5_expected{R"(C:\path\to\myRootDir\..\path\to\tmp.txt)"};
        EXPECT_EQ(fn5_expected, test_windows::native_resolvedFilePath(fn5, myRootDir));

        // "C:tmp.txt"
        const std::string fn6{"C:tmp.txt"};
        const std::string fn6_expected{R"(C:\path\to\myRootDir\tmp.txt)"};
        EXPECT_EQ(fn6_expected, test_windows::native_resolvedFilePath(fn6, myRootDir));
        EXPECT_EQ(fn3_expected,
                  test_windows::native_resolvedFilePath(
                      fn6, myRootDir));  // our path is equivalent to fn3's "./temp.txt"
    }
}
//
// ####################################################################################################
