// Copyright (c) 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromeos/components/nearby/lock_impl.h"

#include <memory>

#include "base/containers/flat_set.h"
#include "base/single_thread_task_runner.h"
#include "base/stl_util.h"
#include "base/task/post_task.h"
#include "base/test/gtest_util.h"
#include "base/test/scoped_task_environment.h"
#include "base/test/test_timeouts.h"
#include "base/threading/platform_thread.h"
#include "base/unguessable_token.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace chromeos {

namespace nearby {

class LockImplTest : public testing::Test {
 protected:
  LockImplTest()
      : lock_(std::make_unique<LockImpl>()),
        different_thread_task_runner_(
            base::CreateSingleThreadTaskRunnerWithTraits(base::MayBlock())) {}

  // testing::Test
  void SetUp() override {
    // |different_thread_task_runner_| is expected to run on a different thread
    // than the main test thread.
    EXPECT_FALSE(different_thread_task_runner_->BelongsToCurrentThread());
  }

  void TearDown() override {
    // Releases the test thread's ownership of |lock_|.
    int times_to_unlock;
    {
      base::AutoLock al(lock_->bookkeeping_lock_);
      times_to_unlock = lock_->num_acquisitions_;
    }
    for (int i = 0; i < times_to_unlock; ++i)
      lock_->unlock();

    // Makes sure that outstanding LockAndUnlockFromDifferentThread() tasks in
    // |different_thread_task_runner_| finish running after the test thread
    // relinquishes its ownership of |lock_|.
    scoped_task_environment_.RunUntilIdle();

    base::AutoLock al(lock_->bookkeeping_lock_);
    EXPECT_EQ(0u, lock_->num_acquisitions_);
    EXPECT_EQ(base::kInvalidThreadId, lock_->owning_thread_id_);
  }

  void PostLockAndUnlockFromDifferentThread(
      const base::UnguessableToken& attempt_id) {
    different_thread_task_runner_->PostTask(
        FROM_HERE,
        base::BindOnce(&LockImplTest::LockAndUnlockFromDifferentThread,
                       base::Unretained(this), attempt_id));
  }

  // Invoked whenever attempting to verify that a parallel task has indeed
  // blocked, since there's no way to deterministically find out if that task
  // will ever unblock.
  void TinyTimeout() {
    base::PlatformThread::Sleep(TestTimeouts::tiny_timeout());
  }

  bool HasSuccessfullyLockedWithAttemptId(
      const base::UnguessableToken& attempt_id) {
    lock_->lock();
    bool contains_key =
        base::ContainsKey(successful_lock_attempts_, attempt_id);
    lock_->unlock();
    return contains_key;
  }

  location::nearby::Lock* lock() { return lock_.get(); }

  base::test::ScopedTaskEnvironment scoped_task_environment_;

 private:
  // Only meant to be posted via PostLockAndUnlockFromDifferentThread() on
  // |different_thread_task_runner_|.
  //
  // This method will only insert |attempt_id| into |successful_lock_attempts_|
  // if it succeeds in acquiring |lock_|. It will also immediately unlock()
  // after doing so because unlock() may only be called from the same thread
  // that originally called lock().
  void LockAndUnlockFromDifferentThread(
      const base::UnguessableToken& attempt_id) {
    lock_->lock();
    successful_lock_attempts_.insert(attempt_id);
    lock_->unlock();
  }

  std::unique_ptr<LockImpl> lock_;
  scoped_refptr<base::SingleThreadTaskRunner> different_thread_task_runner_;
  base::flat_set<base::UnguessableToken> successful_lock_attempts_;

  DISALLOW_COPY_AND_ASSIGN(LockImplTest);
};

TEST_F(LockImplTest, LockOnce_UnlockOnce) {
  lock()->lock();
  lock()->unlock();
}

TEST_F(LockImplTest, LockThrice_UnlockThrice) {
  lock()->lock();
  lock()->lock();
  lock()->lock();
  lock()->unlock();
  lock()->unlock();
  lock()->unlock();
}

TEST_F(LockImplTest,
       LockOnce_DisallowRelockingFromDifferentThreadUntilCurrentThreadUnlocks) {
  // Lock on current thread.
  lock()->lock();

  // Try to lock again, but on different thread.
  base::UnguessableToken attempt_id = base::UnguessableToken::Create();
  PostLockAndUnlockFromDifferentThread(attempt_id);

  // Wait for a little, then check to see if the lock attempt failed.
  TinyTimeout();
  EXPECT_FALSE(HasSuccessfullyLockedWithAttemptId(attempt_id));

  // Outstanding lock attempt succeed after unlocking from current thread.
  lock()->unlock();
  scoped_task_environment_.RunUntilIdle();
  EXPECT_TRUE(HasSuccessfullyLockedWithAttemptId(attempt_id));
}

TEST_F(
    LockImplTest,
    LockThrice_DisallowRelockingFromDifferentThreadUntilCurrentThreadUnlocks) {
  // Lock on current thread.
  lock()->lock();
  lock()->lock();
  lock()->lock();

  // Try to lock again, but on different thread.
  base::UnguessableToken attempt_id = base::UnguessableToken::Create();
  PostLockAndUnlockFromDifferentThread(attempt_id);

  // Wait for a little, then check to see if the lock attempt failed.
  TinyTimeout();
  EXPECT_FALSE(HasSuccessfullyLockedWithAttemptId(attempt_id));

  // Outstanding lock attempt succeed after unlocking from current thread.
  lock()->unlock();
  lock()->unlock();
  lock()->unlock();
  scoped_task_environment_.RunUntilIdle();
  EXPECT_TRUE(HasSuccessfullyLockedWithAttemptId(attempt_id));
}

TEST_F(LockImplTest, InterweavedLocking) {
  base::UnguessableToken attempt_id1 = base::UnguessableToken::Create();
  base::UnguessableToken attempt_id2 = base::UnguessableToken::Create();
  base::UnguessableToken attempt_id3 = base::UnguessableToken::Create();

  lock()->lock();
  PostLockAndUnlockFromDifferentThread(attempt_id1);
  lock()->lock();
  PostLockAndUnlockFromDifferentThread(attempt_id2);
  lock()->lock();
  PostLockAndUnlockFromDifferentThread(attempt_id3);

  TinyTimeout();
  EXPECT_FALSE(HasSuccessfullyLockedWithAttemptId(attempt_id1));
  EXPECT_FALSE(HasSuccessfullyLockedWithAttemptId(attempt_id2));
  EXPECT_FALSE(HasSuccessfullyLockedWithAttemptId(attempt_id3));

  lock()->unlock();
  lock()->unlock();
  lock()->unlock();
  scoped_task_environment_.RunUntilIdle();
  EXPECT_TRUE(HasSuccessfullyLockedWithAttemptId(attempt_id1));
  EXPECT_TRUE(HasSuccessfullyLockedWithAttemptId(attempt_id2));
  EXPECT_TRUE(HasSuccessfullyLockedWithAttemptId(attempt_id3));
}

TEST_F(LockImplTest, CannotUnlockBeforeAnyLocks) {
  EXPECT_DCHECK_DEATH(lock()->unlock());
}

}  // namespace nearby

}  // namespace chromeos
