require '../helpers'

{Battle} = require('../../server/bw/battle')
{Weather} = require('../../shared/weather')
{Pokemon} = require('../../server/bw/pokemon')
{Status, Attachment, BaseAttachment, VolatileAttachment} = require('../../server/bw/attachment')
{Moves, SpeciesData} = require('../../server/bw/data')
{Protocol} = require '../../shared/protocol'
{Factory} = require '../factory'
should = require 'should'
shared = require '../shared'

describe 'Pokemon', ->
  it 'should have a species of Missingno by default', ->
    new Pokemon().species.should.equal 'Missingno'

  it 'can change the default species', ->
    new Pokemon(species: 'Pikachu').species.should.equal 'Pikachu'

  it 'should have a level of 100 by default', ->
    new Pokemon().level.should.equal 100

  it 'can change the default level', ->
    new Pokemon(level: 5).level.should.equal 5

  it 'gets its current hp populated from its max hp', ->
    new Pokemon().currentHP.should.equal 341

  it 'has pp for each move', ->
    pokemon = new Pokemon(moves: ["Tackle", "Splash"])
    pokemon.pp(Moves['Tackle']).should.equal 35 * 8/5
    pokemon.pp(Moves['Splash']).should.equal 40 * 8/5

  describe '#iv', ->
    it 'has default iv of 31', ->
      new Pokemon().iv('hp').should.equal 31

    it 'retrieves iv successfully', ->
      new Pokemon(ivs: {'hp': 25}).iv('hp').should.equal 25

    it "doesn't default to 31 if iv is 0", ->
      new Pokemon(ivs: {'hp': 0}).iv('hp').should.equal 0

  describe '#ev', ->
    it 'has default ev of 0', ->
      new Pokemon().ev('hp').should.equal 0

    it 'retrieves ev successfully', ->
      new Pokemon(evs: {hp: 25}).ev('hp').should.equal 25

  describe '#stat', ->
    it 'calculates hp correctly', ->
      pokemon = new Pokemon(level: 100, evs: { hp: 255 })
      pokemon.stat('hp').should.equal 404
      pokemon = new Pokemon(level: 50, evs: { hp: 255 })
      pokemon.stat('hp').should.equal 207
      # todo: test other numbers later

    it 'calculates 1 base HP correctly', ->
      pokemon = new Pokemon(level: 100, evs: { hp: 255 })
      pokemon.baseStats.hp = 1
      pokemon.stat('hp').should.equal 1

    it 'calculates other stats correctly', ->
      pokemon = new Pokemon(level: 100, evs: { attack: 255 })
      pokemon.stat('attack').should.equal 299
      pokemon = new Pokemon(level: 50, evs: { attack: 255 })
      pokemon.stat('attack').should.equal 152
      # todo: test other numbers later

    it "calculates a stat with a nature boost correctly", ->
      pokemon = new Pokemon(nature: 'Adamant')
      pokemon.stat('attack').should.equal 259

    it "calculates a stat with a nature decrease correctly", ->
      pokemon = new Pokemon(nature: 'Bold')
      pokemon.stat('attack').should.equal 212

  describe 'stat boosts', ->
    it 'increase the stat by (n+2)/2 if positive', ->
      pokemon = new Pokemon()
      speed = pokemon.stat('speed')
      pokemon.stages.speed = 3
      pokemon.stat('speed').should.equal Math.floor(2.5 * speed)

    it 'decrease the stat by 2/(n+2) if negative', ->
      pokemon = new Pokemon()
      speed = pokemon.stat('speed')
      pokemon.stages.speed = -3
      pokemon.stat('speed').should.equal Math.floor(speed / 2.5)

  describe '#natureBoost', ->
    it "returns 1 by default for non-existent natures", ->
      new Pokemon(nature: 'Super').natureBoost('attack').should.equal 1

    it "returns 1.1 for natures that boost a certain stat", ->
      new Pokemon(nature: 'Adamant').natureBoost('attack').should.equal 1.1

    it "returns 1.0 for natures do not affect a certain stat", ->
      new Pokemon(nature: 'Adamant').natureBoost('speed').should.equal 1

    it "returns 0.9 for natures that decrease a certain stat", ->
      new Pokemon(nature: 'Timid').natureBoost('attack').should.equal 0.9

  describe '#hasType', ->
    it 'returns false if the pokemon does not have that type', ->
      new Pokemon().hasType('Grass').should.be.false

    it 'returns true if the pokemon has that type', ->
      pokemon = new Pokemon()
      pokemon.types = ['Dark', 'Grass']
      pokemon.hasType('Grass').should.be.true

  describe '#switchOut', ->
    it 'resets stat boosts', ->
      pokemon = new Pokemon()
      pokemon.boost(specialAttack: 2)
      pokemon.switchOut()
      pokemon.stages['specialAttack'].should.equal 0

    it 'removes blocked moves', ->
      pokemon = new Pokemon(moves: ['Earthquake'])
      pokemon.blockMove(Moves['Earthquake'])
      pokemon.switchOut()
      pokemon.isMoveBlocked(Moves['Earthquake']).should.be.false

    it 'removes volatile attachments', ->
      pokemon = new Pokemon()
      pokemon.attach(VolatileAttachment)
      pokemon.switchOut()
      pokemon.has(VolatileAttachment).should.be.false

  describe '#endTurn', ->
    it 'removes blocked moves', ->
      pokemon = new Pokemon(moves: ['Earthquake'])
      pokemon.blockMove(Moves['Earthquake'])
      pokemon.switchOut()
      pokemon.isMoveBlocked(Moves['Earthquake']).should.be.false

  describe '#attach', ->
    it 'adds an attachment to a list of attachments', ->
      pokemon = new Pokemon()
      pokemon.attach(BaseAttachment)
      pokemon.has(BaseAttachment).should.be.true

  describe '#unattach', ->
    it 'removes an attachment from the list of attachments', ->
      pokemon = new Pokemon()
      pokemon.attach(BaseAttachment)
      pokemon.unattach(BaseAttachment)
      pokemon.has(BaseAttachment).should.be.false

  describe '#unattachAll', ->
    it 'removes every attachment', ->
      pokemon = new Pokemon()
      pokemon.attach(BaseAttachment)
      pokemon.attach(VolatileAttachment)
      pokemon.unattachAll()
      pokemon.has(BaseAttachment).should.be.false
      pokemon.has(VolatileAttachment).should.be.false

  describe '#blockMove', ->
    it 'adds a move to a list of blocked moves', ->
      pokemon = new Pokemon(moves: ['Earthquake'])
      pokemon.blockMove(Moves['Earthquake'])
      pokemon.blockedMoves.should.containEql Moves['Earthquake']

  describe '#isMoveBlocked', ->
    it 'returns true if the move is blocked', ->
      pokemon = new Pokemon(moves: ['Earthquake'])
      pokemon.blockMove(Moves['Earthquake'])
      pokemon.isMoveBlocked(Moves['Earthquake']).should.be.true

    it 'returns false if the move is not blocked', ->
      pokemon = new Pokemon(moves: ['Earthquake'])
      pokemon.isMoveBlocked(Moves['Earthquake']).should.be.false

  describe '#validMoves', ->
    it 'returns moves without blocked moves', ->
      pokemon = new Pokemon(moves: ['Splash', 'Earthquake'])
      pokemon.blockMove(Moves['Earthquake'])
      pokemon.validMoves().should.eql([ Moves['Splash'] ])

    it 'excludes moves with 0 PP', ->
      pokemon = new Pokemon(moves: ['Splash', 'Earthquake'])
      move = pokemon.moves[0]
      otherMoves = pokemon.moves.filter((m) -> m != move)
      pokemon.setPP(move, 0)
      pokemon.validMoves().should.eql(otherMoves)

  describe '#reducePP', ->
    it 'reduces PP of a move by 1', ->
      pokemon = new Pokemon(moves: ['Splash', 'Earthquake'])
      move = Moves['Splash']
      pokemon.reducePP(move)
      pokemon.pp(move).should.equal pokemon.maxPP(move) - 1

    it 'does not go below 0', ->
      pokemon = new Pokemon(moves: ['Splash', 'Earthquake'])
      move = Moves['Splash']
      for x in [0..pokemon.maxPP(move)]
        pokemon.reducePP(move)
      pokemon.pp(move).should.equal 0

  describe '#setPP', ->
    it "sets the PP of a move", ->
      pokemon = new Pokemon(moves: ['Splash', 'Earthquake'])
      move = Moves['Splash']
      pokemon.setPP(move, 1)
      pokemon.pp(move).should.equal 1

    it "cannot go below 0", ->
      pokemon = new Pokemon(moves: ['Splash', 'Earthquake'])
      move = Moves['Splash']
      pokemon.setPP(move, -1)
      pokemon.pp(move).should.equal 0

    it "cannot go above the max PP possible", ->
      pokemon = new Pokemon(moves: ['Splash', 'Earthquake'])
      move = Moves['Splash']
      pokemon.setPP(move, pokemon.maxPP(move) + 1)
      pokemon.pp(move).should.equal pokemon.maxPP(move)

  describe '#positiveBoostCount', ->
    it "returns the number of boosts higher than 0", ->
      pokemon = new Pokemon()
      pokemon.boost(attack: 4, defense: -3, speed: 1)
      pokemon.positiveBoostCount().should.equal 5

  describe '#isItemBlocked', ->
    it 'returns true if the Pokemon has no item', ->
      pokemon = new Pokemon()
      pokemon.isItemBlocked().should.be.true

  describe "attaching statuses", ->
    it "returns null if attaching too many statuses", ->
      pokemon = new Pokemon()
      pokemon.attach(Status.Freeze)
      should.not.exist pokemon.attach(Status.Paralyze)

    it "does not override statuses", ->
      pokemon = new Pokemon()
      pokemon.attach(Status.Freeze)
      pokemon.attach(Status.Paralyze)
      pokemon.has(Status.Freeze).should.be.true
      pokemon.has(Status.Paralyze).should.be.false

    it "sets the status of the pokemon", ->
      for species, status of Status
        pokemon = new Pokemon()
        pokemon.attach(status)
        pokemon.status.should.equal(status)

    it "sets the corresponding attachment on the pokemon", ->
      for species, status of Status
        pokemon = new Pokemon()
        pokemon.attach(status)
        pokemon.has(status).should.be.true

    it "doesn't poison Poison types", ->
      pokemon = new Pokemon()
      pokemon.types = ["Poison"]
      pokemon.attach(Status.Poison)
      pokemon.attach(Status.Toxic)
      pokemon.has(Status.Poison).should.be.false
      pokemon.has(Status.Toxic).should.be.false

    it "doesn't poison Steel types", ->
      pokemon = new Pokemon()
      pokemon.types = ["Steel"]
      pokemon.attach(Status.Poison)
      pokemon.attach(Status.Toxic)
      pokemon.has(Status.Poison).should.be.false
      pokemon.has(Status.Toxic).should.be.false

    it "doesn't burn Fire types", ->
      pokemon = new Pokemon()
      pokemon.types = ["Fire"]
      pokemon.attach(Status.Burn)
      pokemon.has(Status.Burn).should.be.false

    it "doesn't freeze Ice types", ->
      pokemon = new Pokemon()
      pokemon.types = ["Ice"]
      pokemon.attach(Status.Freeze)
      pokemon.has(Status.Freeze).should.be.false

    it "doesn't freeze under Sun", ->
      battle = new Battle('id', {"a": [], "b": []})
      battle.setWeather(Weather.SUN)
      pokemon = new Pokemon(battle: battle)
      pokemon.attach(Status.Freeze)
      pokemon.has(Status.Freeze).should.be.false

  describe "#cureStatus", ->
    it "removes all statuses if no argument is passed", ->
      pokemon = new Pokemon()
      for species, status of Status
        pokemon.attach(status)
        pokemon.cureStatus()
        pokemon.hasStatus().should.be.false

    it "removes only a certain status if an argument is passed", ->
      pokemon = new Pokemon()
      pokemon.attach(Status.Freeze)
      pokemon.cureStatus(Status.Paralyze)
      pokemon.has(Status.Freeze).should.be.true

  describe '#hasTakeableItem', ->
    it "returns false if the pokemon has no item", ->
      pokemon = new Pokemon()
      pokemon.hasTakeableItem().should.be.false

    it "returns true if the item can be taken", ->
      pokemon = new Pokemon(item: "Leftovers")
      pokemon.hasTakeableItem().should.be.true

    it "returns false if the pokemon has a mail", ->
      pokemon = new Pokemon(item: "Air Mail")
      pokemon.hasTakeableItem().should.be.false

    it "returns false if the pokemon has a key item", ->
      pokemon = new Pokemon(item: "Acro Bike")
      pokemon.hasTakeableItem().should.be.false

    it "returns false if the pokemon has Multitype and a plate", ->
      pokemon = new Pokemon(ability: "Multitype", item: "Draco Plate")
      pokemon.hasTakeableItem().should.be.false

    it "returns false if the pokemon is Giratina-O", ->
      pokemon = new Pokemon(species: "Giratina", forme: "origin", item: "Griseous Orb")
      pokemon.hasTakeableItem().should.be.false

    it "returns false if the pokemon is Genesect with a Drive item", ->
      pokemon = new Pokemon(species: "Genesect", item: "Burn Drive")
      pokemon.hasTakeableItem().should.be.false

  describe "#isWeatherDamageImmune", ->
    it "returns true if it's hailing and the Pokemon is Ice type", ->
      pokemon = new Pokemon()
      pokemon.types = [ "Ice" ]
      pokemon.isWeatherDamageImmune(Weather.HAIL).should.be.true

    it "returns true if it's sandstorming and the Pokemon is Rock type", ->
      pokemon = new Pokemon()
      pokemon.types = [ "Rock" ]
      pokemon.isWeatherDamageImmune(Weather.SAND).should.be.true

    it "returns true if it's sandstorming and the Pokemon is Steel type", ->
      pokemon = new Pokemon()
      pokemon.types = [ "Steel" ]
      pokemon.isWeatherDamageImmune(Weather.SAND).should.be.true

    it "returns true if it's sandstorming and the Pokemon is Ground type", ->
      pokemon = new Pokemon()
      pokemon.types = [ "Ground" ]
      pokemon.isWeatherDamageImmune(Weather.SAND).should.be.true

    it "returns false otherwise", ->
      pokemon = new Pokemon()
      pokemon.types = [ "Grass" ]
      pokemon.isWeatherDamageImmune(Weather.SAND).should.be.false
      pokemon.isWeatherDamageImmune(Weather.HAIL).should.be.false

  describe "#useItem", ->
    it "records the item in lastItem", ->
      pokemon = new Pokemon(item: "Leftovers")
      pokemon.activate()
      pokemon.useItem()
      should.exist pokemon.lastItem
      pokemon.lastItem.displayName.should.equal("Leftovers")

    it "removes the item", ->
      pokemon = new Pokemon(item: "Leftovers")
      pokemon.activate()
      pokemon.useItem()
      pokemon.hasItem().should.be.false

  describe "#removeItem", ->
    it "removes the item", ->
      pokemon = new Pokemon(item: "Leftovers")
      pokemon.activate()
      pokemon.removeItem()
      pokemon.hasItem().should.be.false

    it "does not remove prior records of an item", ->
      pokemon = new Pokemon(item: "Flying Gem")
      fake = new Pokemon(item: "Leftovers")
      pokemon.activate()
      fake.activate()
      pokemon.useItem()
      pokemon.setItem(fake.getItem())
      pokemon.removeItem()
      should.exist pokemon.lastItem

  describe '#changeForme', ->
    it "changes the Pokemon's forme from one to another", ->
      pokemon = new Pokemon(species: 'Shaymin')
      pokemon.forme.should.equal('default')
      pokemon.changeForme('sky')
      pokemon.forme.should.equal('sky')

    it 'changes base stats if applicable', ->
      pokemon = new Pokemon(species: 'Shaymin')
      pokemon.baseStats.should.eql(
        attack: 100, defense: 100, hp: 100,
        specialAttack: 100, specialDefense: 100, speed: 100)
      pokemon.changeForme('sky')
      pokemon.baseStats.should.eql(
        attack: 103, defense: 75, hp: 100,
        specialAttack: 120, specialDefense: 75, speed: 127)

    it 'changes weight if applicable', ->
      pokemon = new Pokemon(species: 'Shaymin')
      pokemon.weight.should.equal(21)
      pokemon.changeForme('sky')
      pokemon.weight.should.equal(52)

    it 'changes type if applicable', ->
      pokemon = new Pokemon(species: 'Shaymin')
      pokemon.types.should.eql([ 'Grass' ])
      pokemon.changeForme('sky')
      pokemon.types.should.eql([ 'Grass', 'Flying' ])

    it 'does nothing if the new forme is an invalid forme', ->
      pokemon = new Pokemon(species: 'Shaymin')
      pokemon.forme.should.equal('default')
      pokemon.changeForme('batman')
      pokemon.forme.should.equal('default')

  describe "#toJSON", ->
    it "can hide information", ->
      pokemon = new Pokemon(species: 'Shaymin')
      json = pokemon.toJSON(hidden: true)
      json.should.not.have.property("hp")
      json.should.not.have.property("maxHP")
      json.should.not.have.property("moves")
      json.should.not.have.property("moveTypes")
      json.should.not.have.property("pp")
      json.should.not.have.property("maxPP")
      json.should.not.have.property("ivs")

    it "shows all information otherwise", ->
      pokemon = new Pokemon(species: 'Shaymin')
      json = pokemon.toJSON()
      json.should.have.property("hp")
      json.should.have.property("maxHP")
      json.should.have.property("moves")
      json.should.have.property("moveTypes")
      json.should.have.property("pp")
      json.should.have.property("maxPP")
      json.should.have.property("ivs")

describe "Shedinja holding Focus Sash", ->
  it "faints from burn damage", ->
    shared.create.call(this, team1: [Factory("Shedinja", item: "Focus Sash")])
    @p1.attach(Status.Burn)
    @p1.currentHP.should.equal(1)

    @battle.endTurn()
    @p1.currentHP.should.equal(0)

  it "faints from switching into hazards", ->
    shared.create.call this,
      team2: [Factory("Magikarp"), Factory("Shedinja", item: "Focus Sash")]

    @battle.performMove(@p1, @battle.getMove("Stealth Rock"))
    @battle.performSwitch(@team2.first(), 1)

    pokemon = @team2.first()
    (pokemon.stat('hp') - pokemon.currentHP).should.equal(1)

  it "faints from Perish Song", ->
    shared.create.call(this, team1: [Factory("Shedinja", item: "Focus Sash")])

    @battle.performMove(@p2, @battle.getMove('Perish Song'))
    @battle.endTurn() for x in [0..3]
    @p1.isFainted().should.be.true

  it "faints from Destiny Bond", ->
    shared.create.call(this, team1: [Factory("Shedinja", item: "Focus Sash")])

    @p2.currentHP = 1
    @battle.performMove(@p2, @battle.getMove("Destiny Bond"))
    @battle.performMove(@p1, @battle.getMove("Tackle"))
    @p1.isFainted().should.be.true

  it "faints from using Final Gambit", ->
    shared.create.call(this, team1: [Factory("Shedinja", item: "Focus Sash")])
    finalGambit = @battle.getMove("Final Gambit")
    @battle.performMove(@p1, finalGambit)
    @p1.isFainted().should.be.true
