-- maps/BattleTowerBattleRoom.asm's own loop, driven in a real game: the
-- opponent draw, the walk-in, the battle, and the counter the room script
-- reads back.
--
-- Nothing is shimmed: the startTowerBattle / setObjectSprite hooks and the
-- battleTower field all come from World itself.
--
--   POKEPORT_GAME=crystal POKEPORT_VERSION=crystal \
--     POKEPORT_SHOT_DIR=/tmp/tower-battle \
--     POKEPORT_DRIVER=tests/drivers/gate_tower_noshim.lua love .
local U = require("tests.drivers.util")

local BattleTower = require("src.core.gen2.BattleTower")
local Mon = require("src.battle.gen2.Mon")

-- ---- the run --------------------------------------------------------------

return function(game)
  local out = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/crystal-tower-battle"
  local fails, shots = 0, 0

  local function say(line) print("[driver] " .. line) end
  local function ok(cond, line)
    if not cond then fails = fails + 1 end
    say((cond and "OK   " or "FAIL ") .. line)
  end
  local function shot(name)
    shots = shots + 1
    U.shot(game, ("%s/%02d-%s.png"):format(out, shots, name))
  end
  local function tap(button, frames)
    game.input.pressQueue[#game.input.pressQueue + 1] = button
    game.input.state[button] = true
    U.wait(2)
    game.input.state[button] = false
    U.wait(frames or 4)
  end

  U.wait(60)
  local world = game.world
  assert(world and world.map, "crystal world did not boot")

  ok(type(world.vm.specials.startTowerBattle) == "function",
    "World:specialHooks supplies startTowerBattle, unshimmed")
  ok(type(world.vm.specials.setObjectSprite) == "function",
    "World:specialHooks supplies setObjectSprite, unshimmed")

  local roster = BattleTower.roster(game.data)
  ok(roster ~= nil, "trainers.lua carries the Battle Tower roster")
  if roster then
    say(("roster: %d trainers, %d groups of %d, sample ceiling %d")
      :format(#roster.trainers, roster.levelGroups, roster.uniqueMon,
        roster.sampleTrainers))
  end

  -- Three legal mons over the L10 room, each with ONE strong move so a blind
  -- A-press run cannot pick a status move for a hundred turns.
  local party = {}
  for _, row in ipairs({ { "TYPHLOSION", 60, "FLAMETHROWER" },
      { "FERALIGATR", 60, "SURF", "BERRY" },
      { "MEGANIUM", 60, "BODY_SLAM", "GOLD_BERRY" } }) do
    local def = game.data.moves[row[3]]
    local mon = Mon.new(game.data, row[1], row[2], {
      moves = { { id = row[3], pp = def.pp, maxPp = def.pp } },
    })
    mon.item = row[4]
    party[#party + 1] = mon
  end
  game.save.party = party

  -- The desk's own SAVELEVELGROUP write, so the room the script walks into is
  -- the L10 one (battle_tower.asm:1129-1141).
  local tower = BattleTower.state(game.save)
  tower.levelGroup = 1
  tower.streak = 0
  tower.trainers = {}
  tower.challenge = BattleTower.NO_CHALLENGE

  while game.stack:top() do game.stack:pop() end
  assert(world:setMap("BATTLE_TOWER_BATTLE_ROOM", 3, 7, "up"),
    "no BATTLE_TOWER_BATTLE_ROOM in the cache")
  U.wait(30)
  shot("room-entered")

  -- The scene script walks the player in, draws the opponent and starts the
  -- battle; nothing here presses anything until a text box wants it.
  local sawOpponent, sawBattle = false, false
  for _ = 1, 900 do
    local vm = world.vm
    if vm and vm.btOpponent and not sawOpponent then
      sawOpponent = true
      say(("opponent: %s %s (row %d), sprite %s, group %d")
        :format(tostring(vm.btOpponent.classId), tostring(vm.btOpponent.name),
          vm.btOpponent.index, tostring(vm.btOpponent.sprite),
          vm.btOpponent.group))
      for slot, mon in ipairs(vm.btOpponent.rows) do
        say(("  mon %d: %s L%d %s"):format(slot, mon.species, mon.level,
          tostring(mon.item)))
      end
      U.wait(20)
      shot("opponent-walked-in")
    end
    if world.battleActive then
      sawBattle = true
      break
    end
    if world.textbox or world.choicebox then tap("a", 4) else U.wait(2) end
  end
  ok(sawOpponent, "special LoadOpponentTrainerAndPokemonWithOTSprite drew one")
  ok(sawBattle, "special BattleTowerBattle pushed the battle screen")

  local screen, battle = nil, nil
  if sawBattle then
    U.wait(90)
    shot("battle-open")
    screen = game.stack:top()
    battle = screen and screen.battle
    ok(battle ~= nil, "the battle screen owns a Battle")
    if battle then
      ok(battle.inBattleTowerBattle == true,
        "wInBattleTowerBattle is set, so DoBadgeTypeBoosts is off")
      say("enemy trainer: " .. tostring(battle.trainer and battle.trainer.name))
      say("enemy lead: " .. tostring(battle.enemy and battle.enemy.species)
        .. " L" .. tostring(battle.enemy and battle.enemy.level))
      ok(battle.trainer ~= nil and #battle.trainer.party == 3,
        "with a three-mon Tower party")
    end
  end

  -- One press per look at the phase: FIGHT off the 2x2 menu, then the hardest
  -- move with PP left.
  local function hardestMove(mon)
    local best, bestPower = 1, -1
    for index, move in ipairs((mon and mon.moves) or {}) do
      local def = game.data.moves and game.data.moves[move.id]
      local power = (def and def.power) or 0
      if (move.pp or 0) > 0 and power > bestPower then
        best, bestPower = index, power
      end
    end
    return best
  end

  -- PP topped up between turns: an unattended run otherwise Struggles itself
  -- to death long before the third opponent mon is down.
  local function refill(mon)
    for _, move in ipairs((mon and mon.moves) or {}) do
      move.pp = move.maxPp or move.pp
    end
  end

  local menus = 0
  for _ = 1, 900 do
    if not world.battleActive or (battle and battle.over) then break end
    local phase = screen and screen.phase
    if phase == "menu" then
      menus = menus + 1
      if menus == 1 then shot("battle-menu") end
      refill(battle and battle.player)
      screen.menuIndex = 1
      tap("a", 4)
    elseif phase == "moves" then
      screen.menuIndex = hardestMove(battle and battle.player)
      tap("a", 4)
    elseif phase == "submenu" then
      -- A forced switch cannot be cancelled, so walk off the fainted lead.
      tap("down", 3)
      tap("a", 4)
    else
      tap("a", 3)
    end
  end
  if battle then
    say("battle over=" .. tostring(battle.over)
      .. " outcome=" .. tostring(battle.outcome)
      .. " phase=" .. tostring(screen and screen.phase))
    ok(battle.over, "the battle resolved")
    ok(battle.outcome == "win", "and the L60 party won it")
    shot("battle-end")
  end
  for _ = 1, 200 do
    if not world.battleActive then break end
    tap("a", 3)
  end
  ok(not world.battleActive, "the battle screen came down")
  U.wait(30)
  shot("after-battle")

  local vm = world.vm
  local state = BattleTower.state(game.save)
  say(("streak=%d challenge=%d scriptVar=%s wNrOfBeaten=%s strbuf=%q")
    :format(state.streak, state.challenge, tostring(vm and vm.scriptVar),
      tostring(vm and vm.mem and vm.mem[BattleTower.WRAM_NR_BEATEN]),
      tostring(vm and vm.stringBuffer)))
  ok(state.streak >= 1, "ReadBTTrainerParty stepped the streak counter")
  ok(state.challenge == BattleTower.CHALLENGE_IN_PROGRESS,
    "and armed sBattleTowerChallengeState")
  ok(#(state.trainers or {}) >= 1, "sBTTrainers recorded who was fought")
  ok(state.prevTeams and #state.prevTeams.prev == 3,
    "and sBTMonPrevTrainer recorded the team")

  -- The receptionist's heal, the "next up, opponent no. N" prompt and the
  -- second draw all follow on their own.
  local pages, secondDraw = {}, nil
  for _ = 1, 400 do
    local body = world.lastText
    if type(body) == "string" and body ~= "" and pages[#pages] ~= body then
      pages[#pages + 1] = body
      if #pages <= 6 then shot("page-" .. #pages) end
    end
    if world.battleActive then
      secondDraw = world.vm and world.vm.btOpponent
      break
    end
    if world.choicebox then tap("a", 4) else tap("a", 3) end
  end
  say("pages: " .. table.concat(pages, " | "))
  local joined = table.concat(pages, " | ")
  ok(joined:find("healed to full health", 1, true) ~= nil
    or joined:find("Next up", 1, true) ~= nil,
    "the room script carried on past the battle")
  ok(joined:find("Next up, opponent\nno.2", 1, true) ~= nil
    or joined:find("no.2", 1, true) ~= nil,
    "and wStringBuffer3 named the SECOND opponent")

  if world.battleActive then
    U.wait(60)
    shot("second-battle")
    ok(true, "the loop came back round for opponent two")
    say("second opponent: " .. tostring(secondDraw and secondDraw.name))
  end

  say(("streak after round two: %d"):format(BattleTower.state(game.save).streak))
  say(fails == 0 and "PASS" or (fails .. " FAILURES"))
  love.event.quit(fails == 0 and 0 or 1)
end
