//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2015 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

#include "SourceKit/Core/Context.h"
#include "SourceKit/Core/LangSupport.h"
#include "SourceKit/Core/NotificationCenter.h"
#include "llvm/Support/MemoryBuffer.h"
#include "llvm/Support/Path.h"
#include "gtest/gtest.h"

// FIXME: Portability.
#include <dispatch/dispatch.h>

using namespace SourceKit;
using namespace llvm;

static StringRef getRuntimeLibPath() {
  return sys::path::parent_path(SWIFTLIB_DIR);
}

namespace {

class NullEditorConsumer : public EditorConsumer {
  bool needsSemanticInfo() override { return false; }

  void handleRequestError(const char *Description) override {
    llvm_unreachable("unexpected error");
  }

  bool handleSyntaxMap(unsigned Offset, unsigned Length, UIdent Kind) override {
    return false;
  }

  bool handleSemanticAnnotation(unsigned Offset, unsigned Length,
                                UIdent Kind, bool isSystem) override {
    return false;
  }

  bool beginDocumentSubStructure(unsigned Offset, unsigned Length,
                                 UIdent Kind, UIdent AccessLevel,
                                 UIdent SetterAccessLevel,
                                 unsigned NameOffset,
                                 unsigned NameLength,
                                 unsigned BodyOffset,
                                 unsigned BodyLength,
                                 StringRef DisplayName,
                                 StringRef TypeName,
                                 StringRef RuntimeName,
                                 StringRef SelectorName,
                                 ArrayRef<StringRef> InheritedTypes,
                                 ArrayRef<UIdent> Attrs) override {
    return false;
  }

  bool endDocumentSubStructure() override { return false; }

  bool handleDocumentSubStructureElement(UIdent Kind,
                                         unsigned Offset,
                                         unsigned Length) override {
    return false;
  }

  bool recordAffectedRange(unsigned Offset, unsigned Length) override {
    return false;
  }
  
  bool recordAffectedLineRange(unsigned Line, unsigned Length) override {
    return false;
  }

  bool recordFormattedText(StringRef Text) override { return false; }

  bool setDiagnosticStage(UIdent DiagStage) override { return false; }
  bool handleDiagnostic(const DiagnosticEntryInfo &Info,
                        UIdent DiagStage) override {
    return false;
  }

  bool handleSourceText(StringRef Text) override { return false; }
};

struct TestCursorInfo {
  std::string Name;
  std::string Typename;
  std::string Filename;
  Optional<std::pair<unsigned, unsigned>> DeclarationLoc;
};

class CursorInfoTest : public ::testing::Test {
  SourceKit::Context Ctx{ getRuntimeLibPath() };
  std::atomic<int> NumTasks;
  dispatch_semaphore_t TaskSema = nullptr;

public:
  LangSupport &getLang() { return Ctx.getSwiftLangSupport(); }

  void SetUp() {
    NumTasks = 0;
    TaskSema = dispatch_semaphore_create(0);
  }

  void TearDown() {
    dispatch_release(TaskSema);
  }

  void addNotificationReceiver(DocumentUpdateNotificationReceiver Receiver) {
    Ctx.getNotificationCenter().addDocumentUpdateNotificationReceiver(Receiver);
  }

  void open(StringRef DocName, StringRef Text) {
    NullEditorConsumer Consumer;
    auto Buf = MemoryBuffer::getMemBufferCopy(Text, DocName);
    getLang().editorOpen(DocName, Buf.get(), /*EnableSyntaxMap=*/false, Consumer,
                         /*Args=*/{});
  }

  void replaceText(StringRef DocName, unsigned Offset, unsigned Length,
                   StringRef Text) {
    NullEditorConsumer Consumer;
    auto Buf = MemoryBuffer::getMemBufferCopy(Text, DocName);
    getLang().editorReplaceText(DocName, Buf.get(), Offset, Length, Consumer);
  }

  TestCursorInfo getCursor(const char *DocName, unsigned Offset,
                           ArrayRef<const char *> CArgs) {
    auto Args = makeArgs(DocName, CArgs);
    dispatch_semaphore_t sema = dispatch_semaphore_create(0);

    TestCursorInfo TestInfo;
    getLang().getCursorInfo(DocName, Offset, Args,
      [&](const CursorInfo &Info) {
        TestInfo.Name = Info.Name;
        TestInfo.Typename = Info.TypeName;
        TestInfo.Filename = Info.Filename;
        TestInfo.DeclarationLoc = Info.DeclarationLoc;
        dispatch_semaphore_signal(sema);
      });

    dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 60);
    bool expired = dispatch_semaphore_wait(sema, when);
    if (expired)
      llvm::report_fatal_error("check took too long");
    dispatch_release(sema);
    return TestInfo;
  }

  void checkCursorAsync(const char *DocName, unsigned Offset,
                        ArrayRef<const char *> CArgs,
                        StringRef ExpectedName, StringRef ExpectedTypename) {
    auto Args = makeArgs(DocName, CArgs);
    ++NumTasks;
    getLang().getCursorInfo(DocName, Offset, Args,
      [this, ExpectedName, ExpectedTypename](const CursorInfo &Info) {
        EXPECT_EQ(ExpectedName, Info.Name);
        EXPECT_EQ(ExpectedTypename, Info.TypeName);

        int Num = --NumTasks;
        if (Num == 0)
          dispatch_semaphore_signal(TaskSema);
      });
  }

  void waitForChecks() {
    dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 60);
    bool expired = dispatch_semaphore_wait(TaskSema, when);
    if (expired)
      llvm::report_fatal_error("checks took too long");
  }

  unsigned findOffset(StringRef Val, StringRef Text) {
    auto pos = Text.find(Val);
    assert(pos != StringRef::npos);
    return pos;
  }

private:
  std::vector<const char *> makeArgs(const char *DocName,
                                     ArrayRef<const char *> CArgs) {
    std::vector<const char *> Args = CArgs;
    Args.push_back(DocName);
    return Args;
  }
};

} // anonymous namespace

TEST_F(CursorInfoTest, FileNotExist) {
  const char *DocName = "/test.swift";
  const char *Contents =
    "let foo = 0\n";
  const char *Args[] = { "/<not-existent-file>" };

  open(DocName, Contents);
  auto FooOffs = findOffset("foo =", Contents);
  auto Info = getCursor(DocName, FooOffs, Args);
  EXPECT_STREQ("foo", Info.Name.c_str());
  EXPECT_STREQ("Int", Info.Typename.c_str());
}

static const char *ExpensiveInit =
    "[0:0,0:0,0:0,0:0,0:0,0:0,0:0]";

TEST_F(CursorInfoTest, EditAfter) {
  const char *DocName = "/test.swift";
  const char *Contents =
    "let value = foo\n"
    "let foo = 0\n";
  const char *Args[] = { "-parse-as-library" };

  open(DocName, Contents);
  auto FooRefOffs = findOffset("foo", Contents);
  auto FooOffs = findOffset("foo =", Contents);
  auto Info = getCursor(DocName, FooRefOffs, Args);
  EXPECT_STREQ("foo", Info.Name.c_str());
  EXPECT_STREQ("Int", Info.Typename.c_str());
  EXPECT_STREQ(DocName, Info.Filename.c_str());
  ASSERT_TRUE(Info.DeclarationLoc.hasValue());
  EXPECT_EQ(FooOffs, Info.DeclarationLoc->first);
  EXPECT_EQ(strlen("foo"), Info.DeclarationLoc->second);

  StringRef TextToReplace = "0";
  replaceText(DocName, findOffset(TextToReplace, Contents), TextToReplace.size(),
              ExpensiveInit);
  // Insert a space in front of 'foo' decl.
  replaceText(DocName, FooOffs, 0, " ");
  ++FooOffs;

  // Should not wait for the new AST, it should give the previous answer.
  Info = getCursor(DocName, FooRefOffs, Args);
  EXPECT_STREQ("foo", Info.Name.c_str());
  EXPECT_STREQ("Int", Info.Typename.c_str());
  EXPECT_STREQ(DocName, Info.Filename.c_str());
  ASSERT_TRUE(Info.DeclarationLoc.hasValue());
  EXPECT_EQ(FooOffs, Info.DeclarationLoc->first);
  EXPECT_EQ(strlen("foo"), Info.DeclarationLoc->second);
}

TEST_F(CursorInfoTest, EditBefore) {
  const char *DocName = "/test.swift";
  const char *Contents =
    "let foo = 0\n"
    "let value = foo;\n";
  const char *Args[] = { "-parse-as-library" };

  open(DocName, Contents);
  auto FooRefOffs = findOffset("foo;", Contents);
  auto FooOffs = findOffset("foo =", Contents);
  auto Info = getCursor(DocName, FooRefOffs, Args);
  EXPECT_STREQ("foo", Info.Name.c_str());
  EXPECT_STREQ("Int", Info.Typename.c_str());
  EXPECT_STREQ(DocName, Info.Filename.c_str());
  ASSERT_TRUE(Info.DeclarationLoc.hasValue());
  EXPECT_EQ(FooOffs, Info.DeclarationLoc->first);
  EXPECT_EQ(strlen("foo"), Info.DeclarationLoc->second);

  StringRef TextToReplace = "0";
  replaceText(DocName, findOffset(TextToReplace, Contents), TextToReplace.size(),
              ExpensiveInit);
  FooRefOffs += StringRef(ExpensiveInit).size() - TextToReplace.size();
  // Insert a space in front of 'foo' decl.
  replaceText(DocName, FooOffs, 0, " ");
  ++FooOffs;
  ++FooRefOffs;

  // Should not wait for the new AST, it should give the previous answer.
  Info = getCursor(DocName, FooRefOffs, Args);
  EXPECT_STREQ("foo", Info.Name.c_str());
  EXPECT_STREQ("Int", Info.Typename.c_str());
  EXPECT_STREQ(DocName, Info.Filename.c_str());
  ASSERT_TRUE(Info.DeclarationLoc.hasValue());
  EXPECT_EQ(FooOffs, Info.DeclarationLoc->first);
  EXPECT_EQ(strlen("foo"), Info.DeclarationLoc->second);
}

TEST_F(CursorInfoTest, CursorInfoMustWaitDueDeclLoc) {
  const char *DocName = "/test.swift";
  const char *Contents =
    "let value = foo\n"
    "let foo = 0\n";
  const char *Args[] = { "-parse-as-library" };

  open(DocName, Contents);
  auto FooRefOffs = findOffset("foo", Contents);
  auto FooOffs = findOffset("foo =", Contents);
  auto Info = getCursor(DocName, FooRefOffs, Args);
  EXPECT_STREQ("foo", Info.Name.c_str());
  EXPECT_STREQ("Int", Info.Typename.c_str());

  StringRef TextToReplace = "0";
  replaceText(DocName, findOffset(TextToReplace, Contents), TextToReplace.size(),
              ExpensiveInit);
  // Edit over the 'foo' decl.
  replaceText(DocName, FooOffs, strlen("foo"), "foo");

  // Should wait for the new AST, because the declaration location for the 'foo'
  // reference has been edited out.
  Info = getCursor(DocName, FooRefOffs, Args);
  EXPECT_STREQ("foo", Info.Name.c_str());
  EXPECT_STREQ("[Int : Int]", Info.Typename.c_str());
  ASSERT_TRUE(Info.DeclarationLoc.hasValue());
  EXPECT_EQ(FooOffs, Info.DeclarationLoc->first);
  EXPECT_EQ(strlen("foo"), Info.DeclarationLoc->second);
}

TEST_F(CursorInfoTest, CursorInfoMustWaitDueOffset) {
  const char *DocName = "/test.swift";
  const char *Contents =
    "let value = foo\n"
    "let foo = 0\n";
  const char *Args[] = { "-parse-as-library" };

  open(DocName, Contents);
  auto FooRefOffs = findOffset("foo", Contents);
  auto FooOffs = findOffset("foo =", Contents);
  auto Info = getCursor(DocName, FooRefOffs, Args);
  EXPECT_STREQ("foo", Info.Name.c_str());
  EXPECT_STREQ("Int", Info.Typename.c_str());

  StringRef TextToReplace = "0";
  replaceText(DocName, findOffset(TextToReplace, Contents), TextToReplace.size(),
              ExpensiveInit);
  // Edit over the 'foo' reference.
  replaceText(DocName, FooRefOffs, strlen("foo"), "foo");

  // Should wait for the new AST, because the cursor location has been edited
  // out.
  Info = getCursor(DocName, FooRefOffs, Args);
  EXPECT_STREQ("foo", Info.Name.c_str());
  EXPECT_STREQ("[Int : Int]", Info.Typename.c_str());
  ASSERT_TRUE(Info.DeclarationLoc.hasValue());
  EXPECT_EQ(FooOffs, Info.DeclarationLoc->first);
  EXPECT_EQ(strlen("foo"), Info.DeclarationLoc->second);
}

TEST_F(CursorInfoTest, CursorInfoMustWaitDueToken) {
  const char *DocName = "/test.swift";
  const char *Contents =
    "let value = foo\n"
    "let foo = 0\n";
  const char *Args[] = { "-parse-as-library" };

  open(DocName, Contents);
  auto FooRefOffs = findOffset("foo", Contents);
  auto FooOffs = findOffset("foo =", Contents);
  auto Info = getCursor(DocName, FooRefOffs, Args);
  EXPECT_STREQ("foo", Info.Name.c_str());
  EXPECT_STREQ("Int", Info.Typename.c_str());

  StringRef TextToReplace = "0";
  replaceText(DocName, findOffset(TextToReplace, Contents), TextToReplace.size(),
              ExpensiveInit);
  // Change 'foo' to 'fog' by replacing the last character.
  replaceText(DocName, FooRefOffs+2, 1, "g");
  replaceText(DocName, FooOffs+2, 1, "g");

  // Should wait for the new AST, because the cursor location points to a
  // different token.
  Info = getCursor(DocName, FooRefOffs, Args);
  EXPECT_STREQ("fog", Info.Name.c_str());
  EXPECT_STREQ("[Int : Int]", Info.Typename.c_str());
  ASSERT_TRUE(Info.DeclarationLoc.hasValue());
  EXPECT_EQ(FooOffs, Info.DeclarationLoc->first);
  EXPECT_EQ(strlen("fog"), Info.DeclarationLoc->second);
}
