// Copyright 2013 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 "components/undo/undo_manager.h"

#include "base/auto_reset.h"
#include "base/macros.h"
#include "base/memory/ptr_util.h"
#include "components/undo/undo_manager_observer.h"
#include "components/undo/undo_operation.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace {

std::vector<UndoOperation*> ConvertToRawPtrVector(
    const std::vector<std::unique_ptr<UndoOperation>>& args) {
  std::vector<UndoOperation*> args_rawptrs;
  for (auto i = args.begin(); i != args.end(); ++i)
    args_rawptrs.push_back(i->get());
  return args_rawptrs;
}

}  // namespace

// UndoManagerTestApi ----------------------------------------------------------

class UndoManagerTestApi {
 public:
  // Returns all UndoOperations that are awaiting Undo or Redo.
  static std::vector<UndoOperation*> GetAllUndoOperations(
      const UndoManager& undo_manager);

 private:
  DISALLOW_IMPLICIT_CONSTRUCTORS(UndoManagerTestApi);
};

std::vector<UndoOperation*> UndoManagerTestApi::GetAllUndoOperations(
    const UndoManager& undo_manager) {
  std::vector<UndoOperation*> result;
  for (size_t i = 0; i < undo_manager.undo_actions_.size(); ++i) {
    const std::vector<UndoOperation*> operations =
        ConvertToRawPtrVector(undo_manager.undo_actions_[i]->undo_operations());
    result.insert(result.end(), operations.begin(), operations.end());
  }
  for (size_t i = 0; i < undo_manager.redo_actions_.size(); ++i) {
    const std::vector<UndoOperation*> operations =
        ConvertToRawPtrVector(undo_manager.redo_actions_[i]->undo_operations());
    result.insert(result.end(), operations.begin(), operations.end());
  }
  // Ensure that if an Undo is in progress the UndoOperations part of that
  // UndoGroup are included in the returned set. This will ensure that any
  // changes (such as renumbering) will be applied to any potentially
  // unprocessed UndoOperations.
  if (undo_manager.undo_in_progress_action_) {
    const std::vector<UndoOperation*> operations = ConvertToRawPtrVector(
        undo_manager.undo_in_progress_action_->undo_operations());
    result.insert(result.end(), operations.begin(), operations.end());
  }

  return result;
}

namespace {

class TestUndoOperation;

// TestUndoService -------------------------------------------------------------

class TestUndoService {
 public:
  TestUndoService();
  ~TestUndoService();

  void Redo();
  void TriggerOperation();
  void RecordUndoCall();

  UndoManager undo_manager_;

  bool performing_redo_;

  int undo_operation_count_;
  int redo_operation_count_;
};

// TestUndoOperation -----------------------------------------------------------

class TestUndoOperation : public UndoOperation {
 public:
  explicit TestUndoOperation(TestUndoService* undo_service);
  ~TestUndoOperation() override;

  // UndoOperation:
  void Undo() override;
  int GetUndoLabelId() const override;
  int GetRedoLabelId() const override;

 private:
  TestUndoService* undo_service_;

  DISALLOW_COPY_AND_ASSIGN(TestUndoOperation);
};

TestUndoOperation::TestUndoOperation(TestUndoService* undo_service)
      : undo_service_(undo_service) {
}

TestUndoOperation::~TestUndoOperation() {
}

void TestUndoOperation::Undo() {
  undo_service_->TriggerOperation();
  undo_service_->RecordUndoCall();
}

int TestUndoOperation::GetUndoLabelId() const {
  return 0;
}

int TestUndoOperation::GetRedoLabelId() const {
  return 0;
}

// TestUndoService -------------------------------------------------------------

TestUndoService::TestUndoService() : performing_redo_(false),
                                     undo_operation_count_(0),
                                     redo_operation_count_(0) {
}

TestUndoService::~TestUndoService() {
}

void TestUndoService::Redo() {
  base::AutoReset<bool> incoming_changes(&performing_redo_, true);
  undo_manager_.Redo();
}

void TestUndoService::TriggerOperation() {
  undo_manager_.AddUndoOperation(base::MakeUnique<TestUndoOperation>(this));
}

void TestUndoService::RecordUndoCall() {
  if (performing_redo_)
    ++redo_operation_count_;
  else
    ++undo_operation_count_;
}

// TestObserver ----------------------------------------------------------------

class TestObserver : public UndoManagerObserver {
 public:
  TestObserver() : state_change_count_(0) {}
  // Returns the number of state change callbacks
  int state_change_count() { return state_change_count_; }

  void OnUndoManagerStateChange() override { ++state_change_count_; }

 private:
  int state_change_count_;

  DISALLOW_COPY_AND_ASSIGN(TestObserver);
};

// Tests -----------------------------------------------------------------------

TEST(UndoServiceTest, AddUndoActions) {
  TestUndoService undo_service;

  undo_service.TriggerOperation();
  undo_service.TriggerOperation();
  EXPECT_EQ(2U, undo_service.undo_manager_.undo_count());
  EXPECT_EQ(0U, undo_service.undo_manager_.redo_count());
}

TEST(UndoServiceTest, UndoMultipleActions) {
  TestUndoService undo_service;

  undo_service.TriggerOperation();
  undo_service.TriggerOperation();

  undo_service.undo_manager_.Undo();
  EXPECT_EQ(1U, undo_service.undo_manager_.undo_count());
  EXPECT_EQ(1U, undo_service.undo_manager_.redo_count());

  undo_service.undo_manager_.Undo();
  EXPECT_EQ(0U, undo_service.undo_manager_.undo_count());
  EXPECT_EQ(2U, undo_service.undo_manager_.redo_count());

  EXPECT_EQ(2, undo_service.undo_operation_count_);
  EXPECT_EQ(0, undo_service.redo_operation_count_);
}

TEST(UndoServiceTest, RedoAction) {
  TestUndoService undo_service;

  undo_service.TriggerOperation();

  undo_service.undo_manager_.Undo();
  EXPECT_EQ(0U, undo_service.undo_manager_.undo_count());
  EXPECT_EQ(1U, undo_service.undo_manager_.redo_count());

  undo_service.Redo();
  EXPECT_EQ(1U, undo_service.undo_manager_.undo_count());
  EXPECT_EQ(0U, undo_service.undo_manager_.redo_count());

  EXPECT_EQ(1, undo_service.undo_operation_count_);
  EXPECT_EQ(1, undo_service.redo_operation_count_);
}

TEST(UndoServiceTest, GroupActions) {
  TestUndoService undo_service;

  // Add two operations in a single action.
  undo_service.undo_manager_.StartGroupingActions();
  undo_service.TriggerOperation();
  undo_service.TriggerOperation();
  undo_service.undo_manager_.EndGroupingActions();

  // Check that only one action is created.
  EXPECT_EQ(1U, undo_service.undo_manager_.undo_count());
  EXPECT_EQ(0U, undo_service.undo_manager_.redo_count());

  undo_service.undo_manager_.Undo();
  EXPECT_EQ(0U, undo_service.undo_manager_.undo_count());
  EXPECT_EQ(1U, undo_service.undo_manager_.redo_count());

  undo_service.Redo();
  EXPECT_EQ(1U, undo_service.undo_manager_.undo_count());
  EXPECT_EQ(0U, undo_service.undo_manager_.redo_count());

  // Check that both operations were called in Undo and Redo.
  EXPECT_EQ(2, undo_service.undo_operation_count_);
  EXPECT_EQ(2, undo_service.redo_operation_count_);
}

TEST(UndoServiceTest, SuspendUndoTracking) {
  TestUndoService undo_service;

  undo_service.undo_manager_.SuspendUndoTracking();
  EXPECT_TRUE(undo_service.undo_manager_.IsUndoTrakingSuspended());

  undo_service.TriggerOperation();

  undo_service.undo_manager_.ResumeUndoTracking();
  EXPECT_FALSE(undo_service.undo_manager_.IsUndoTrakingSuspended());

  EXPECT_EQ(0U, undo_service.undo_manager_.undo_count());
  EXPECT_EQ(0U, undo_service.undo_manager_.redo_count());
}

TEST(UndoServiceTest, RedoEmptyAfterNewAction) {
  TestUndoService undo_service;

  undo_service.TriggerOperation();
  undo_service.undo_manager_.Undo();
  EXPECT_EQ(0U, undo_service.undo_manager_.undo_count());
  EXPECT_EQ(1U, undo_service.undo_manager_.redo_count());

  undo_service.TriggerOperation();
  EXPECT_EQ(1U, undo_service.undo_manager_.undo_count());
  EXPECT_EQ(0U, undo_service.undo_manager_.redo_count());
}

TEST(UndoServiceTest, GetAllUndoOperations) {
  TestUndoService undo_service;

  undo_service.TriggerOperation();

  undo_service.undo_manager_.StartGroupingActions();
  undo_service.TriggerOperation();
  undo_service.TriggerOperation();
  undo_service.undo_manager_.EndGroupingActions();

  undo_service.TriggerOperation();

  undo_service.undo_manager_.Undo();
  ASSERT_EQ(2U, undo_service.undo_manager_.undo_count());
  ASSERT_EQ(1U, undo_service.undo_manager_.redo_count());

  std::vector<UndoOperation*> all_operations =
      UndoManagerTestApi::GetAllUndoOperations(undo_service.undo_manager_);
  EXPECT_EQ(4U, all_operations.size());
}

TEST(UndoServiceTest, ObserverCallbacks) {
  TestObserver observer;
  TestUndoService undo_service;
  undo_service.undo_manager_.AddObserver(&observer);
  EXPECT_EQ(0, observer.state_change_count());

  undo_service.TriggerOperation();
  EXPECT_EQ(1, observer.state_change_count());

  undo_service.undo_manager_.StartGroupingActions();
  undo_service.TriggerOperation();
  undo_service.TriggerOperation();
  undo_service.undo_manager_.EndGroupingActions();
  EXPECT_EQ(2, observer.state_change_count());

  // There should be at least 1 observer callback for undo.
  undo_service.undo_manager_.Undo();
  int callback_count_after_undo = observer.state_change_count();
  EXPECT_GT(callback_count_after_undo, 2);

  // There should be at least 1 observer callback for redo.
  undo_service.undo_manager_.Redo();
  int callback_count_after_redo = observer.state_change_count();
  EXPECT_GT(callback_count_after_redo, callback_count_after_undo);

  undo_service.undo_manager_.RemoveObserver(&observer);
  undo_service.undo_manager_.Undo();
  EXPECT_EQ(callback_count_after_redo, observer.state_change_count());
}

} // namespace
