-- Driver: the Pewter JIGGLYPUFF sings AND dances (#249).
-- scripts/PewterPokecenter.asm PewterPokecenterJigglypuffText: stop the music,
-- DelayFrames 32, MUSIC_JIGGLYPUFF_SONG, then a clockwise quarter turn every 24
-- frames until the song ends, 48 more frames, PlayDefaultMusic.  TextScriptEnd
-- is the only thing that closes the box.  Not under POKEPORT_SPEED.
--   POKEPORT_DRIVER=tests/drivers/jigglypuff_bug249_test.lua love .
return function(game)
  local U = dofile("tests/drivers/util.lua")
  local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
  local mapScripts = require("data.scripts.init")
  local TextBox = require("src.render.TextBox")
  local Music = require("src.core.Music")

  local MAP = "PEWTER_POKECENTER"
  local NPC = "PEWTERPOKECENTER_JIGGLYPUFF"
  local TEXT = "TEXT_PEWTERPOKECENTER_JIGGLYPUFF"
  local SONG, MAP_SONG = "Music_JigglypuffSong", "Music_Pokecenter"
  -- scripts/PewterPokecenter.asm .FacingDirections, in order
  local RING = { "down", "left", "up", "right" }
  local SILENCE, STEP, TAIL = 32, 24, 48 -- the three DelayFrames counts

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

  local function nextInRing(dir)
    for i, d in ipairs(RING) do
      if d == dir then return RING[i % #RING + 1] end
    end
    return nil
  end

  -- ---- preconditions the eye cannot check --------------------------------
  -- A missing handler, text entry or song def all look the same on screen: a
  -- JIGGLYPUFF that just stands there.
  local handler = mapScripts.talkScript(MAP, TEXT)
  check(MAP .. "/" .. TEXT .. " has a talk handler",
        type(handler) == "function")

  local line = game.data.text and game.data.text._PewterPokecenterJigglypuffText
  check("_PewterPokecenterJigglypuffText resolves to a string",
        type(line) == "string" and line ~= "")
  if type(line) == "string" then
    U.log("the box should read:", (line:gsub("\n", " / ")))
  end

  -- Music.playOnce returns false on a missing def, and the dance then skips
  -- straight to its 48-frame tail with no spin at all.
  local songs = game.data.audio and game.data.audio.songs
  check("audio.songs." .. SONG .. " resolves (PlayMusic MUSIC_JIGGLYPUFF_SONG)",
        songs ~= nil and songs[SONG] ~= nil)
  check("audio.songs." .. MAP_SONG .. " resolves (PlayDefaultMusic comes back to it)",
        songs ~= nil and songs[MAP_SONG] ~= nil)
  local mapSong = game.data.audio and game.data.audio.mapSongs
                  and game.data.audio.mapSongs[MAP]
  check("the Center's map theme is " .. MAP_SONG, mapSong == MAP_SONG)

  -- SPRITE_FAIRY has to be a walker or three of the four facings have no
  -- frames to draw and the "turn" is invisible even when it happens
  local fairy = game.data.sprites and game.data.sprites.SPRITE_FAIRY
  check("SPRITE_FAIRY renders all four facings",
        fairy ~= nil and fairy.walker == true and (fairy.frames or 0) >= 4)

  local opts = game.save.options or {}
  U.log("audio device present:", love.audio ~= nil,
        "  MUSIC VOL (0-7):", tostring(opts.musicVol),
        "  SFX VOL (0-7):", tostring(opts.sfxVol))
  if not love.audio or opts.musicVol == 0 then
    U.log("WARNING: music output is off, so the cut to silence and the song",
          "itself will not be audible; raise MUSIC VOL in OPTION first")
  end

  -- ---- park the player against the JIGGLYPUFF -----------------------------
  -- data/maps/objects/PewterPokecenter.asm: object_event 1, 3, SPRITE_FAIRY.
  -- It sits against the west wall, so approach from the floor to its east.
  local STAND = { x = 2, y = 3, facing = "left" }
  U.teleport(game, MAP, STAND.x, STAND.y, STAND.facing)
  U.wait(10)

  local function puffIn(ow)
    for _, n in ipairs(ow.npcs or {}) do
      if n.def and n.def.name == NPC then return n end
    end
    return nil
  end

  -- re-reads game.overworld every call: the fallback below teleports again,
  -- which rebuilds the state and its npc list
  local function facingThePuff()
    local ow = game.overworld
    local puff = ow and puffIn(ow)
    if not puff then return false end
    local fx, fy = ow.player:facingCell()
    return ow:npcAtCell(fx, fy) == puff
  end

  local ow = game.overworld
  local puff = ow and puffIn(ow)
  check("JIGGLYPUFF object loaded on " .. MAP, puff ~= nil)

  if puff and not facingThePuff() then
    -- Approach cell is blocked (map edit, or a mod moved the object): take any
    -- free neighbour instead.  {dx, dy, facing} is the offset from the fairy
    -- plus the direction that looks back at it, so +1 on x means facing left.
    local sides = {
      { 1, 0, "left" }, { 0, 1, "up" }, { 0, -1, "down" }, { -1, 0, "right" },
    }
    for _, s in ipairs(sides) do
      local cx, cy = puff.cellX + s[1], puff.cellY + s[2]
      if ow.map:isWalkableCell(cx, cy) and not ow:npcAtCell(cx, cy) then
        U.log(("approach cell (%d, %d) is blocked, standing on")
                :format(STAND.x, STAND.y), cx, cy, "facing", s[3])
        U.teleport(game, MAP, cx, cy, s[3])
        U.wait(10)
        break
      end
    end
    puff = game.overworld and puffIn(game.overworld)
  end
  check("player is standing against the JIGGLYPUFF", facingThePuff())

  -- ---- one frame, sampling everything at once ----------------------------
  local function boxOnTop()
    local top = game.stack:top()
    if getmetatable(top) == TextBox then return top end
    return nil
  end

  local function sample(pressA)
    if pressA then U.tap(game, "a") else U.wait(1) end
    local box = boxOnTop()
    return {
      box = box ~= nil,
      typed = box ~= nil and box.done == true,
      tick = box ~= nil and box.auto ~= nil and type(box.auto.tick) == "function",
      facing = puff and puff.facing or nil,
      song = Music.oneShotPlaying(),
    }
  end

  -- ---- 1. talk to it and watch the whole dance, mashing A throughout -----
  U.log("--- run 1: the full dance, with A held down on it ---------------")
  local before = puff and puff.facing
  U.tap(game, "a")
  U.wait(2)
  local box = boxOnTop()
  check("pressing A opened a text box", box ~= nil)
  if box then
    U.log("the JIGGLYPUFF turned to face the player:",
          tostring(before), "->", tostring(puff.facing))
    check("the box carries an auto.tick hook (the per-frame dance driver)",
          box.auto ~= nil and type(box.auto.tick) == "function")
    check("the box is an auto box, so no blinking arrow and no A dismissal",
          box.auto ~= nil)
  end

  -- Mash A every 3rd frame for the whole run; in the original the box cannot
  -- be dismissed at all, so this must change nothing.
  local log, turns = {}, {}
  local typedAt, songAt, songEnd, closedAt = nil, nil, nil, nil
  local last = puff and puff.facing
  for i = 1, 3000 do
    log[i] = sample(i % 3 == 0)
    if not typedAt and log[i].typed then typedAt = i end
    if not songAt and log[i].song then songAt = i end
    if songAt and not songEnd and not log[i].song then songEnd = i end
    if log[i].facing and log[i].facing ~= last then
      turns[#turns + 1] = { at = i, from = last, to = log[i].facing }
      last = log[i].facing
    end
    if not log[i].box then closedAt = i break end
  end

  U.log(("frames: text finished typing at %s, song started at %s, song ended " ..
         "at %s, box closed at %s")
          :format(tostring(typedAt), tostring(songAt), tostring(songEnd),
                  tostring(closedAt)))
  for n, t in ipairs(turns) do
    U.log(("  turn %d on frame %d: %s -> %s"):format(n, t.at, tostring(t.from),
                                                     tostring(t.to)))
  end

  if check("the song played", songAt ~= nil) then
    -- SFX_STOP_ALL_MUSIC, DelayFrames 32, PlayMusic
    local gap = songAt - (typedAt or 1)
    U.log(("silence between the text finishing and the song starting: %d frames " ..
           "(pokered waits %d)"):format(gap, SILENCE))
    check(("that silence is about %d frames"):format(SILENCE),
          gap >= SILENCE - 4 and gap <= SILENCE + 8)
  end

  check("the JIGGLYPUFF turned at least four times", #turns >= 4)
  local ringOk, spacingOk = true, true
  for n, t in ipairs(turns) do
    if t.to ~= nextInRing(t.from) then
      ringOk = false
      U.log(("     turn %d is not a clockwise quarter turn: %s -> %s (expected %s)")
              :format(n, tostring(t.from), tostring(t.to),
                      tostring(nextInRing(t.from))))
    end
    if n > 1 then
      local d = t.at - turns[n - 1].at
      if d < STEP - 6 or d > STEP + 6 then
        spacingOk = false
        U.log(("     turn %d came %d frames after the last one (expected %d)")
                :format(n, d, STEP))
      end
    end
  end
  check("every turn is one clockwise quarter turn (DOWN->LEFT->UP->RIGHT)",
        ringOk and #turns >= 4)
  check(("the turns are %d frames apart"):format(STEP),
        spacingOk and #turns >= 2)

  -- not just "it eventually closed": an A-dismissable box closes too, only far
  -- too early, and that is the bug
  check("mashing A never closed the box early",
        (closedAt or #log) >= SILENCE + 4 * STEP)
  if check("the box closed itself with no button press", closedAt ~= nil) then
    check("it stayed up for the whole song, A mashing and all",
          songEnd ~= nil and closedAt > songEnd)
    if songEnd then
      local tail = closedAt - songEnd
      U.log(("the box lingered %d frames after the song (pokered waits %d, " ..
             "plus the box's own pop delay)"):format(tail, TAIL))
      check(("that tail is about %d frames"):format(TAIL),
            tail >= TAIL - 6 and tail <= TAIL + 20)
    end
  end

  -- ---- 2. do it again, screenshot each quarter turn, hand off mid-song ---
  U.log("--- run 2: screenshots, then the pad is yours -------------------")
  U.wait(30)
  puff = game.overworld and puffIn(game.overworld)
  check("still standing against the JIGGLYPUFF", facingThePuff())
  U.tap(game, "a")
  U.wait(2)
  U.shot(game, DIR .. "/bug249_0_box.png")

  local shots, seen = 0, puff and puff.facing
  for _ = 1, 600 do
    U.wait(1)
    if not boxOnTop() then break end
    if puff and puff.facing ~= seen then
      seen = puff.facing
      shots = shots + 1
      -- U.shot costs a frame or two, which is why run 1 did the timing
      local path = ("%s/bug249_%d_%s.png"):format(DIR, shots, seen)
      if U.shot(game, path) then U.log("captured", path) end
      if shots >= 4 then break end
    end
  end
  check("captured four quarter turns", shots >= 4)

  U.log("The JIGGLYPUFF has been talked to and is mid-song. Mash A: the box has no")
  U.log("arrow and must not close. The fairy should turn a quarter turn clockwise")
  U.log("(down, left, up, right) every 24 frames until the song ends, then close")
  U.log("itself (#249). The Center theme returns just before it does; expected.")

  while true do
    coroutine.yield()
  end
end
