-- Parity (#552, a regression of #105): the Rocket Hideout B4F grunt drops
-- the LIFT KEY on Yellow too, and the ball he drops reaches the bag.
--
-- Oracle: pokeyellow/scripts/RocketHideoutB4F.asm.  Yellow replaces
-- Rocket1/Rocket2 with Jessie & James and leaves one grunt, spelled
-- ROCKETHIDEOUTB4F_ROCKET / TEXT_ROCKETHIDEOUTB4F_ROCKET where Red and
-- Blue say ROCKET3, so the hand-ported Red/Blue key never matched and the
-- talk fell through to the generic trainer path: battle, no drop, no key,
-- and the B1F elevator stays locked for good.  Yellow also moves the drop
-- off the follow-up talk -- RocketHideoutB4FRocketEndBattleText is
-- text_promptbutton + text_asm, so its SetEvent EVENT_ROCKET_DROPPED_LIFT_KEY
-- and ShowObject TOGGLE_ROCKET_HIDEOUT_B4F_ITEM_5 run the moment the
-- battle ends, not on the next conversation.
--
-- The Red/Blue half stays in tests/parity_lift_key.lua; this one owns the
-- Yellow spelling and the pickup that #105 was originally about.
--
-- Self-contained: `luajit tests/parity_lift_key_yellow_bug552.lua`; also
-- globbed by tests/run_tests.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.maps and Data.maps.PALLET_TOWN) then Data:load() end

local S = require("tests.harness").suite("parity Yellow lift key (#552)")
local check, eq = S.check, S.eq

local mapScripts = require("data.scripts.init")
local OverworldState = require("src.world.OverworldController")
local SaveData = require("src.core.SaveData")
local TextBox = require("src.render.TextBox")
require("src.render.Font").load(Data)

local MAP = "ROCKET_HIDEOUT_B4F"
local BALL = "ROCKETHIDEOUTB4F_LIFT_KEY"

local ballObj
for _, o in ipairs(Data.maps[MAP].objects) do
  if o.name == BALL then ballObj = o end
end
check(ballObj ~= nil, "the LIFT KEY ball is an object_event on B4F")
check(ballObj and ballObj.hidden == true, "it starts hidden")
eq(ballObj and ballObj.item, "LIFT_KEY", "and it carries the LIFT KEY")

-- Yellow's object_event names the same grunt trainer party the Red/Blue
-- Rocket3 does (OPP_ROCKET, 18), so only the text id differs
local script = mapScripts.talkScript(MAP, "TEXT_ROCKETHIDEOUTB4F_ROCKET")
check(type(script) == "function",
      "TEXT_ROCKETHIDEOUTB4F_ROCKET resolves to a hand-ported handler (#552)")
check(type(mapScripts.talkScript(MAP, "TEXT_ROCKETHIDEOUTB4F_ROCKET3")) == "function",
      "the Red/Blue ROCKET3 handler is still registered alongside it")

-- ------------------------------------------------------------------ setup
local function newWorld(defeated)
  local save = SaveData.newGame()
  save.objectToggles = {}
  save.itemsTaken = {}
  save.defeatedTrainers = {}
  local pushed = {}
  local game = {
    data = Data,
    save = save,
    stack = { push = function(_, state) pushed[#pushed + 1] = state end },
  }
  local npc = {
    id = MAP .. "_obj_4",
    def = { name = "ROCKETHIDEOUTB4F_ROCKET", index = 4,
            text = "TEXT_ROCKETHIDEOUTB4F_ROCKET",
            trainerClass = "OPP_ROCKET", trainerParty = 18 },
  }
  if defeated then save.defeatedTrainers[npc.id] = true end
  local ow = {
    map = { id = MAP, def = Data.maps[MAP] },
    npcs = {}, entities = {},
    engagements = 0,
    trainerDefeated = function(_, who)
      return save.defeatedTrainers[who.id] == true
    end,
  }
  -- engageTrainer writes the win before it runs its callback, which is
  -- where the end-battle text's SetEvent + ShowObject belong
  ow.engageTrainer = function(self, who, after)
    self.engagements = self.engagements + 1
    save.defeatedTrainers[who.id] = true
    if after then after() end
  end
  return game, ow, npc, save, pushed
end

-- ------------------------------------------- winning the battle drops it
do
  local game, ow, npc, save, pushed = newWorld(false)
  local done = false
  check(not OverworldState.objectVisible(save, MAP, ballObj),
        "the ball is not on the floor before the fight")
  script(game, ow, npc, function() done = true end)

  eq(ow.engagements, 1, "an undefeated grunt still starts his battle")
  check(save.flags.EVENT_ROCKET_DROPPED_LIFT_KEY == true,
        "EVENT_ROCKET_DROPPED_LIFT_KEY is set the moment the battle ends")
  eq(save.objectToggles[MAP] and save.objectToggles[MAP][BALL], true,
     "ShowObject reveals ROCKETHIDEOUTB4F_LIFT_KEY (TOGGLE_..._ITEM_5)")
  check(OverworldState.objectVisible(save, MAP, ballObj),
        "the LIFT KEY ball is on the floor without a second conversation")
  check(done, "the talk finishes and hands input back")
  eq(#pushed, 0, "the end-battle text is the battle's, not a second box")
end

-- --------------------------------------------- losing it drops nothing
do
  local game, ow, npc, save = newWorld(false)
  ow.engageTrainer = function(self, _, after)
    self.engagements = self.engagements + 1
    if after then after() end -- blacked out: no win recorded
  end
  script(game, ow, npc, function() end)
  check(not save.flags.EVENT_ROCKET_DROPPED_LIFT_KEY,
        "a lost battle drops no key")
  check(not OverworldState.objectVisible(save, MAP, ballObj),
        "and the ball stays hidden")
end

-- ------------------------------------- later talks only reprint the line
do
  local game, ow, npc, save, pushed = newWorld(true)
  save.flags.EVENT_ROCKET_DROPPED_LIFT_KEY = true
  save.objectToggles[MAP] = { [BALL] = true }
  local done = false
  script(game, ow, npc, function() done = true end)
  eq(ow.engagements, 0, "a beaten grunt does not re-engage")
  eq(#pushed, 1, "he says his after-battle line")
  eq(pushed[1] and getmetatable(pushed[1]), TextBox, "in a text box")
  if pushed[1] and pushed[1].onDone then pushed[1].onDone() end
  check(done, "closing that box hands input back to the overworld")
  eq(save.objectToggles[MAP][BALL], true, "the ball is not re-toggled")
end

-- ------------------------------------------ the ball itself, #105's half
-- talkTo's item-ball branch is what turns the revealed object into a bag
-- entry; without it the fix above only puts a sprite on the floor.  Same
-- `Game` upvalue rewiring tests/parity_D.lua uses for the Safari counters.
do
  local game, _, _, save, pushed = newWorld(true)
  save.flags.EVENT_ROCKET_DROPPED_LIFT_KEY = true
  save.objectToggles[MAP] = { [BALL] = true }

  local function bind(fn, upvalue, value)
    local i = 1
    while true do
      local name = debug.getupvalue(fn, i)
      if not name then return false end
      if name == upvalue then debug.setupvalue(fn, i, value); return true end
      i = i + 1
    end
  end
  check(bind(OverworldState.talkTo, "Game", game), "talkTo binds the Game upvalue")
  check(bind(OverworldState.talkTo, "mapScripts", mapScripts),
        "talkTo binds the map-script registry")

  local ball = {
    id = MAP .. "_obj_" .. ballObj.index,
    def = ballObj,
    cellX = ballObj.x, cellY = ballObj.y,
  }
  local world = setmetatable({
    map = { id = MAP, def = Data.maps[MAP] },
    npcs = { ball }, entities = { ball },
  }, { __index = OverworldState })

  world:talkTo(ball)
  eq(save.inventory.LIFT_KEY, 1, "picking the ball up puts the LIFT KEY in the bag (#105)")
  eq(save.itemsTaken[ball.id], true, "the ball is marked taken")
  eq(#world.npcs, 0, "and it leaves the floor")
  check(not OverworldState.objectVisible(save, MAP, ballObj),
        "a taken ball does not respawn on the next map load")
  eq(#pushed, 1, "one box: the found-item line")
  local lines = {}
  for _, page in ipairs(pushed[1] and pushed[1].pages or {}) do
    for _, line in ipairs(page) do lines[#lines + 1] = line end
  end
  check(table.concat(lines, " "):find("LIFT KEY", 1, true) ~= nil,
        "the box names the LIFT KEY")
  eq(Data.items.LIFT_KEY and Data.items.LIFT_KEY.keyItem, true,
     "LIFT KEY is a key item, so it cannot be tossed away again")
end

S.finish()
