-- Parity test: which animation BIDE plays on which turn (#375).  BIDE is a
-- ResidualEffects2 entry (data/battle/residual_effects_2.asm:18), so the
-- ordinary PlayMoveAnimation is skipped on the storing turn and BideEffect
-- (engine/battle/effects.asm:764-789) ends on
-- `ldh a, [hWhoseTurn] / add XSTATITEM_ANIM / jp PlayBattleAnimation2`:
-- the X-item spiral, duplicated for the enemy side.  BIDE's own hit
-- animation belongs to .UnleashEnergy (engine/battle/core.asm:3501-3529),
-- which re-points wPlayerMoveNum at BIDE and rejoins
-- HandleIfPlayerMoveMissed, i.e. after UnleashedEnergyText and before the
-- HP bar drains.  The accuracy half of the release is in tests/parity_J.lua.
package.path = "./?.lua;./?/init.lua;" .. package.path
if not _G.love then _G.love = require("tests.love_stub") end

local Data = require("src.core.Data")
if not (Data.moves and Data.moves.BIDE) then Data:load() end
local TypeChart = require("src.battle.TypeChart")
TypeChart.load(Data)

local Font = require("src.render.Font")
if not pcall(Font.encode, "A") then Font.load(Data) end

local Game = require("src.core.Game")
Game.data = Data
Game.save = require("src.core.SaveData").newGame()

local Pokemon = require("src.pokemon.Pokemon")
local BattleState = require("src.battle.BattleState")
local S = require("tests.harness").suite("parity bide anim")
local check, eq = S.check, S.eq

local function freshBattle()
  Game.save.party = { Pokemon.new(Data, "BULBASAUR", 20) }
  local tb = BattleState.newWild(Game, "PIDGEY", 10)
  tb.queue, tb.nextInsert = {}, 0
  return tb
end

-- the rows this suite cares about, in queue order: text lines and anim names
local function trace(tb)
  local out = {}
  for _, row in ipairs(tb.queue) do
    if row.text then
      out[#out + 1] = { text = (row.text:gsub("\n", " ")) }
    elseif row.anim then
      out[#out + 1] = { anim = row.anim, isPlayer = row.attackerIsPlayer,
                        hit = row.hit }
    elseif row.drain then
      out[#out + 1] = { drain = true }
    end
  end
  return out
end

local function indexOfAnim(rows, name)
  for i, r in ipairs(rows) do
    if r.anim == name then return i end
  end
  return nil
end

local function indexOfText(rows, needle)
  for i, r in ipairs(rows) do
    if r.text and r.text:find(needle, 1, true) then return i end
  end
  return nil
end

local function anyAnim(rows, name)
  return indexOfAnim(rows, name) ~= nil
end

-- the three animations this behavior needs must actually be in the cache, or
-- AnimPlayer:start warns and plays nothing
do
  local anims = Data.battle_anims and Data.battle_anims.moveAnims
  check(anims ~= nil, "battle_anims carries moveAnims")
  if anims then
    check(anims.BIDE ~= nil, "BIDE has an animation")
    check(anims.XSTATITEM_ANIM ~= nil, "XSTATITEM_ANIM has an animation")
    check(anims.XSTATITEM_DUPLICATE_ANIM ~= nil,
          "XSTATITEM_DUPLICATE_ANIM has an animation")
  end
end

-- storing turn, player side: used-BIDE line, X-item spiral, storing line.
-- The bug queued BIDE's hit row (sound = "BIDE", subanim 4) here instead.
do
  local tb = freshBattle()
  tb:performMove(tb.player, tb.enemy, { id = "BIDE", pp = 10 }, false)
  local rows = trace(tb)
  eq(tb.player.bideTurns ~= nil, true, "the storing turn locks BIDE in")
  eq(tb.player.bideDamage, 0, "and zeroes the accumulator")
  check(not anyAnim(rows, "BIDE"),
        "BIDE's own animation does not play on the storing turn (#375)")
  local spiral = indexOfAnim(rows, "XSTATITEM_ANIM")
  check(spiral ~= nil, "the storing turn plays XSTATITEM_ANIM")
  eq(rows[spiral] and rows[spiral].isPlayer, true,
     "on the player's side of the field")
  -- effects.asm:1461-1471
  eq(rows[spiral] and rows[spiral].hit and rows[spiral].hit.animType, 6,
     "PlayBattleAnimation2 stamps wAnimationType 6 on the player's turn (#1564)")
  local used = indexOfText(rows, "used BIDE!")
  local storing = indexOfText(rows, "storing energy!")
  check(used and spiral and used < spiral,
        "the spiral comes after the used-BIDE line")
  check(spiral and storing and spiral < storing,
        "and before the storing-energy line")
  check(tb.moveAnimRow == nil, "the peeled move-anim row is not left dangling")
  check(not anyAnim(rows, "XSTATITEM_DUPLICATE_ANIM"),
        "the player never gets the enemy-side duplicate")
end

-- enemy side: PlayBattleAnimation2 adds hWhoseTurn, so the foe gets
-- XSTATITEM_DUPLICATE_ANIM (AttackAnimationPointers[174])
do
  local tb = freshBattle()
  tb:performMove(tb.enemy, tb.player, { id = "BIDE", pp = 10 }, false)
  local rows = trace(tb)
  check(not anyAnim(rows, "BIDE"),
        "the foe's storing turn is animation-free of BIDE too")
  check(not anyAnim(rows, "XSTATITEM_ANIM"),
        "and does not borrow the player row")
  local dup = indexOfAnim(rows, "XSTATITEM_DUPLICATE_ANIM")
  check(dup ~= nil, "the foe's storing turn plays XSTATITEM_DUPLICATE_ANIM")
  check(dup and not rows[dup].isPlayer, "attributed to the enemy side")
  eq(rows[dup] and rows[dup].hit and rows[dup].hit.animType, 3,
     "and wAnimationType 3 on the enemy's turn (#1564)")
end

-- locked turns: .BideCheck decrements with no text of its own in pokered and
-- no animation either; the port prints the storing line, and that is all
do
  local tb = freshBattle()
  tb.player.bideTurns = 3
  tb.player.bideDamage = 0
  tb:continueBide(tb.player, tb.enemy)
  local rows = trace(tb)
  eq(tb.player.bideTurns, 2, "a locked turn just counts down")
  check(indexOfText(rows, "storing energy!") ~= nil, "and reprints the line")
  for _, r in ipairs(rows) do
    check(r.anim == nil, "no animation on a locked turn: " .. tostring(r.anim))
  end
end

-- release turn: text, then BIDE's animation, then the bar drains
do
  local tb = freshBattle()
  tb.enemy.mon.stats.hp = 200
  tb.enemy.mon.hp = 200
  tb.player.bideTurns = 1
  tb.player.bideDamage = 40
  tb:continueBide(tb.player, tb.enemy)
  local rows = trace(tb)
  eq(tb.enemy.mon.hp, 120, "the release deals bideDamage*2")
  local unleashed = indexOfText(rows, "unleashed energy!")
  local hit = indexOfAnim(rows, "BIDE")
  check(unleashed ~= nil, "the release prints UnleashedEnergyText")
  check(hit ~= nil, "the release plays BIDE's own animation (#375)")
  eq(rows[hit] and rows[hit].isPlayer, true, "from the player's side")
  -- core.asm:3526-3528 -> effects.asm:1476-1477
  check(rows[hit] and rows[hit].hit == nil,
        ".UnleashEnergy rejoins PlayCurrentMoveAnimation, which zeroes "
        .. "wAnimationType")
  check(unleashed and hit and unleashed < hit,
        "the animation follows the text")
  local drainAt
  for i, r in ipairs(rows) do
    if r.drain then drainAt = i; break end
  end
  check(drainAt ~= nil, "the HP bar drain is queued")
  check(hit and drainAt and hit < drainAt,
        "and the animation runs before the bar moves")
  check(not anyAnim(rows, "XSTATITEM_ANIM"),
        "the release is not the X-item spiral")
end

-- zero stored damage sets wMoveMissed, so the failure path stays anim-free
do
  local tb = freshBattle()
  local hpBefore = tb.enemy.mon.hp
  tb.player.bideTurns = 1
  tb.player.bideDamage = 0
  tb:continueBide(tb.player, tb.enemy)
  local rows = trace(tb)
  eq(tb.enemy.mon.hp, hpBefore, "nothing stored means no damage")
  check(indexOfText(rows, "But, it failed!") ~= nil, "it prints the failure")
  check(not anyAnim(rows, "BIDE"), "and plays no animation")
end

S.finish()
