Game = require '../../src/models/game'
Cycle = require '../../src/models/cycle'

describe 'Game', ->
  beforeEach -> @game = new Game(numberOfPlayers: 2)

  describe 'construction', ->
    context 'by default', ->
      beforeEach -> @game = new Game()

      it 'generates a random name', ->
        expect(@game.name).to.exist
        expect(@game.name).to.not.eq((new Game).name)

      it 'has an empty array of cycles', ->
        expect(@game.cycles).to.be.empty

      it 'has a waiting state', ->
        expect(@game.state).to.eq(Game.STATES.WAITING)

      it 'provides an initial count of 3', ->
        expect(@game.count).to.eq(3)

      it 'has 1 player practice mode', ->
        expect(@game.numberOfPlayers).to.eq(1)

      it 'has a grid width of 80', ->
        expect(@game.width).to.eq(80)

      it 'has a grid height of 22', ->
        expect(@game.height).to.eq(22)

    context 'given a name', ->
      beforeEach ->
        @name = 'my-game'
        @game = new Game(name: @name)

      it 'provides a game with that name', ->
        expect(@game.name).to.eq(@name)

    context 'given a number of players', ->
      beforeEach ->
        @numberOfPlayers = 3
        attributes = { name: 'new name', numberOfPlayers: @numberOfPlayers }
        @game = new Game(attributes)

      it 'allows that many players', ->
        expect(@game.numberOfPlayers).to.eq(@numberOfPlayers)

    context 'given a grid width', ->
      beforeEach ->
        @width = 75
        attributes = { name: 'new name', width: @width }
        @game = new Game(attributes)

      it 'sets that grid width', ->
        expect(@game.width).to.eq(@width)

    context 'given a grid height', ->
      beforeEach ->
        @height = 75
        attributes = { name: 'new name', height: @height }
        @game = new Game(attributes)

      it 'sets that grid height', ->
        expect(@game.height).to.eq(@height)

  describe '#addCycle', ->
    beforeEach ->
      @game = new Game(numberOfPlayers: 2)

    context 'when the game is not waiting for players', ->
      beforeEach ->
        @game.state = Game.STATES.STARTED
        @addedCycle = @game.addCycle()

      it 'returns nothing', ->
        expect(@addedCycle).to.not.be.ok

    context 'when the game is waiting', ->
      beforeEach ->
        @game.state = Game.STATES.WAITING
        @addedCycle = @game.addCycle()

      it 'returns a new cycle', ->
        expect(@addedCycle).to.be.ok
        expect(@addedCycle).to.be.instanceOf(Cycle)

      it 'adds the cycle to list of cycles', ->
        expect(@game.cycles[0]).to.be.eq(@addedCycle)

  describe '#removeCycle', ->
    context 'given a cycle', ->
      beforeEach ->
        @game.state = Game.STATES.STARTED
        @winnerCheck = sinon.stub(@game, 'checkForWinner')
        @firstCycle = sinon.createStubInstance(Cycle)
        @secondCycle = sinon.createStubInstance(Cycle)
        @game.cycles = [ @firstCycle, @secondCycle ]
        @removedCycle = @game.removeCycle(@firstCycle)

      it 'removes the cycle from the list', ->
        expect(@game.cycles.length).to.eq(1)
        expect(@game.cycles[0]).to.not.eq(@firstCycle)

      it 'preserves the other cycle in the list', ->
        expect(@game.cycles[0]).to.eq(@secondCycle)

      it 'checks for the winner', ->
        expect(@winnerCheck).to.have.been.called

  describe '#checkForWinner', ->
    beforeEach ->
      @stop = sinon.stub @game, 'finishGame'

    context 'given more than 1 active cycle', ->
      beforeEach ->
        @game.cycles = [
          { state: Cycle.STATES.EXPLODING }
          { state: Cycle.STATES.RACING }
          { state: Cycle.STATES.DEAD }
          { state: Cycle.STATES.RACING }
        ]
        @game.checkForWinner()

      it 'does not stop the game', ->
        expect(@stop).to.not.have.been.called

    context 'given 1 or less living cycles', ->
      beforeEach ->
        @game.cycles = [
          { state: Cycle.STATES.DEAD }
          { state: Cycle.STATES.RACING }
          { state: Cycle.STATES.DEAD }
          { state: Cycle.STATES.DEAD }
        ]
        @game.checkForWinner()

      it 'stops the game', ->
        expect(@stop).to.have.been.called

  describe '#start', ->
    context 'when called', ->
      beforeEach ->
        @gameLoop = sinon.stub(@game, 'loop')
        @clock = sinon.useFakeTimers()
        @game.start()

      afterEach ->
        @clock.restore()

      it 'initiates a game loop to trigger every 100 milliseconds', ->
        @clock.tick(301)
        expect(@gameLoop.callCount).to.eq(4)

  describe '#loop', ->
    beforeEach ->
      @emit = sinon.stub(@game, 'emit')

    context 'when the game has not started', ->
      beforeEach ->
        @game.loop()

      it 'emits a game event', ->
        expect(@emit).to.have.been.calledWith('game', @game)

    context 'when the game is waiting', ->
      context 'and when the set number of players is not reached', ->
        beforeEach -> @game.loop()

        it 'stays in a waiting state', ->
          expect(@game.state).to.eq(Game.STATES.WAITING)

      context 'and when the set number of ready players is reached', ->
        beforeEach ->
          @game.cycles = [
            { ready: true }
            { ready: true }
          ]
          @game.loop()

        it 'moves into the countdown state', ->
          expect(@game.state).to.eq(Game.STATES.COUNTDOWN)

    context 'when the game has started', ->
      beforeEach ->
        @game.state = Game.STATES.STARTED
        @game.cycles = (sinon.createStubInstance(Cycle) for i in [1..4])
        @checkWinner = sinon.stub(@game, 'checkForWinner')
        @game.loop()

      it 'moves the cycles', ->
        expect(cycle.step).to.have.been.called for cycle in @game.cycles

      it 'checks for cycle collisions', ->
        expect(cycle.checkCollisions).to.have.been.calledWith(@game.cycles) for cycle in @game.cycles

      it 'checks for the winner', ->
        expect(@checkWinner).to.have.been.calledOnce

      it 'emits a game event', ->
        expect(@emit).to.have.been.calledWith('game', @game)

  describe '#countdown', ->
    context 'given a count greater than 1', ->
      beforeEach ->
        expect(@game.count).to.eq(3)

      context 'and after called 20 times', ->
        beforeEach ->
          @game.countdown() for i in [1..20]

        it 'decreases the count by 1', ->
          expect(@game.count).to.eq(2)

    context 'given a count equal to 1', ->
      beforeEach ->
        @game.count = 1

      context 'and after called 20 times', ->
        beforeEach ->
          @game.countdown() for i in [1..21]

        it 'changes the game state to started', ->
          expect(@game.state).to.eq(Game.STATES.STARTED)

  describe '#finishGame', ->
    context 'when called', ->
      beforeEach ->
        sinon.stub(@game, 'determineWinner')
        @game.finishGame()

      it 'changes the game state to finished', ->
        expect(@game.state).to.eq(Game.STATES.FINISHED)

      it 'determines the winner', ->
        expect(@game.determineWinner).to.have.been.calledOnce

  describe '#stop', ->
    context 'when called', ->
      beforeEach ->
        sinon.stub(@game, 'emit')
        @clock = sinon.useFakeTimers()

        @fake = sinon.stub()
        @game.gameLoop = setInterval @fake, 100
        @clock.tick(101)
        expect(@fake).to.have.been.calledOnce

        @game.stop()

      afterEach ->
        @clock.restore()

      it 'clears the gameLoop interval', ->
        @clock.tick(201)
        expect(@fake).to.not.have.been.calledTwice

      it 'emits a stopped event', ->
        expect(@game.emit).to.have.been.calledWith('stopped', @game)

  describe '#determineWinner', ->
    context 'given a single cycle is racing', ->
      beforeEach ->
        @expectedWinnerCycle = sinon.createStubInstance(Cycle)
        @expectedWinnerCycle.state = Cycle.STATES.RACING
        @expectedLoserCycles = (sinon.createStubInstance(Cycle) for i in [1..3])
        for cycle in @expectedLoserCycles
          cycle.state = Cycle.STATES.DEAD
        @game.cycles = [
          @expectedLoserCycles[0]
          @expectedLoserCycles[1]
          @expectedWinnerCycle
          @expectedLoserCycles[2]
        ]
        @game.determineWinner()

      it 'makes the racing cycle the winner', ->
        expect(@expectedWinnerCycle.makeWinner).to.have.been.called

      it 'makes the non-racing cycles the losers', ->
        expect(cycle.makeWinner).to.not.have.been.called for cycle in @expectedLoserCycles

  describe '#toJSON', ->
    context 'when called', ->
      beforeEach ->
        @game.cycles = for i in [1..3]
          cycle = sinon.createStubInstance(Cycle)
          cycle.toJSON.returns { foo: i }
          cycle
        @json = @game.toJSON()

      it 'returns the JSON with the game properties', ->
        expect(@json[property]).to.eq(@game[property]) for property in [
          'name', 'state', 'count', 'numberOfPlayers', 'width', 'height'
        ]

      it 'returns the JSON for each cycle in the game', ->
        expect(@json['cycles']).to.have.members (cycle.toJSON() for cycle in @game.cycles)

  describe '#inProgress', ->
    context 'when the game is not in the waiting state', ->
      it 'returns true', ->
        nonWaitingStates = [
          Game.STATES.COUNTDOWN,
          Game.STATES.STATES,
          Game.STATES.FINISHED
        ]
        for state in nonWaitingStates
          @game.state = state
          expect(@game.inProgress).to.be.true

    context 'when the game is in the waiting state', ->
      it 'returns false', ->
        @game.state = Game.STATES.WAITING
        expect(@game.inProgress).to.be.false

  describe 'isPractice', ->
    context 'when a game has only 1 player', ->
      beforeEach -> @game = new Game(numberOfPlayers: 1)

      it 'is considered a practice game', ->
        expect(@game.isPractice).to.be.true

    context 'when a game has more than 1 player', ->
      beforeEach -> @game = new Game(numberOfPlayers: 3)

      it 'is not considered a practice game', ->
        expect(@game.isPractice).to.be.false
