#include "ClangTidyOptions.h"
#include "ClangTidyCheck.h"
#include "ClangTidyDiagnosticConsumer.h"
#include "llvm/ADT/StringExtras.h"
#include "llvm/Support/ScopedPrinter.h"
#include "llvm/Testing/Support/Annotations.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"

namespace clang {
namespace tidy {

enum class Colours { Red, Orange, Yellow, Green, Blue, Indigo, Violet };

template <> struct OptionEnumMapping<Colours> {
  static llvm::ArrayRef<std::pair<Colours, StringRef>> getEnumMapping() {
    static constexpr std::pair<Colours, StringRef> Mapping[] = {
        {Colours::Red, "Red"},       {Colours::Orange, "Orange"},
        {Colours::Yellow, "Yellow"}, {Colours::Green, "Green"},
        {Colours::Blue, "Blue"},     {Colours::Indigo, "Indigo"},
        {Colours::Violet, "Violet"}};
    return makeArrayRef(Mapping);
  }
};

namespace test {

TEST(ParseLineFilter, EmptyFilter) {
  ClangTidyGlobalOptions Options;
  EXPECT_FALSE(parseLineFilter("", Options));
  EXPECT_TRUE(Options.LineFilter.empty());
  EXPECT_FALSE(parseLineFilter("[]", Options));
  EXPECT_TRUE(Options.LineFilter.empty());
}

TEST(ParseLineFilter, InvalidFilter) {
  ClangTidyGlobalOptions Options;
  EXPECT_TRUE(!!parseLineFilter("asdf", Options));
  EXPECT_TRUE(Options.LineFilter.empty());

  EXPECT_TRUE(!!parseLineFilter("[{}]", Options));
  EXPECT_TRUE(!!parseLineFilter("[{\"name\":\"\"}]", Options));
  EXPECT_TRUE(
      !!parseLineFilter("[{\"name\":\"test\",\"lines\":[[1]]}]", Options));
  EXPECT_TRUE(
      !!parseLineFilter("[{\"name\":\"test\",\"lines\":[[1,2,3]]}]", Options));
  EXPECT_TRUE(
      !!parseLineFilter("[{\"name\":\"test\",\"lines\":[[1,-1]]}]", Options));
}

TEST(ParseLineFilter, ValidFilter) {
  ClangTidyGlobalOptions Options;
  std::error_code Error = parseLineFilter(
      "[{\"name\":\"file1.cpp\",\"lines\":[[3,15],[20,30],[42,42]]},"
      "{\"name\":\"file2.h\"},"
      "{\"name\":\"file3.cc\",\"lines\":[[100,1000]]}]",
      Options);
  EXPECT_FALSE(Error);
  EXPECT_EQ(3u, Options.LineFilter.size());
  EXPECT_EQ("file1.cpp", Options.LineFilter[0].Name);
  EXPECT_EQ(3u, Options.LineFilter[0].LineRanges.size());
  EXPECT_EQ(3u, Options.LineFilter[0].LineRanges[0].first);
  EXPECT_EQ(15u, Options.LineFilter[0].LineRanges[0].second);
  EXPECT_EQ(20u, Options.LineFilter[0].LineRanges[1].first);
  EXPECT_EQ(30u, Options.LineFilter[0].LineRanges[1].second);
  EXPECT_EQ(42u, Options.LineFilter[0].LineRanges[2].first);
  EXPECT_EQ(42u, Options.LineFilter[0].LineRanges[2].second);
  EXPECT_EQ("file2.h", Options.LineFilter[1].Name);
  EXPECT_EQ(0u, Options.LineFilter[1].LineRanges.size());
  EXPECT_EQ("file3.cc", Options.LineFilter[2].Name);
  EXPECT_EQ(1u, Options.LineFilter[2].LineRanges.size());
  EXPECT_EQ(100u, Options.LineFilter[2].LineRanges[0].first);
  EXPECT_EQ(1000u, Options.LineFilter[2].LineRanges[0].second);
}

TEST(ParseConfiguration, ValidConfiguration) {
  llvm::ErrorOr<ClangTidyOptions> Options =
      parseConfiguration(llvm::MemoryBufferRef("Checks: \"-*,misc-*\"\n"
                                               "HeaderFilterRegex: \".*\"\n"
                                               "AnalyzeTemporaryDtors: true\n"
                                               "User: some.user",
                                               "Options"));
  EXPECT_TRUE(!!Options);
  EXPECT_EQ("-*,misc-*", *Options->Checks);
  EXPECT_EQ(".*", *Options->HeaderFilterRegex);
  EXPECT_EQ("some.user", *Options->User);
}

TEST(ParseConfiguration, MergeConfigurations) {
  llvm::ErrorOr<ClangTidyOptions> Options1 =
      parseConfiguration(llvm::MemoryBufferRef(R"(
      Checks: "check1,check2"
      HeaderFilterRegex: "filter1"
      AnalyzeTemporaryDtors: true
      User: user1
      ExtraArgs: ['arg1', 'arg2']
      ExtraArgsBefore: ['arg-before1', 'arg-before2']
      UseColor: false
  )",
                                               "Options1"));
  ASSERT_TRUE(!!Options1);
  llvm::ErrorOr<ClangTidyOptions> Options2 =
      parseConfiguration(llvm::MemoryBufferRef(R"(
      Checks: "check3,check4"
      HeaderFilterRegex: "filter2"
      AnalyzeTemporaryDtors: false
      User: user2
      ExtraArgs: ['arg3', 'arg4']
      ExtraArgsBefore: ['arg-before3', 'arg-before4']
      UseColor: true
  )",
                                               "Options2"));
  ASSERT_TRUE(!!Options2);
  ClangTidyOptions Options = Options1->merge(*Options2, 0);
  EXPECT_EQ("check1,check2,check3,check4", *Options.Checks);
  EXPECT_EQ("filter2", *Options.HeaderFilterRegex);
  EXPECT_EQ("user2", *Options.User);
  ASSERT_TRUE(Options.ExtraArgs.hasValue());
  EXPECT_EQ("arg1,arg2,arg3,arg4", llvm::join(Options.ExtraArgs->begin(),
                                              Options.ExtraArgs->end(), ","));
  ASSERT_TRUE(Options.ExtraArgsBefore.hasValue());
  EXPECT_EQ("arg-before1,arg-before2,arg-before3,arg-before4",
            llvm::join(Options.ExtraArgsBefore->begin(),
                       Options.ExtraArgsBefore->end(), ","));
  ASSERT_TRUE(Options.UseColor.hasValue());
  EXPECT_TRUE(*Options.UseColor);
}

namespace {
class DiagCollecter {
public:
  struct Diag {
  private:
    static size_t posToOffset(const llvm::SMLoc Loc,
                              const llvm::SourceMgr *Src) {
      return Loc.getPointer() -
             Src->getMemoryBuffer(Src->FindBufferContainingLoc(Loc))
                 ->getBufferStart();
    }

  public:
    Diag(const llvm::SMDiagnostic &D)
        : Message(D.getMessage()), Kind(D.getKind()),
          Pos(posToOffset(D.getLoc(), D.getSourceMgr())) {
      if (!D.getRanges().empty()) {
        // Ranges are stored as column numbers on the line that has the error.
        unsigned Offset = Pos - D.getColumnNo();
        Range.emplace();
        Range->Begin = Offset + D.getRanges().front().first,
        Range->End = Offset + D.getRanges().front().second;
      }
    }
    std::string Message;
    llvm::SourceMgr::DiagKind Kind;
    size_t Pos;
    Optional<llvm::Annotations::Range> Range;

    friend void PrintTo(const Diag &D, std::ostream *OS) {
      *OS << (D.Kind == llvm::SourceMgr::DK_Error ? "error: " : "warning: ")
          << D.Message << "@" << llvm::to_string(D.Pos);
      if (D.Range)
        *OS << ":[" << D.Range->Begin << ", " << D.Range->End << ")";
    }
  };

  DiagCollecter() = default;
  DiagCollecter(const DiagCollecter &) = delete;

  std::function<void(const llvm::SMDiagnostic &)>
  getCallback(bool Clear = true) & {
    if (Clear)
      Diags.clear();
    return [&](const llvm::SMDiagnostic &Diag) { Diags.emplace_back(Diag); };
  }

  std::function<void(const llvm::SMDiagnostic &)>
  getCallback(bool Clear = true) && = delete;

  llvm::ArrayRef<Diag> getDiags() const { return Diags; }

private:
  std::vector<Diag> Diags;
};

MATCHER_P(DiagMessage, M, "") { return arg.Message == M; }
MATCHER_P(DiagKind, K, "") { return arg.Kind == K; }
MATCHER_P(DiagPos, P, "") { return arg.Pos == P; }
MATCHER_P(DiagRange, P, "") { return arg.Range && *arg.Range == P; }
} // namespace

using ::testing::AllOf;
using ::testing::ElementsAre;
using ::testing::UnorderedElementsAre;

TEST(ParseConfiguration, CollectDiags) {
  DiagCollecter Collector;
  auto ParseWithDiags = [&](llvm::StringRef Buffer) {
    return parseConfigurationWithDiags(llvm::MemoryBufferRef(Buffer, "Options"),
                                       Collector.getCallback());
  };
  llvm::Annotations Options(R"(
    [[Check]]: llvm-include-order
  )");
  llvm::ErrorOr<ClangTidyOptions> ParsedOpt = ParseWithDiags(Options.code());
  EXPECT_TRUE(!ParsedOpt);
  EXPECT_THAT(Collector.getDiags(),
              testing::ElementsAre(AllOf(DiagMessage("unknown key 'Check'"),
                                         DiagKind(llvm::SourceMgr::DK_Error),
                                         DiagPos(Options.range().Begin),
                                         DiagRange(Options.range()))));

  Options = llvm::Annotations(R"(
    UseColor: [[NotABool]]
  )");
  ParsedOpt = ParseWithDiags(Options.code());
  EXPECT_TRUE(!ParsedOpt);
  EXPECT_THAT(Collector.getDiags(),
              testing::ElementsAre(AllOf(DiagMessage("invalid boolean"),
                                         DiagKind(llvm::SourceMgr::DK_Error),
                                         DiagPos(Options.range().Begin),
                                         DiagRange(Options.range()))));
}

namespace {
class TestCheck : public ClangTidyCheck {
public:
  TestCheck(ClangTidyContext *Context) : ClangTidyCheck("test", Context) {}

  template <typename... Args> auto getLocal(Args &&... Arguments) {
    return Options.get(std::forward<Args>(Arguments)...);
  }

  template <typename... Args> auto getGlobal(Args &&... Arguments) {
    return Options.getLocalOrGlobal(std::forward<Args>(Arguments)...);
  }

  template <typename IntType = int, typename... Args>
  auto getIntLocal(Args &&... Arguments) {
    return Options.get<IntType>(std::forward<Args>(Arguments)...);
  }

  template <typename IntType = int, typename... Args>
  auto getIntGlobal(Args &&... Arguments) {
    return Options.getLocalOrGlobal<IntType>(std::forward<Args>(Arguments)...);
  }
};

#define CHECK_VAL(Value, Expected)                                             \
  do {                                                                         \
    auto Item = Value;                                                         \
    ASSERT_TRUE(!!Item);                                                       \
    EXPECT_EQ(*Item, Expected);                                                \
  } while (false)

MATCHER_P(ToolDiagMessage, M, "") { return arg.Message.Message == M; }
MATCHER_P(ToolDiagLevel, L, "") { return arg.DiagLevel == L; }

} // namespace

} // namespace test

static constexpr auto Warning = tooling::Diagnostic::Warning;
static constexpr auto Error = tooling::Diagnostic::Error;

static void PrintTo(const ClangTidyError &Err, ::std::ostream *OS) {
  *OS << (Err.DiagLevel == Error ? "error: " : "warning: ")
      << Err.Message.Message;
}

namespace test {

TEST(CheckOptionsValidation, MissingOptions) {
  ClangTidyOptions Options;
  ClangTidyContext Context(std::make_unique<DefaultOptionsProvider>(
      ClangTidyGlobalOptions(), Options));
  ClangTidyDiagnosticConsumer DiagConsumer(Context);
  DiagnosticsEngine DE(new DiagnosticIDs(), new DiagnosticOptions,
                       &DiagConsumer, false);
  Context.setDiagnosticsEngine(&DE);
  TestCheck TestCheck(&Context);
  EXPECT_FALSE(TestCheck.getLocal("Opt").hasValue());
  EXPECT_EQ(TestCheck.getLocal("Opt", "Unknown"), "Unknown");
  // Missing options aren't errors.
  EXPECT_TRUE(DiagConsumer.take().empty());
}

TEST(CheckOptionsValidation, ValidIntOptions) {
  ClangTidyOptions Options;
  auto &CheckOptions = Options.CheckOptions;
  CheckOptions["test.IntExpected"] = "1";
  CheckOptions["test.IntInvalid1"] = "1WithMore";
  CheckOptions["test.IntInvalid2"] = "NoInt";
  CheckOptions["GlobalIntExpected"] = "1";
  CheckOptions["GlobalIntInvalid"] = "NoInt";
  CheckOptions["test.DefaultedIntInvalid"] = "NoInt";
  CheckOptions["test.BoolITrueValue"] = "1";
  CheckOptions["test.BoolIFalseValue"] = "0";
  CheckOptions["test.BoolTrueValue"] = "true";
  CheckOptions["test.BoolFalseValue"] = "false";
  CheckOptions["test.BoolTrueShort"] = "Y";
  CheckOptions["test.BoolFalseShort"] = "N";
  CheckOptions["test.BoolUnparseable"] = "Nothing";

  ClangTidyContext Context(std::make_unique<DefaultOptionsProvider>(
      ClangTidyGlobalOptions(), Options));
  ClangTidyDiagnosticConsumer DiagConsumer(Context);
  DiagnosticsEngine DE(new DiagnosticIDs(), new DiagnosticOptions,
                       &DiagConsumer, false);
  Context.setDiagnosticsEngine(&DE);
  TestCheck TestCheck(&Context);

  CHECK_VAL(TestCheck.getIntLocal("IntExpected"), 1);
  CHECK_VAL(TestCheck.getIntGlobal("GlobalIntExpected"), 1);
  EXPECT_FALSE(TestCheck.getIntLocal("IntInvalid1").hasValue());
  EXPECT_FALSE(TestCheck.getIntLocal("IntInvalid2").hasValue());
  EXPECT_FALSE(TestCheck.getIntGlobal("GlobalIntInvalid").hasValue());
  ASSERT_EQ(TestCheck.getIntLocal("DefaultedIntInvalid", 1), 1);

  CHECK_VAL(TestCheck.getIntLocal<bool>("BoolITrueValue"), true);
  CHECK_VAL(TestCheck.getIntLocal<bool>("BoolIFalseValue"), false);
  CHECK_VAL(TestCheck.getIntLocal<bool>("BoolTrueValue"), true);
  CHECK_VAL(TestCheck.getIntLocal<bool>("BoolFalseValue"), false);
  CHECK_VAL(TestCheck.getIntLocal<bool>("BoolTrueShort"), true);
  CHECK_VAL(TestCheck.getIntLocal<bool>("BoolFalseShort"), false);
  EXPECT_FALSE(TestCheck.getIntLocal<bool>("BoolUnparseable").hasValue());

  EXPECT_THAT(
      DiagConsumer.take(),
      UnorderedElementsAre(
          AllOf(ToolDiagMessage(
                    "invalid configuration value '1WithMore' for option "
                    "'test.IntInvalid1'; expected an integer"),
                ToolDiagLevel(Warning)),
          AllOf(
              ToolDiagMessage("invalid configuration value 'NoInt' for option "
                              "'test.IntInvalid2'; expected an integer"),
              ToolDiagLevel(Warning)),
          AllOf(
              ToolDiagMessage("invalid configuration value 'NoInt' for option "
                              "'GlobalIntInvalid'; expected an integer"),
              ToolDiagLevel(Warning)),
          AllOf(ToolDiagMessage(
                    "invalid configuration value 'NoInt' for option "
                    "'test.DefaultedIntInvalid'; expected an integer"),
                ToolDiagLevel(Warning)),
          AllOf(ToolDiagMessage(
                    "invalid configuration value 'Nothing' for option "
                    "'test.BoolUnparseable'; expected a bool"),
                ToolDiagLevel(Warning))));
}

TEST(ValidConfiguration, ValidEnumOptions) {

  ClangTidyOptions Options;
  auto &CheckOptions = Options.CheckOptions;

  CheckOptions["test.Valid"] = "Red";
  CheckOptions["test.Invalid"] = "Scarlet";
  CheckOptions["test.ValidWrongCase"] = "rED";
  CheckOptions["test.NearMiss"] = "Oragne";
  CheckOptions["GlobalValid"] = "Violet";
  CheckOptions["GlobalInvalid"] = "Purple";
  CheckOptions["GlobalValidWrongCase"] = "vIOLET";
  CheckOptions["GlobalNearMiss"] = "Yelow";

  ClangTidyContext Context(std::make_unique<DefaultOptionsProvider>(
      ClangTidyGlobalOptions(), Options));
  ClangTidyDiagnosticConsumer DiagConsumer(Context);
  DiagnosticsEngine DE(new DiagnosticIDs(), new DiagnosticOptions,
                       &DiagConsumer, false);
  Context.setDiagnosticsEngine(&DE);
  TestCheck TestCheck(&Context);

  CHECK_VAL(TestCheck.getIntLocal<Colours>("Valid"), Colours::Red);
  CHECK_VAL(TestCheck.getIntGlobal<Colours>("GlobalValid"), Colours::Violet);

  CHECK_VAL(
      TestCheck.getIntLocal<Colours>("ValidWrongCase", /*IgnoreCase*/ true),
      Colours::Red);
  CHECK_VAL(TestCheck.getIntGlobal<Colours>("GlobalValidWrongCase",
                                            /*IgnoreCase*/ true),
            Colours::Violet);

  EXPECT_FALSE(TestCheck.getIntLocal<Colours>("ValidWrongCase").hasValue());
  EXPECT_FALSE(TestCheck.getIntLocal<Colours>("NearMiss").hasValue());
  EXPECT_FALSE(TestCheck.getIntGlobal<Colours>("GlobalInvalid").hasValue());
  EXPECT_FALSE(
      TestCheck.getIntGlobal<Colours>("GlobalValidWrongCase").hasValue());
  EXPECT_FALSE(TestCheck.getIntGlobal<Colours>("GlobalNearMiss").hasValue());

  EXPECT_FALSE(TestCheck.getIntLocal<Colours>("Invalid").hasValue());
  EXPECT_THAT(
      DiagConsumer.take(),
      UnorderedElementsAre(
          AllOf(ToolDiagMessage("invalid configuration value "
                                "'Scarlet' for option 'test.Invalid'"),
                ToolDiagLevel(Warning)),
          AllOf(ToolDiagMessage("invalid configuration value 'rED' for option "
                                "'test.ValidWrongCase'; did you mean 'Red'?"),
                ToolDiagLevel(Warning)),
          AllOf(
              ToolDiagMessage("invalid configuration value 'Oragne' for option "
                              "'test.NearMiss'; did you mean 'Orange'?"),
              ToolDiagLevel(Warning)),
          AllOf(ToolDiagMessage("invalid configuration value "
                                "'Purple' for option 'GlobalInvalid'"),
                ToolDiagLevel(Warning)),
          AllOf(
              ToolDiagMessage("invalid configuration value 'vIOLET' for option "
                              "'GlobalValidWrongCase'; did you mean 'Violet'?"),
              ToolDiagLevel(Warning)),
          AllOf(
              ToolDiagMessage("invalid configuration value 'Yelow' for option "
                              "'GlobalNearMiss'; did you mean 'Yellow'?"),
              ToolDiagLevel(Warning))));
}

#undef CHECK_VAL

} // namespace test
} // namespace tidy
} // namespace clang
