text: /* global define, QUnit, module, test, ok, equal, deepEqual, notEqual */
  define([
      "jquery",
      "libs/jquery/jstorage",
      "libs/bootstrap",
      "sinon-qunit",
      "mvc/workflow/workflow-view",
      "mvc/workflow/workflow-node",
      "mvc/workflow/workflow-view-node",
      "mvc/workflow/workflow-terminals",
      "mvc/workflow/workflow-view-terminals",
      "mvc/workflow/workflow-connector"
  ], function(
      $,
      jstorage,
      bootstrap,
      sinon,
      App,
      Node,
      NodeView,
      Terminals,
      TerminalsView,
      Connector
  ){
      "use strict";
      window.show_modal = function(a, b, c) {}
      window.hide_modal = function() {}
  
      // create body and app
      var create_app = function() {
          // build body
          $('body').append(   '<div id="canvas-viewport">' +
                                  '<div id="canvas-container"/>' +
                              '</div>' +
                              '<div id="overview">' +
                                  '<canvas id="overview-canvas"/>' +
                                  '<div id="overview-viewport"/>' +
                              '</div>');
  
          // build app
          return new App({
              id      : null,
              urls    : { get_datatypes : galaxy_config.root + 'api/datatypes/mapping' }
          });
      };
  
      // create server (fake)
      var create_server = function() {
          var server = sinon.fakeServer.create();
          server.respondWith('GET', galaxy_config.root + 'api/datatypes/mapping', [200, { 'Content-Type': 'application/json' },
                              '{"ext_to_class_name" : {"txt" : "Text", "data":"Data","tabular":"Tabular", "binary": "Binary", "bam": "Bam" }, "class_to_classes": { "Data": { "Data": true }, "Text": { "Text": true, "Data": true }, "Tabular": { "Tabular": true, "Text": true, "Data": true }, "Binary": { "Data": true, "Binary": true }, "Bam": { "Data": true, "Binary": true, "Bam": true }}}']);
          server.respondWith('GET', galaxy_config.root + 'api/datatypes', [200, { 'Content-Type': 'application/json' },
                              '["RData", "ab1", "affybatch", "txt"]']);
          return server;
      };
  
      module( "Input terminal model test", {
          setup: function( ) {
              this.server = create_server();
              this.node = new Node( create_app(), {  } );
              this.input = { extensions: [ "txt" ], multiple: false };
              this.input_terminal = new Terminals.InputTerminal( { input: this.input } );
              this.input_terminal.node = this.node;
          },
          teardown: function() {
              this.server.restore();
              delete this.server;
          },
          multiple: function( ) {
              this.input.multiple = true;
              this.input_terminal.update( this.input );
          },
          test_connector: function( ) {
              var outputTerminal = new Terminals.OutputTerminal( { datatypes: [ 'input' ] } );
              var inputTerminal = this.input_terminal;
              var connector = new Connector( outputTerminal, inputTerminal );
              return connector;
          },
          with_test_connector: function( f ) {
              this.test_connector( );
              f();
              this.reset_connectors();
          },
          reset_connectors: function( ) {
              this.input_terminal.connectors = [];
          },
          test_accept: function( other ) {
              other = other || { node: {}, datatypes: [ "txt" ] };
              if( ! other.mapOver ) {
                  other.mapOver = function() { return Terminals.NULL_COLLECTION_TYPE_DESCRIPTION; };
              }
              return this.input_terminal.canAccept( other );
          },
          pja_change_datatype_node: function( output_name, newtype ) {
              var pja = { action_type: "ChangeDatatypeAction", output_name: output_name, action_arguments: { newtype: newtype } };
              var otherNode = { post_job_actions: [ pja ] };
              return otherNode;
          }
      } );
  
      test( "test update", function() {
          deepEqual( this.input_terminal.datatypes, [ 'txt' ] );
          equal( this.input_terminal.multiple, false );
          this.input_terminal.update( { extensions: [ 'bam' ], multiple: true } );
          deepEqual( this.input_terminal.datatypes, [ 'bam' ] );
          equal( this.input_terminal.multiple, true );
      } );
  
      test( "test connect", function() {
          this.node.markChanged = sinon.spy();
          var connector = {};
          this.input_terminal.connect( connector );
          // Assert node markChanged called
          ok( this.node.markChanged.called );
          // Assert connectors updated
          ok( this.input_terminal.connectors[ 0 ] === connector );
      } );
  
      test( "test disconnect", function() {
          this.node.markChanged = sinon.spy();
          var connector = this.test_connector( );
          this.input_terminal.disconnect( connector );
          // Assert node markChanged called
          ok( this.node.markChanged.called );
          // Assert connectors updated
          equal( this.input_terminal.connectors.length, 0 );
      } );
  
      test( "test redraw", function() {
          var connector = this.test_connector(  );
          connector.redraw = sinon.spy();
          this.input_terminal.redraw();
          // Assert connectors were redrawn
          ok( connector.redraw.called );
      } );
  
      test( "test destroy", function() {
          var connector = this.test_connector();
          connector.destroy = sinon.spy();
          this.input_terminal.destroy();
          // Assert connectors were destroyed
          ok( connector.destroy.called );
      } );
  
      test( "can accept exact datatype", function() {
          var other = { node: {}, datatypes: [ "txt" ] }; // input also txt
          ok( this.test_accept( other ) );
      } );
  
      test( "can accept subclass datatype", function() {
          var other = { node: {}, datatypes: [ "tabular" ] }; // tabular subclass of input txt
          ok( this.test_accept( other ) ) ;
      } );
  
      test( "cannot accept incorrect datatype", function() {
          var other = { node: {}, datatypes: [ "binary" ] }; // binary is not txt
          ok( ! this.test_accept( other ) );
      } );
  
      test( "can accept incorrect datatype if converted with PJA", function() {
          var otherNode = this.pja_change_datatype_node( "out1", "txt" );
          var other = { node: otherNode, datatypes: [ "binary" ], name: "out1" }; // Was binary but converted to txt
          ok( this.test_accept( other ) );
      } );
  
      test( "cannot accept incorrect datatype if converted with PJA to incompatible type", function() {
          var otherNode = this.pja_change_datatype_node( "out1", "bam" ); // bam's are not txt
          var other = { node: otherNode, datatypes: [ "binary" ], name: "out1" };
          ok( ! this.test_accept( other ) );
      } );
  
      test( "cannot accept incorrect datatype if some other output converted with PJA to compatible type", function() {
          var otherNode = this.pja_change_datatype_node( "out2", "txt" );
          var other = { node: otherNode, datatypes: [ "binary" ], name: "out1" };
          ok( ! this.test_accept( other ) );
      } );
  
      test( "can accept inputs", function() {
          // Other is data input module - always accept (currently - could be
          // more intelligent by looking at what else input is connected to.
          var other = { node: {}, datatypes: [ "input" ] };
          ok( this.test_accept( other ) );
      } );
  
      test( "cannot accept when already connected", function() {
          var self = this;
          // If other is subtype but already connected, cannot accept
          this.with_test_connector( function() {
              ok( ! self.test_accept() );
          } );
      } );
  
      test( "can accept already connected inputs if input is multiple", function() {
          var self = this;
          this.multiple();
          this.with_test_connector( function() {
              ok( self.test_accept() );
          } );
      } );
  
      test( "cannot accept already connected inputs if input is multiple but datatypes don't match", function() {
          var other = { node: {}, datatypes: [ "binary" ] }; // binary is not txt
  
          var self = this;
          this.multiple();
          this.with_test_connector( function() {
              ok( ! self.test_accept( other ) );
          } );
      } );
  
      test( "can accept list collection for multiple input parameters if datatypes match", function() {
          var self = this;
          this.multiple();
          ok( self.test_accept() );
      } );
  
      test( "can accept list collection for empty multiple inputs", function() {
          var other = { node: {}, datatypes: [ "tabular" ], mapOver: function() { return new Terminals.CollectionTypeDescription( "list" ) } };
          var self = this;
          this.multiple();
          ok( self.test_accept( other ) );
      } );
  
      test( "cannot accept list collection for multiple input if collection already connected", function() {
          var other = { node: {}, datatypes: [ "tabular" ], mapOver: function() { return new Terminals.CollectionTypeDescription( "list" ) } };
          var self = this;
          this.multiple();
          this.with_test_connector( function() {
              ok( ! self.test_accept( other ) );
          } );
      } );
  
      module( "Connector test", {});
  
      test( "connects only if both valid handles", function() {
          var input = { connect: sinon.spy() };
          var output = { connect: sinon.spy() };
          new Connector( input, null );
          new Connector( null, output );
          // Not attempts to connect...
          ok( ! input.connect.called );
          ok( ! output.connect.called );
          new Connector( input, output );
          ok( input.connect.called );
          ok( output.connect.called );
      });
  
      test( "default attributes", function() {
          var input = { connect: sinon.spy() };
          var output = { connect: sinon.spy() };
          var connector = new Connector( input, output );
          equal( connector.dragging, false );
          equal( connector.canvas, null );
          equal( connector.inner_color, "#FFFFFF" );
          equal( connector.outer_color, "#D8B365" );
      } );
  
      test( "destroy", function() {
          var input = { connect: sinon.spy(), disconnect: sinon.spy() };
          var output = { connect: sinon.spy(), disconnect: sinon.spy() };
          var connector = new Connector( input, output );
          connector.destroy();
          ok( input.disconnect.called );
          ok( output.disconnect.called );
      } );
  
      test( "initial redraw", function() {
          var input = { connect: sinon.spy(), element: $("<div>"), isMappedOver: function() { return false; } };
          var output = { connect: sinon.spy(), element: $("<div>"), isMappedOver: function() { return false; } };
          var connector = new Connector( input, output );
          var n = $('#canvas-container').find('canvas').length;
          connector.redraw();
          // Ensure canvas gets set
          ok( connector.canvas );
          // Ensure it got added to canvas container
          equal (n + 1, $('#canvas-container').find('canvas').length);
      } );
  
      module( "Input collection terminal model test", {
          setup: function( ) {
              this.node = new Node(  create_app(), {  } );
              this.input = { extensions: [ "txt" ], collection_type: "list" };
              this.input_terminal = new Terminals.InputCollectionTerminal( { input: this.input } );
              this.input_terminal.node = this.node;
          }
      } );
  
      test( "Collection output can connect to same collection input type", function() {
          var self = this;
          var inputTerminal = self.input_terminal;
          var outputTerminal = new Terminals.OutputCollectionTerminal( {
              datatypes: 'txt',
              collection_type: 'list'
          } );
          outputTerminal.node = {};
          ok( this.input_terminal.canAccept( outputTerminal ) );
      } );
  
      test( "Collection output cannot connect to different collection input type", function() {
          var self = this;
          var inputTerminal = self.input_terminal;
          var outputTerminal = new Terminals.OutputCollectionTerminal( {
              datatypes: 'txt',
              collection_type: 'paired'
          } );
          outputTerminal.node = {};
          ok( ! this.input_terminal.canAccept( outputTerminal ) );
      } );
  
      module( "Node unit test", {
          setup: function() {
              this.input_terminal = { destroy: sinon.spy(), redraw: sinon.spy() };
              this.output_terminal = { destroy: sinon.spy(), redraw: sinon.spy() };
              this.element = $("<div><div class='toolFormBody'></div></div>");
              this.app = create_app();
              this.node = new Node( this.app, { element: this.element } );
              this.node.input_terminals.i1 = this.input_terminal;
              this.node.output_terminals.o1 = this.output_terminal;
          },
          $: function( selector ) {
              return $( this.node.element.find( selector ) );
          },
          expect_workflow_node_changed: function( f ) {
              var node = this.node;
              var node_changed_spy = sinon.spy( this.app.workflow, "node_changed" );
              f();
              ok( node_changed_spy.calledWith( node ) );
          },
          init_field_data_simple: function() {
              var data = {
                  data_inputs: [ {name: "input1", extensions: [ "data" ] } ],
                  data_outputs: [ {name: "output1", extensions: [ "data" ] } ],
              };
              this.node.init_field_data( data );
          },
          update_field_data_with_new_input: function() {
              var new_data = {
                  data_inputs: [
                      { name: "input1", extensions: [ "data" ] },
                      { name: "extra_0|input1", extensions: [ "data" ] },
                  ],
                  data_outputs: [ {name: "output1", extensions: [ "data" ] } ],
                  post_job_actions: "{}"
              };
              this.node.update_field_data( new_data );
          }
      } );
  
      test( "make active", function() {
          ok( ! this.element.hasClass( "toolForm-active" ) );
          this.node.make_active();
          ok( this.element.hasClass( "toolForm-active" ) );
      } );
  
      test( "destroy", function() {
          var remove_node_spy = sinon.spy( this.app.workflow, "remove_node" );
          this.node.destroy();
          ok( this.input_terminal.destroy.called );
          ok( this.output_terminal.destroy.called );
          ok( remove_node_spy.calledWith( this.node ) );
      } );
  
      test( "error", function() {
          // Test body of toolFormBody div updated and workflow notified of change.
          var test = this;
          this.expect_workflow_node_changed( function() {
              test.node.error( "TOOL ERROR" );
              equal( $( test.$(".toolFormBody").children()[ 0 ] ).html(), "TOOL ERROR" );
          } );
      } );
  
      test( "init_field_data properties", function() {
          var node = this.node;
          this.expect_workflow_node_changed( function( ) {
              var data = {
                  data_inputs: [],
                  data_outputs: [],
                  type: "tool",
                  name: "cat1",
                  form_html: "<form>",
                  tool_state: "ok",
                  tool_errors: false,
                  tooltip: "tool tooltip",
                  annotation: "tool annotation",
                  workflow_outputs: [ "out1" ],
              };
              node.init_field_data( data );
              equal( node.type, "tool" );
              equal( node.name, "cat1" );
              equal( node.form_html, "<form>" );
              equal( node.tool_state, "ok" );
              equal( node.tooltip, "tool tooltip" );
              equal( node.annotation, "tool annotation" );
              deepEqual( node.post_job_actions, {} );
              deepEqual( node.workflow_outputs, [ "out1" ] );
          } );
      } );
  
      test( "init_field_data data", function() {
          var test = this;
          this.expect_workflow_node_changed( function( ) {
              // pre-init not tool form body...
              equal( test.$( ".output-terminal" ).length, 0 );
              equal( test.$( ".input-terminal" ).length, 0 );
              equal( test.$( ".rule" ).length, 0 );
              test.init_field_data_simple();
              // After init tool form should have three "rows"/divs - , inputs div, one output, and rule...
              equal( test.$( ".output-terminal" ).length, 1 );
              equal( test.$( ".input-terminal" ).length, 1 );
              equal( test.$( ".rule" ).length, 1 );
              equal( test.$( ".toolFormBody" ).children().length, 3 );
          } );
      } );
  
      test( "update_field_data updated data inputs and outputs", function() {
          var test = this;
          this.expect_workflow_node_changed( function( ) {
              // Call init with one input and output.
              test.init_field_data_simple();
  
              test.update_field_data_with_new_input();
  
              // Now there are 2 inputs...
              equal( test.$( ".input-terminal" ).length, 2 );
              equal( test.$( ".output-terminal" ).length, 1 );
              equal( test.$( ".rule" ).length, 1 );
          } );
      } );
  
      test( "update_field_data preserves connectors", function() {
          var test = this;
          var node = this.node;
          this.expect_workflow_node_changed( function( ) {
              // Call init with one input and output.
              test.init_field_data_simple();
  
              var connector = new Connector();
              var old_input_terminal = node.input_terminals.input1;
              old_input_terminal.connectors.push( connector );
  
              // Update node, make sure connector still the same...
              test.update_field_data_with_new_input();
              var new_input_terminal = node.input_terminals.input1;
              equal( connector, new_input_terminal.connectors[ 0 ] );
  
              // Update a second time, make sure connector still the same...
              test.update_field_data_with_new_input();
              new_input_terminal = node.input_terminals.input1;
              equal( connector, new_input_terminal.connectors[ 0 ] );
          } );
      } );
  
      test( "update_field_data destroys old terminals", function() {
          var test = this;
          var node = this.node;
          this.expect_workflow_node_changed( function( ) {
              var data = {
                  data_inputs: [ { name: "input1", extensions: [ "data" ] },
                                 { name: "willDisappear", extensions: [ "data" ] } ],
                  data_outputs: [ {name: "output1", extensions: [ "data" ] } ],
              };
              node.init_field_data( data );
              var old_input_terminal = node.input_terminals.willDisappear;
              var destroy_spy = sinon.spy( old_input_terminal, "destroy" );
              // Update
              test.update_field_data_with_new_input();
              ok( destroy_spy.called );
          } );
      } );
  
      module( "create_node", {
          setup: function() {
              this.app = create_app();
          }
      });
  
      test( "node added to workflow", function() {
          var add_node_spy = sinon.spy( this.app.workflow, "add_node" );
          var node = this.app.workflow.create_node( "tool", "Cat Files", "cat1" );
          ok( add_node_spy.calledWith( node ) );
      } );
  
      // global NodeView
      module( "Node view ", {
         setup: function() {
              this.set_for_node( { input_terminals: {}, output_terminals: {}, markChanged: function() {}, terminalMapping: { disableMapOver: function() {} } } );
          },
          set_for_node: function( node ) {
              var element = $("<div><div class='toolFormBody'></div></div>");
              this.view = new NodeView( { node: node, el: element[ 0 ] } );
          },
          connectAttachedTerminal: function( inputType, outputType ) {
              this.view.addDataInput( { name: "TestName", extensions: [ inputType ] } );
              var terminal = this.view.node.input_terminals[ "TestName" ];
  
              var outputTerminal = new Terminals.OutputTerminal( { name: "TestOuptut", datatypes: [ outputType ] } );
              outputTerminal.node = { markChanged: function() {}, post_job_actions: [], hasMappedOverInputTerminals: function() { return false; }, hasConnectedOutputTerminals: function() { return true; } };
              outputTerminal.terminalMapping = { disableMapOver: function() {}, mapOver: Terminals.NULL_COLLECTION_TYPE_DESCRIPTION }; 
              var c = new Connector( outputTerminal, terminal );
  
              return c;
          },
          connectAttachedMultiInputTerminal: function( inputType, outputType ) {
              this.view.addDataInput( { name: "TestName", extensions: [ inputType ], multiple: true } );
              var terminal = this.view.node.input_terminals[ "TestName" ];
  
              var outputTerminal = new Terminals.OutputTerminal( { name: "TestOuptut", datatypes: [ "txt" ] } );
              outputTerminal.node = { markChanged: function() {}, post_job_actions: [], hasMappedOverInputTerminals: function() { return false; }, hasConnectedOutputTerminals: function() { return true; } };
              outputTerminal.terminalMapping = { disableMapOver: function() {}, mapOver: new Terminals.CollectionTypeDescription( "list" ) };
              var c = new Connector( outputTerminal, terminal );
  
              return c;
          },
          connectAttachedMappedOutput: function( ) {
              this.view.addDataInput( { name: "TestName", extensions: [ "txt" ], input_type: "dataset_collection" } );
              var terminal = this.view.node.input_terminals[ "TestName" ];
  
              var outputTerminal = new Terminals.OutputTerminal( { name: "TestOuptut", datatypes: [ "txt" ] } );
              outputTerminal.node = { markChanged: function() {}, post_job_actions: [], hasMappedOverInputTerminals: function() { return false; }, hasConnectedOutputTerminals: function() { return true; } };
              outputTerminal.terminalMapping = { disableMapOver: function() {}, mapOver: new Terminals.CollectionTypeDescription( "list" ) };
              var c = new Connector( outputTerminal, terminal );
  
              return c;
          }
      } );
  
      test( "tool error styling", function() {
          this.set_for_node( { tool_errors: false } );
          this.view.render();
          ok( ! this.view.$el.hasClass( "tool-node-error" ) );
          this.set_for_node( { tool_errors: true } );
          this.view.render();
          ok( this.view.$el.hasClass( "tool-node-error" ) );
      } );
  
      test( "rendering correct width", function() {
          // Default width is 150
          this.view.render();
          equal( this.view.$el.width(), 150 );
  
          // If any data rows are greater, it will update
          this.view.updateMaxWidth( 200 );
          this.view.render();
          equal( this.view.$el.width(), 200 );
  
          // However 250 is the maximum width of node
          this.view.updateMaxWidth( 300 );
          this.view.render();
          equal( this.view.$el.width(), 250 );
  
      } );
  
      test( "replacing terminal on data input update preserves connections", function() {
          var connector = this.connectAttachedTerminal( "txt", "txt" );
          var newElement = $("<div class='inputs'></div>");
          this.view.addDataInput( { name: "TestName", extensions: ["txt"] }, newElement );
          var terminal = newElement.find(".input-terminal")[ 0 ].terminal;
          ok( connector.handle2 === terminal );
      } );
  
      test( "replacing terminal on data multiple input update preserves collection connections", function() {
          var connector = this.connectAttachedMultiInputTerminal( "txt", "txt" );
          var connector_destroy_spy = sinon.spy( connector, "destroy" );
          var newElement = $("<div class='inputs'></div>");
          this.view.addDataInput( { name: "TestName", extensions: ["txt"], multiple: true }, newElement );
          ok( ! connector_destroy_spy.called );
      } );
  
      test( "replacing mapped terminal on data collection input update preserves connections", function() {
          var connector = this.connectAttachedMappedOutput();
          var newElement = $("<div class='inputs'></div>");
          this.view.addDataInput( { name: "TestName", extensions: ["txt"], input_type: "dataset_collection" }, newElement );
          var terminal = newElement.find(".input-terminal")[ 0 ].terminal;
          ok( connector.handle2 === terminal );
      } );
  
      test( "replacing terminal on data input destroys invalid connections", function() {
          var connector = this.connectAttachedTerminal( "txt", "txt" );
          var newElement = $("<div class='inputs'></div>");
          var connector_destroy_spy = sinon.spy( connector, "destroy" );
          // Replacing input with same name - but now of type bam should destroy connection.
          this.view.addDataInput( { name: "TestName", extensions: ["bam"] }, newElement );
          var terminal = newElement.find(".input-terminal")[ 0 ].terminal;
          ok( connector_destroy_spy.called );
      } );
  
      test( "replacing terminal on data input with collection changes mapping view type", function() {
          var connector = this.connectAttachedTerminal( "txt", "txt" );
          var newElement = $("<div class='inputs'></div>");
          var connector_destroy_spy = sinon.spy( connector, "destroy" );
          this.view.addDataInput( { name: "TestName", extensions: ["txt"], input_type: "dataset_collection" }, newElement );
          // Input type changed to dataset_collection - old connections are reset.
          // Would be nice to preserve these connections and make them map over.
          var terminal = newElement.find(".input-terminal")[ 0 ].terminal;
          ok( connector_destroy_spy.called );
      } );
  
      test( "replacing terminal on data collection input with simple input changes mapping view type", function() {
          var connector = this.connectAttachedMappedOutput();
          var newElement = $("<div class='inputs'></div>");
          var connector_destroy_spy = sinon.spy( connector, "destroy" );
          this.view.addDataInput( { name: "TestName", extensions: ["txt"], input_type: "dataset" }, newElement );
          var terminal = newElement.find(".input-terminal")[ 0 ].terminal;
          ok( connector_destroy_spy.called );
      } );
  
      // global InputTerminalView
      module( "Input terminal view", {
          setup: function() {
              this.node = { input_terminals: [] };
              this.input = { name: "i1", extensions: "txt", multiple: false };
              this.view = new TerminalsView.InputTerminalView( {
                  node: this.node,
                  input: this.input,
              });
          }
      } );
  
      test( "terminal added to node", function() {
          ok( this.node.input_terminals.i1 );
          equal( this.node.input_terminals.i1.datatypes, [ "txt" ] );
          equal( this.node.input_terminals.i1.multiple, false );
      } );
  
      test( "terminal element", function() {
          var el = this.view.el;
          equal( el.tagName, "DIV" );
          equal( el.className, "terminal input-terminal");
      } );
  
      // global OutputTerminalView
      module( "Output terminal view", {
          setup: function() {
              this.node = { output_terminals: [] };
              this.output = { name: "o1", extensions: "txt" };
              this.view = new TerminalsView.OutputTerminalView( {
                  node: this.node,
                  output: this.output,
              });
          }
      } );
  
      test( "terminal added to node", function() {
          ok( this.node.output_terminals.o1 );
          equal( this.node.output_terminals.o1.datatypes, [ "txt" ] );
      } );
  
      test( "terminal element", function() {
          var el = this.view.el;
          equal( el.tagName, "DIV" );
          equal( el.className, "terminal output-terminal");
      } );
  
      module( "CollectionTypeDescription", {
          listType: function() {
              return new Terminals.CollectionTypeDescription( "list" );
          },
          pairedType: function() {
              return new Terminals.CollectionTypeDescription( "paired" );
          },
          pairedListType: function() {
              return new Terminals.CollectionTypeDescription( "list:paired" );
          }
      } );
  
      test( "canMatch", function() {
          ok( this.listType().canMatch( this.listType() ) );
          ok( ! this.listType().canMatch( this.pairedType() ) );
          ok( ! this.listType().canMatch( this.pairedListType() ) );
      } );
  
      test( "canMatch special types", function() {
          ok( this.listType().canMatch( Terminals.ANY_COLLECTION_TYPE_DESCRIPTION ) );
          ok( Terminals.ANY_COLLECTION_TYPE_DESCRIPTION.canMatch( this.pairedListType() ) );
  
          ok( ! this.listType().canMatch( Terminals.NULL_COLLECTION_TYPE_DESCRIPTION ) );
          ok( ! Terminals.NULL_COLLECTION_TYPE_DESCRIPTION.canMatch( this.pairedListType() ) );
      } );
  
      test( "canMapOver", function() {
          ok( ! this.listType().canMapOver( this.listType() ) );
          ok( ! this.listType().canMapOver( this.pairedType() ) );
          ok( this.pairedListType().canMapOver( this.pairedType() ) );
          ok( ! this.listType().canMapOver( this.pairedListType() ) );
      } );
  
      test( "canMapOver special types", function() {
          ok( ! this.listType().canMapOver( Terminals.NULL_COLLECTION_TYPE_DESCRIPTION ) );
          ok( ! Terminals.NULL_COLLECTION_TYPE_DESCRIPTION.canMapOver( this.pairedListType() ) );
  
          // Following two should be able to be relaxed someday maybe - but the
          // tracking gets tricky I think. For now mapping only works for explicitly
          // defined collection types.
          ok( ! this.listType().canMapOver( Terminals.ANY_COLLECTION_TYPE_DESCRIPTION ) );
          ok( ! Terminals.ANY_COLLECTION_TYPE_DESCRIPTION.canMapOver( this.pairedListType() ) );
      } );
  
      test( "append", function( other ) {
          var appendedType = this.listType().append( this.pairedType() );
          equal( appendedType.collectionType, "list:paired" );
      } );
  
      test( "isCollection", function() {
          ok( this.listType().isCollection );
          ok( Terminals.ANY_COLLECTION_TYPE_DESCRIPTION.isCollection );
          ok( ! Terminals.NULL_COLLECTION_TYPE_DESCRIPTION.isCollection );
      } );
  
      test( "equal", function() {
          ok( ! this.listType().equal( this.pairedType() ) );
          ok( this.listType().equal( this.listType() ) );
  
          ok( Terminals.ANY_COLLECTION_TYPE_DESCRIPTION.equal( Terminals.ANY_COLLECTION_TYPE_DESCRIPTION ) );
          ok( ! Terminals.ANY_COLLECTION_TYPE_DESCRIPTION.equal( Terminals.NULL_COLLECTION_TYPE_DESCRIPTION ) );
          ok( ! Terminals.ANY_COLLECTION_TYPE_DESCRIPTION.equal( this.pairedType() ) );
          ok( ! this.pairedType().equal( Terminals.ANY_COLLECTION_TYPE_DESCRIPTION ) );
  
          ok( Terminals.NULL_COLLECTION_TYPE_DESCRIPTION.equal( Terminals.NULL_COLLECTION_TYPE_DESCRIPTION ) );
          ok( ! Terminals.NULL_COLLECTION_TYPE_DESCRIPTION.equal( Terminals.ANY_COLLECTION_TYPE_DESCRIPTION ) );
          ok( ! Terminals.NULL_COLLECTION_TYPE_DESCRIPTION.equal( this.listType() ) );
          ok( ! this.listType().equal( Terminals.NULL_COLLECTION_TYPE_DESCRIPTION ) );
  
      } );
  
      module( "TerminalMapping", {
      } );
  
      test( "default constructor", function() {
          var terminal = {};
          var mapping = new Terminals.TerminalMapping( { terminal: terminal } );
          ok( terminal.terminalMapping === mapping );
          ok( mapping.mapOver === Terminals.NULL_COLLECTION_TYPE_DESCRIPTION );
      } );
  
      test( "constructing with mapOver", function() {
          var terminal = {};
          var mapping = new Terminals.TerminalMapping( { terminal: terminal, mapOver: new Terminals.CollectionTypeDescription( "list" ) } );
          ok( mapping.mapOver.collectionType == "list" );
      } );
  
      test( "disableMapOver", function() {
          var terminal = {};
          var mapping = new Terminals.TerminalMapping( { terminal: terminal, mapOver: new Terminals.CollectionTypeDescription( "list" ) } );
          var changeSpy = sinon.spy();
          mapping.bind( "change", changeSpy );
          mapping.disableMapOver();
          ok( mapping.mapOver === Terminals.NULL_COLLECTION_TYPE_DESCRIPTION );
          ok( changeSpy.called );
      } );
  
      module( "terminal mapping logic", {
          newInputTerminal: function( mapOver, input, node ) {
              input = input || {};
              node = node || this.newNode();
              if( ! ( 'extensions' in input ) ) {
                  input[ 'extensions'] = [ 'data' ];
              }
              var inputEl = $("<div>")[ 0 ];
              var inputTerminal = new Terminals.InputTerminal( { element: inputEl, input: input } );
              var inputTerminalMapping = new Terminals.TerminalMapping( { terminal: inputTerminal } );
              inputTerminal.node = node;
              if( mapOver ) {
                  inputTerminal.setMapOver( new Terminals.CollectionTypeDescription( mapOver ) );
              }
              return inputTerminal;
          },
          newInputCollectionTerminal: function( input, node ) {
              input = input || {};
              node = node || this.newNode();
              if( ! ( 'extensions' in input ) ) {
                  input[ 'extensions'] = [ 'data' ];
              }
              var inputEl = $("<div>")[ 0 ];
              var inputTerminal = new Terminals.InputCollectionTerminal( { element: inputEl, input: input } );
              var inputTerminalMapping = new Terminals.TerminalMapping( { terminal: inputTerminal } );
              inputTerminal.node = node;
              return inputTerminal;
          },
          newOutputTerminal: function( mapOver, output, node ) {
              output = output || {};
              node = node || this.newNode();
              if( ! ( 'extensions' in output ) ) {
                  output[ 'extensions'] = [ 'data' ];
              }
              var outputEl = $("<div>")[ 0 ];
              var outputTerminal = new Terminals.OutputTerminal( { element: outputEl, datatypes: output.extensions } );
              var outputTerminalMapping = new Terminals.TerminalMapping( { terminal: outputTerminal } );
              outputTerminal.node = node;
              if( mapOver ) {
                  outputTerminal.setMapOver( new Terminals.CollectionTypeDescription( mapOver ) );
              }
              return outputTerminal;
          },
          newOutputCollectionTerminal: function( collectionType, output, node, mapOver ) {
              collectionType = collectionType || "list";
              output = output || {};
              node = node || this.newNode();
              if( ! ( 'extensions' in output ) ) {
                  output[ 'extensions'] = [ 'data' ];
              }
              var outputEl = $("<div>")[ 0 ];
              var outputTerminal = new Terminals.OutputCollectionTerminal( { element: outputEl, datatypes: output.extensions, collection_type: collectionType } );
              var outputTerminalMapping = new Terminals.TerminalMapping( { terminal: outputTerminal } );
              outputTerminal.node = node;
              if( mapOver ) {
                  outputTerminal.setMapOver( new Terminals.CollectionTypeDescription( mapOver ) );
              }
              return outputTerminal;
          },
          newNode: function( ) {
              var nodeEl = $("<div>")[ 0 ];
              var node = new Node( create_app(), { element: nodeEl } );
              return node;
          },
          _addExistingOutput: function( terminal, output, connected ) {
              var self = this;
              var node = terminal.node;
              if( connected ) {
                  var inputTerminal = self.newInputTerminal();
                  new Connector( inputTerminal, output );
              }
              this._addTerminalTo( output, node.output_terminals );
              return output;
          },
          addOutput: function( terminal, connected ) {
              var connectedOutput = this.newOutputTerminal();
              return this._addExistingOutput( terminal, connectedOutput, connected );
          },
          addCollectionOutput: function( terminal, connected ) {
              var collectionOutput = this.newOutputCollectionTerminal();
              return this._addExistingOutput( terminal, collectionOutput, connected );
          },
          addConnectedOutput: function( terminal ) {
              return this.addOutput( terminal, true );
          },
          addConnectedCollectionOutput: function( terminal ) {
              var connectedOutput = this.newOutputCollectionTerminal();
              return this._addExistingOutput( terminal, connectedOutput, true );
          },
          addConnectedInput: function( terminal ) {
              var self = this;
              var connectedInput = this.newInputTerminal();
              var node = terminal.node;
              var outputTerminal = self.newOutputTerminal();
              new Connector( connectedInput, outputTerminal );
              this._addTerminalTo( connectedInput, node.input_terminals );
              return connectedInput;
          },
          _addTerminalTo: function( terminal, terminals ) {
              var name = "other";
              while( name in terminals ) {
                  name += "_";
              }
              terminals[ name ] = terminal;
          },
          verifyNotAttachable: function( inputTerminal, output ) {
              var outputTerminal;
              var outputTerminal;
              if( typeof( output ) == "string" ) {
                  // Just given a collection type... create terminal out of it.
                  outputTerminal = this.newOutputTerminal( output );
              } else {
                  outputTerminal = output;
              }
  
              ok( ! inputTerminal.attachable( outputTerminal ) );
          },
          verifyAttachable: function( inputTerminal, output ) {
              var outputTerminal;
              if( typeof( output ) == "string" ) {
                  // Just given a collection type... create terminal out of it.
                  outputTerminal = this.newOutputTerminal( output );
              } else {
                  outputTerminal = output;
              }
  
              ok( inputTerminal.attachable( outputTerminal ) );
              
              // Go further... make sure datatypes are being enforced
              inputTerminal.datatypes = [ "bam" ];
              outputTerminal.datatypes = [ "txt" ];
              ok( ! inputTerminal.attachable( outputTerminal ) );
          },
          verifyMappedOver: function( terminal ) {
              ok( terminal.terminalMapping.mapOver.isCollection );
          },
          verifyNotMappedOver: function( terminal ) {
              ok( ! terminal.terminalMapping.mapOver.isCollection );
          },
      } );
  
      test( "unconstrained input can be mapped over", function() {
          var inputTerminal1 = this.newInputTerminal();
          this.verifyAttachable( inputTerminal1, "list" );
      } );
  
      test( "unmapped input can be mapped over if matching connected input terminals map type", function() {
          var inputTerminal1 = this.newInputTerminal();
          var connectedInput1 = this.addConnectedInput( inputTerminal1 );
          var connectedInput2 = this.addConnectedInput( inputTerminal1 );
          connectedInput2.setMapOver( new Terminals.CollectionTypeDescription( "list") );
          this.verifyAttachable( inputTerminal1, "list" );
      } );
  
      test( "unmapped input cannot be mapped over if not matching connected input terminals map type", function() {
          var inputTerminal1 = this.newInputTerminal();
          var connectedInput = this.addConnectedInput( inputTerminal1 );
          connectedInput.setMapOver( new Terminals.CollectionTypeDescription( "paired" ) );
          this.verifyNotAttachable( inputTerminal1, "list" );
      } );
  
      test( "unmapped input can be attached to by output collection if matching connected input terminals map type", function() {
          var inputTerminal1 = this.newInputTerminal();
          var connectedInput1 = this.addConnectedInput( inputTerminal1 );
          var connectedInput2 = this.addConnectedInput( inputTerminal1 );
          connectedInput2.setMapOver( new Terminals.CollectionTypeDescription( "list") );
          var outputTerminal = this.newOutputCollectionTerminal( "list" );
          this.verifyAttachable( inputTerminal1, outputTerminal );
      } );
  
      test( "unmapped input cannot be attached to by output collection if matching connected input terminals don't match map type", function() {
          var inputTerminal1 = this.newInputTerminal();
          var connectedInput1 = this.addConnectedInput( inputTerminal1 );
          var connectedInput2 = this.addConnectedInput( inputTerminal1 );
          connectedInput2.setMapOver( new Terminals.CollectionTypeDescription( "list") );
          var outputTerminal = this.newOutputCollectionTerminal( "paired" );
          this.verifyNotAttachable( inputTerminal1, outputTerminal );
      } );
  
      test( "unmapped input can be attached to by output collection if effective output type (output+mapover) is same as mapped over input", function() {
          var inputTerminal1 = this.newInputTerminal();
          var connectedInput1 = this.addConnectedInput( inputTerminal1 );
          var connectedInput2 = this.addConnectedInput( inputTerminal1 );
          connectedInput2.setMapOver( new Terminals.CollectionTypeDescription( "list:paired") );
          var outputTerminal = this.newOutputCollectionTerminal( "paired" );
          outputTerminal.setMapOver( new Terminals.CollectionTypeDescription( "list" ) );
          this.verifyAttachable( inputTerminal1, outputTerminal );
      } );
  
      test( "unmapped input cannot be attached to by output collection if effective output type (output+mapover) is not same as mapped over input (1)", function() {
          var inputTerminal1 = this.newInputTerminal();
          var connectedInput1 = this.addConnectedInput( inputTerminal1 );
          var connectedInput2 = this.addConnectedInput( inputTerminal1 );
          connectedInput2.setMapOver( new Terminals.CollectionTypeDescription( "list:paired") );
          var outputTerminal = this.newOutputCollectionTerminal( "list" );
          outputTerminal.setMapOver( new Terminals.CollectionTypeDescription( "list" ) );
          this.verifyNotAttachable( inputTerminal1, outputTerminal );
      } );
  
      test( "unmapped input cannot be attached to by output collection if effective output type (output+mapover) is not same as mapped over input (2)", function() {
          var inputTerminal1 = this.newInputTerminal();
          var connectedInput1 = this.addConnectedInput( inputTerminal1 );
          var connectedInput2 = this.addConnectedInput( inputTerminal1 );
          connectedInput2.setMapOver( new Terminals.CollectionTypeDescription( "list:paired") );
          var outputTerminal = this.newOutputCollectionTerminal( "list" );
          outputTerminal.setMapOver( new Terminals.CollectionTypeDescription( "paired" ) );
          this.verifyNotAttachable( inputTerminal1, outputTerminal );
      } );
  
      test( "unmapped input with unmapped, connected outputs cannot be mapped over", function() {
          // It would invalidate the connections - someday maybe we could try to
          // recursively map over everything down the DAG - it would be expensive
          // to check that though.
          var inputTerminal1 = this.newInputTerminal();
          this.addConnectedOutput( inputTerminal1 );
          this.verifyNotAttachable( inputTerminal1, "list" );
      } );
  
      test( "unmapped input with connected mapped outputs can be mapped over if matching", function() {
          // It would invalidate the connections - someday maybe we could try to
          // recursively map over everything down the DAG - it would be expensive
          // to check that though.
          var inputTerminal1 = this.newInputTerminal();
          var connectedOutput = this.addConnectedOutput( inputTerminal1 );
          connectedOutput.setMapOver( new Terminals.CollectionTypeDescription( "list" ) );
          this.verifyAttachable( inputTerminal1, "list" );
      } );
  
      test( "unmapped input with connected mapped outputs cannot be mapped over if mapover not matching", function() {
          // It would invalidate the connections - someday maybe we could try to
          // recursively map over everything down the DAG - it would be expensive
          // to check that though.
          var inputTerminal1 = this.newInputTerminal();
          var connectedOutput = this.addConnectedOutput( inputTerminal1 );
          connectedOutput.setMapOver( new Terminals.CollectionTypeDescription( "paired" ) );
          this.verifyNotAttachable( inputTerminal1, "list" );
      } );
  
      test( "explicitly constrained input can not be mapped over by incompatible collection type", function() {
          var inputTerminal1 = this.newInputTerminal();
          inputTerminal1.setMapOver( new Terminals.CollectionTypeDescription( "paired" ) );
          this.verifyNotAttachable( inputTerminal1, "list" );
      } );
  
      test( "explicitly constrained input can be mapped over by compatible collection type", function() {
          var inputTerminal1 = this.newInputTerminal();
          inputTerminal1.setMapOver( new Terminals.CollectionTypeDescription( "list" ) );
          this.verifyAttachable( inputTerminal1, "list" );
      } );
  
      test( "unconstrained collection input can be mapped over", function() {
          var inputTerminal1 = this.newInputCollectionTerminal( { collection_type: "paired" } );
          this.verifyAttachable( inputTerminal1, "list:paired" );
      } );
  
      test( "unconstrained collection input cannot be mapped over by incompatible type", function() {
          var inputTerminal1 = this.newInputCollectionTerminal( { collection_type: "list" } ); // Would need to be paired...
          this.verifyNotAttachable( inputTerminal1, "list:paired" );
      } );
  
      test( "explicitly mapped over collection input can be attached by explicit mapping", function() {
          var inputTerminal1 = this.newInputCollectionTerminal( { collection_type: "paired" } );
          inputTerminal1.setMapOver( new Terminals.CollectionTypeDescription( "list" ) );
          this.verifyAttachable( inputTerminal1, "list:paired" );
      } );
  
      test( "explicitly mapped over collection input can be attached by explicit mapping", function() {
          var inputTerminal1 = this.newInputCollectionTerminal( { collection_type: "list:paired" } );
          inputTerminal1.setMapOver( new Terminals.CollectionTypeDescription( "list" ) );
          // effectively input is list:list:paired so shouldn't be able to attach
          this.verifyNotAttachable( inputTerminal1, "list:paired" );
      } );
  
      test( "unconnected multiple inputs can be connected to rank 1 collections", function() {
          var inputTerminal1 = this.newInputTerminal( null, { multiple: true } );
          this.verifyAttachable( inputTerminal1, "list" );
      } );
  
      test( "multiple input attachable by collections", function() {
          var inputTerminal1 = this.newInputTerminal( null, { multiple: true } );
          var connectedInput1 = this.addConnectedInput( inputTerminal1 );
          this.addConnectedOutput( connectedInput1 );
          this.verifyAttachable( inputTerminal1, "list" );
      } );
  
      test( "unconnected multiple inputs cannot be connected to rank > 1 collections (yet...)", function() {
          var inputTerminal1 = this.newInputTerminal( null, { multiple: true } );
          this.verifyNotAttachable( inputTerminal1, "list:paired" );
      } );
  
      test( "resetMappingIfNeeded does nothing if not mapped", function() {
          var inputTerminal1 = this.newInputTerminal();
          inputTerminal1.resetMappingIfNeeded();
          this.verifyNotMappedOver( inputTerminal1 );
      } );
  
      test( "resetMappingIfNeeded resets unconstrained input", function() {
          var inputTerminal1 = this.newInputTerminal( "list" );
          this.verifyMappedOver( inputTerminal1 );
          inputTerminal1.resetMappingIfNeeded();
          this.verifyNotMappedOver( inputTerminal1 );
      } );
  
      test( "resetMappingIfNeeded does not reset if connected output depends on being mapped", function() {
          var inputTerminal1 = this.newInputTerminal( "list" );
          var connectedOutput = this.addConnectedOutput( inputTerminal1 );
          connectedOutput.setMapOver( new Terminals.CollectionTypeDescription( "list" ) );
          inputTerminal1.resetMappingIfNeeded();
          this.verifyMappedOver( inputTerminal1 );
      } );
  
      test( "resetMappingIfNeeded resets if node outputs are not connected to anything", function() {
          var inputTerminal1 = this.newInputTerminal( "list" );
          var output = this.addOutput( inputTerminal1 );
          output.setMapOver( new Terminals.CollectionTypeDescription( "list" ) );
          inputTerminal1.resetMappingIfNeeded();
          this.verifyNotMappedOver( inputTerminal1 );
      } );
  
      test( "resetMappingIfNeeded an input resets node outputs if they not connected to anything", function() {
          var inputTerminal1 = this.newInputTerminal( "list" );
          var output = this.addOutput( inputTerminal1 );
          output.setMapOver( new Terminals.CollectionTypeDescription( "list" ) );
          inputTerminal1.resetMappingIfNeeded();
          this.verifyNotMappedOver( output );
      } );
  
      test( "resetMappingIfNeeded an input resets node collection outputs if they not connected to anything", function() {
          var inputTerminal1 = this.newInputTerminal( "list" );
          var output = this.addCollectionOutput( inputTerminal1 );
          output.setMapOver( new Terminals.CollectionTypeDescription( "list" ) );
          inputTerminal1.resetMappingIfNeeded();
          this.verifyNotMappedOver( output );
      } );
  
      test( "resetMappingIfNeeded resets if not last mapped over input", function() {
          // Idea here is that other nodes are forcing output to still be mapped
          // over so don't need to disconnect output nodes.
          var inputTerminal1 = this.newInputTerminal( "list" );
          var connectedInput1 = this.addConnectedInput( inputTerminal1 );
          connectedInput1.setMapOver( new Terminals.CollectionTypeDescription( "list" ) );
          var connectedOutput = this.addConnectedOutput( inputTerminal1 );
          connectedOutput.setMapOver( new Terminals.CollectionTypeDescription( "list" ) );
          inputTerminal1.resetMappingIfNeeded();
          // inputTerminal1 can be reset because connectedInput1
          // is still forcing connectedOutput to be mapped over,
          // so verify inputTerminal1 is rest and connectedInput1
          // and connectedOutput are untouched.
          this.verifyNotMappedOver( inputTerminal1 );
          this.verifyMappedOver( connectedInput1 );
          this.verifyMappedOver( connectedOutput );
      } );
  
      test( "simple mapping over collection outputs works correctly", function() {
          var inputTerminal1 = this.newInputTerminal();
          var connectedOutput = this.addConnectedCollectionOutput( inputTerminal1 );
          inputTerminal1.setMapOver( new Terminals.CollectionTypeDescription( "list" ) );
  
          // Can attach list output of collection type list that is being mapped
          // over another list to a list:list (because this is what it is) but not
          // to a list:list:list.
          var testTerminal2 = this.newInputTerminal( "list:list" );
          this.verifyAttachable( testTerminal2, connectedOutput );
  
          var testTerminal1 = this.newInputTerminal( "list:list:list" );
          this.verifyNotAttachable( testTerminal1, connectedOutput );
      } );
  });