-- Driver: the evolution flash must put the WHOLE screen on PAL_BLACK (#279).
-- pokered engine/movie/evolution.asm EvolveMon flashes under PAL_BLACK; only the
-- settled form wears a mon palette, and PAL_BLACK keeps colour 0 as paper white
-- (sgb_palettes.asm:46), so the background behind the silhouettes stays put.
--   POKEPORT_DRIVER=tests/drivers/evolution_black_bug279_test.lua \
--     POKEPORT_IDENTITY=bug279 SHOT_DIR=/tmp/shots love .
return function(game)
  local U = dofile("tests/drivers/util.lua")
  local Probe = dofile("tests/drivers/shot_probe.lua")
  local PaletteFX = require("src.render.PaletteFX")
  local EvolutionState = require("src.ui.EvolutionState")
  local Pokemon = require("src.pokemon.Pokemon")
  local Evolution = require("src.pokemon.Evolution")
  local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"

  local fails = 0
  local function check(ok, msg)
    U.log(ok and "PASS" or "FAIL", msg)
    if not ok then fails = fails + 1 end
    return ok
  end
  local function rgb(c)
    return c and ("(%d,%d,%d)"):format(c[1], c[2], c[3]) or "nil"
  end
  local function ramp(p)
    if not p then return "nil" end
    local s = {}
    for i = 1, 4 do s[i] = rgb(p[i]) end
    return table.concat(s, " ")
  end
  local function samePal(a, b)
    if not a or not b then return false end
    for i = 1, 4 do
      if not a[i] or not b[i] then return false end
      for k = 1, 3 do if a[i][k] ~= b[i][k] then return false end end
    end
    return true
  end

  -- Text speed 1 keeps the "Congratulations!" box brisk; FLASH_FRAMES is a
  -- frame count, so the flash itself is unaffected.
  game.save.options = game.save.options or {}
  game.save.options.textSpeed = 1

  -- =====================================================================
  -- Part 1: the halves the eye cannot check.  A missing BLACK entry or a zone
  -- that does not cover the screen looks exactly like the bug.
  -- =====================================================================
  game.save.options.colors = "gbc"
  PaletteFX.setMode("gbc")

  local BLACK = PaletteFX.pal(game.data, "BLACK")
  check(BLACK ~= nil, "data.palettes carries a BLACK entry (PAL_BLACK)")
  U.log("BLACK ramp:", ramp(BLACK))
  -- sgb_palettes.asm RGB 31,29,31 / 07,07,07 / 02,03,03 / 03,02,02, scaled
  -- to 8 bits by the extractor
  check(BLACK and BLACK[1][1] == 255 and BLACK[1][2] == 239 and BLACK[1][3] == 255,
        "PAL_BLACK colour 0 is still paper white (the background does NOT black out)")
  check(BLACK and BLACK[2][1] == 58 and BLACK[3][1] == 16 and BLACK[4][1] == 25,
        "PAL_BLACK shades 1..3 are crushed (58,58,58 / 16,25,25 / 25,16,16)")

  -- sgbPalettes is a pure function of the state's own fields, so drive it
  -- directly for the cases the live run cannot hold still.  MAGIKARP is REDMON
  -- and GYARADOS BLUEMON: a CATERPIE line settles on GREENMON either way and
  -- would prove nothing.
  local function zonesFor(fields)
    local fake = setmetatable(fields, EvolutionState)
    return EvolutionState.sgbPalettes(fake, game)
  end
  local function colorsFor(fields)
    local z = zonesFor(fields)
    return z and z[1] and z[1].colors, z and z[1]
  end

  local flashCols, flashZone = colorsFor({
    done = false, canceled = false,
    mon = { species = "MAGIKARP" }, newSpecies = "GYARADOS",
  })
  check(samePal(flashCols, BLACK),
        "mid-flash the whole screen is PAL_BLACK, not a mon palette")
  U.log("mid-flash zone resolves to:", ramp(flashCols))
  check(flashZone ~= nil and flashZone.x == 0 and flashZone.y == 0
        and flashZone.w == 160 and flashZone.h == 144,
        "that zone covers the whole 160x144 screen (SetPal_PokemonWholeScreen)")

  local settled = colorsFor({
    done = true, canceled = false,
    mon = { species = "MAGIKARP" }, newSpecies = "GYARADOS",
  })
  check(samePal(settled, PaletteFX.monPal(game.data, "GYARADOS")),
        "once .done, the NEW form wears its own palette again (GYARADOS/BLUEMON)")
  local cancelled = colorsFor({
    done = true, canceled = true,
    mon = { species = "MAGIKARP" }, newSpecies = "GYARADOS",
  })
  check(samePal(cancelled, PaletteFX.monPal(game.data, "MAGIKARP")),
        "a B-cancelled evolution settles on the OLD form (MAGIKARP/REDMON)")

  -- Mode guards: routing the blackout through PaletteFX.pal is what keeps the
  -- other COLORS modes honest.  A hardcoded {0,0,0} passes everything above.
  PaletteFX.setMode("ogred")
  local ogFlash = colorsFor({
    done = false, canceled = false,
    mon = { species = "WEEDLE" }, newSpecies = "KAKUNA",
  })
  check(samePal(ogFlash, PaletteFX.ogBg()),
        "OG RED does NOT black out (a Game Boy Color never sees the SGB packet)")
  PaletteFX.setMode("redpp")
  check(PaletteFX.pal(game.data, "BLACK") ~= nil,
        "RED++ resolves BLACK from data/palettes_gbc.lua (no ROM fallback needed)")
  PaletteFX.setMode("og")
  check(samePal(PaletteFX.effectiveColors(BLACK), PaletteFX.GRAYS),
        "plain DMG replaces it wholesale, so the mono modes are unchanged")
  PaletteFX.setMode("gbc")
  game.save.options.colors = "gbc"

  -- The bytes the pixel probe hunts for, read out of the data and read while
  -- the mode is SGB (under OG RED, PaletteFX.pal short-circuits every name to
  -- the one palette).
  local YELLOW = PaletteFX.monPal(game.data, "WEEDLE")   -- YELLOWMON
  local REDMON = PaletteFX.monPal(game.data, "MAGIKARP") -- REDMON
  local BLUE = PaletteFX.monPal(game.data, "GYARADOS")   -- BLUEMON
  local OGBG = PaletteFX.GBC_BG
  check(YELLOW ~= nil and REDMON ~= nil and BLUE ~= nil,
        "WEEDLE/MAGIKARP/GYARADOS mon palettes resolve")
  U.log("WEEDLE:", ramp(YELLOW), " MAGIKARP:", ramp(REDMON),
        " GYARADOS:", ramp(BLUE))

  local function probe(label, wanted)
    local shot = Probe.grab()
    if not shot then
      U.log("WARN pixel probe unavailable (no screenshot capture); judge",
            label, "by eye only")
      return nil
    end
    local counts, total = Probe.count(shot, wanted)
    local parts = {}
    for name, n in pairs(counts) do
      parts[#parts + 1] = ("%s=%d"):format(name, n)
    end
    table.sort(parts)
    U.log(("probe[%s] %d px sampled: %s"):format(label, total,
          table.concat(parts, " ")))
    return counts
  end

  -- =====================================================================
  -- Part 2: reach the moment and photograph it.
  -- =====================================================================
  U.teleport(game, "ROUTE_1", 5, 5, "down")
  U.wait(10)

  local function evoTop()
    local t = game.stack:top()
    return (t and t.screenId == "EvolutionState") and t or nil
  end
  local function waitFor(cond, max)
    for _ = 1, max or 600 do
      if cond() then return true end
      U.wait(1)
    end
    return false
  end
  local function pagesText(st)
    if not st or not st.pages then return nil end
    local parts = {}
    for _, page in ipairs(st.pages) do
      if type(page) == "table" then
        for _, line in ipairs(page) do
          if type(line) == "string" then parts[#parts + 1] = line end
        end
      elseif type(page) == "string" then
        parts[#parts + 1] = page
      end
    end
    return table.concat(parts, " ")
  end
  local function findText(needle)
    for _, st in ipairs(game.stack.states or {}) do
      local blob = pagesText(st)
      if blob and blob:find(needle, 1, true) then return st end
    end
    return nil
  end
  local function mashUntil(cond, max)
    for _ = 1, max or 200 do
      if cond() then return true end
      U.tap(game, "a")
      U.wait(3)
    end
    return cond()
  end

  -- Jump the flash clock to just under FLASH_FRAMES (368) once the shot is
  -- taken.  EvolutionState:update only compares self.t against that constant,
  -- so this ends the animation early without touching the palette logic.
  local function skipToEnd()
    local st = evoTop()
    if st then st.t = 362 end
  end

  local function startEvo(species, into)
    local mon = Pokemon.new(game.data, species, 7)
    table.insert(game.save.party, 1, mon)
    Evolution.evolve(game, mon, into)
    if not waitFor(evoTop, 300) then
      check(false, "EvolutionState opened for " .. species .. " -> " .. into)
      return nil
    end
    return mon
  end

  -- ---- case 1: the reported case, SGB, mid-flash --------------------------
  local weedle = startEvo("WEEDLE", "KAKUNA")
  U.wait(24) -- into the flash, far short of FLASH_FRAMES = 368
  U.shot(game, DIR .. "/evo279_1_flash_sgb.png")
  local c1 = probe("flash/SGB", {
    monYellow1 = YELLOW[2], monYellow2 = YELLOW[3],
    black1 = BLACK[2], black2 = BLACK[3], paper = BLACK[1],
  })
  if c1 then
    check(c1.monYellow1 == 0 and c1.monYellow2 == 0,
          "no WEEDLE/KAKUNA yellow anywhere on screen during the flash")
    check(c1.black1 + c1.black2 > 0,
          "the crushed PAL_BLACK shades ARE on screen (the silhouette)")
    check(c1.paper > 0,
          "paper white still fills the background (PAL_BLACK keeps colour 0)")
  end

  -- ---- case 1b: it comes back once the flash is over ----------------------
  skipToEnd()
  if not waitFor(function() return findText("evolved into") ~= nil end, 240) then
    check(false, "the Congratulations text appears when the flash ends")
  end
  U.wait(50) -- let the box finish typing before the shot
  U.shot(game, DIR .. "/evo279_2_congrats_sgb.png")
  local c2 = probe("congrats/SGB", {
    monYellow1 = YELLOW[2], monYellow2 = YELLOW[3], black1 = BLACK[2],
  })
  if c2 then
    check(c2.monYellow1 + c2.monYellow2 > 0,
          "the settled KAKUNA is yellow again under the Congratulations box")
  end
  check(weedle == nil or weedle.species == "KAKUNA",
        "the mon actually evolved (control: the fix touched no game logic)")
  mashUntil(function() return not evoTop() and not findText("evolved into") end, 90)
  U.wait(10)

  -- ---- case 2: B-cancel settles on the OLD form's colours -----------------
  local karp = startEvo("MAGIKARP", "GYARADOS")
  U.wait(100) -- past the 80-frame pre-animLoop delay before the poll starts
  U.hold(game, "b", 20) -- evolution.asm Evolution_CheckForCancel
  if not waitFor(function() return findText("stopped evolving") ~= nil end, 240) then
    check(false, "holding B prints \"stopped evolving\"")
  end
  U.wait(50)
  U.shot(game, DIR .. "/evo279_3_cancelled_sgb.png")
  local c3 = probe("cancelled/SGB", {
    red1 = REDMON[2], red2 = REDMON[3], blue1 = BLUE[2], blue2 = BLUE[3],
  })
  if c3 then
    check(c3.red1 + c3.red2 > 0,
          "a cancelled MAGIKARP settles back into MAGIKARP's own red")
    check(c3.blue1 == 0 and c3.blue2 == 0,
          "no GYARADOS blue leaks in (wEvoOldSpecies, not the new species)")
  end
  check(karp == nil or karp.species == "MAGIKARP", "the cancelled mon kept its species")
  mashUntil(function() return not evoTop() and not findText("stopped evolving") end, 90)
  U.wait(10)

  -- ---- case 3: OG RED must NOT black out ---------------------------------
  -- A Game Boy Color never sees the SGB packet, so its one global BG palette
  -- applies all the way through.  Switch mode with only the overworld on the
  -- stack: setMode's cache drop / map reload must not land on a live evolution.
  game.save.options.colors = "ogred"
  PaletteFX.setMode("ogred")
  U.wait(20)
  startEvo("WEEDLE", "KAKUNA")
  U.wait(24)
  U.shot(game, DIR .. "/evo279_4_flash_ogred.png")
  local c4 = probe("flash/OG RED", {
    ogPink = OGBG[2], ogRed = OGBG[3], black1 = BLACK[2], black2 = BLACK[3],
  })
  if c4 then
    check(c4.ogPink + c4.ogRed > 0, "OG RED stays red/pink through the flash")
    check(c4.black1 == 0 and c4.black2 == 0,
          "PAL_BLACK's crushed shades never appear in OG RED")
  end
  skipToEnd()
  waitFor(function() return findText("evolved into") ~= nil end, 240)
  mashUntil(function() return not evoTop() and not findText("evolved into") end, 90)

  game.save.options.colors = "gbc"
  PaletteFX.setMode("gbc")
  U.wait(20)

  U.log(fails == 0 and "all #279 preconditions passed"
                    or (fails .. " #279 precondition(s) FAILED -- read up"))

  -- =====================================================================
  -- Part 3: one more, live, for the human.
  -- =====================================================================
  startEvo("WEEDLE", "KAKUNA")

  U.log("A WEEDLE is evolving now in SGB colours, about four seconds.  While the")
  U.log("two forms trade places the whole screen is on PAL_BLACK: near-black")
  U.log("silhouettes on the unchanged off-white background, colour back only")
  U.log("under the Congratulations box.  #279 flashed both forms in full colour.")
  U.log("Hold B mid-flash to cancel; it settles on the OLD form's colours.")
  U.log("Screenshots: " .. DIR .. "/evo279_*.png")

  while true do
    coroutine.yield()
  end
end
