if typeof process isnt 'undefined' and process.execPath and process.execPath.match /node|iojs/
  chai = require 'chai' unless chai
  noflo = require '../src/lib/NoFlo.coffee'
  path = require 'path'
  root = path.resolve __dirname, '../'
else
  noflo = require 'noflo'
  root = 'noflo'

describe 'NoFlo Network', ->
  Split = ->
    new noflo.Component
      inPorts:
        in: datatype: 'all'
      outPorts:
        out: datatype: 'all'
      process: (input, output) ->
        output.sendDone
          out: input.get 'in'
  Merge = ->
    new noflo.Component
      inPorts:
        in: datatype: 'all'
      outPorts:
        out: datatype: 'all'
      process: (input, output) ->
        output.sendDone
          out: input.get 'in'
  Callback = ->
    new noflo.Component
      inPorts:
        in: datatype: 'all'
        callback:
          datatype: 'all'
          control: true
      process: (input, output) ->
        # Drop brackets
        return unless input.hasData 'callback', 'in'
        cb = input.getData 'callback'
        data = input.getData 'in'
        cb data
        output.done()

  describe 'with an empty graph', ->
    g = null
    n = null
    before (done) ->
      g = new noflo.Graph
      g.baseDir = root
      n = new noflo.Network g
      n.connect done
    it 'should initially be marked as stopped', ->
      chai.expect(n.isStarted()).to.equal false
    it 'should initially have no processes', ->
      chai.expect(n.processes).to.be.empty
    it 'should initially have to connections', ->
      chai.expect(n.connections).to.be.empty
    it 'should initially have no IIPs', ->
      chai.expect(n.initials).to.be.empty
    it 'should have reference to the graph', ->
      chai.expect(n.graph).to.equal g
    it 'should know its baseDir', ->
      chai.expect(n.baseDir).to.equal g.baseDir
    it 'should have a ComponentLoader', ->
      chai.expect(n.loader).to.be.an 'object'
    it 'should have transmitted the baseDir to the Component Loader', ->
      chai.expect(n.loader.baseDir).to.equal g.baseDir
    it 'should be able to list components', (done) ->
      @timeout 60 * 1000
      n.loader.listComponents (err, components) ->
        return done err if err
        chai.expect(components).to.be.an 'object'
        done()
      return
    it 'should have an uptime', ->
      chai.expect(n.uptime()).to.be.at.least 0

    describe 'with new node', ->
      it 'should contain the node', (done) ->
        g.once 'addNode', ->
          setTimeout ->
            chai.expect(n.processes).not.to.be.empty
            chai.expect(n.processes.Graph).to.exist
            done()
          , 10
        g.addNode 'Graph', 'Graph',
          foo: 'Bar'
      it 'should have transmitted the node metadata to the process', ->
        chai.expect(n.processes.Graph.component.metadata).to.exist
        chai.expect(n.processes.Graph.component.metadata).to.be.an.object
        chai.expect(n.processes.Graph.component.metadata).to.eql g.getNode('Graph').metadata
      it 'should not contain the node after removal', (done) ->
        g.once 'removeNode', ->
          setTimeout ->
            chai.expect(n.processes).to.be.empty
            done()
          , 10
        g.removeNode 'Graph'

  describe 'with a simple graph', ->
    g = null
    n = null
    cb = null
    before (done) ->
      @timeout 60 * 1000
      g = new noflo.Graph
      g.baseDir = root
      g.addNode 'Merge', 'Merge'
      g.addNode 'Callback', 'Callback'
      g.addEdge 'Merge', 'out', 'Callback', 'in'
      g.addInitial (data) ->
        chai.expect(data).to.equal 'Foo'
        cb()
      , 'Callback', 'callback'
      g.addInitial 'Foo', 'Merge', 'in'
      noflo.createNetwork g, (err, nw) ->
        return done err if err
        nw.loader.components.Split = Split
        nw.loader.components.Merge = Merge
        nw.loader.components.Callback = Callback
        n = nw
        nw.connect (err) ->
          return done err if err
          done()
      , true

    it 'should send some initials when started', (done) ->
      chai.expect(n.initials).not.to.be.empty
      cb = done
      n.start (err) ->
        return done err if err

    it 'should contain two processes', ->
      chai.expect(n.processes).to.not.be.empty
      chai.expect(n.processes.Merge).to.exist
      chai.expect(n.processes.Merge).to.be.an 'Object'
      chai.expect(n.processes.Callback).to.exist
      chai.expect(n.processes.Callback).to.be.an 'Object'
    it 'the ports of the processes should know the node names', ->
      for name, port of n.processes.Callback.component.inPorts.ports
        chai.expect(port.name).to.equal name
        chai.expect(port.node).to.equal 'Callback'
        chai.expect(port.getId()).to.equal "Callback #{name.toUpperCase()}"
      for name, port of n.processes.Callback.component.outPorts.ports
        chai.expect(port.name).to.equal name
        chai.expect(port.node).to.equal 'Callback'
        chai.expect(port.getId()).to.equal "Callback #{name.toUpperCase()}"

    it 'should contain 1 connection between processes and 2 for IIPs', ->
      chai.expect(n.connections).to.not.be.empty
      chai.expect(n.connections.length).to.equal 3

    it 'should have started in debug mode', ->
      chai.expect(n.debug).to.equal true

    it 'should emit a process-error when a component throws', (done) ->
      g.removeInitial 'Callback', 'callback'
      g.removeInitial 'Merge', 'in'
      g.addInitial (data) ->
        throw new Error 'got Foo'
      , 'Callback', 'callback'
      g.addInitial 'Foo', 'Merge', 'in'
      n.once 'process-error', (err) ->
        chai.expect(err).to.be.an 'object'
        chai.expect(err.error.message).to.equal 'got Foo'
        done()
      n.sendInitials()

    describe 'once started', ->
      it 'should be marked as started', ->
        chai.expect(n.isStarted()).to.equal true

    describe 'with a renamed node', ->
      it 'should have the process in a new location', (done) ->
        g.once 'renameNode', ->
          chai.expect(n.processes.Func).to.be.an 'object'
          done()
        g.renameNode 'Callback', 'Func'
      it 'shouldn\'t have the process in the old location', ->
        chai.expect(n.processes.Callback).to.be.undefined
      it 'should have informed the ports of their new node name', ->
        for name, port of n.processes.Func.component.inPorts.ports
          chai.expect(port.name).to.equal name
          chai.expect(port.node).to.equal 'Func'
          chai.expect(port.getId()).to.equal "Func #{name.toUpperCase()}"
        for name, port of n.processes.Func.component.outPorts.ports
          chai.expect(port.name).to.equal name
          chai.expect(port.node).to.equal 'Func'
          chai.expect(port.getId()).to.equal "Func #{name.toUpperCase()}"

    describe 'with process icon change', ->
      it 'should emit an icon event', (done) ->
        n.once 'icon', (data) ->
          chai.expect(data).to.be.an 'object'
          chai.expect(data.id).to.equal 'Func'
          chai.expect(data.icon).to.equal 'flask'
          done()
        n.processes.Func.component.setIcon 'flask'

    describe 'once stopped', ->
      it 'should be marked as stopped', (done) ->
        n.stop ->
          chai.expect(n.isStarted()).to.equal false
          done()

  describe 'with nodes containing default ports', ->
    g = null
    testCallback = null
    c = null
    cb = null

    beforeEach ->
      testCallback = null
      c = null
      cb = null

      c = new noflo.Component
      c.inPorts.add 'in',
        required: true
        datatype: 'string'
        default: 'default-value',
        (e, data) =>
          if e is 'data'
            c.outPorts.out.send data
            c.outPorts.out.disconnect()
      c.outPorts.add 'out'

      cb = new noflo.Component
      cb.inPorts.add 'in',
        required: true
        datatype: 'all'
        (e, data) =>
          if e is 'data'
            testCallback(data)

      g = new noflo.Graph
      g.baseDir = root
      g.addNode 'Def', 'Def'
      g.addNode 'Cb', 'Cb'
      g.addEdge 'Def', 'out', 'Cb', 'in'

    it 'should send default values to nodes without an edge', (done) ->
      @timeout 60 * 1000
      testCallback = (data) ->
        chai.expect(data).to.equal 'default-value'
        done()
      noflo.createNetwork g, (err, nw) ->
        return done err if err
        nw.loader.components.Def = -> c
        nw.loader.components.Cb = -> cb
        nw.connect (err) ->
          return done err if err
          nw.start (err) ->
            return done err if err
      , true

    it 'should not send default values to nodes with an edge', (done) ->
      @timeout 60 * 1000
      testCallback = (data) ->
        chai.expect(data).to.equal 'from-edge'
        done()
      g.addNode 'Merge', 'Merge'
      g.addEdge 'Merge', 'out', 'Def', 'in'
      g.addInitial 'from-edge', 'Merge', 'in'
      noflo.createNetwork g, (err, nw) ->
        return done err if err
        nw.loader.components.Def = -> c
        nw.loader.components.Cb = -> cb
        nw.loader.components.Merge = Merge
        nw.connect (err) ->
          return done err if err
          nw.start (err) ->
            return done err if err
      , true

    it 'should not send default values to nodes with IIP', (done) ->
      @timeout 60 * 1000
      testCallback = (data) ->
        chai.expect(data).to.equal 'from-IIP'
        done()
      g.addInitial 'from-IIP', 'Def', 'in'
      noflo.createNetwork g, (err, nw) ->
        return done err if err
        nw.loader.components.Def = -> c
        nw.loader.components.Cb = -> cb
        nw.loader.components.Merge = Merge
        nw.connect (err) ->
          return done err if err
          nw.start (err) ->
            return done err if err
      , true

  describe "Nodes are added first, then edges, then initializers (i.e. IIPs), and in order of definition order within each", ->
    g = null
    n = null
    stubbed = {}
    actual = []
    expected = []

    # Poor man's way of stubbing the Network. Investigate using
    # [sinon-chai](https://github.com/domenic/sinon-chai) when we need stubbing
    # for other parts of testing as well.
    stub = ->
      stubbed.addNode = noflo.Network::addNode
      stubbed.addEdge = noflo.Network::addEdge
      stubbed.addInitial = noflo.Network::addInitial

      # Record the node/edge/initial and pass it along
      noflo.Network::addNode = (node, cb) ->
        actual.push node
        stubbed.addNode.call this, node, cb
      noflo.Network::addEdge = (edge, cb) ->
        actual.push edge
        stubbed.addEdge.call this, edge, cb
      noflo.Network::addInitial = (initial, cb) ->
        actual.push initial
        stubbed.addInitial.call this, initial, cb

    # Clean up after ourselves
    restore = ->
      noflo.Network::addNode = stubbed.addNode
      noflo.Network::addEdge = stubbed.addEdge
      noflo.Network::addInitial = stubbed.addInitial

    before (done) ->
      stub()
      @timeout 6000

      g = new noflo.Graph
      g.baseDir = root
      # Save the nodes/edges/initial for order testing later. The index numbers
      # are the expected positions.
      expected[0] = g.addNode "D", "Callback"
      expected[10] = g.addInitial (->), "D", "callback"
      expected[1] = g.addNode "A", "Split"
      expected[11] = g.addInitial "Hello", "A", "in"
      expected[2] = g.addNode "B1", "Merge"
      expected[3] = g.addNode "B2", "Merge"
      expected[5] = g.addEdge "A", "out", "B1", "in"
      expected[6] = g.addEdge "A", "out", "B2", "in"
      expected[4] = g.addNode "C", "Merge"
      expected[7] = g.addEdge "B1", "out", "C", "in"
      expected[12] = g.addInitial "World", "C", "in"
      expected[8] = g.addEdge "B2", "out", "C", "in"
      expected[9] = g.addEdge "C", "out", "D", "in"
      noflo.createNetwork g, (err, nw) ->
        return done err if err
        nw.loader.components.Split = Split
        nw.loader.components.Merge = Merge
        nw.loader.components.Callback = Callback
        n = nw
        nw.connect (err) ->
          return done err if err
          nw.start done
      , true

    after restore

    it "should add nodes, edges, and initials, in that order", ->
      chai.expect(actual).to.deep.equal expected

  describe 'with an existing IIP', ->
    g = null
    n = null
    before ->
      g = new noflo.Graph
      g.baseDir = root
      g.addNode 'Callback', 'Callback'
      g.addNode 'Repeat', 'Split'
      g.addEdge 'Repeat', 'out', 'Callback', 'in'
    it 'should call the Callback with the original IIP value', (done) ->
      @timeout 6000
      cb = (packet) ->
        chai.expect(packet).to.equal 'Foo'
        done()
      g.addInitial cb, 'Callback', 'callback'
      g.addInitial 'Foo', 'Repeat', 'in'
      setTimeout ->
        noflo.createNetwork g, (err, nw) ->
          return done err if err
          nw.loader.components.Split = Split
          nw.loader.components.Merge = Merge
          nw.loader.components.Callback = Callback
          n = nw
          nw.connect (err) ->
            return done err if err
            nw.start (err) ->
              return done err if err
        , true
      , 10
    it 'should allow removing the IIPs', (done) ->
      @timeout 6000
      removed = 0
      onRemove = ->
        removed++
        return if removed < 2
        chai.expect(n.initials.length).to.equal 0, 'No IIPs left'
        chai.expect(n.connections.length).to.equal 1, 'Only one connection'
        g.removeListener 'removeInitial', onRemove
        done()
      g.on 'removeInitial', onRemove
      g.removeInitial 'Callback', 'callback'
      g.removeInitial 'Repeat', 'in'
    it 'new IIPs to replace original ones should work correctly', (done) ->
      cb = (packet) ->
        chai.expect(packet).to.equal 'Baz'
        done()
      g.addInitial cb, 'Callback', 'callback'
      g.addInitial 'Baz', 'Repeat', 'in'
      n.start (err) ->
        return done err if err

    describe 'on stopping', ->
      it 'processes should be running before the stop call', ->
        chai.expect(n.started).to.be.true
        chai.expect(n.processes.Repeat.component.started).to.equal true
      it 'should emit the end event', (done) ->
        @timeout 5000
        # Ensure we have a connection open
        n.once 'end', (endTimes) ->
          chai.expect(endTimes).to.be.an 'object'
          done()
        n.stop (err) ->
          return done err if err
      it 'should have called the shutdown method of each process', ->
        chai.expect(n.processes.Repeat.component.started).to.equal false

  describe 'with a very large network', ->
    it 'should be able to connect without errors', (done) ->
      @timeout 100000
      g = new noflo.Graph
      g.baseDir = root
      called = 0
      for n in [0..10000]
        g.addNode "Repeat#{n}", 'Split'
      g.addNode 'Callback', 'Callback'
      for n in [0..10000]
        g.addEdge "Repeat#{n}", 'out', 'Callback', 'in'
      g.addInitial ->
        called++
      , 'Callback', 'callback'
      for n in [0..10000]
        g.addInitial n, "Repeat#{n}", 'in'

      nw = new noflo.Network g
      nw.loader.listComponents (err) ->
        return done err if err
        nw.loader.components.Split = Split
        nw.loader.components.Callback = Callback
        nw.once 'end', ->
          chai.expect(called).to.equal 10001
          done()
        nw.connect (err) ->
          return done err if err
          nw.start (err) ->
            return done err if err
      return
