/**
 * @license
 * Visual Blocks Editor
 *
 * Copyright 2017 Google Inc.
 * https://developers.google.com/blockly/
 *
 * 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.
 */

 /**
 * @fileoverview Tests for Blockly.Events
 * @author marisaleung@google.com (Marisa Leung)
 */
'use strict';

goog.require('goog.testing');
goog.require('goog.testing.MockControl');

var mockControl_;
var workspace;

function eventTest_setUp() {
  workspace = new Blockly.Workspace();
  mockControl_ = new goog.testing.MockControl();
}

function eventTest_setUpWithMockBlocks() {
  eventTest_setUp();
  // TODO: Replace with defineGetVarBlock();
  Blockly.defineBlocksWithJsonArray([{
    'type': 'field_variable_test_block',
    'message0': '%1',
    'args0': [
      {
        'type': 'field_variable',
        'name': 'VAR',
        'variable': 'item'
      }
    ],
  },
  {
    'type': 'simple_test_block',
    'message0': 'simple test block'
  }]);
}

function eventTest_tearDown() {
  delete Blockly.Blocks['field_variable_test_block'];
  delete Blockly.Blocks['simple_test_block'];
  mockControl_.$tearDown();
  workspace.dispose();
}

function eventTest_tearDownWithMockBlocks() {
  eventTest_tearDown();
  delete Blockly.Blocks.field_variable_test_block;
}

function test_block_base_constructor() {
  eventTest_setUpWithMockBlocks();
  setUpMockMethod(mockControl_, Blockly.utils, 'genUid', null, '1');
  try {
    var block = createSimpleTestBlock(workspace);

    // Here's the event we care about.
    var event = new Blockly.Events.BlockBase(block);
    assertUndefined(event.varId);
    checkExactEventValues(event, {'blockId': '1', 'workspaceId': workspace.id,
      'group': '', 'recordUndo': true});
  } finally {
    eventTest_tearDownWithMockBlocks();
  }
}

function test_var_base_constructor() {
  eventTest_setUpWithMockBlocks();
  setUpMockMethod(mockControl_, Blockly.utils, 'genUid', null, '1');
  try {
    var variable = workspace.createVariable('name1', 'type1', 'id1');

    var event = new Blockly.Events.VarBase(variable);
    assertUndefined(event.blockId);
    checkExactEventValues(event, {'varId': 'id1',
      'workspaceId': workspace.id, 'group': '', 'recordUndo': true});
  } finally {
    eventTest_tearDownWithMockBlocks();
  }
}

function test_abstract_constructor() {
  eventTest_setUpWithMockBlocks();
  try {
    var event = new Blockly.Events.Abstract();
    assertUndefined(event.blockId);
    assertUndefined(event.workspaceId);
    assertUndefined(event.varId);
    checkExactEventValues(event, {'group': '', 'recordUndo': true});
  } finally {
    eventTest_tearDownWithMockBlocks();
  }
}

// Test util
function checkCreateEventValues(event, block, ids, type) {
  var expected_xml = Blockly.Xml.domToText(Blockly.Xml.blockToDom(block));
  var result_xml = Blockly.Xml.domToText(event.xml);
  assertEquals(expected_xml, result_xml);
  isEqualArrays(ids, event.ids);
  assertEquals(type, event.type);
}

// Test util
function checkDeleteEventValues(event, block, ids, type) {
  var expected_xml = Blockly.Xml.domToText(Blockly.Xml.blockToDom(block));
  var result_xml = Blockly.Xml.domToText(event.oldXml);
  assertEquals(expected_xml, result_xml);
  isEqualArrays(ids, event.ids);
  assertEquals(type, event.type);
}

// Test util
function checkExactEventValues(event, values) {
  var keys = Object.keys(values);
  for (var i = 0, field; field = keys[i]; i++) {
    assertEquals(values[field], event[field]);
  }
}

// Test util
function createSimpleTestBlock(workspace) {
  // Disable events while constructing the block: this is a test of the
  // Blockly.Event constructors, not the block constructor.
  Blockly.Events.disable();
  var block = new Blockly.Block(workspace, 'simple_test_block');
  Blockly.Events.enable();
  return block;
}

function test_create_constructor() {
  eventTest_setUpWithMockBlocks();
  setUpMockMethod(mockControl_, Blockly.utils, 'genUid', null, ['1']);
  try {
    var block = createSimpleTestBlock(workspace);

    var event = new Blockly.Events.Create(block);
    checkCreateEventValues(event, block, ['1'], 'create');
  } finally {
    eventTest_tearDownWithMockBlocks();
  }
}

function test_blockCreate_constructor() {
  // expect that blockCreate behaves the same as create.
  eventTest_setUpWithMockBlocks();
  setUpMockMethod(mockControl_, Blockly.utils, 'genUid', null, ['1']);
  try {
    var block = createSimpleTestBlock(workspace);

    var event = new Blockly.Events.BlockCreate(block);
    checkCreateEventValues(event, block, ['1'], 'create');
  } finally {
    eventTest_tearDownWithMockBlocks();
  }
}

function test_delete_constructor() {
  eventTest_setUpWithMockBlocks();
  setUpMockMethod(mockControl_, Blockly.utils, 'genUid', null, ['1']);
  try {
    var block = createSimpleTestBlock(workspace);
    var event = new Blockly.Events.Delete(block);
    checkDeleteEventValues(event, block, ['1'], 'delete');
  } finally {
    eventTest_tearDownWithMockBlocks();
  }
}

function test_blockDelete_constructor() {
  eventTest_setUpWithMockBlocks();
  setUpMockMethod(mockControl_, Blockly.utils, 'genUid', null, ['1']);
  try {
    var block = createSimpleTestBlock(workspace);
    var event = new Blockly.Events.BlockDelete(block);
    checkDeleteEventValues(event, block, ['1'], 'delete');
  } finally {
    eventTest_tearDownWithMockBlocks();
  }
}

function test_change_constructor() {
  eventTest_setUpWithMockBlocks();
  setUpMockMethod(mockControl_, Blockly.utils, 'genUid', null, ['1']);
  try {
    Blockly.Events.disable();
    var block = new Blockly.Block(workspace, 'field_variable_test_block');
    Blockly.Events.enable();

    var event = new Blockly.Events.Change(block, 'field', 'VAR', 'id1', 'id2');
    checkExactEventValues(event, {'element': 'field', 'name': 'VAR',
      'oldValue': 'id1', 'newValue': 'id2', 'type': 'change'});
  } finally {
    eventTest_tearDownWithMockBlocks();
  }
}

function test_blockChange_constructor() {
  eventTest_setUpWithMockBlocks();
  setUpMockMethod(mockControl_, Blockly.utils, 'genUid', null, ['1']);
  try {
    Blockly.Events.disable();
    var block = new Blockly.Block(workspace, 'field_variable_test_block');
    Blockly.Events.enable();

    var event = new Blockly.Events.BlockChange(block, 'field', 'VAR', 'id1',
        'id2');
    checkExactEventValues(event, {'element': 'field', 'name': 'VAR',
      'oldValue': 'id1', 'newValue': 'id2', 'type': 'change'});
  } finally {
    eventTest_tearDownWithMockBlocks();
  }
}

function test_move_constructorCoordinate() {
  // Expect the oldCoordinate to be set.
  eventTest_setUpWithMockBlocks();
  setUpMockMethod(mockControl_, Blockly.utils, 'genUid', null, ['1', '2']);
  try {
    var block1 = createSimpleTestBlock(workspace);
    var coordinate = new goog.math.Coordinate(3,4);
    block1.xy_ = coordinate;

    var event = new Blockly.Events.Move(block1);
    // Need to check for individual equality of the coordinate values since
    // the move event creates a new goog.math.Coordinate object
    assertEquals(event.oldCoordinate.x, coordinate.x);
    assertEquals(event.oldCoordinate.y, coordinate.y);
    assertEquals(event.type, 'move');

  } finally {
    eventTest_tearDownWithMockBlocks();
  }
}

function test_move_constructoroldParentId() {
  // Expect the oldParentId to be set but not the oldCoordinate to be set.
  eventTest_setUpWithMockBlocks();
  setUpMockMethod(mockControl_, Blockly.utils, 'genUid', null, ['1', '2']);
  try {
    var block1 = createSimpleTestBlock(workspace);
    var block2 = createSimpleTestBlock(workspace);
    block1.parentBlock_ = block2;
    block1.xy_ = new goog.math.Coordinate(3,4);

    var event = new Blockly.Events.Move(block1);
    checkExactEventValues(event, {'oldCoordinate': undefined,
      'oldParentId': '2', 'type': 'move'});
    block1.parentBlock_ = null;
  } finally {
    eventTest_tearDownWithMockBlocks();
  }
}

function test_blockMove_constructorCoordinate() {
  // Expect the oldCoordinate to be set.
  eventTest_setUpWithMockBlocks();
  setUpMockMethod(mockControl_, Blockly.utils, 'genUid', null, ['1', '2']);
  try {
    var block1 = createSimpleTestBlock(workspace);
    var coordinate = new goog.math.Coordinate(3,4);
    block1.xy_ = coordinate;

    var event = new Blockly.Events.BlockMove(block1);
    // Need to check for individual equality of the coordinate values since
    // the move event creates a new goog.math.Coordinate object
    assertEquals(event.oldCoordinate.x, coordinate.x);
    assertEquals(event.oldCoordinate.y, coordinate.y);
    assertEquals(event.type, 'move');
  } finally {
    eventTest_tearDownWithMockBlocks();
  }
}

function test_blockMove_constructoroldParentId() {
  // Expect the oldParentId to be set but not the oldCoordinate to be set.
  eventTest_setUpWithMockBlocks();
  setUpMockMethod(mockControl_, Blockly.utils, 'genUid', null, ['1', '2']);
  try {
    var block1 = createSimpleTestBlock(workspace);
    var block2 = createSimpleTestBlock(workspace);
    block1.parentBlock_ = block2;
    block1.xy_ = new goog.math.Coordinate(3,4);

    var event = new Blockly.Events.BlockMove(block1);
    checkExactEventValues(event, {'oldCoordinate': undefined,
      'oldParentId': '2', 'type': 'move'});
    block1.parentBlock_ = null;
  } finally {
    eventTest_tearDownWithMockBlocks();
  }
}

function test_uiEvent_constructor_null() {
  try {
    Blockly.Events.setGroup('testGroup');
    var event = new Blockly.Events.Ui(null, 'foo', 'bar', 'baz');
    checkExactEventValues(event,
        {
          'blockId': null,
          'workspaceId': null,
          'type': 'ui',
          'oldValue': 'bar',
          'newValue': 'baz',
          'element': 'foo',
          'recordUndo': false,
          'group': 'testGroup'
        }
    );
  } finally {
    Blockly.Events.setGroup(false);
  }
}

function test_uiEvent_constructor_block() {
  eventTest_setUpWithMockBlocks();
  setUpMockMethod(mockControl_, Blockly.utils, 'genUid', null, ['1']);
  try {
    var block1 = createSimpleTestBlock(workspace);
    Blockly.Events.setGroup('testGroup');
    var event = new Blockly.Events.Ui(block1, 'foo', 'bar', 'baz');
    checkExactEventValues(event,
        {
          'blockId': '1',
          'workspaceId': workspace.id,
          'type': 'ui',
          'oldValue': 'bar',
          'newValue': 'baz',
          'element': 'foo',
          'recordUndo': false,
          'group': 'testGroup'
        }
    );
  } finally {
    Blockly.Events.setGroup(false);
    eventTest_tearDownWithMockBlocks();
  }
}

function test_varCreate_constructor() {
  eventTest_setUp();
  try {
    var variable = workspace.createVariable('name1', 'type1', 'id1');
    var event = new Blockly.Events.VarCreate(variable);
    checkExactEventValues(event, {'varName': 'name1', 'varType': 'type1',
      'type': 'var_create'});
  } finally {
    eventTest_tearDown();
  }
}

function test_varCreate_toJson() {
  eventTest_setUp();
  try {
    var variable = workspace.createVariable('name1', 'type1', 'id1');
    var event = new Blockly.Events.VarCreate(variable);
    var json = event.toJson();
    var expectedJson = ({type: "var_create", varId: "id1", varType: "type1",
      varName: "name1", isLocal: false, isCloud: false});

    assertEquals(JSON.stringify(expectedJson), JSON.stringify(json));

    var localVariable = workspace.createVariable('name2', 'type2', 'id2', true);
    var event2 = new Blockly.Events.VarCreate(localVariable);
    var json2 = event2.toJson();
    var expectedJson2 = ({type: "var_create", varId: "id2", varType: "type2",
      varName: "name2", isLocal: true, isCloud: false});

    assertEquals(JSON.stringify(expectedJson2), JSON.stringify(json2));

    var cloudVariable = workspace.createVariable('name3', 'type3', 'id3', false, true);
    var event3 = new Blockly.Events.VarCreate(cloudVariable);
    var json3 = event3.toJson();
    var expectedJson3 = ({type: "var_create", varId: "id3", varType: "type3",
      varName: "name3", isLocal: false, isCloud: true});

    assertEquals(JSON.stringify(expectedJson3), JSON.stringify(json3));

  } finally {
    eventTest_tearDown();
  }
}

function test_varCreate_fromJson() {
  eventTest_setUp();
  try {
    var variable = workspace.createVariable('name1', 'type1', 'id1');
    var event = new Blockly.Events.VarCreate(variable);
    var event2 = new Blockly.Events.VarCreate(null);
    var json = event.toJson();
    event2.fromJson(json);

    assertEquals(JSON.stringify(json), JSON.stringify(event2.toJson()));
  } finally {
    eventTest_tearDown();
  }
}

function test_varCreate_runForward() {
  eventTest_setUp();
  var json = {type: "var_create", varId: "id1", varType: "type1",
    varName: "name1"};
  var event = Blockly.Events.fromJson(json, workspace);
  assertNull(workspace.getVariableById('id1'));
  event.run(true);
  checkVariableValues(workspace, 'name1', 'type1', 'id1');
  eventTest_tearDown();
}

function test_varCreate_runBackwards() {
  eventTest_setUp();
  var variable = workspace.createVariable('name1', 'type1', 'id1');
  var event = new Blockly.Events.VarCreate(variable);
  assertNotNull(workspace.getVariableById('id1'));
  event.run(false);
  assertNull(workspace.getVariableById('id1'));
  eventTest_tearDown();
}

function test_varDelete_constructor() {
  eventTest_setUp();
  var variable = workspace.createVariable('name1', 'type1', 'id1');
  var event = new Blockly.Events.VarDelete(variable);
  checkExactEventValues(event, {'varName': 'name1', 'varType': 'type1',
    'varId':'id1', 'type': 'var_delete'});
  eventTest_tearDown();
}

function test_varDelete_toJson() {
  eventTest_setUp();
  var variable = workspace.createVariable('name1', 'type1', 'id1');
  var event = new Blockly.Events.VarDelete(variable);
  var json = event.toJson();
  var expectedJson = ({type: "var_delete", varId: "id1", varType: "type1",
    varName: "name1", isLocal: false, isCloud: false});

  assertEquals(JSON.stringify(expectedJson), JSON.stringify(json));

  var localVariable = workspace.createVariable('name2', 'type2', 'id2', true);
  var event2 = new Blockly.Events.VarDelete(localVariable);
  var json2 = event2.toJson();
  var expectedJson2 = ({type: "var_delete", varId: "id2", varType: "type2",
    varName: "name2", isLocal: true, isCloud: false});

  assertEquals(JSON.stringify(expectedJson2), JSON.stringify(json2));

  var cloudVariable = workspace.createVariable('name3', 'type3', 'id3', false, true);
  var event3 = new Blockly.Events.VarDelete(cloudVariable);
  var json3 = event3.toJson();
  var expectedJson3 = ({type: "var_delete", varId: "id3", varType: "type3",
    varName: "name3", isLocal: false, isCloud: true});

  assertEquals(JSON.stringify(expectedJson2), JSON.stringify(json2));
  eventTest_tearDown();
}

function test_varDelete_fromJson() {
  eventTest_setUp();
  var variable = workspace.createVariable('name1', 'type1', 'id1');
  var event = new Blockly.Events.VarDelete(variable);
  var event2 = new Blockly.Events.VarDelete(null);
  var json = event.toJson();
  event2.fromJson(json);

  assertEquals(JSON.stringify(json), JSON.stringify(event2.toJson()));
  eventTest_tearDown();
}

function test_varDelete_runForwards() {
  eventTest_setUp();
  var variable = workspace.createVariable('name1', 'type1', 'id1');
  var event = new Blockly.Events.VarDelete(variable);
  assertNotNull(workspace.getVariableById('id1'));
  event.run(true);
  assertNull(workspace.getVariableById('id1'));
  eventTest_tearDown();
}

function test_varDelete_runBackwards() {
  eventTest_setUp();
  var json = {type: "var_delete", varId: "id1", varType: "type1",
    varName: "name1"};
  var event = Blockly.Events.fromJson(json, workspace);
  assertNull(workspace.getVariableById('id1'));
  event.run(false);
  checkVariableValues(workspace, 'name1', 'type1', 'id1');
  eventTest_tearDown();
}

function test_varRename_constructor() {
  eventTest_setUp();
  var variable = workspace.createVariable('name1', 'type1', 'id1');
  var event = new Blockly.Events.VarRename(variable, 'name2');
  checkExactEventValues(event, {'varId': 'id1', 'oldName': 'name1',
    'newName': 'name2', 'type': 'var_rename'});
  eventTest_tearDown();
}

function test_varRename_toJson() {
  eventTest_setUp();
  var variable = workspace.createVariable('name1', 'type1', 'id1');
  var event = new Blockly.Events.VarRename(variable, 'name2');
  var json = event.toJson();
  var expectedJson = ({type: "var_rename", varId: "id1", oldName: "name1",
    newName: "name2"});

  assertEquals(JSON.stringify(expectedJson), JSON.stringify(json));
  eventTest_tearDown();
}

function test_varRename_fromJson() {
  eventTest_setUp();
  var variable = workspace.createVariable('name1', 'type1', 'id1');
  var event = new Blockly.Events.VarRename(variable, '');
  var event2 = new Blockly.Events.VarRename(null);
  var json = event.toJson();
  event2.fromJson(json);

  assertEquals(JSON.stringify(json), JSON.stringify(event2.toJson()));
  eventTest_tearDown();
}

function test_varRename_runForward() {
  eventTest_setUp();
  var variable = workspace.createVariable('name1', 'type1', 'id1');
  var event = new Blockly.Events.VarRename(variable, 'name2');
  event.run(true);
  assertNull(workspace.getVariable('name1'));
  checkVariableValues(workspace, 'name2', 'type1', 'id1');
  eventTest_tearDown();
}

function test_varBackard_runForward() {
  eventTest_setUp();
  var variable = workspace.createVariable('name1', 'type1', 'id1');
  var event = new Blockly.Events.VarRename(variable, 'name2');
  event.run(false);
  assertNull(workspace.getVariable('name2'));
  checkVariableValues(workspace, 'name1', 'type1', 'id1');
  eventTest_tearDown();
}

function test_events_filter() {
  eventTest_setUpWithMockBlocks();
  try {
    var block1 = workspace.newBlock('field_variable_test_block', '1');
    var events = [
      new Blockly.Events.BlockCreate(block1),
      new Blockly.Events.BlockMove(block1),
      new Blockly.Events.BlockChange(block1, 'field', 'VAR', 'id1', 'id2'),
      new Blockly.Events.Ui(block1, 'click')
    ];
    var filteredEvents = Blockly.Events.filter(events, true);
    assertEquals(4, filteredEvents.length);  // no event should have been removed.
    // test that the order hasn't changed
    assertTrue(filteredEvents[0] instanceof Blockly.Events.BlockCreate);
    assertTrue(filteredEvents[1] instanceof Blockly.Events.BlockMove);
    assertTrue(filteredEvents[2] instanceof Blockly.Events.BlockChange);
    assertTrue(filteredEvents[3] instanceof Blockly.Events.Ui);
  } finally {
    eventTest_tearDownWithMockBlocks();
  }
}

function test_events_filterForward() {
  eventTest_setUpWithMockBlocks();
  try {
    var block1 = workspace.newBlock('field_variable_test_block', '1');
    var events = [
      new Blockly.Events.BlockCreate(block1),
    ];
    helper_addMoveEvent(events, block1, 1, 1);
    helper_addMoveEvent(events, block1, 2, 2);
    helper_addMoveEvent(events, block1, 3, 3);
    var filteredEvents = Blockly.Events.filter(events, true);
    assertEquals(2, filteredEvents.length);  // duplicate moves should have been removed.
    // test that the order hasn't changed
    assertTrue(filteredEvents[0] instanceof Blockly.Events.BlockCreate);
    assertTrue(filteredEvents[1] instanceof Blockly.Events.BlockMove);
    assertEquals(3, filteredEvents[1].newCoordinate.x);
    assertEquals(3, filteredEvents[1].newCoordinate.y);
  } finally {
    eventTest_tearDownWithMockBlocks();
  }
}

function test_events_filterBackward() {
  eventTest_setUpWithMockBlocks();
  try {
    var block1 = workspace.newBlock('field_variable_test_block', '1');
    var events = [
      new Blockly.Events.BlockCreate(block1),
    ];
    helper_addMoveEvent(events, block1, 1, 1);
    helper_addMoveEvent(events, block1, 2, 2);
    helper_addMoveEvent(events, block1, 3, 3);
    var filteredEvents = Blockly.Events.filter(events, false);
    assertEquals(2, filteredEvents.length);  // duplicate event should have been removed.
    // test that the order hasn't changed
    assertTrue(filteredEvents[0] instanceof Blockly.Events.BlockCreate);
    assertTrue(filteredEvents[1] instanceof Blockly.Events.BlockMove);
    assertEquals(1, filteredEvents[1].newCoordinate.x);
    assertEquals(1, filteredEvents[1].newCoordinate.y);
  } finally {
    eventTest_tearDownWithMockBlocks();
  }
}

function test_events_filterDifferentBlocks() {
  eventTest_setUpWithMockBlocks();
  var block1 = workspace.newBlock('field_variable_test_block', '1');
  var block2 = workspace.newBlock('field_variable_test_block', '2');
  var events = [
    new Blockly.Events.BlockCreate(block1),
    new Blockly.Events.BlockMove(block1),
    new Blockly.Events.BlockCreate(block2),
    new Blockly.Events.BlockMove(block2)
  ];
  var filteredEvents = Blockly.Events.filter(events, true);
  assertEquals(4, filteredEvents.length);  // no event should have been removed.
  eventTest_tearDownWithMockBlocks();
}

function test_events_mergeMove() {
  eventTest_setUpWithMockBlocks();
  var block1 = workspace.newBlock('field_variable_test_block', '1');
  var events = [];
  helper_addMoveEvent(events, block1, 0, 0);
  helper_addMoveEvent(events, block1, 1, 1);
  var filteredEvents = Blockly.Events.filter(events, true);
  assertEquals(1, filteredEvents.length);  // second move event merged into first
  assertEquals(1, filteredEvents[0].newCoordinate.x);
  assertEquals(1, filteredEvents[0].newCoordinate.y);
  eventTest_tearDownWithMockBlocks();
}

function test_events_mergeChange() {
  eventTest_setUpWithMockBlocks();
  var block1 = workspace.newBlock('field_variable_test_block', '1');
  var events = [
    new Blockly.Events.Change(block1, 'field', 'VAR', 'item', 'item1'),
    new Blockly.Events.Change(block1, 'field', 'VAR', 'item1', 'item2')
  ];
  var filteredEvents = Blockly.Events.filter(events, true);
  assertEquals(1, filteredEvents.length);  // second change event merged into first
  assertEquals('item', filteredEvents[0].oldValue);
  assertEquals('item2', filteredEvents[0].newValue);
  eventTest_tearDownWithMockBlocks();
}

function test_events_mergeUi() {
  eventTest_setUpWithMockBlocks();
  var block1 = workspace.newBlock('field_variable_test_block', '1');
  var block2 = workspace.newBlock('field_variable_test_block', '2');
  var block3 = workspace.newBlock('field_variable_test_block', '3');
  var events = [
    new Blockly.Events.Ui(block1, 'commentOpen', 'false', 'true'),
    new Blockly.Events.Ui(block1, 'click', 'false', 'true'),
    new Blockly.Events.Ui(block2, 'mutatorOpen', 'false', 'true'),
    new Blockly.Events.Ui(block2, 'click', 'false', 'true'),
    new Blockly.Events.Ui(block3, 'warningOpen', 'false', 'true'),
    new Blockly.Events.Ui(block3, 'click', 'false', 'true')
  ];
  var filteredEvents = Blockly.Events.filter(events, true);
  assertEquals(3, filteredEvents.length);  // click event merged into corresponding *Open event
  assertEquals('commentOpen', filteredEvents[0].element);
  assertEquals('mutatorOpen', filteredEvents[1].element);
  assertEquals('warningOpen', filteredEvents[2].element);
  eventTest_tearDownWithMockBlocks();
}

/**
 * Tests that events that collide on a (event, block, workspace) tuple
 * but cannot be merged do not get dropped during filtering.
 */
function test_events_stackclick() {
  eventTest_setUpWithMockBlocks();
  var block = workspace.newBlock('field_variable_test_block', '1');
  var events = [
    new Blockly.Events.Ui(block, 'click', undefined, undefined),
    new Blockly.Events.Ui(block, 'stackclick', undefined, undefined)
  ];
  var filteredEvents = Blockly.Events.filter(events, true);
  // click and stackclick should both exist
  assertEquals(2, filteredEvents.length);
  assertEquals('click', filteredEvents[0].element);
  assertEquals('stackclick', filteredEvents[1].element);
  eventTest_tearDownWithMockBlocks();
}

/**
 * Mutator composition could result in move events for blocks
 * connected to the mutated block that were null operations. This
 * leads to events in the undo/redo queue that do nothing, requiring
 * an extra undo/redo to proceed to the next event. This test ensures
 * that two move events that do get merged (disconnecting and
 * reconnecting a block in response to a mutator change) are filtered
 * from the queue.
 */
function test_events_filteraftermerge() {
  eventTest_setUpWithMockBlocks();
  var block = workspace.newBlock('field_variable_test_block', '1');
  block.setParent(null);
  var events = [];
  helper_addMoveEventParent(events, block, null);
  helper_addMoveEventParent(events, block, null);
  var filteredEvents = Blockly.Events.filter(events, true);
  // The two events should be merged, but because nothing has changed
  // they will be filtered out.
  assertEquals(0, filteredEvents.length);
  eventTest_tearDownWithMockBlocks();
}

/**
 * Helper function to simulate block move events.
 *
 * @param {!Array.<Blockly.Events.Abstract>} events a queue of events.
 * @param {!Blockly.Block} block the block to be moved
 * @param {number} newX new X coordinate of the block
 * @param {number} newY new Y coordinate of the block
 */
function helper_addMoveEvent(events, block, newX, newY) {
  events.push(new Blockly.Events.BlockMove(block));
  block.xy_ = new goog.math.Coordinate(newX, newY);
  events[events.length-1].recordNew();
}

function helper_addMoveEventParent(events, block, parent) {
  events.push(new Blockly.Events.BlockMove(block));
  block.setParent(parent);
  events[events.length-1].recordNew();
}

function test_events_newblock_newvar() {
  eventTest_setUpWithMockBlocks();

  Blockly.Events.fire = temporary_fireEvent;
  temporary_fireEvent.firedEvents_ = [];
  // Expect three calls to genUid: one to set the block's ID, one for the event
  // group's id, and one for the variable's ID.
  setUpMockMethod(mockControl_, Blockly.utils, 'genUid', null, ['1', '2', '3']);
  try {
    var block = workspace.newBlock('field_variable_test_block');

    var firedEvents = workspace.undoStack_;
    // Expect two events: varCreate and block create.
    assertEquals(2, firedEvents.length);

    var event0 = firedEvents[0];
    var event1 = firedEvents[1];
    assertEquals('var_create', event0.type);
    assertEquals('create', event1.type);

    // Expect the events to have the same group ID.
    assertEquals(event0.group, event1.group);

    // Expect the group ID to be the result of the second call to genUid.
    assertEquals('2', event0.group);

    // Expect the workspace to have a variable with ID '3'.
    assertNotNull(workspace.getVariableById('3'));
    assertEquals('3', event0.varId);
  } finally {
    eventTest_tearDownWithMockBlocks();
    Blockly.Events.fire = savedFireFunc;
  }
}

// The sequence of events should be the same whether the block was created from
// XML or directly.
function test_events_newblock_newvar_xml() {
  eventTest_setUpWithMockBlocks();

  Blockly.Events.fire = temporary_fireEvent;
  temporary_fireEvent.firedEvents_ = [];
  try {
        var dom = Blockly.Xml.textToDom(
        '<xml>' +
        '  <block type="field_variable_test_block" id="block1">' +
        '    <field name="VAR" id="id1" variabletype="">name1</field>' +
        '  </block>' +
        '</xml>');
    Blockly.Xml.domToWorkspace(dom, workspace);

    var firedEvents = workspace.undoStack_;
    // Expect two events: varCreate and block create.
    // TODO: scratch-blocks also has move and delete events.
    // assertEquals(2, firedEvents.length);

    var event0 = firedEvents[0];
    var event1 = firedEvents[1];
    assertEquals('var_create', event0.type);
    assertEquals('create', event1.type);

    // Expect the events to have the same group ID.
    assertEquals(event0.group, event1.group);

    // Expect the workspace to have a variable with ID 'id1'.
    assertNotNull(workspace.getVariableById('id1'));
    assertEquals('id1', event0.varId);
  } finally {
    eventTest_tearDownWithMockBlocks();
    Blockly.Events.fire = savedFireFunc;
  }
}

function test_events_filter_nomerge_move() {
  // Move events should only merge if they refer to the same block and are
  // consecutive.
  // See github.com/google/blockly/pull/1892 for a worked example showing
  // how merging non-consecutive events can fail when replacing a shadow
  // block.
  eventTest_setUpWithMockBlocks();
  try {
    var block1 = createSimpleTestBlock(workspace);
    var block2 = createSimpleTestBlock(workspace);

    var events = [];
    helper_addMoveEvent(events, block1, 1, 1);
    helper_addMoveEvent(events, block2, 1, 1);
    events.push(new Blockly.Events.BlockDelete(block2));
    helper_addMoveEvent(events, block1, 2, 2);

    var filteredEvents = Blockly.Events.filter(events, true);
    // Nothing should have merged.
    assertEquals(4, filteredEvents.length);
    // test that the order hasn't changed
    assertTrue(filteredEvents[0] instanceof Blockly.Events.BlockMove);
    assertTrue(filteredEvents[1] instanceof Blockly.Events.BlockMove);
    assertTrue(filteredEvents[2] instanceof Blockly.Events.BlockDelete);
    assertTrue(filteredEvents[3] instanceof Blockly.Events.BlockMove);
  } finally {
    eventTest_tearDownWithMockBlocks();
  }
}