/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * 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 <android-base/errors.h>
#include <android-base/file.h>
#include <android-base/stringprintf.h>
#include <android-base/utf8.h>
#include <androidfw/FileStream.h>
#include <androidfw/StringPiece.h>
#include <dirent.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>

#include "Diagnostics.h"
#include "cmd/Compile.h"
#include "cmd/Link.h"
#include "util/Files.h"

using testing::Eq;
using testing::Ne;

namespace aapt {

const char* CommandTestFixture::kDefaultPackageName = "com.aapt.command.test";

void ClearDirectory(android::StringPiece path) {
  const std::string root_dir(path);
  std::unique_ptr<DIR, decltype(closedir)*> dir(opendir(root_dir.data()), closedir);
  if (!dir) {
    StdErrDiagnostics().Error(android::DiagMessage()
                              << android::base::SystemErrorCodeToString(errno));
    return;
  }

  while (struct dirent* entry = readdir(dir.get())) {
    // Do not delete hidden files and do not recurse to the parent of this directory
    if (util::StartsWith(entry->d_name, ".")) {
      continue;
    }

    std::string full_path = file::BuildPath({root_dir, entry->d_name});
    if (file::GetFileType(full_path) == file::FileType::kDirectory) {
      ClearDirectory(full_path);
#ifdef _WIN32
      _rmdir(full_path.c_str());
#else
      rmdir(full_path.c_str());
#endif
    } else {
      android::base::utf8::unlink(full_path.c_str());
    }
  }
}

void TestDirectoryFixture::SetUp() {
  temp_dir_ = file::BuildPath({testing::TempDir(), "_temp",
                               testing::UnitTest::GetInstance()->current_test_case()->name(),
                               testing::UnitTest::GetInstance()->current_test_info()->name()});
  ASSERT_TRUE(file::mkdirs(temp_dir_));
  ClearDirectory(temp_dir_);
}

void TestDirectoryFixture::TearDown() {
  ClearDirectory(temp_dir_);
}

void TestDirectoryFixture::WriteFile(const std::string& path, const std::string& contents) {
  // Create any intermediate directories specified in the path
  auto pos = std::find(path.rbegin(), path.rend(), file::sDirSep);
  if (pos != path.rend()) {
    std::string dirs = path.substr(0, (&*pos - path.data()));
    file::mkdirs(dirs);
  }

  CHECK(android::base::WriteStringToFile(contents, path));
}

bool CommandTestFixture::CompileFile(const std::string& path, const std::string& contents,
                                     android::StringPiece out_dir, android::IDiagnostics* diag) {
  WriteFile(path, contents);
  CHECK(file::mkdirs(out_dir.data()));
  return CompileCommand(diag).Execute({path, "-o", out_dir, "-v"}, &std::cerr) == 0;
}

bool CommandTestFixture::Link(const std::vector<std::string>& args, android::IDiagnostics* diag) {
  std::vector<android::StringPiece> link_args;
  for(const std::string& arg : args) {
    link_args.emplace_back(arg);
  }

  // Link against the android SDK
  std::string android_sdk =
      file::BuildPath({android::base::GetExecutableDirectory(), "integration-tests", "CommandTests",
                       "android-33.jar"});
  link_args.insert(link_args.end(), {"-I", android_sdk});

  return LinkCommand(diag).Execute(link_args, &std::cerr) == 0;
}

bool CommandTestFixture::Link(const std::vector<std::string>& args, android::StringPiece flat_dir,
                              android::IDiagnostics* diag) {
  std::vector<android::StringPiece> link_args;
  for(const std::string& arg : args) {
    link_args.emplace_back(arg);
  }

  // Link against the android SDK
  std::string android_sdk =
      file::BuildPath({android::base::GetExecutableDirectory(), "integration-tests", "CommandTests",
                       "android-33.jar"});
  link_args.insert(link_args.end(), {"-I", android_sdk});

  // Add the files from the compiled resources directory to the link file arguments
  std::optional<std::vector<std::string>> compiled_files = file::FindFiles(flat_dir, diag);
  if (compiled_files) {
    for (std::string& compile_file : compiled_files.value()) {
      compile_file = file::BuildPath({flat_dir, compile_file});
      link_args.emplace_back(std::move(compile_file));
    }
  }

  return LinkCommand(diag).Execute(link_args, &std::cerr) == 0;
}

std::string CommandTestFixture::GetDefaultManifest(const char* package_name) {
  const std::string manifest_file = GetTestPath("AndroidManifest.xml");
  WriteFile(manifest_file, android::base::StringPrintf(R"(
      <manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="%s">
      </manifest>)", package_name));
  return manifest_file;
}

std::unique_ptr<io::IData> CommandTestFixture::OpenFileAsData(LoadedApk* apk,
                                                              android::StringPiece path) {
  return apk
      ->GetFileCollection()
      ->FindFile(path)
      ->OpenAsData();
}

void CommandTestFixture::AssertLoadXml(LoadedApk* apk, const io::IData* data,
                                       android::ResXMLTree *out_tree) {
  ASSERT_THAT(apk, Ne(nullptr));

  out_tree->setTo(data->data(), data->size());
  ASSERT_THAT(out_tree->getError(), Eq(android::OK));
  while (out_tree->next() != android::ResXMLTree::START_TAG) {
    ASSERT_THAT(out_tree->getEventType(), Ne(android::ResXMLTree::BAD_DOCUMENT));
    ASSERT_THAT(out_tree->getEventType(), Ne(android::ResXMLTree::END_DOCUMENT));
  }
}

ManifestBuilder::ManifestBuilder(CommandTestFixture* fixture) : fixture_(fixture) {
}

ManifestBuilder& ManifestBuilder::SetPackageName(const std::string& package_name) {
  package_name_ = package_name;
  return *this;
}

ManifestBuilder& ManifestBuilder::AddContents(const std::string& contents) {
  contents_ += contents + "\n";
  return *this;
}

std::string ManifestBuilder::Build(const std::string& file_path) {
  const char* manifest_template = R"(
      <manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="%s">
          %s
      </manifest>)";

  fixture_->WriteFile(file_path, android::base::StringPrintf(
                                     manifest_template, package_name_.c_str(), contents_.c_str()));
  return file_path;
}

std::string ManifestBuilder::Build() {
  return Build(fixture_->GetTestPath("AndroidManifest.xml"));
}

LinkCommandBuilder::LinkCommandBuilder(CommandTestFixture* fixture) : fixture_(fixture) {
}

LinkCommandBuilder& LinkCommandBuilder::SetManifestFile(const std::string& file) {
  manifest_supplied_ = true;
  args_.emplace_back("--manifest");
  args_.emplace_back(file);
  return *this;
}

LinkCommandBuilder& LinkCommandBuilder::AddFlag(const std::string& flag) {
  args_.emplace_back(flag);
  return *this;
}

LinkCommandBuilder& LinkCommandBuilder::AddCompiledResDir(const std::string& dir,
                                                          android::IDiagnostics* diag) {
  if (auto files = file::FindFiles(dir, diag)) {
    for (std::string& compile_file : files.value()) {
      args_.emplace_back(file::BuildPath({dir, compile_file}));
    }
  }
  return *this;
}

LinkCommandBuilder& LinkCommandBuilder::AddParameter(const std::string& param,
                                                     const std::string& value) {
  args_.emplace_back(param);
  args_.emplace_back(value);
  return *this;
}

std::vector<std::string> LinkCommandBuilder::Build(const std::string& out_apk) {
  if (!manifest_supplied_) {
    SetManifestFile(ManifestBuilder(fixture_).Build());
  }
  args_.emplace_back("-o");
  args_.emplace_back(out_apk);
  return args_;
}

}  // namespace aapt
