-- Parity test: the party-menu field moves print their text with the party
-- menu still on screen, and the GBPalWhiteOutWithDelay3 blink IS the menu
-- closing afterwards (engine/menus/start_sub_menus.asm .flash / .strength /
-- .surf, each PrintText -> GBPalWhiteOutWithDelay3 -> jp .goBackToMap,
-- whose RestoreScreenTilesAndReloadTilePatterns + CloseTextDisplay tear the
-- menu down only after the white-out).  Popping the menu first left the
-- opaque WhiteFlash as the only state under the message, i.e. the white
-- screen in #385, and lit a dark cave under the FLASH text.
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.maps and Data.maps.PALLET_TOWN) then Data:load() end
local S = require("tests.harness").suite("parity field move layering")
local check, eq = S.check, S.eq

require("src.render.Font").load(Data)
local Game       = require("src.core.Game")
local Input      = require("src.core.Input")
local StateStack = require("src.core.StateStack")
local Renderer   = require("src.render.Renderer")
local SaveData   = require("src.core.SaveData")
local Pokemon    = require("src.pokemon.Pokemon")
local PartyMenu  = require("src.ui.PartyMenu")
local OW         = require("src.world.OverworldController")

Game.data = Data
Game.input = Input; Input:init()
Game.renderer = Renderer; Renderer:init()
Game.stack = StateStack; StateStack:init()
Game.save = SaveData.newGame()

-- tests/parity_bills_pc.lua swaps the OverworldController chunk's TextBox
-- upvalue for a stub and never puts it back, and run_tests.lua dofiles every
-- parity suite into one process: point it back at the real module so trySurf
-- pushes a real box here.
local function setUpvalue(fn, name, val)
  local i = 1
  while true do
    local n = debug.getupvalue(fn, i)
    if not n then return false end
    if n == name then debug.setupvalue(fn, i, val); return true end
    i = i + 1
  end
end
setUpvalue(OW.trySurf, "TextBox", require("src.render.TextBox"))

local function frame(btns)
  Input.pressed = {}
  for _, b in ipairs(btns or {}) do Input.pressed[b] = true; Input.state[b] = true end
  StateStack:update(1 / 60)
  for _, b in ipairs(btns or {}) do Input.state[b] = false end
end
local function popAll() while Game.stack:top() do Game.stack:pop() end end
local function pushOW(mapId, x, y, facing)
  popAll()
  Game.stack:push(OW, mapId, x, y, facing)
  return Game.stack:top()
end
local function mkMon(species, ...)
  local m = Pokemon.new(Data, species, 20)
  m.moves = {}
  for _, id in ipairs({ ... }) do m.moves[#m.moves + 1] = { id = id, pp = 15 } end
  return m
end
local function selectSubItem(pm, idx)
  Game.stack:push(pm)
  frame({ "a" })
  for _ = 2, idx do frame({ "down" }) end
  frame({ "a" })
end

local function isText(s) return s ~= nil and s.pages ~= nil end
-- Transition.whiteFlash keeps its remaining frame budget and draws a full
-- opaque rect; the overworld and the menu have no `frames`
local function isBlink(s)
  return s ~= nil and s.pages == nil and s.isOpaque == true
     and type(s.frames) == "number"
end
-- the state the compositor starts from: StateStack:draw walks up from the
-- highest opaque state, so this is literally what the player sees behind
-- the message
local function backdrop()
  return Game.stack.states[Game.stack:visibleBase()]
end
-- dismiss exactly one box (an auto page advances on its own timer, a
-- prompt page on A), leaving whatever it pushed on top
local function drainOne()
  local box = Game.stack:top()
  local guard = 0
  while Game.stack:top() == box and guard < 400 do
    guard = guard + 1
    frame({ "a" })
  end
end
-- run frames until the stack is back on the map (the blink pops itself and
-- fires its onDone)
local function settle(ow)
  local guard = 0
  while Game.stack:top() ~= ow and guard < 240 do
    guard = guard + 1
    frame({})
  end
end

check(PartyMenu.isOpaque == true, "PartyMenu is opaque, so it can be the backdrop")

-- =====================================================================
-- .strength: PrintStrengthText's two pages, then the blink
-- =====================================================================
Game.save.party = { mkMon("MACHOP", "STRENGTH") }
Game.save.inventory = { RAINBOWBADGE = true }
local ow = pushOW("SEAFOAM_ISLANDS_1F", 17, 10, "right")
local pmStr = PartyMenu.new(Game)
selectSubItem(pmStr, 1)
check(isText(Game.stack:top()), "STRENGTH opens _UsedStrengthText")
eq(backdrop(), pmStr, "the party menu is the backdrop of _UsedStrengthText")
drainOne()
check(isText(Game.stack:top()), "_CanMoveBouldersText follows")
eq(backdrop(), pmStr, "the party menu is still the backdrop of the second page")
drainOne()
check(isBlink(Game.stack:top()), "the blink comes after both pages")
eq(backdrop(), Game.stack:top(), "the blink is the top state, never under a message")
settle(ow)
eq(Game.stack:top(), ow, "STRENGTH ends on the map")

-- =====================================================================
-- .surf: ItemUseSurfboard's got-on text, then the blink that carries the
-- mount forward (#320 put the blink under the text, which is what left
-- the white rect as the backdrop)
-- =====================================================================
Game.save.party = { mkMon("SQUIRTLE", "SURF") }
Game.save.inventory = { SOULBADGE = true }
ow = pushOW("PALLET_TOWN", 4, 13, "down")
ow.player.surfing = false
local pmSurf = PartyMenu.new(Game)
selectSubItem(pmSurf, 1)
check(isText(Game.stack:top()), "SURF opens _SurfingGotOnText")
eq(backdrop(), pmSurf, "the party menu is the backdrop of _SurfingGotOnText")
drainOne()
check(isBlink(Game.stack:top()), "the blink follows the got-on text")
eq(ow.player.surfing, true, "the mount happens with the blink, not before the text")
settle(ow)
eq(Game.stack:top(), ow, "SURF ends on the map")

-- =====================================================================
-- .cannotStopSurfing: prints and still closes, same layering
-- =====================================================================
ow = pushOW("PALLET_TOWN", 4, 15, "down")
ow.player.surfing = true
local pmNoOff = PartyMenu.new(Game)
selectSubItem(pmNoOff, 1)
check(isText(Game.stack:top()), "a blocked dismount opens _SurfingNoPlaceToGetOffText")
eq(backdrop(), pmNoOff, "the party menu is the backdrop of the no-place message")
drainOne()
check(isBlink(Game.stack:top()), "the blink follows the no-place message")
settle(ow)
eq(Game.stack:top(), ow, "the blocked dismount ends on the map")
ow.player.surfing = false

-- =====================================================================
-- .flash: `xor a / ld [wMapPalOffset], a` is undone before PrintText in
-- the asm, but the map is not on screen then -- the party menu is -- so
-- the lit cave may only appear once the blink hands the screen back.
-- The lighting itself happens as the message closes the menu, ahead of
-- the blink like the asm: in ADVANCED that call rebakes every resident
-- map, and behind the blink's completion that cost was spent with a
-- blank white frame as the newest thing on screen (#610).
-- =====================================================================
Game.save.flashLit = false
Game.save.party = { mkMon("PIKACHU", "FLASH") }
Game.save.inventory = { BOULDERBADGE = true }
ow = pushOW("ROCK_TUNNEL_1F", 15, 4, "down")
eq(ow.dark, true, "ROCK_TUNNEL_1F loads dark before FLASH")
local pmFlash = PartyMenu.new(Game)
selectSubItem(pmFlash, 1)
check(isText(Game.stack:top()), "FLASH opens _FlashLightsAreaText")
eq(backdrop(), pmFlash, "the party menu is the backdrop of _FlashLightsAreaText")
eq(ow.dark, true, "the tunnel is still dark while the message is up")
drainOne()
check(isBlink(Game.stack:top()), "the blink follows the FLASH message")
-- lit before the blink is pushed, so no rebuild can run while the white
-- frame is the newest one presented (#610); the blink hides it either way
eq(ow.dark, false, "the tunnel is lit by the time the blink starts")
settle(ow)
eq(Game.stack:top(), ow, "FLASH ends on the map")
eq(ow.dark, false, "the tunnel is lit once the blink hands the map back")
eq(Game.save.flashLit, true, "FLASH is recorded on the save")

popAll()
S.finish()
