-- One-column launcher reach (#852) and the safe-area launcher anchor (#810).
-- No pokered cite: the launcher is port-only chrome.
--
-- #852: minPanelHeight in src/import/LauncherView.lua was a flat 460*s tuned
-- for the two-column layout.  A one-column window (portrait phone, squat 4:3
-- device) stacks title + actions card + slot card + the pinned
-- Play/Reset-rebinds/Touch-Controls block, which needs more room; the flat
-- threshold read "tall enough", so the short-window page scroll never
-- engaged, buildSlotCard was cut by Kit.pushClip against the pinned block,
-- and Kit's clip-bounded hit-testing (src/ui/kit/Kit.lua) left every slot
-- row, the pager and "+ New save slot" drawn-but-inert.  The fix makes the
-- threshold column-aware, so those windows scroll instead of clipping.
--
-- The seam is LauncherView.draw itself: it publishes the page-scroll extent
-- on the importer (imp._pageScroll / imp._pageScrollMax, the values the
-- touch-drag and wheel paths feed), so a headless draw shows whether the
-- scroll engaged without reading any file-local constant.
--
-- What is asserted here is REACHABILITY, not scrolling: "+ New save slot" is
-- the control that sits at the very bottom of the one-column pile, and the
-- bug was that it drew where no tap could land.  Either it fits in the
-- window outright, or the page scrolls far enough to bring it in -- both are
-- correct, and which one a given window gets depends on how tall the panel's
-- content happens to be.  Asserting "this window scrolls" instead pinned the
-- test to the size of the stack: when the pinned Touch-Controls /
-- Reset-rebinds pair moved behind the gear and the save-file buttons moved
-- into the slot card, 480x900 started fitting outright and a scroll
-- assertion failed on a window that had just got BETTER.
--
-- #810 gets its unit-conversion pin in tests/engine/safe_area_units_test.lua;
-- here the complementary end-to-end anchor: Layout.metrics must place the
-- launcher at the corrected safe-area origin, not a DPI-inflated band down
-- the screen.
--   luajit tests/engine/launcher_one_column_reach_bug852.lua

package.path = "./?.lua;./?/init.lua;" .. package.path

local T = require("tests.harness")
local check, eq = T.check, T.eq
love = love or require("tests.love_stub")

-- The launcher touches two graphics calls the shared stub does not carry
-- (focus-ring joins, the footer's BCG invert shader); both are draw-only, so
-- inert fills are enough for the layout arithmetic under test.
love.graphics.setLineJoin = love.graphics.setLineJoin or function() end
love.graphics.newShader = love.graphics.newShader or function() return {} end

local Layout = require("src.ui.kit.Layout")
local Kit = require("src.ui.kit.Kit")
local RomImporter = require("src.import.RomImporter")
local LauncherView = require("src.import.LauncherView")

local function window(w, h)
  love.graphics.getDimensions = function() return w, h end
  love.graphics.getPixelDimensions = function() return w, h end
end

-- A fresh launcher on the Red tab; no cache exists headless, so every
-- version sits in its "ROM required" state, which still lays out the full
-- one-column pile (title, actions card, slot card, pinned block).
local function freshLauncher()
  return RomImporter.new(function() end, { launcher = true })
end

-- Draw a frame with the layout audit on and report where "+ New save slot"
-- landed.  Kit records the rect every clickable control occupies plus the
-- clip that bounds its hit test, so this sees exactly what a tap would.
local function newSlotRect(imp)
  Kit.audit = {}
  LauncherView.draw(imp)
  local found
  for _, r in ipairs(Kit.audit) do
    if r.class == "control" and tostring(r.label):find("New save slot", 1, true) then
      found = r
    end
  end
  Kit.audit = nil
  return found
end

-- The control is reachable when its rect, intersected with whatever clip
-- bounds it, still has real area inside the window -- either straight away or
-- after the page is scrolled to the bottom.
local function reachable(imp, H)
  local function visible()
    local r = newSlotRect(imp)
    if not r then return false end
    local y1, y2 = r.y, r.y + r.h
    if r.clip then
      y1 = math.max(y1, r.clip.y)
      y2 = math.min(y2, r.clip.y + r.clip.h)
    end
    y1, y2 = math.max(y1, 0), math.min(y2, H)
    return (y2 - y1) > 1
  end
  if visible() then return true end
  imp._pageScroll = 1e6          -- clamps to the extent inside draw()
  return visible()
end

-- ------------------------------------------------ #852: the bottom is reachable
-- 480x900 one column: enough room for the old flat 460*s threshold, not for
-- the one-column stack.  Before the fix draw() left _pageScrollMax at 0 here
-- and the slot card sat clipped inert against the pinned buttons.
window(480, 900)
local m = Layout.metrics(1200)
eq(m.twoCol, false, "480-wide window lays out one column")
local imp = freshLauncher()
LauncherView.draw(imp)
check(reachable(imp, 900),
  "one-column window can reach the bottom of the slot card")
imp = freshLauncher()
LauncherView.draw(imp)
eq(imp._pageScroll, 0, "a fresh page starts at the top")

-- The reporter's portrait phone (360x780 units) is shorter and narrower, so
-- it is the one that still engages the scroll; before the fix its slot list
-- was unreachable at any offset.
window(360, 780)
local pm = Layout.metrics(1200)
local phone = freshLauncher()
LauncherView.draw(phone)
check((phone._pageScrollMax or 0) > 0,
  "portrait-phone one-column window engages the page scroll")
eq(phone._pageScroll, 0, "a fresh page starts at the top")

-- The wheel moves the page (the same offset the touch drag feeds), and the
-- offset clamps to the extent, so the whole stack down to "+ New save slot"
-- and the footer is reachable rather than clipped away.
local extent = phone._pageScrollMax
phone._wheelY = -1
LauncherView.draw(phone)
eq(phone._pageScroll, math.min(math.floor(48 * pm.s), extent),
  "one wheel notch scrolls the page down by its step")
phone._pageScroll = 1e6
LauncherView.draw(phone)
eq(phone._pageScroll, phone._pageScrollMax,
  "an offset past the end clamps to the extent, so the bottom is reachable")
check(reachable(phone, 780),
  "the scrolled portrait phone reaches the bottom of the slot card")

-- A one-column window tall enough for the whole stack stays inert: the
-- column-aware minimum is a floor, not a permanent scroll.
window(480, 1200)
local tall = freshLauncher()
LauncherView.draw(tall)
eq(tall._pageScrollMax, 0,
  "a tall one-column window does not scroll for nothing")

-- --------------------------------------- #810: launcher anchored in units
-- Layout.metrics anchors the launcher at SafeArea.rect's origin.  Feed it
-- the iOS 16 portrait frame that reported the safe rect in framebuffer
-- pixels (3x DPI): the launcher must start at the 44-unit notch inset, not
-- 132 units down with the top of the window black (the #810 report).  The
-- rescale itself is pinned in tests/engine/safe_area_units_test.lua.
love.graphics.getDimensions = function() return 375, 812 end
love.graphics.getPixelDimensions = function() return 1125, 2436 end
local oldSafe = love.window.getSafeArea
love.window.getSafeArea = function() return 0, 132, 1125, 2232 end
local ios = Layout.metrics(1200)
eq(ios.top, 44, "launcher anchors at the unit-space notch inset")
eq(ios.h, 744, "launcher gets the full unit-space safe height")
love.window.getSafeArea = oldSafe

T.finish("launcher one-column reach")
