-- Parity test,  the Pallet Town intro cutscene (Oak escort) and the
-- Oak-speech shrink assets.
-- Self-contained: run via `luajit tests/parity_intro.lua`; also dofile'd
-- by tests/run_tests.lua's aggregator.
--
-- Sources: scripts/PalletTown.asm, engine/overworld/auto_movement.asm
-- (PalletMovementScriptPointerTable, RLEList_ProfOakWalkToLab,
-- RLEList_PlayerWalkToLab), engine/overworld/pathfinding.asm
-- (FindPathToPlayer), engine/movie/oak_speech/oak_speech.asm.
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 intro")
local check, eq = S.check, S.eq

local mapScripts = require("data.scripts.init")
local pallet = mapScripts.get("PALLET_TOWN")
check(pallet and pallet.escort, "PALLET_TOWN exposes the escort tables")
local escort = pallet.escort

local function joined(t) return table.concat(t, ",") end

-- =====================================================================
-- (A) Oak's walk to the player: FindPathToPlayer from his object spot
-- (8,5) to one tile below the player (hNPCPlayerYDistance decremented
-- before the predef).  Reducing the greater remaining axis each step
-- (ties go to X) yields a strict zigzag.
-- =====================================================================
eq(joined(escort.oakApproach(10)), "up,right,up,right,up",
   "Oak approach to the left tile (player at 10,1)")
eq(joined(escort.oakApproach(11)), "right,up,right,up,right,up",
   "Oak approach to the right tile (player at 11,1)")

-- walking the approach from (8,5) must land exactly on (playerX, 2)
for _, px in ipairs({ 10, 11 }) do
  local cx, cy = 8, 5
  local D = { up = { 0, -1 }, down = { 0, 1 }, left = { -1, 0 }, right = { 1, 0 } }
  for _, dir in ipairs(escort.oakApproach(px)) do
    cx, cy = cx + D[dir][1], cy + D[dir][2]
  end
  check(cx == px and cy == 2,
        ("Oak's approach ends below the player at (%d,2)"):format(px))
end

-- =====================================================================
-- (B) The escort to the lab.  Oak: RLEList_ProfOakWalkToLab = DOWN x5,
-- LEFT, DOWN x5, RIGHT x3, UP (the NPC_CHANGE_FACING tail is a march-
-- in-place beat, not a step).  Player: RLEList_PlayerWalkToLab plays in
-- reverse buffer order = DOWN x6, LEFT, DOWN x5, RIGHT x3, UP x2, and
-- the last UP is consumed by the door-warp frame -- so the realized
-- player path is Oak's path with one extra leading DOWN.
-- =====================================================================
eq(joined(escort.oakSteps),
   "down,down,down,down,down,left,down,down,down,down,down,right,right,right,up",
   "Oak's walk-to-lab movement (RLEList_ProfOakWalkToLab)")
eq(#escort.oakSteps, 15, "Oak takes 15 steps")
eq(joined(escort.playerSteps),
   "down," ..
   "down,down,down,down,down,left,down,down,down,down,down,right,right,right,up",
   "player's realized walk (RLEList_PlayerWalkToLab reversed, warp eats the 17th press)")
eq(#escort.playerSteps, 16, "player takes 16 real steps")

-- both start one apart and stay in lockstep; both paths end on the lab
-- door at (12,11)
do
  local D = { up = { 0, -1 }, down = { 0, 1 }, left = { -1, 0 }, right = { 1, 0 } }
  local ox, oy = 10, 2   -- Oak, below the player on the left tile
  local px, py = 10, 1   -- player
  for i = 1, #escort.playerSteps do
    local od = escort.oakSteps[i]
    if od then ox, oy = ox + D[od][1], oy + D[od][2] end
    local pd = escort.playerSteps[i]
    px, py = px + D[pd][1], py + D[pd][2]
    if i < #escort.playerSteps then
      check(math.abs(ox - px) + math.abs(oy - py) == 1,
            ("beat %d: player stays exactly one tile behind Oak"):format(i))
    end
  end
  check(ox == 12 and oy == 11, "Oak's walk ends on the lab door (12,11)")
  check(px == 12 and py == 11, "player's walk ends on the lab door (12,11)")
end

-- the door tile is the real Pallet lab door and resolves to the lab's
-- second warp, the (5,11) mat the walk-in starts from
local MapLoader = require("src.world.MapLoader")
local Warp = require("src.world.Warp")
local town = MapLoader.load(Data, "PALLET_TOWN")
local w = town:warpAtCell(12, 11)
check(w and w.def.destMap == "OAKS_LAB", "(12,11) is the Oak's Lab door warp")
local dm, dx, dy = Warp.destination(Data, { destMap = "OAKS_LAB", destWarp = 2 })
check(dm == "OAKS_LAB" and dx == 5 and dy == 11,
      "the door warps onto the lab mat at (5,11)")

-- the walk-in cast: door Oak at (5,10), desk Oak at (5,2), both
-- initially hidden; Pallet's Oak object at (8,5), initially hidden
local function obj(mapId, name)
  for _, o in ipairs(Data.maps[mapId].objects) do
    if o.name == name then return o end
  end
end
local oak2 = obj("OAKS_LAB", "OAKSLAB_OAK2")
check(oak2 and oak2.x == 5 and oak2.y == 10 and oak2.hidden and oak2.index == 8,
      "OAKSLAB_OAK2 hides at the door (5,10)")
local oak1 = obj("OAKS_LAB", "OAKSLAB_OAK1")
check(oak1 and oak1.x == 5 and oak1.y == 2 and oak1.hidden and oak1.index == 5,
      "OAKSLAB_OAK1 hides behind the desk (5,2)")
local poak = obj("PALLET_TOWN", "PALLETTOWN_OAK")
check(poak and poak.x == 8 and poak.y == 5 and poak.hidden and poak.index == 1,
      "PALLETTOWN_OAK hides at (8,5)")

-- =====================================================================
-- (C) Oak speech shrink: the extracted ShrinkPic1/ShrinkPic2 manifest
-- and the sounds/music the sequence uses.
-- =====================================================================
local oakGfx = Data.field.oakSpeech
check(oakGfx and oakGfx.shrink1 and oakGfx.shrink2,
      "field.oakSpeech lists both shrink frames")
if oakGfx then
  for _, key in ipairs({ "shrink1", "shrink2" }) do
    local fh = io.open(oakGfx[key], "rb")
    check(fh ~= nil, ("%s exists on disk (%s)"):format(key, tostring(oakGfx[key])))
    if fh then fh:close() end
  end
end
check(Data.audio.sfx and Data.audio.sfx.Shrink ~= nil, "SFX_SHRINK is extracted")
check(Data.audio.songs and Data.audio.songs.Music_Routes2 ~= nil
      and Data.audio.songs.Music_MeetProfOak ~= nil,
      "Routes2 + MeetProfOak songs are extracted")

-- =====================================================================
-- (D) Lab walk-in with UP held: a Delay3 emote queued from Oak's entry
-- onDone must not leave a one-frame handleInput gap, or the held press
-- walks an extra tile and PlayerEntryMovementRLE (up x8) lands on desk
-- Oak at (5,2) instead of (5,3).
-- =====================================================================
do
  local SaveData = require("src.core.SaveData")
  local Game = require("src.core.Game")
  local StateStack = require("src.core.StateStack")
  local OverworldState = require("src.world.OverworldController")
  local Commands = require("src.script.Commands")
  local prev = { data = Game.data, save = Game.save, stack = Game.stack,
                 input = Game.input, renderer = Game.renderer,
                 overworld = Game.overworld }
  Game.data = Data
  Game.save = SaveData.newGame(Data)
  Game.save.player.name = "RED"
  Game.save.objectToggles = { OAKS_LAB = { OAKSLAB_OAK2 = true } }
  StateStack:init()
  Game.stack = StateStack
  Game.input = {
    isDown = function(_, b) return b == "up" end,
    wasPressed = function() return false end,
    step = function() end, state = {}, pressQueue = {},
  }
  Game.renderer = {
    beginWorldPass = function() end, endWorldPass = function() end,
    beginUIPass = function() end, endUIPass = function() end,
    worldViewSize = function() return 160, 144 end,
    setSGBZones = function() end,
  }
  StateStack:push(OverworldState, "OAKS_LAB", 5, 11, "up")
  local ow = OverworldState
  Game.overworld = ow
  local ctx = { save = Game.save, game = Game, overworld = ow }
  local finished = false
  local function swapOaks()
    Commands.hide_object(ctx, "OAKS_LAB", "OAKSLAB_OAK2")
    Commands.show_object(ctx, "OAKS_LAB", "OAKSLAB_OAK1")
    ow.emote = { frames = 3, onDone = function()
      ow:scriptMove(ow.player, "up", 8, function() finished = true end)
    end }
  end
  ow:scriptMove(ow:npcByIndex(8), "up", 3, swapOaks)
  for _ = 1, 600 do
    ow:update(1)
    if finished then break end
  end
  check(finished, "held-UP lab walk-in completes")
  eq(ow.player.cellX, 5, "held-UP walk-in x stays in the aisle")
  eq(ow.player.cellY, 3, "held-UP walk-in ends at (5,3), not on desk Oak")
  local oak1 = ow:npcByIndex(5)
  check(oak1 and not (oak1.cellX == ow.player.cellX and oak1.cellY == ow.player.cellY),
        "player does not stack on desk Oak after walk-in")
  for k, v in pairs(prev) do Game[k] = v end
end

S.finish()
