-- Driver: watch the Route 22 rival leave without clipping the cliff (#236).
-- scripts/Route22.asm keys the ambush on which coord you stepped on: (29,4)
-- parks him at (29,5) with ExitMovementData1, (29,5) at (28,5) with ...Data2.
--   POKEPORT_DRIVER=tests/drivers/route22_rival_exit_bug236_test.lua \
--     POKEPORT_IDENTITY=bug236 POKEPORT_TOUCH=0 POKEPORT_VERSION=red \
--     SHOT_DIR=/tmp/shots love .   (BUG236_TILE=5 runs the other tile)
return function(game)
  local U = dofile("tests/drivers/util.lua")
  local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
  local Pokemon = require("src.pokemon.Pokemon")
  local Flags = require("src.script.Flags")
  local TextBox = require("src.render.TextBox")
  local mapScripts = require("data.scripts.init")

  local MAP = "ROUTE_22"
  local TILE_Y = tonumber(os.getenv("BUG236_TILE") or "4")
  if TILE_Y ~= 4 and TILE_Y ~= 5 then TILE_Y = 4 end
  local TRIG_X = 29
  -- what pokered does on this tile
  local WANT = (TILE_Y == 4)
    and { rx = 29, ry = 5, rivalFacing = "up", playerFacing = "down",
          -- Route22Rival1ExitMovementData1
          dirs = { "right", "right", "down", "down", "down", "down", "down" },
          endX = 31, endY = 10 }
     or { rx = 28, ry = 5, rivalFacing = "right", playerFacing = "left",
          -- Route22Rival1ExitMovementData2
          dirs = { "up", "right", "right", "right",
                   "down", "down", "down", "down", "down", "down" },
          endX = 31, endY = 10 }

  local function check(label, ok)
    U.log(ok and "PASS" or "FAIL", label)
    return ok
  end

  -- ---- a party that ends OPP_RIVAL1 in one turn --------------------------
  -- the walk-off only exists after a win, and the fight is not under test
  local tank = Pokemon.new(game.data, "MEWTWO", 100)
  tank.moves = {
    { id = "PSYCHIC_M", pp = 99 },
    { id = "THUNDERBOLT", pp = 99 },
    { id = "ICE_BEAM", pp = 99 },
    { id = "RECOVER", pp = 99 },
  }
  game.save.party = { tank }
  game.save.player = game.save.player or {}
  game.save.player.name = game.save.player.name or "RED"
  game.save.player.rival = game.save.player.rival or "BLUE"
  game.save.defeatedTrainers = {}
  game.save.objectToggles = game.save.objectToggles or {}
  game.save.objectToggles[MAP] = nil
  -- the window Route22DefaultScript arms: Pokedex in hand, Brock not yet
  -- beaten, this rival not yet fought
  Flags.set(game.save, "EVENT_CHOSE_SQUIRTLE")
  Flags.set(game.save, "EVENT_GOT_POKEDEX")
  Flags.clear(game.save, "EVENT_BEAT_BROCK")
  Flags.clear(game.save, "EVENT_BEAT_ROUTE22_RIVAL_1ST_BATTLE")
  Flags.clear(game.save, "EVENT_BEAT_GIOVANNI")
  Flags.clear(game.save, "EVENT_BEAT_ROUTE22_RIVAL_2ND_BATTLE")

  -- ---- park one cell west of the trigger ---------------------------------
  -- rows 4 and 5 are open path from x=24 to x=33; row 3 above them is the
  -- cliff the buggy exit walked into
  U.teleport(game, MAP, TRIG_X - 1, TILE_Y, "right")
  U.wait(10)
  local ow = game.overworld

  -- ---- preconditions -----------------------------------------------------
  -- a missing text entry, a moved object and an onStep that never fires all
  -- look like a movement bug on screen: nothing happens
  local t = game.data.text
  for _, key in ipairs({ "_Route22RivalBeforeBattleText1",
                         "_Route22Rival1DefeatedText",
                         "_Route22RivalAfterBattleText1" }) do
    check(key .. " resolves to a string",
          type(t[key]) == "string" and t[key] ~= "")
  end

  local spawn
  for _, o in ipairs(game.data.maps[MAP].objects or {}) do
    if o.name == "ROUTE22_RIVAL1" then spawn = o end
  end
  check("ROUTE22_RIVAL1 has an object_event", spawn ~= nil)
  check("it spawns on (25,5) as in data/maps/objects/Route22.asm",
        spawn ~= nil and spawn.x == 25 and spawn.y == 5)
  check("ROUTE_22 (28,3) is solid cliff (the cell #236 walked into)",
        not ow.map:isWalkableCell(28, 3))

  -- Dry-run the scene on a throwaway overworld so the exit list can be walked
  -- against real collision before anything happens on screen.  Music.play is
  -- silenced or the rival sting fires the scene early.
  local Music = require("src.core.Music")
  local realPlay = Music.play
  Music.play = function() end
  local rows, probeFacing
  do
    local probe = {
      runner = { isRunning = function() return false end,
                 run = function(_, r) rows = r end },
      player = { facing = "down" },
      npcByIndex = function() return { def = { name = "X" } } end,
    }
    local script = mapScripts.get(MAP)
    check("ROUTE_22 has an onStep hook", script ~= nil and script.onStep ~= nil)
    if script and script.onStep then
      check(("onStep fires on the ambush tile (%d,%d)"):format(TRIG_X, TILE_Y),
            script.onStep(game, probe, TRIG_X, TILE_Y) == true)
    end
    probeFacing = probe.player.facing
  end
  Music.play = realPlay

  local moveTo, face, walk
  for _, r in ipairs(rows or {}) do
    if r[1] == "move_npc_to" then moveTo = r end
    if r[1] == "face_object" then face = r end
    if r[1] == "walk_npc" then walk = r end
  end
  check("the scene carries move/face/walk rows",
        moveTo ~= nil and face ~= nil and walk ~= nil)
  if moveTo and face and walk then
    U.log(("plan: rival to (%d,%d) facing %s, player turned %s, exit %s")
            :format(moveTo[3], moveTo[4], tostring(face[3]),
                    tostring(probeFacing), table.concat(walk[3], ", ")))
    check(("rival parks on (%d,%d), where Route22MoveRivalRightScript leaves him")
            :format(WANT.rx, WANT.ry),
          moveTo[3] == WANT.rx and moveTo[4] == WANT.ry)
    check("rival faces " .. WANT.rivalFacing .. " at the player",
          face[3] == WANT.rivalFacing)
    check("player is turned " .. WANT.playerFacing .. " at the rival",
          probeFacing == WANT.playerFacing)
    local same = #walk[3] == #WANT.dirs
    for i = 1, #WANT.dirs do
      if walk[3][i] ~= WANT.dirs[i] then same = false end
    end
    check("exit list is " .. table.concat(WANT.dirs, ", "), same)
    -- replay it cell by cell against the real map: this is the assertion the
    -- cliff clip failed
    local D = { up = { 0, -1 }, down = { 0, 1 },
                left = { -1, 0 }, right = { 1, 0 } }
    local x, y, clean = moveTo[3], moveTo[4], true
    for i, d in ipairs(walk[3]) do
      x, y = x + D[d][1], y + D[d][2]
      if not ow.map:isWalkableCell(x, y) then
        clean = false
        U.log(("  exit step %d (%s) walks into solid ground at (%d,%d)")
                :format(i, d, x, y))
      end
      if x == TRIG_X and y == TILE_Y then
        clean = false
        U.log(("  exit step %d (%s) walks through the player on (%d,%d)")
                :format(i, d, x, y))
      end
    end
    check("every exit cell is walkable and none is the player's", clean)
    check(("the exit ends on (%d,%d), off the bottom of the screen")
            :format(WANT.endX, WANT.endY),
          x == WANT.endX and y == WANT.endY)
  end

  if U.shot(game, DIR .. "/bug236_0_before.png") then
    U.log("captured", DIR .. "/bug236_0_before.png")
  end

  -- ---- step onto the ambush tile -----------------------------------------
  U.hold(game, "right", 24)

  local function boxText()
    local top = game.stack:top()
    if getmetatable(top) ~= TextBox then return "" end
    local parts = {}
    for _, page in ipairs(top.pages or {}) do
      if type(page) == "table" then
        for _, line in ipairs(page) do parts[#parts + 1] = tostring(line) end
      end
    end
    return table.concat(parts, " ")
  end

  local function rivalNpc()
    for _, n in ipairs(game.overworld.npcs or {}) do
      if n.def and n.def.name and n.def.name:find("RIVAL") then return n end
    end
  end

  -- wait for the approach walk to finish: he is on his mark when the script
  -- moves drain and the pre-battle box is up
  local parked
  for _ = 1, 400 do
    local r = rivalNpc()
    if r and #ow.scriptMoves == 0 and not r.moving and boxText() ~= "" then
      parked = r
      break
    end
    U.wait(2)
  end
  check("the rival showed up and stopped walking", parked ~= nil)
  if parked then
    U.log(("rival parked on (%d,%d) facing %s; player on (%d,%d) facing %s")
            :format(parked.cellX, parked.cellY, tostring(parked.facing),
                    ow.player.cellX, ow.player.cellY, tostring(ow.player.facing)))
    check(("he is on (%d,%d) live, not just on paper"):format(WANT.rx, WANT.ry),
          parked.cellX == WANT.rx and parked.cellY == WANT.ry)
    check("he is not standing on top of the player",
          not (parked.cellX == ow.player.cellX and parked.cellY == ow.player.cellY))
  end
  if U.shot(game, DIR .. "/bug236_1_ambush.png") then
    U.log("captured", DIR .. "/bug236_1_ambush.png")
  end

  -- ---- fight it for the human --------------------------------------------
  -- FIGHT + first move every prompt; PSYCHIC one-shots the level 5-9 party.
  local sawBattle = false
  for f = 1, 4000 do
    local top = game.stack:top()
    if top and top.phase then
      sawBattle = true
      if top.phase == "menu" then top.menuIndex = 1
      elseif top.phase == "moveSelect" then top.moveIndex = 1 end
      U.tap(game, "a")
      if f > 2400 and top.onFinish then
        -- safety valve: never leave the run wedged in a stalled battle
        U.log("force-finishing a stalled battle")
        top.onFinish("win")
        if game.stack:top() == top then game.stack:pop() end
      end
    elseif sawBattle then
      break
    elseif top ~= ow then
      U.tap(game, "a") -- pre-battle dialogue
    end
    U.wait(2)
  end
  check("the battle ran and ended", sawBattle)
  check("EVENT_BEAT_ROUTE22_RIVAL_1ST_BATTLE is set",
        game.save.flags.EVENT_BEAT_ROUTE22_RIVAL_1ST_BATTLE == true)

  -- ---- stop on the LAST post-battle box ----------------------------------
  -- Rows 8 and 9 are DefeatedText then AfterBattleText; the walk (row 10)
  -- starts when row 9's box closes, so hand over with row 9 on screen.
  local after = t._Route22RivalAfterBattleText1 or ""
  local needle = after:match("dawdling") and "dawdling" or "LEAGUE"
  local onLastBox = false
  for _ = 1, 600 do
    if boxText():find(needle, 1, true) then onLastBox = true break end
    if game.stack:top() ~= ow then U.tap(game, "a") end
    U.wait(3)
  end
  check("the last post-battle box is on screen", onLastBox)
  U.wait(20)
  if U.shot(game, DIR .. "/bug236_2_lastbox.png") then
    U.log("captured", DIR .. "/bug236_2_lastbox.png")
  end

  -- ---- hand off ----------------------------------------------------------
  local r = rivalNpc()
  U.log(("BLUE is beaten on the (%d,%d) ambush tile and the box on screen is")
          :format(TRIG_X, TILE_Y))
  U.log("his last line.  Press A and watch him leave: that walk is #236.  He")
  U.log("must not clip the cliff at the top, step on your cell, or vanish")
  U.log("before he is off the bottom of the screen.")
  if r then
    U.log(("He is standing on (%d,%d); you are on (%d,%d)."):format(
            r.cellX, r.cellY, ow.player.cellX, ow.player.cellY))
  end
  if TILE_Y == 4 then
    U.log("Expect two steps right, then five straight down.")
  else
    U.log("Expect one step up around you, three right, then six down.")
  end

  while true do
    coroutine.yield()
  end
end
