require '../helpers'

sinon = require('sinon')

{Attachment} = require('../../server/bw/attachment')
{Battle} = require('../../server/bw/battle')
{Weather} = require('../../shared/weather')
{Conditions} = require '../../shared/conditions'
{Factory} = require('../factory')
{BattleServer} = require('../../server/server')
{Protocol} = require '../../shared/protocol'
shared = require('../shared')
should = require 'should'
{_} = require 'underscore'
ratings = require('../../server/ratings')

describe 'Battle', ->
  beforeEach ->
    @server = new BattleServer()
    shared.create.call this,
      format: 'xy1000'
      team1: [Factory('Hitmonchan'), Factory('Heracross')]
      team2: [Factory('Hitmonchan'), Factory('Heracross')]

  it 'starts at turn 1', ->
    @battle.turn.should.equal 1

  describe '#hasWeather(weatherName)', ->
    it 'returns true if the current battle weather is weatherName', ->
      @battle.weather = "Sunny"
      @battle.hasWeather("Sunny").should.be.true

    it 'returns false on non-None in presence of a weather-cancel ability', ->
      @battle.weather = "Sunny"
      @sandbox.stub(@battle, 'hasWeatherCancelAbilityOnField', -> true)
      @battle.hasWeather("Sunny").should.be.false

    it 'returns true on None in presence of a weather-cancel ability', ->
      @battle.weather = "Sunny"
      @sandbox.stub(@battle, 'hasWeatherCancelAbilityOnField', -> true)
      @battle.hasWeather("None").should.be.true

  describe '#recordMove', ->
    it "records a player's move", ->
      @battle.recordMove(@id1, @battle.getMove('Tackle'))
      action = _(@battle.pokemonActions).find((a) => a.pokemon == @p1)
      should.exist(action)
      action.should.have.property("move")
      action.move.should.equal @battle.getMove('Tackle')

    it "does not record a move if player has already made an action", ->
      length = @battle.pokemonActions.length
      @battle.recordMove(@id1, @battle.getMove('Tackle'))
      @battle.recordMove(@id1, @battle.getMove('Tackle'))
      @battle.pokemonActions.length.should.equal(1 + length)

  describe '#undoCompletedRequest', ->
    it "fails if the player didn't make any action", ->
      @battle.undoCompletedRequest(@id1).should.be.false

    it "fails on the second turn as well if the player didn't make any action", ->
      @controller.makeMove(@id1, "Mach Punch")
      @controller.makeMove(@id2, "Mach Punch")
      @battle.turn.should.equal 2
      @battle.undoCompletedRequest(@id1).should.be.false

    it "succeeds if the player selected an action already", ->
      @battle.recordMove(@id1, @battle.getMove('Tackle'))
      @battle.pokemonActions.should.not.be.empty
      @battle.undoCompletedRequest(@id1).should.be.true
      @battle.pokemonActions.should.be.empty

    it "can cancel an action multiple times", ->
      for i in [0..5]
        @battle.recordMove(@id1, @battle.getMove('Tackle'))
        @battle.pokemonActions.should.not.be.empty
        @battle.undoCompletedRequest(@id1).should.be.true
        @battle.pokemonActions.should.be.empty

  describe '#recordSwitch', ->
    it "records a player's switch", ->
      @battle.recordSwitch(@id1, 1)
      action = _(@battle.pokemonActions).find((a) => a.pokemon == @p1)
      should.exist(action)
      action.should.have.property("to")
      action.to.should.equal 1

    it "does not record a switch if player has already made an action", ->
      length = @battle.pokemonActions.length
      @battle.recordSwitch(@id1, 1)
      @battle.recordSwitch(@id1, 1)
      @battle.pokemonActions.length.should.equal(1 + length)

  describe '#performSwitch', ->
    it "swaps pokemon positions of a player's team", ->
      [poke1, poke2] = @team1.pokemon
      @battle.performSwitch(@p1, 1)
      @team1.pokemon.slice(0, 2).should.eql [poke2, poke1]

    it "calls the pokemon's switchOut() method", ->
      pokemon = @p1
      mock = @sandbox.mock(pokemon)
      mock.expects('switchOut').once()
      @battle.performSwitch(@p1, 1)
      mock.verify()

  describe "#setWeather", ->
    it "can last a set number of turns", ->
      @battle.setWeather(Weather.SUN, 5)
      for i in [0...5]
        @battle.endTurn()
      @battle.weather.should.equal Weather.NONE

  describe "weather", ->
    it "damages pokemon who are not of a certain type", ->
      @battle.setWeather(Weather.SAND)
      @battle.endTurn()
      maxHP = @p1.stat('hp')
      (maxHP - @p1.currentHP).should.equal Math.floor(maxHP / 16)
      (maxHP - @p2.currentHP).should.equal Math.floor(maxHP / 16)

      @battle.setWeather(Weather.HAIL)
      @battle.endTurn()
      maxHP = @p1.stat('hp')
      (maxHP - @p1.currentHP).should.equal 2*Math.floor(maxHP / 16)
      (maxHP - @p2.currentHP).should.equal 2*Math.floor(maxHP / 16)

  describe "move PP", ->
    it "goes down after a pokemon uses a move", ->
      pokemon = @p1
      move = @p1.moves[0]
      @battle.performMove(@p1, move)
      @p1.pp(move).should.equal(@p1.maxPP(move) - 1)

  describe "#performMove", ->
    it "records this move as the battle's last move", ->
      move = @p1.moves[0]
      @battle.performMove(@p1, move)

      should.exist @battle.lastMove
      @battle.lastMove.should.equal move

  describe "#bump", ->
    it "bumps a pokemon to the front of its priority bracket", ->
      @battle.recordMove(@id1, @battle.getMove('Tackle'))
      @battle.recordMove(@id2, @battle.getMove('Splash'))
      @battle.determineTurnOrder()

      # Get last pokemon to move and bump it up
      {pokemon} = @battle.pokemonActions[@battle.pokemonActions.length - 1]
      @battle.bump(pokemon)
      @battle.pokemonActions[0].pokemon.should.eql pokemon

    it "bumps a pokemon to the front of a specific priority bracket", ->
      @battle.recordMove(@id1, @battle.getMove('Tackle'))
      @battle.recordMove(@id2, @battle.getMove('Mach Punch'))
      queue = @battle.determineTurnOrder()

      @battle.bump(@p1, @battle.getMove('Mach Punch').priority)
      queue[0].pokemon.should.eql @p1

    it "still works even if there's nothing in that bracket", ->
      @battle.recordMove(@id1, @battle.getMove('Tackle'))
      @battle.recordMove(@id2, @battle.getMove('Mach Punch'))
      queue = @battle.determineTurnOrder()

      queue.should.have.length(2)
      @battle.bump(@p1)
      queue.should.have.length(2)
      queue[0].pokemon.should.eql @p2
      queue[1].pokemon.should.eql @p1

    it "is a no-op if no actions", ->
      queue = @battle.determineTurnOrder()

      queue.should.have.length(0)
      @battle.bump(@p1)
      queue.should.have.length(0)

  describe "#delay", ->
    it "delays a pokemon to the end of its priority bracket", ->
      @battle.recordMove(@id1, @battle.getMove('Tackle'))
      @battle.recordMove(@id2, @battle.getMove('Splash'))
      @battle.determineTurnOrder()

      # Get first pokemon to move and delay it
      {pokemon} = @battle.pokemonActions[0]
      @battle.delay(pokemon)
      @battle.pokemonActions[1].pokemon.should.eql pokemon

    it "delays a pokemon to the end of a specific priority bracket", ->
      @battle.recordMove(@id1, @battle.getMove('Mach Punch'))
      @battle.recordMove(@id2, @battle.getMove('Tackle'))
      queue = @battle.determineTurnOrder()

      @battle.delay(@p1, @battle.getMove('Tackle').priority)
      queue[1].pokemon.should.eql @p1

    it "still works even if there's nothing in that bracket", ->
      @battle.recordMove(@id1, @battle.getMove('Tackle'))
      @battle.recordMove(@id2, @battle.getMove('Mach Punch'))
      queue = @battle.determineTurnOrder()

      queue.should.have.length(2)
      @battle.delay(@p1)
      queue.should.have.length(2)
      queue[0].pokemon.should.eql @p2
      queue[1].pokemon.should.eql @p1

    it "is a no-op if no actions", ->
      queue = @battle.determineTurnOrder()

      queue.should.have.length(0)
      @battle.delay(@p1)
      queue.should.have.length(0)

  describe "#weatherUpkeep", ->
    it "does not damage Pokemon if a weather-cancel ability is on the field", ->
      @battle.setWeather(Weather.HAIL)
      @sandbox.stub(@battle, 'hasWeatherCancelAbilityOnField', -> true)
      @battle.endTurn()
      @p1.currentHP.should.not.be.lessThan @p1.stat('hp')
      @p2.currentHP.should.not.be.lessThan @p2.stat('hp')

    it "does not damage a Pokemon who is immune to a weather", ->
      @battle.setWeather(Weather.HAIL)
      @sandbox.stub(@p2, 'isWeatherDamageImmune', -> true)
      @battle.endTurn()
      @p1.currentHP.should.be.lessThan @p1.stat('hp')
      @p2.currentHP.should.not.be.lessThan @p2.stat('hp')

  describe "#add", ->
    it "adds the spectator to an internal array", ->
      connection = @stubSpark()
      spectator = @server.findOrCreateUser(id: 1, name: "derp", connection)
      length = @battle.sparks.length
      @battle.add(connection)
      @battle.sparks.should.have.length(length + 1)
      @battle.sparks.should.containEql(connection)

    it "gives the spectator battle information", ->
      connection = @stubSpark()
      spectator = @server.findOrCreateUser(id: 1, name: "derp", connection)
      spy = @sandbox.spy(connection, 'send')
      @battle.add(connection)
      {id, numActive, log} = @battle
      spy.calledWithMatch("spectateBattle", id, sinon.match.string, numActive, null, @battle.playerNames, log).should.be.true

    it "receives the correct set of initial teams", ->
      connection = @stubSpark()
      spectator = @server.findOrCreateUser(id: 1, name: "derp", connection)
      spy = @sandbox.spy(connection, 'send')
      teams = @battle.getTeams().map((team) -> team.toJSON(hidden: true))
      @team1.switch(@p1, 1)

      @battle.add(connection)
      {id, numActive, log} = @battle
      spy.calledWithMatch("spectateBattle", id, sinon.match.string, numActive, null, @battle.playerNames, log).should.be.true

    it "does not add a spectator twice", ->
      connection = @stubSpark()
      spectator = @server.findOrCreateUser(id: 1, name: "derp", connection)
      length = @battle.sparks.length
      @battle.add(connection)
      @battle.add(connection)
      @battle.sparks.should.have.length(length + 1)

  describe "#getWinner", ->
    it "returns player 1 if player 2's team has all fainted", ->
      pokemon.faint()  for pokemon in @team2.pokemon
      @battle.getWinner().should.equal(@id1)

    it "returns player 2 if player 1's team has all fainted", ->
      pokemon.faint()  for pokemon in @team1.pokemon
      @battle.getWinner().should.equal(@id2)

    it "declares the pokemon dying of recoil the winner", ->
      shared.create.call this,
        team1: [Factory('Talonflame', moves: ["Brave Bird"])]
        team2: [Factory('Magikarp')]

      @p1.currentHP = @p2.currentHP = 1
      spy = @sandbox.spy(@battle, 'getWinner')
      @controller.makeMove(@id1, "Brave Bird")
      @controller.makeMove(@id2, "Splash")
      spy.returned(@id1).should.be.true

  describe "#endBattle", ->
    it "cannot end multiple times", ->
      spy = @sandbox.spy(@battle, 'emit')
      spy.withArgs("end")
      @battle.endBattle()
      @battle.endBattle()
      spy.withArgs("end").calledOnce.should.be.true

    it "marks the battle as over", ->
      @battle.endBattle()
      @battle.isOver().should.be.true

    it "updates the winner and losers' ratings", (done) ->
      shared.create.call this,
        team1: [Factory('Hitmonchan')]
        team2: [Factory('Mew')]
        conditions: [ Conditions.RATED_BATTLE ]
      @battle.on "ratingsUpdated", =>
        ratings.getPlayer @id1, (err, rating1) =>
          ratings.getPlayer @id2, (err, rating2) =>
            rating1.rating.should.be.greaterThan(ratings.DEFAULT_RATING)
            rating2.rating.should.be.lessThan(ratings.DEFAULT_RATING)
            done()

      @p2.currentHP = 1
      mock = @sandbox.mock(@controller)
      @controller.makeMove(@id1, 'Mach Punch')
      @controller.makeMove(@id2, 'Psychic')

    it "doesn't update ratings if an unrated battle", (done) ->
      shared.create.call this,
        team1: [Factory('Hitmonchan')]
        team2: [Factory('Mew')]
      @battle.on 'end', =>
        ratings.getPlayer @id1, (err, rating1) =>
          ratings.getPlayer @id2, (err, rating2) =>
            rating1.rating.should.equal(ratings.DEFAULT_RATING)
            rating2.rating.should.equal(ratings.DEFAULT_RATING)
            done()

      @p2.currentHP = 1
      mock = @sandbox.mock(@controller)
      @controller.makeMove(@id1, 'Mach Punch')
      @controller.makeMove(@id2, 'Psychic')

  describe "#forfeit", ->
    it "prematurely ends the battle", ->
      spy = @sandbox.spy(@battle, 'tell')
      spy.withArgs(Protocol.FORFEIT_BATTLE, @battle.getPlayerIndex(@id1))
      @battle.forfeit(@id1)
      spy.withArgs(Protocol.FORFEIT_BATTLE, @battle.getPlayerIndex(@id1))
        .called.should.be.true

    it "does not forfeit if the player given is invalid", ->
      mock = @sandbox.mock(@battle).expects('tell').never()
      @battle.forfeit('this definitely should not work')
      mock.verify()

    it "cannot forfeit multiple times", ->
      spy = @sandbox.spy(@battle, 'tell')
      spy.withArgs(Protocol.FORFEIT_BATTLE, @battle.getPlayerIndex(@id1))
      @battle.forfeit(@id1)
      @battle.forfeit(@id1)
      spy.withArgs(Protocol.FORFEIT_BATTLE, @battle.getPlayerIndex(@id1))
        .calledOnce.should.be.true

    it "marks the battle as over", ->
      @battle.forfeit(@id1)
      @battle.isOver().should.be.true

    it "doesn't update the winner and losers' ratings if not a rated battle", (done) ->
      @battle.on 'end', =>
        ratings.getPlayer @id1, (err, rating1) =>
          ratings.getPlayer @id2, (err, rating2) =>
            rating1.rating.should.equal(ratings.DEFAULT_RATING)
            rating2.rating.should.equal(ratings.DEFAULT_RATING)
            done()
      @battle.forfeit(@id2)

  describe "#hasStarted", ->
    it "returns false if the battle has not started", ->
      battle = new Battle('id', [])
      battle.hasStarted().should.be.false

    it "returns true if the battle has started", ->
      battle = new Battle('id', [])
      battle.begin()
      battle.hasStarted().should.be.true

  describe "#getAllAttachments", ->
    it "returns a list of attachments for all pokemon, teams, and battles", ->
      @battle.attach(Attachment.TrickRoom)
      @team2.attach(Attachment.Reflect)
      @p1.attach(Attachment.Ingrain)
      attachments = @battle.getAllAttachments()
      should.exist(attachments)
      attachments = attachments.map((a) -> a.constructor)
      attachments.length.should.be.greaterThan(2)
      attachments.should.containEql(Attachment.TrickRoom)
      attachments.should.containEql(Attachment.Reflect)
      attachments.should.containEql(Attachment.Ingrain)

  describe "#query", ->
    it "queries all attachments attached to a specific event", ->
      @battle.attach(Attachment.TrickRoom)
      @team2.attach(Attachment.Reflect)
      @p1.attach(Attachment.Ingrain)
      mocks = []
      mocks.push @sandbox.mock(Attachment.TrickRoom.prototype)
      mocks.push @sandbox.mock(Attachment.Reflect.prototype)
      mocks.push @sandbox.mock(Attachment.Ingrain.prototype)
      mock.expects("endTurn").once()  for mock in mocks
      attachments = @battle.query("endTurn")
      mock.verify()  for mock in mocks

  describe "#getOpponents", ->
    it "returns all opponents of a particular pokemon as an array", ->
      @battle.getOpponents(@p1).should.be.an.instanceOf(Array)
      @battle.getOpponents(@p1).should.have.length(1)

    it "does not include fainted opponents", ->
      @p2.faint()
      @battle.getOpponents(@p1).should.have.length(0)

  describe "#sendRequestTo", ->
    it "sends all requests to a certain player", ->
      mock = @sandbox.mock(@battle).expects('tellPlayer').once()
      mock.withArgs(@id1, Protocol.REQUEST_ACTIONS)
      @battle.sendRequestTo(@id1)
      mock.verify()

  describe "expiration", ->
    beforeEach ->
      shared.create.call(this)

    it "ends ongoing battles after a specific amount", ->
      time = Battle::ONGOING_BATTLE_TTL
      @clock.tick(time)
      @battle.isOver().should.be.true

    it "sends BATTLE_EXPIRED after a specific amount after battle end", ->
      time = Battle::ENDED_BATTLE_TTL
      delta = 5000
      spy = @sandbox.spy(@battle, 'tell').withArgs(Protocol.BATTLE_EXPIRED)
      @clock.tick(delta)
      @battle.forfeit(@id1)
      @battle.isOver().should.be.true
      spy.withArgs(Protocol.BATTLE_EXPIRED).called.should.be.false

      @battle.tell.restore()
      spy = @sandbox.spy(@battle, 'tell').withArgs(Protocol.BATTLE_EXPIRED)
      @clock.tick(time)
      @battle.isOver().should.be.true
      spy.withArgs(Protocol.BATTLE_EXPIRED).calledOnce.should.be.true

    it "doesn't call 'expire' if expiring twice", ->
      firstExpire = 24 * 60 * 60 * 1000
      secondExpire = 48 * 60 * 60 * 1000
      spy = @sandbox.spy(@battle, 'expire')
      @clock.tick(firstExpire)
      @battle.isOver().should.be.true
      @clock.tick(secondExpire)
      spy.calledOnce.should.be.true

    it "doesn't call 'end' twice", ->
      longExpire = 48 * 60 * 60 * 1000
      spy = @sandbox.spy()
      @battle.on('end', spy)
      @battle.forfeit(@id1)
      @battle.isOver().should.be.true

      @clock.tick(longExpire)
      spy.calledOnce.should.be.true

describe "Rated battles", ->
  beforeEach ->
    shared.create.call this,
        team1: [Factory('Hitmonchan')]
        team2: [Factory('Mew')]
        conditions: [ Conditions.RATED_BATTLE ]

  it "updates the winner and losers' ratings", (done) ->
    @battle.on "ratingsUpdated", =>
      ratings.getPlayer @id1, (err, rating1) =>
        ratings.getPlayer @id2, (err, rating2) =>
          defaultPlayer = ratings.algorithm.createPlayer()
          rating1.rating.should.be.greaterThan(defaultPlayer.rating)
          rating2.rating.should.be.lessThan(defaultPlayer.rating)
          done()
    @battle.forfeit(@id2)
