// Copyright 2015 The Weave 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 "src/commands/command_queue.h"

#include <set>
#include <string>
#include <vector>

#include <base/bind.h>
#include <base/memory/weak_ptr.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <weave/provider/test/fake_task_runner.h>

#include "src/bind_lambda.h"
#include "src/string_utils.h"

namespace weave {

using testing::Return;
using testing::StrictMock;

class CommandQueueTest : public testing::Test {
 public:
  std::unique_ptr<CommandInstance> CreateDummyCommandInstance(
      const std::string& name,
      const std::string& id) {
    std::unique_ptr<CommandInstance> cmd{
        new CommandInstance{name, Command::Origin::kLocal, {}}};
    cmd->SetID(id);
    return cmd;
  }

  bool Remove(const std::string& id) { return queue_.Remove(id); }

  void Cleanup(const base::TimeDelta& interval) {
    return queue_.Cleanup(task_runner_.GetClock()->Now() + interval);
  }

  std::string GetFirstCommandToBeRemoved() const {
    return queue_.remove_queue_.top().second;
  }

  StrictMock<provider::test::FakeTaskRunner> task_runner_;
  CommandQueue queue_{&task_runner_, task_runner_.GetClock()};
};

// Keeps track of commands being added to and removed from the queue_.
// Aborts if duplicate commands are added or non-existent commands are removed.
class FakeDispatcher {
 public:
  explicit FakeDispatcher(CommandQueue* queue) {
    queue->AddCommandAddedCallback(base::Bind(&FakeDispatcher::OnCommandAdded,
                                              weak_ptr_factory_.GetWeakPtr()));
    queue->AddCommandRemovedCallback(base::Bind(
        &FakeDispatcher::OnCommandRemoved, weak_ptr_factory_.GetWeakPtr()));
  }

  void OnCommandAdded(Command* command) {
    CHECK(ids_.insert(command->GetID()).second) << "Command ID already exists: "
                                                << command->GetID();
    CHECK(commands_.insert(command).second)
        << "Command instance already exists";
  }

  void OnCommandRemoved(Command* command) {
    CHECK_EQ(1u, ids_.erase(command->GetID())) << "Command ID not found: "
                                               << command->GetID();
    CHECK_EQ(1u, commands_.erase(command)) << "Command instance not found";
  }

  // Get the comma-separated list of command IDs currently accumulated in the
  // command queue_.
  std::string GetIDs() const {
    return Join(",", std::vector<std::string>(ids_.begin(), ids_.end()));
  }

 private:
  std::set<std::string> ids_;
  std::set<Command*> commands_;
  base::WeakPtrFactory<FakeDispatcher> weak_ptr_factory_{this};
};

TEST_F(CommandQueueTest, Empty) {
  EXPECT_TRUE(queue_.IsEmpty());
  EXPECT_EQ(0u, queue_.GetCount());
}

TEST_F(CommandQueueTest, Add) {
  queue_.Add(CreateDummyCommandInstance("base.reboot", "id1"));
  queue_.Add(CreateDummyCommandInstance("base.reboot", "id2"));
  queue_.Add(CreateDummyCommandInstance("base.reboot", "id3"));
  EXPECT_EQ(3u, queue_.GetCount());
  EXPECT_FALSE(queue_.IsEmpty());
}

TEST_F(CommandQueueTest, Remove) {
  const std::string id1 = "id1";
  const std::string id2 = "id2";
  queue_.Add(CreateDummyCommandInstance("base.reboot", id1));
  queue_.Add(CreateDummyCommandInstance("base.reboot", id2));
  EXPECT_FALSE(queue_.IsEmpty());
  EXPECT_FALSE(Remove("dummy"));
  EXPECT_EQ(2u, queue_.GetCount());
  EXPECT_TRUE(Remove(id1));
  EXPECT_EQ(1u, queue_.GetCount());
  EXPECT_FALSE(Remove(id1));
  EXPECT_EQ(1u, queue_.GetCount());
  EXPECT_TRUE(Remove(id2));
  EXPECT_EQ(0u, queue_.GetCount());
  EXPECT_FALSE(Remove(id2));
  EXPECT_EQ(0u, queue_.GetCount());
  EXPECT_TRUE(queue_.IsEmpty());
}

TEST_F(CommandQueueTest, RemoveLater) {
  const std::string id1 = "id1";
  queue_.Add(CreateDummyCommandInstance("base.reboot", id1));
  EXPECT_EQ(1u, queue_.GetCount());

  queue_.RemoveLater(id1);
  EXPECT_EQ(1u, queue_.GetCount());

  Cleanup(base::TimeDelta::FromMinutes(1));
  EXPECT_EQ(1u, queue_.GetCount());

  Cleanup(base::TimeDelta::FromMinutes(15));
  EXPECT_EQ(0u, queue_.GetCount());
}

TEST_F(CommandQueueTest, RemoveLaterOnCleanupTask) {
  const std::string id1 = "id1";
  queue_.Add(CreateDummyCommandInstance("base.reboot", id1));
  EXPECT_EQ(1u, queue_.GetCount());

  queue_.RemoveLater(id1);
  EXPECT_EQ(1u, queue_.GetCount());
  ASSERT_EQ(1u, task_runner_.GetTaskQueueSize());

  task_runner_.RunOnce();

  EXPECT_EQ(0u, queue_.GetCount());
  EXPECT_EQ(0u, task_runner_.GetTaskQueueSize());
}

TEST_F(CommandQueueTest, CleanupMultipleCommands) {
  const std::string id1 = "id1";
  const std::string id2 = "id2";

  queue_.Add(CreateDummyCommandInstance("base.reboot", id1));
  queue_.Add(CreateDummyCommandInstance("base.reboot", id2));
  auto remove_task = [this](const std::string& id) { queue_.RemoveLater(id); };
  remove_task(id1);
  task_runner_.PostDelayedTask(FROM_HERE, base::Bind(remove_task, id2),
                               base::TimeDelta::FromSeconds(10));
  EXPECT_EQ(2u, queue_.GetCount());
  ASSERT_EQ(2u, task_runner_.GetTaskQueueSize());
  task_runner_.RunOnce();  // Executes "remove_task(id2) @ T+10s".
  ASSERT_EQ(2u, queue_.GetCount());
  ASSERT_EQ(1u, task_runner_.GetTaskQueueSize());
  EXPECT_EQ(id1, GetFirstCommandToBeRemoved());
  task_runner_.RunOnce();  // Should remove task "id1" from queue.
  ASSERT_EQ(1u, queue_.GetCount());
  ASSERT_EQ(1u, task_runner_.GetTaskQueueSize());
  EXPECT_EQ(id2, GetFirstCommandToBeRemoved());
  task_runner_.RunOnce();  // Should remove task "id2" from queue.
  EXPECT_EQ(0u, queue_.GetCount());
  EXPECT_EQ(0u, task_runner_.GetTaskQueueSize());
}

TEST_F(CommandQueueTest, Dispatch) {
  FakeDispatcher dispatch(&queue_);
  const std::string id1 = "id1";
  const std::string id2 = "id2";
  queue_.Add(CreateDummyCommandInstance("base.reboot", id1));
  queue_.Add(CreateDummyCommandInstance("base.reboot", id2));
  std::set<std::string> ids{id1, id2};  // Make sure they are sorted properly.
  std::string expected_set =
      Join(",", std::vector<std::string>(ids.begin(), ids.end()));
  EXPECT_EQ(expected_set, dispatch.GetIDs());
  Remove(id1);
  EXPECT_EQ(id2, dispatch.GetIDs());
  Remove(id2);
  EXPECT_EQ("", dispatch.GetIDs());
}

TEST_F(CommandQueueTest, Find) {
  const std::string id1 = "id1";
  const std::string id2 = "id2";
  queue_.Add(CreateDummyCommandInstance("base.reboot", id1));
  queue_.Add(CreateDummyCommandInstance("base.shutdown", id2));
  EXPECT_EQ(nullptr, queue_.Find("dummy"));
  auto cmd1 = queue_.Find(id1);
  EXPECT_NE(nullptr, cmd1);
  EXPECT_EQ("base.reboot", cmd1->GetName());
  EXPECT_EQ(id1, cmd1->GetID());
  auto cmd2 = queue_.Find(id2);
  EXPECT_NE(nullptr, cmd2);
  EXPECT_EQ("base.shutdown", cmd2->GetName());
  EXPECT_EQ(id2, cmd2->GetID());
}

}  // namespace weave