-- BATTLE SIZE "fixed" + BATTLE BG "world": the battle draws as a fixed
-- letterbox over the map, stepped down with the survey zoom.  The PKMN and
-- ITEM menus it opens have to stay that size, and its YES/NO prompt has to
-- stay inside it.
--
-- Both broke the same way -- by reading a fact about THIS FRAME instead of
-- about the battle:
--
--   * Renderer:uiScale follows the zoom only while a world is behind the UI,
--     gated on worldActive (beginWorldPass set it this frame).  A "world"-bg
--     battle is non-opaque so the map keeps drawing under it -- but PartyMenu
--     and ListMenu ARE opaque, so pushing one makes StateStack:visibleBase
--     skip the map, the world pass never runs, and the menu loses the
--     step-down and blits a whole integer scale larger than the battle it
--     just covered.  ("fill" hid this: it overrides the scale outright.)
--
--   * ChoiceBox bottom-anchored unconditionally, which docks it to the
--     WINDOW's bottom edge.  That is right only when it is riding the
--     dialogue box below it, which is anchored there too.  The battle draws
--     its own text inside the battle canvas, so the switch offer's YES/NO was
--     the only piece of that prompt flung to the window edge -- further off
--     the smaller the fixed battle is drawn.
--   luajit tests/engine/battle_fixed_menu_scale.lua

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

local T = require("tests.modkit")
local Renderer = require("src.render.Renderer")
local Zoom = require("src.render.Zoom")
local TextBox = require("src.render.TextBox")
local ChoiceBox = require("src.ui.ChoiceBox")

-- ------------------------------------------------- the UI scale holds its size

-- pin the window so the scales below are exact numbers rather than whatever
-- the runner happens to be sized at (this suite also runs under real LOVE)
local g = love.graphics
local realDims, realPixelDims = g.getDimensions, g.getPixelDimensions
g.getDimensions = function() return 640, 576 end
g.getPixelDimensions = function() return 640, 576 end

-- 640x576 fits the 160x144 classic surface at exactly 4x
T.eq(Renderer:fitScale(), 4, "the fixture window fits the classic surface at 4x")

local function uiScaleWith(offset, worldActive, hold)
  local oldOffset, oldActive, oldHold =
    Zoom.offset, Renderer.worldActive, Renderer.uiWorldHold
  Zoom.offset, Renderer.worldActive, Renderer.uiWorldHold =
    offset, worldActive, hold
  local s = Renderer:uiScale()
  Zoom.offset, Renderer.worldActive, Renderer.uiWorldHold =
    oldOffset, oldActive, oldHold
  return s
end

T.eq(uiScaleWith(0, true, false), 4, "unzoomed, the UI is the fit scale")
T.eq(uiScaleWith(-2, true, false), 2,
  "zoomed out over a live world pass, the UI steps down with it")
T.eq(uiScaleWith(-2, false, false), 4,
  "with no world behind it at all (title screen), the zoom is ignored")

-- the fix: the party menu / bag ended the world pass, but the battle under
-- them is still drawn over the map, so the surface must not grow
T.eq(uiScaleWith(-2, false, true), 2,
  "an opaque menu over a world-bg battle keeps the battle's stepped-down scale")
T.eq(uiScaleWith(0, false, true), 4,
  "and the hold changes nothing when the player never zoomed out")

g.getDimensions, g.getPixelDimensions = realDims, realPixelDims

-- the hold is the same whole-stack answer the dim already uses, so a menu
-- opened over the battle cannot drop it for a frame (Game:draw wires
-- uiWorldHold to worldBgBattleDim ~= nil; battle_fit_option covers the scan)
local Game = require("src.core.Game")
local BattleState = require("src.battle.BattleState")
-- isOpaque false is what "world" actually does to a live battle (BattleState
-- drops it per-instance at start, so the class default stays opaque for every
-- other battle) -- and it is the whole reason the map draws underneath
local worldBattle = setmetatable(
  { game = { save = { options = { battleBg = "world" } } }, isOpaque = false },
  { __index = BattleState })
T.check(Game.worldBgBattleDim({ states = { {}, worldBattle, {} } }) ~= nil,
  "the stack scan the hold reads still finds the battle under an opaque menu")

-- --------------------------------------------- the backdrop holds under menus

-- The battle establishes the surround too, and the same opaque menu used to
-- take that over: it becomes visibleBase, the overworld stops drawing, and
-- the world the battle was composed over collapses to a flat black clear.
local overworld = { isOpaque = true }
local menu = { isOpaque = true } -- PartyMenu / ListMenu
local whiteBattle = setmetatable(
  { game = { save = { options = { battleBg = "white" } } } },
  { __index = BattleState })
local wideWhiteBattle = setmetatable(
  { game = { save = { options = {
      battleBg = "white", battleLayout = "wide",
  } } } },
  { __index = BattleState })
local function stack(...) return { states = { ... },
  visibleBase = function(self)
    for i = #self.states, 1, -1 do
      if self.states[i].isOpaque then return i end
    end
    return 1
  end } end

local s = stack(overworld, worldBattle, menu)
T.eq(s:visibleBase(), 3, "the menu is the topmost opaque state, as before")
T.eq(Game.drawBaseInStack(s, s:visibleBase()), 1,
  "but the frame still starts at the overworld, so the map keeps drawing")

-- unchanged everywhere else
local s2 = stack(overworld, worldBattle)
T.eq(s2:visibleBase(), 1, "the battle alone already drew from the overworld")
T.eq(Game.drawBaseInStack(s2, s2:visibleBase()), 1, "and still does")
local s3 = stack(overworld, whiteBattle, menu)
T.eq(Game.drawBaseInStack(s3, s3:visibleBase()), 3,
  "a classic white-bg battle has no presentation to hold, so nothing moves")
local s3wide = stack(overworld, wideWhiteBattle, menu)
T.eq(Game.drawBaseInStack(s3wide, s3wide:visibleBase()), 2,
  "an opaque WIDE battle still draws beneath its classic menu")
local s4 = stack(overworld, menu)
T.eq(Game.drawBaseInStack(s4, s4:visibleBase()), 2,
  "and a menu outside a battle is untouched")
T.eq(Game.drawBaseInStack(stack(overworld), 1), 1, "a lone base is safe")
T.eq(Game.drawBaseInStack(nil, 1), 1, "and so is no stack at all")

-- ------------------------------------------- the YES/NO stays with its screen

-- a bare choice box -- the battle's switch offer, a shop or PC confirm -- has
-- no anchored dialogue box under it to ride
T.eq(ChoiceBox.new({}, function() end).anchor, nil,
  "a bare choice box does not anchor itself to the window edge")
T.eq(ChoiceBox.new({}, function() end, { defaultNo = true }).anchor, nil,
  "and neither does one that only asked to start on NO")

-- ...but the one a dialogue box opens does, because that box is anchored too
local game = { save = { player = {} }, data = { text = {} } }
game.stack = {
  states = {},
  push = function(self, s) table.insert(self.states, s) end,
  pop = function(self) return table.remove(self.states) end,
  top = function(self) return self.states[#self.states] end,
}
game.input = {
  wasPressed = function() return false end,
  isDown = function() return false end,
}

local box = TextBox.new(game, "Shall we heal\nyour POKEMON?", nil,
                        { choice = function() end })
game.stack:push(box)
for _ = 1, 600 do
  if box.done then break end
  box:update(1 / 60)
end
T.check(box.done, "the question finished typing")
box:update(1 / 60) -- the update after done is the one that pushes the choice
T.eq(#game.stack.states, 2, "the dialogue box opened its YES/NO")
T.eq(game.stack:top().anchor, "bottom",
  "a choice box riding a dialogue box shares its bottom anchor")

-- ...and inside a battle even THAT one stays put, because the battle is the
-- screen: the caught-mon nickname prompt prints on the blanked battle field
-- (BattleState.blankForAskName), and docking it to the window edge drops it a
-- whole letterbox below what it is printed on.
T.eq(BattleState.holdsUIAnchors, true, "a battle composes its own screen")
T.eq(Game.uiAnchorsHeldInStack(stack(overworld, worldBattle)), true,
  "so the anchors are held while it is up")
T.eq(Game.uiAnchorsHeldInStack(stack(overworld, worldBattle, {})), true,
  "including for the text box and YES/NO it opens above itself")
T.eq(Game.uiAnchorsHeldInStack(stack(overworld)), false,
  "the overworld's own dialogue box still docks to the screen edge")
T.eq(Game.uiAnchorsHeldInStack(nil), false, "and no stack is safe")

Renderer.uiAnchors = nil
Renderer.uiAnchorHold = true
Renderer:setUIAnchor(0, 96, 160, 48, "bottom")
T.eq(Renderer.uiAnchors, nil, "a held anchor never reaches the frame")
Renderer.uiAnchorHold = false
Renderer:setUIAnchor(0, 96, 160, 48, "bottom")
T.eq(#(Renderer.uiAnchors or {}), 1, "and an unheld one still does")
Renderer.uiAnchors = nil

T.finish("battle fixed menu scale")
