// Copyright 2008 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

goog.provide('goog.editor.plugins.UndoRedoManagerTest');
goog.setTestOnly('goog.editor.plugins.UndoRedoManagerTest');

goog.require('goog.editor.plugins.UndoRedoManager');
goog.require('goog.editor.plugins.UndoRedoState');
goog.require('goog.events');
goog.require('goog.testing.StrictMock');
goog.require('goog.testing.jsunit');

var mockState1;
var mockState2;
var mockState3;
var states;
var manager;
var stateChangeCount;
var beforeUndoCount;
var beforeRedoCount;
var preventDefault;

function setUp() {
  manager = new goog.editor.plugins.UndoRedoManager();
  stateChangeCount = 0;
  goog.events.listen(
      manager, goog.editor.plugins.UndoRedoManager.EventType.STATE_CHANGE,
      function() { stateChangeCount++; });

  beforeUndoCount = 0;
  preventDefault = false;
  goog.events.listen(
      manager, goog.editor.plugins.UndoRedoManager.EventType.BEFORE_UNDO,
      function(e) {
        beforeUndoCount++;
        if (preventDefault) {
          e.preventDefault();
        }
      });

  beforeRedoCount = 0;
  goog.events.listen(
      manager, goog.editor.plugins.UndoRedoManager.EventType.BEFORE_REDO,
      function(e) {
        beforeRedoCount++;
        if (preventDefault) {
          e.preventDefault();
        }
      });

  mockState1 = new goog.testing.StrictMock(goog.editor.plugins.UndoRedoState);
  mockState2 = new goog.testing.StrictMock(goog.editor.plugins.UndoRedoState);
  mockState3 = new goog.testing.StrictMock(goog.editor.plugins.UndoRedoState);
  states = [mockState1, mockState2, mockState3];

  mockState1.equals = mockState2.equals =
      mockState3.equals = function(state) { return this == state; };

  mockState1.isAsynchronous = mockState2.isAsynchronous =
      mockState3.isAsynchronous = function() { return false; };
}


function tearDown() {
  goog.events.removeAll(manager);
  manager.dispose();
}


/**
 * Adds all the mock states to the undo-redo manager.
 */
function addStatesToManager() {
  manager.addState(states[0]);

  for (var i = 1; i < states.length; i++) {
    var state = states[i];
    manager.addState(state);
  }

  stateChangeCount = 0;
}


/**
 * Resets all mock states so that they are ready for testing.
 */
function resetStates() {
  for (var i = 0; i < states.length; i++) {
    states[i].$reset();
  }
}


function testSetMaxUndoDepth() {
  manager.setMaxUndoDepth(2);
  addStatesToManager();
  assertArrayEquals(
      'Undo stack must contain only the two most recent states.',
      [mockState2, mockState3], manager.undoStack_);
}


function testAddState() {
  var stateAddedCount = 0;
  goog.events.listen(
      manager, goog.editor.plugins.UndoRedoManager.EventType.STATE_ADDED,
      function() { stateAddedCount++; });

  manager.addState(mockState1);
  assertArrayEquals(
      'Undo stack must contain added state.', [mockState1], manager.undoStack_);
  assertEquals(
      'Manager must dispatch one state change event on ' +
          'undo stack 0->1 transition.',
      1, stateChangeCount);
  assertEquals('State added must have dispatched once.', 1, stateAddedCount);
  mockState1.$reset();

  // Test adding same state twice.
  manager.addState(mockState1);
  assertArrayEquals(
      'Undo stack must not contain two equal, sequential states.', [mockState1],
      manager.undoStack_);
  assertEquals(
      'Manager must not dispatch state change event when nothing is ' +
          'added to the stack.',
      1, stateChangeCount);
  assertEquals('State added must have dispatched once.', 1, stateAddedCount);

  // Test adding a second state.
  manager.addState(mockState2);
  assertArrayEquals(
      'Undo stack must contain both states.', [mockState1, mockState2],
      manager.undoStack_);
  assertEquals(
      'Manager must not dispatch state change event when second ' +
          'state is added to the stack.',
      1, stateChangeCount);
  assertEquals('State added must have dispatched twice.', 2, stateAddedCount);

  // Test adding a state when there is state on the redo stack.
  manager.undo();
  assertEquals(
      'Manager must dispatch state change when redo stack goes to 1.', 2,
      stateChangeCount);

  manager.addState(mockState3);
  assertArrayEquals(
      'Undo stack must contain states 1 and 3.', [mockState1, mockState3],
      manager.undoStack_);
  assertEquals(
      'Manager must dispatch state change event when redo stack ' +
          'goes to zero.',
      3, stateChangeCount);
  assertEquals(
      'State added must have dispatched three times.', 3, stateAddedCount);
}


function testHasState() {
  assertFalse('New manager must have no undo state.', manager.hasUndoState());
  assertFalse('New manager must have no redo state.', manager.hasRedoState());

  manager.addState(mockState1);
  assertTrue('Manager must have only undo state.', manager.hasUndoState());
  assertFalse('Manager must have no redo state.', manager.hasRedoState());

  manager.undo();
  assertFalse('Manager must have no undo state.', manager.hasUndoState());
  assertTrue('Manager must have only redo state.', manager.hasRedoState());
}


function testClearHistory() {
  addStatesToManager();
  manager.undo();
  stateChangeCount = 0;

  manager.clearHistory();
  assertFalse('Undo stack must be empty.', manager.hasUndoState());
  assertFalse('Redo stack must be empty.', manager.hasRedoState());
  assertEquals(
      'State change count must be 1 after clear history.', 1, stateChangeCount);

  manager.clearHistory();
  assertEquals(
      'Repeated clearHistory must not change state change count.', 1,
      stateChangeCount);
}


function testUndo() {
  addStatesToManager();

  mockState3.undo();
  mockState3.$replay();
  manager.undo();
  assertEquals(
      'Adding first item to redo stack must dispatch state change.', 1,
      stateChangeCount);
  assertEquals('Undo must cause before action to dispatch', 1, beforeUndoCount);
  mockState3.$verify();

  preventDefault = true;
  mockState2.$replay();
  manager.undo();
  assertEquals(
      'No stack transitions between 0 and 1, must not dispatch ' +
          'state change.',
      1, stateChangeCount);
  assertEquals('Undo must cause before action to dispatch', 2, beforeUndoCount);
  mockState2.$verify();  // Verify that undo was prevented.

  preventDefault = false;
  mockState1.undo();
  mockState1.$replay();
  manager.undo();
  assertEquals(
      'Doing last undo operation must dispatch state change.', 2,
      stateChangeCount);
  assertEquals('Undo must cause before action to dispatch', 3, beforeUndoCount);
  mockState1.$verify();
}


function testUndo_Asynchronous() {
  // Using a stub instead of a mock here so that the state can behave as an
  // EventTarget and dispatch events.
  var stubState = new goog.editor.plugins.UndoRedoState(true);
  var undoCalled = false;
  stubState.undo = function() { undoCalled = true; };
  stubState.redo = goog.nullFunction;
  stubState.equals = function() { return false; };

  manager.addState(mockState2);
  manager.addState(mockState1);
  manager.addState(stubState);

  manager.undo();
  assertTrue('undoCalled must be true (undo must be called).', undoCalled);
  assertEquals('Undo must cause before action to dispatch', 1, beforeUndoCount);

  // Calling undo shouldn't actually undo since the first async undo hasn't
  // fired an event yet.
  mockState1.$replay();
  manager.undo();
  mockState1.$verify();
  assertEquals(
      'Before action must not dispatch for pending undo.', 1, beforeUndoCount);

  // Dispatching undo completed on first undo, should cause the second pending
  // undo to happen.
  mockState1.$reset();
  mockState1.undo();
  mockState1.$replay();
  mockState2.$replay();  // Nothing should happen to mockState2.
  stubState.dispatchEvent(goog.editor.plugins.UndoRedoState.ACTION_COMPLETED);
  mockState1.$verify();
  mockState2.$verify();
  assertEquals(
      'Second undo must cause before action to dispatch', 2, beforeUndoCount);

  // Test last undo.
  mockState2.$reset();
  mockState2.undo();
  mockState2.$replay();
  manager.undo();
  mockState2.$verify();
  assertEquals(
      'Third undo must cause before action to dispatch', 3, beforeUndoCount);
}


function testRedo() {
  addStatesToManager();
  manager.undo();
  manager.undo();
  manager.undo();
  resetStates();
  stateChangeCount = 0;

  mockState1.redo();
  mockState1.$replay();
  manager.redo();
  assertEquals(
      'Pushing first item onto undo stack during redo must dispatch ' +
          'state change.',
      1, stateChangeCount);
  assertEquals(
      'First redo must cause before action to dispatch', 1, beforeRedoCount);
  mockState1.$verify();

  preventDefault = true;
  mockState2.$replay();
  manager.redo();
  assertEquals(
      'No stack transitions between 0 and 1, must not dispatch ' +
          'state change.',
      1, stateChangeCount);
  assertEquals(
      'Second redo must cause before action to dispatch', 2, beforeRedoCount);
  mockState2.$verify();  // Verify that redo was prevented.

  preventDefault = false;
  mockState3.redo();
  mockState3.$replay();
  manager.redo();
  assertEquals(
      'Removing last item from redo stack must dispatch state change.', 2,
      stateChangeCount);
  assertEquals(
      'Third redo must cause before action to dispatch', 3, beforeRedoCount);
  mockState3.$verify();
  mockState3.$reset();

  mockState3.undo();
  mockState3.$replay();
  manager.undo();
  assertEquals(
      'Putting item on redo stack must dispatch state change.', 3,
      stateChangeCount);
  assertEquals('Undo must cause before action to dispatch', 4, beforeUndoCount);
  mockState3.$verify();
}


function testRedo_Asynchronous() {
  var stubState = new goog.editor.plugins.UndoRedoState(true);
  var redoCalled = false;
  stubState.redo = function() { redoCalled = true; };
  stubState.undo = goog.nullFunction;
  stubState.equals = function() { return false; };

  manager.addState(stubState);
  manager.addState(mockState1);
  manager.addState(mockState2);

  manager.undo();
  manager.undo();
  manager.undo();
  stubState.dispatchEvent(goog.editor.plugins.UndoRedoState.ACTION_COMPLETED);
  resetStates();

  manager.redo();
  assertTrue('redoCalled must be true (redo must be called).', redoCalled);

  // Calling redo shouldn't actually redo since the first async redo hasn't
  // fired an event yet.
  mockState1.$replay();
  manager.redo();
  mockState1.$verify();

  // Dispatching redo completed on first redo, should cause the second pending
  // redo to happen.
  mockState1.$reset();
  mockState1.redo();
  mockState1.$replay();
  mockState2.$replay();  // Nothing should happen to mockState1.
  stubState.dispatchEvent(goog.editor.plugins.UndoRedoState.ACTION_COMPLETED);
  mockState1.$verify();
  mockState2.$verify();

  // Test last redo.
  mockState2.$reset();
  mockState2.redo();
  mockState2.$replay();
  manager.redo();
  mockState2.$verify();
}

function testUndoAndRedoPeek() {
  addStatesToManager();
  manager.undo();

  assertEquals(
      'redoPeek must return the top of the redo stack.',
      manager.redoStack_[manager.redoStack_.length - 1], manager.redoPeek());
  assertEquals(
      'undoPeek must return the top of the undo stack.',
      manager.undoStack_[manager.undoStack_.length - 1], manager.undoPeek());
}
