-- Parity test,  Hall of Fame credits, autosave and post-credits reset
-- (engine/movie/credits.asm HallOfFamePC/Credits, scripts/HallOfFame.asm
-- HallOfFameResetEventsAndSaveScript).
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 Font = require("src.render.Font")
if not pcall(Font.encode, "A") then Font.load(Data) end
local S = require("tests.harness").suite("parity HOF")
local check, eq = S.check, S.eq

-- === (1) extracted credits data matches CreditsOrder/CreditsMons ===

local credits = Data.field and Data.field.credits
check(credits ~= nil, "field.credits extracted")
credits = credits or { screens = {}, mons = {} }
eq(#credits.screens, 35, "CreditsOrder: 35 screens")
eq(#credits.mons, 15, "CreditsMons: 15 entries")
local s1 = credits.screens[1] or {}
check(s1.fade == true and s1.mon == "VENUSAUR",
      "screen 1 is CRED_TEXT_FADE_MON with VENUSAUR")
local last = credits.screens[#credits.screens] or {}
check(last.copyright == true and last.fade == true and last.mon == "PARASECT",
      "last screen is CRED_COPYRIGHT + CRED_TEXT_FADE_MON with PARASECT")
-- after every mon wipe BGP is left at %11000000 (text invisible), so the
-- following screen must be a FADE variant -- true of the extracted order
local fadeAfterWipe = true
local prevMon = true -- HallOfFamePC enters with BGP %11000000
for _, s in ipairs(credits.screens) do
  if prevMon and not s.fade then fadeAfterWipe = false end
  prevMon = s.mon ~= nil
end
check(fadeAfterWipe, "every screen after a mon wipe fades in")

-- === (2) Credits state machine: pokered's exact frame timing ===

local Credits = require("src.ui.Credits")
local pressed = {}
local fakeInput = { wasPressed = function(_, b) return pressed[b] or false end,
                    isDown = function() return false end }
local function newStack()
  local stack = { states = {} }
  function stack:push(s, ...) table.insert(self.states, s) if s.enter then s:enter(...) end end
  function stack:pop() local s = table.remove(self.states) if s and s.exit then s:exit() end return s end
  function stack:top() return self.states[#self.states] end
  return stack
end

local stack = newStack()
local game = { data = Data, input = fakeInput, stack = stack, save = {} }
local theEndAt, doneRan = nil, false
local frame = 0
local roll = Credits.new(game, function() doneRan = true end,
                         function() theEndAt = frame end)
stack:push(roll)

-- HallOfFamePC lead-in (100 blank + 128 after music starts) + per-screen
-- fade/hold/wipe + THE END (16 blank + 20 fade) + the script's 5x120
-- DelayFrames before WaitForTextScrollButtonPress
local expected = 100 + 128 + 16 + 20 + 600
for _, s in ipairs(credits.screens) do
  expected = expected + (s.fade and 20 or 0)
    + (s.mon and (s.fade and 90 or 110) or (s.fade and 120 or 140))
    -- DisplayCreditsMon: 3 x CreditsCopyTileMapToVRAM (Delay3) then 27 scroll
    -- frames (#703)
    + (s.mon and (9 + 27) or 0)
end
while roll.phase ~= "end_wait" and frame < expected + 120 do
  frame = frame + 1
  roll:update(1 / 60)
  roll:draw() -- exercise every draw path headless
end
eq(frame, expected, "credits reach the A/B wait after the exact frame count")
eq(theEndAt, expected - 600,
   "onTheEnd (the SaveGameData point) fires when THE END finishes fading")
check(not doneRan, "onDone waits for the button press")
roll:update(1 / 60) -- unpressed frame: still waiting
check(roll.phase == "end_wait" and #stack.states == 1, "credits hold on THE END")
pressed.b = true -- WaitForTextScrollButtonPress takes A or B
roll:update(1 / 60)
pressed.b = nil
check(doneRan, "B on THE END pops the credits and calls onDone")
eq(#stack.states, 0, "credits popped itself")

-- === (3) record_hall_of_fame: induction -> credits -> autosave -> Init ===

local Commands = require("src.script.Commands")
local SaveData = require("src.core.SaveData")
local stack2 = newStack()
local game2 = { data = Data, input = fakeInput, stack = stack2,
                save = SaveData.newGame() }
-- battered party: record_hall_of_fame must heal before the autosave (#103)
game2.save.party = { { species = "PIKACHU", level = 81, hp = 1,
                       stats = { hp = 100 }, status = "PAR",
                       moves = { { id = "THUNDERBOLT", pp = 0 } } } }
game2.save.player.map = "HALL_OF_FAME"
game2.save.lastOutdoor = { id = "INDIGO_PLATEAU", x = 9, y = 5 }
local wrote = false
function game2:writeSave() wrote = true; SaveData.save(self.save) end

local co
local runner = {}
function runner:yield() coroutine.yield() end
function runner:resume()
  local ok, err = coroutine.resume(co)
  if not ok then error(err) end
end
local ctx = { game = game2, save = game2.save, runner = runner }
co = coroutine.create(function() Commands.record_hall_of_fame(ctx) end)
local ok, err = coroutine.resume(co)
check(ok, "record_hall_of_fame starts: " .. tostring(err))

eq(#game2.save.hallOfFame, 1, "winning team recorded (SaveHallOfFameTeams)")
local HallOfFame = require("src.ui.HallOfFame")
check(getmetatable(stack2:top()) == HallOfFame, "induction showcase pushed")

-- Gen1 layout (issue #102): pic rests at hlcoord (12,5); mon phase starts
-- with the LEVEL/TYPE info box (not a top "HALL OF FAME" banner alone).
-- HoFShowMonOrPlayer sweeps the BACK pic across the screen first (hSCX
-- $c0 -> $a0) and only then scrolls the front pic in (#847), so the
-- induction opens on the back pass.
local hofUi = stack2:top()
eq(hofUi.phase, "back", "induction opens on the back pic sweep (#847)")
eq(hofUi.scrollX, 160, "back pic enters at the right edge (hSCX = $c0)")
-- drive the back sweep and the front scroll so the info box is armed
local scrollGuard = 0
while (hofUi.phase == "back" or hofUi.scrollX < 12 * 8) and scrollGuard < 400 do
  scrollGuard = scrollGuard + 1
  hofUi:update(1 / 60)
end
eq(hofUi.phase, "mons", "the front pic phase follows the back sweep (#847)")
eq(hofUi.scrollX, 12 * 8, "front pic settles at hlcoord (12,5)")
eq(hofUi.showHofBanner, false, "bottom HALL OF FAME banner waits for the 80-frame hold")
check(hofUi.timer == 80 or hofUi.timer < 80,
      "info hold uses the pokered 80 DelayFrames window")

-- drive induction + full credits with A held (pages are unskippable; A
-- only advances the induction and the final THE END wait)
pressed.a = true
local guard = 0
while coroutine.status(co) ~= "dead" and stack2:top() and guard < 30000 do
  guard = guard + 1
  local top = stack2:top()
  if top.update then top:update(1 / 60) end
end
pressed.a = nil
eq(coroutine.status(co), "dead", "HoF script command runs to completion")
check(guard > expected, "credits pages were not skippable by holding A")

-- the autosave (SaveGameData while THE END is up)
check(wrote, "autosave ran during THE END")
local savedRaw = love.filesystem.read("save.lua")
local saved = savedRaw and SaveData.decode(savedRaw) or nil
check(saved ~= nil, "save.lua written and decodable")
eq(saved and saved.lastHeal and saved.lastHeal.map, "PALLET_TOWN",
   "wLastBlackoutMap := PALLET_TOWN before the save")
-- #103 parked CONTINUE in the upstairs bedroom; #253 corrected that to the
-- blackout point the Hall of Fame script actually writes, so the player
-- resumes outside the front door like the original.
eq(saved and saved.player and saved.player.map, "PALLET_TOWN",
   "CONTINUE lands at the front door, not the bedroom (#253)")
eq(saved and saved.player and saved.player.x, 5, "Pallet spawn x")
eq(saved and saved.player and saved.player.y, 6, "Pallet spawn y")
eq(saved and saved.lastOutdoor and saved.lastOutdoor.id, "PALLET_TOWN",
   "LAST_MAP exits aim at Pallet Town, not Indigo")
check(saved and saved.postGameHomeOk, "postGameHomeOk marks the relocate done")
eq(saved and #(saved.hallOfFame or {}), 1, "hall of fame team persisted")
local healed = saved and saved.party and saved.party[1]
local thunderPp = Data.moves and Data.moves.THUNDERBOLT and Data.moves.THUNDERBOLT.pp
check(healed and healed.hp == healed.stats.hp and healed.status == nil
        and healed.moves[1].pp == thunderPp,
      "party is fully healed in the post-credits save")

-- house exit mats resolve to Pallet's door, not Indigo
local Warp = require("src.world.Warp")
local houseExit = { destMap = "LAST_MAP", destWarp = 1, x = 2, y = 7 }
local destMap, dx, dy = Warp.destination(Data, houseExit, saved.lastOutdoor)
eq(destMap, "PALLET_TOWN", "Red's house LAST_MAP -> PALLET_TOWN")
eq(dx, 5, "Pallet door x")
eq(dy, 5, "Pallet door y")

-- stuck-save rescue: HoF + hallOfFame + no postGameHomeOk -> Pallet Town
local stuck = SaveData.newGame()
stuck.player.map = "HALL_OF_FAME"
stuck.player.x, stuck.player.y = 4, 2
stuck.lastOutdoor = { id = "INDIGO_PLATEAU", x = 9, y = 5 }
stuck.hallOfFame = { { { species = "PIKACHU", level = 81 } } }
check(SaveData.needsPostGameRescue(stuck), "pre-fix softlock save needs rescue")
SaveData.applyPostGameHome(stuck, Data.field.boot)
check(not SaveData.needsPostGameRescue(stuck), "rescue is one-shot")
eq(stuck.player.map, "PALLET_TOWN", "rescue warps to the blackout point")
eq(stuck.lastOutdoor.id, "PALLET_TOWN", "rescue retargets LAST_MAP")

-- `jp Init`: everything popped, boot sequence pushed (intro -> title)
eq(#stack2.states, 1, "soft reset leaves exactly the boot state")
local IntroMovie = require("src.ui.IntroMovie")
check(getmetatable(stack2:top()) == IntroMovie,
      "post-credits state is the IntroMovie (jp Init boot path)")
local Game = require("src.core.Game")
check(type(Game.makeTitleState) == "function",
      "Game:makeTitleState exists for the intro's title handoff")

S.finish()
