-- Decorations: owning them, setting them up, and the two PLAYERS_HOUSE_2F map
-- callbacks that put the room together (engine/overworld/decorations.asm, with
-- data/decorations/attributes.asm and decorations.asm beside it).
--
-- ROM-free: `luajit tests/gen2_decorations_test.lua`.  The cache section at the
-- bottom SKIPs when no Gold cache is present.
package.path = "./?.lua;./?/init.lua;" .. package.path

local S = require("tests.harness").suite("gen2 decorations")
local check, eq = S.check, S.eq

local Decorations = require("src.core.gen2.Decorations")
local Events = require("src.world.gen2.Events")
local Vm = require("src.script.gen2.Vm")

-- constants/deco_constants.asm, the handful this file names.
local BEDS, DECO_FEATHERY_BED, DECO_PINK_BED = 1, 2, 3
local DECO_RED_CARPET = 7
local DECO_TOWN_MAP, DECO_PIKACHU_POSTER = 16, 17
local DECO_FAMICOM = 21
local DECO_BIG_SNORLAX_DOLL = 26
local DOLLS, DECO_PIKACHU_DOLL, DECO_CLEFAIRY_DOLL = 29, 30, 32
local DECO_GOLD_TROPHY_DOLL, DECO_SILVER_TROPHY_DOLL = 51, 52

-- ---- the attributes table -------------------------------------------------
-- Its length is asserted in the asm as NUM_DECOS + NUM_DECO_CATEGORIES + 1 =
-- 45 + 7 + 1, and row 0 is the CANCEL row every category menu ends on.
do
  local rows = 0
  for _ in pairs(Decorations.ATTRIBUTES) do rows = rows + 1 end
  eq(rows, 53, "DecorationAttributes is 53 rows, row 0 included")
  eq(Decorations.ATTRIBUTES[0].name, "CANCEL", "row 0 is CANCEL")
  check(Decorations.ATTRIBUTES[0].action == nil,
    "and DecoAction_nothing, so it does nothing at all")
  eq(Decorations.ATTRIBUTES[BEDS].name, "PUT IT AWAY",
    "the category row is the PUT IT AWAY one")
  eq(Decorations.ATTRIBUTES[BEDS].action, "PUT_AWAY_BED",
    "and it puts away that category's slot")
end

-- ---- GetDecoName ----------------------------------------------------------
do
  eq(Decorations.name(DECO_FEATHERY_BED), "FEATHERY BED", "a bed appends BED")
  eq(Decorations.name(DECO_RED_CARPET), "RED CARPET", "a carpet appends CARPET")
  eq(Decorations.name(DECO_PIKACHU_DOLL), "PIKACHU DOLL", "a doll appends DOLL")
  eq(Decorations.name(DECO_BIG_SNORLAX_DOLL), "BIG SNORLAX",
    "a big doll PREPENDS BIG , it does not append DOLL")
  eq(Decorations.name(DECO_PIKACHU_POSTER), "PIKACHU POSTER",
    "a poster appends POSTER")
  -- Both of these are DECO_PLANT rows precisely so that nothing is appended.
  eq(Decorations.name(DECO_TOWN_MAP), "TOWN MAP",
    "the TOWN MAP poster is a DecorationNames string, not a mon plus POSTER")
  eq(Decorations.name(DECO_GOLD_TROPHY_DOLL), "GOLD TROPHY",
    "and so is the trophy")
end

-- ---- owning ---------------------------------------------------------------
do
  local events = Events.new()
  check(not Decorations.owns(events, DECO_PINK_BED), "a new game owns no beds")
  Decorations.give(events, DECO_PINK_BED)
  check(Decorations.owns(events, DECO_PINK_BED), "give sets the DECOATTR flag")
  check(not Decorations.owns(events, DECO_FEATHERY_BED),
    "and only that one -- the flags are one bit each")

  -- SetSpecificDecorationFlag names its decoration by DECOFLAG_*, which is a
  -- DecorationIDs index and NOT the DECO_* the attributes table is keyed by.
  eq(Decorations.idForFlag(Decorations.DECOFLAG_GOLD_TROPHY_DOLL),
    DECO_GOLD_TROPHY_DOLL, "DECOFLAG_GOLD_TROPHY_DOLL is DECO_GOLD_TROPHY_DOLL")
  eq(Decorations.idForFlag(Decorations.DECOFLAG_SILVER_TROPHY_DOLL),
    DECO_SILVER_TROPHY_DOLL, "and the silver one likewise")
  eq(Decorations.idForFlag(0), DECO_FEATHERY_BED,
    "DECOFLAG_* is a const_def block, so flag 0 is the first bed")

  local boxed = Events.new()
  Decorations.giveFlag(boxed, Decorations.DECOFLAG_SILVER_TROPHY_DOLL)
  check(Decorations.owns(boxed, DECO_SILVER_TROPHY_DOLL),
    "the NORMAL BOX's trophy arrives through the DECOFLAG path")
end

-- ---- the category and item lists ------------------------------------------
do
  local events = Events.new()
  eq(#Decorations.ownedCategories(events), 0,
    "with nothing owned there is not a single category")
  Decorations.give(events, DECO_PIKACHU_DOLL)
  local cats = Decorations.ownedCategories(events)
  eq(#cats, 1, "one doll shows one category")
  eq(cats[1].label, "ORNAMENT", "and it is the ornament menu")

  local rows = Decorations.rows(events, cats[1])
  eq(#rows, 3, "the list is the doll, PUT IT AWAY and CANCEL")
  eq(rows[1], DECO_PIKACHU_DOLL, "the owned decoration first")
  eq(rows[2], DOLLS, "then the category's own row")
  eq(rows[3], 0, "then row 0, which is CANCEL")

  -- The trophies live in the ornament list, past the twenty-one dolls.
  local trophies = Events.new()
  Decorations.give(trophies, DECO_GOLD_TROPHY_DOLL)
  eq(Decorations.ownedCategories(trophies)[1].label, "ORNAMENT",
    "a trophy is an ornament")
end

-- ---- DoDecorationAction2 --------------------------------------------------
do
  local state = { }
  local changed, pages = Decorations.apply(state, DECO_FEATHERY_BED)
  check(changed, "setting up an empty slot changes the room")
  eq(state.bed, DECO_FEATHERY_BED, "and fills wDecoBed")
  eq(pages[1], "Set up the\nFEATHERY BED.", "SetUpTheDecoText")

  changed, pages = Decorations.apply(state, DECO_FEATHERY_BED)
  check(not changed, ".alreadythere writes nothing")
  eq(pages[1], "That's already set\nup.", "AlreadySetUpText")

  changed, pages = Decorations.apply(state, DECO_PINK_BED)
  check(changed, "a second bed replaces the first")
  eq(state.bed, DECO_PINK_BED, "the slot holds ONE thing")
  eq(#pages, 2, "PutAwayAndSetUpText is two pages")
  eq(pages[1], "Put away the\nFEATHERY BED", "the old one is named first")
  eq(pages[2], "and set up the\nPINK BED.", "then the new one")

  -- The PUT IT AWAY row, whose action is the category's put-away half.
  changed, pages = Decorations.apply(state, BEDS)
  check(changed, "putting a bed away changes the room too")
  eq(state.bed, 0, "and empties the slot")
  eq(pages[1], "Put away the\nPINK BED.",
    "the text names what WAS out, not the row that was picked")

  changed, pages = Decorations.apply(state, BEDS)
  check(not changed, "an empty slot has nothing to put away")
  eq(pages[1], "There's nothing to\nput away.", "NothingToPutAwayText")
end

-- ---- the two ornament sides -----------------------------------------------
do
  local state = {}
  local changed = Decorations.apply(state, DECO_PIKACHU_DOLL, nil)
  check(not changed, "no side chosen is DecoAction_AskWhichSide's cancel")
  eq(state.leftOrnament, nil, "and neither side is touched")

  Decorations.apply(state, DECO_PIKACHU_DOLL, "right")
  eq(state.rightOrnament, DECO_PIKACHU_DOLL, "RIGHT SIDE fills wDecoRightOrnament")
  Decorations.apply(state, DECO_CLEFAIRY_DOLL, "left")
  eq(state.leftOrnament, DECO_CLEFAIRY_DOLL, "LEFT SIDE fills the other")

  -- .getwhichside: there is only one of each doll, so putting the Pikachu on
  -- the left takes it off the right.
  Decorations.apply(state, DECO_PIKACHU_DOLL, "left")
  Decorations.clearOtherSide(state, DECO_PIKACHU_DOLL, "left")
  eq(state.leftOrnament, DECO_PIKACHU_DOLL, "the doll moved to the left")
  eq(state.rightOrnament, 0, "and is gone from the right")
end

-- ---- ToggleDecorationsVisibility ------------------------------------------
do
  local state = {}
  local rows = Decorations.visibility(state)
  eq(#rows, 4, "four objects hang off the room: console, two dolls, big doll")
  for _, row in ipairs(rows) do
    check(row.hidden, "an empty slot SETS the object's flag, which hides it")
  end
  eq(rows[1].flag, 1857, "EVENT_PLAYERS_HOUSE_2F_CONSOLE")
  eq(rows[4].flag, 1860, "EVENT_PLAYERS_HOUSE_2F_BIG_DOLL")
  eq(rows[1].sprite, 0, "and the slots are SPRITE_VARS-relative")
  eq(rows[4].sprite, 3, "SPRITE_BIG_DOLL is the fourth")

  state.console = DECO_FAMICOM
  state.bigDoll = DECO_BIG_SNORLAX_DOLL
  rows = Decorations.visibility(state)
  check(not rows[1].hidden, "a filled slot CLEARS the flag")
  eq(rows[1].byte, 0x5c, "and writes SPRITE_FAMICOM into wVariableSprites")
  eq(rows[4].byte, 0x33, "SPRITE_BIG_SNORLAX for the big doll")
  check(rows[2].hidden, "the two ornaments are still empty")
end

-- ---- ToggleMaptileDecorations ---------------------------------------------
do
  local state = {}
  eq(#Decorations.tiles(state), 0,
    "SetDecorationTile's `and a / ret z`: an empty slot paints nothing")
  check(not Decorations.posterVisible(state),
    "and a bare wall is not readable")

  state.bed = DECO_FEATHERY_BED
  state.poster = DECO_TOWN_MAP
  state.carpet = DECO_RED_CARPET
  local tiles = Decorations.tiles(state)
  local function blockAt(x, y)
    for _, tile in ipairs(tiles) do
      if tile.x == x and tile.y == y then return tile.block end
    end
    return nil
  end
  -- The asm's coordinates are CELLS and PadCoords_de/GetBlockLocation halve
  -- them, the same way `changeblock` does.
  eq(blockAt(0, 2), 0x1b, "bed cell (0,4) is block (0,2)")
  eq(blockAt(3, 0), 0x1f, "poster cell (6,0) is block (3,0)")
  eq(blockAt(0, 0), 0x08, "carpet top-left")
  eq(blockAt(0, 1), 0x09, "carpet bottom row is +1")
  eq(blockAt(1, 1), 0x0a, "+2")
  eq(blockAt(2, 1), 0x09, "and +1 again")
  check(Decorations.posterVisible(state), "a poster on the wall is readable")
end

-- ---- the state on the save ------------------------------------------------
do
  local save = {}
  local state = Decorations.state(save)
  -- InitDecorations, farcall'd from intro_menu.asm at New Game.
  eq(state.bed, DECO_FEATHERY_BED, "a new game starts with the feathery bed")
  eq(state.poster, DECO_TOWN_MAP, "and the TOWN MAP on the wall")
  state.bed = DECO_PINK_BED
  eq(Decorations.state(save).bed, DECO_PINK_BED,
    "and the state lives on the save, not in the module")
end

-- ---- describedecoration, through the VM -----------------------------------
-- Script_describedecoration is a ScriptJump: the arm hands back a script and
-- the command never returns.  Which script depends on what is standing there.
do
  local scripts = {
    ["09:718b"] = { { op = "writetext", text = "t:bare" }, { op = "end" } },
    ["09:7000"] = { { op = "writetext", text = "t:map" }, { op = "end" } },
    ["09:71a8"] = { { op = "writetext", text = "t:doll" }, { op = "end" } },
  }
  local TEXTS = {
    ["t:bare"] = "bare wall", ["t:map"] = "TOWN MAP", ["t:doll"] = "adorable",
  }
  local decorations = {
    DECODESC_POSTER = { script = "09:718b", posters = {
      { decoration = DECO_TOWN_MAP, script = "09:7000" },
    } },
    DECODESC_LEFT_DOLL = { script = "09:71a8" },
  }
  local function run(descName, slotFn)
    local shown = {}
    scripts.generation = 2
    scripts["s:t"] = { { op = "describedecoration", args = { 0 },
      decorationName = descName } }
    local vm = Vm.new(scripts, TEXTS, Events.new(), {
      decorationSlot = slotFn,
      eventTables = { decorations = decorations },
      showText = function(text, done) shown[#shown + 1] = text; done() end,
    })
    vm:start("s:t")
    return shown, vm
  end

  local shown = run("DECODESC_POSTER", function() return 0, nil end)
  eq(shown[1], "bare wall",
    "no poster placed falls to DecorationDesc_NullPoster")

  shown = run("DECODESC_POSTER", function() return DECO_TOWN_MAP, nil end)
  eq(shown[1], "TOWN MAP",
    "IsInArray finds the placed poster and jumps to ITS script")

  local _, vm = run("DECODESC_LEFT_DOLL", function()
    return DECO_PIKACHU_DOLL, "PIKACHU DOLL"
  end)
  eq(vm.stringBuffer, "PIKACHU DOLL",
    "the ornament arm puts the name in wStringBuffer3 for the text to print")
end

-- ---- the cache ------------------------------------------------------------
-- Same default every other gen2 suite uses, so a run with no GOLD_CACHE set
-- (tests/run_tests.lua) still reads the cache instead of skipping silently.
local cache = os.getenv("GOLD_CACHE")
  or ((os.getenv("HOME") or "") .. "/Library/Application Support/LOVE/gold-dev/gold")
local function loadCache(name)
  if not cache then return nil end
  local chunk = loadfile(cache .. "/data/generated/" .. name .. ".lua")
  return chunk and chunk() or nil
end

local events = loadCache("events")
if not events then
  check(true, "no GOLD_CACHE: the extracted arms and flags are not checked (SKIP)")
else
  -- Every DECODESC_* the cache knows has a slot here, or describedecoration
  -- would read a wDeco* byte that does not exist.
  for _, name in ipairs(events.decorationOrder or {}) do
    check(Decorations.DESC_SLOTS[name] ~= nil,
      ("%s names a wDeco* slot"):format(name))
  end
  -- The poster arm's table is the four posters, and each of their DECO_* ids
  -- is a real attributes row of the poster category.
  local poster = (events.decorations or {}).DECODESC_POSTER
  eq(#((poster and poster.posters) or {}), 4, "four posters can hang there")
  for _, row in ipairs((poster and poster.posters) or {}) do
    local attr = Decorations.attributes(row.decoration)
    check(attr ~= nil and attr.action == "SET_UP_POSTER",
      ("poster %d is a poster row here too"):format(row.decoration))
  end
end

local initial = loadCache("initial_events")
if not initial then
  check(true, "no GOLD_CACHE: InitializeEventsScript's flags are not checked (SKIP)")
else
  -- PlayersHouse2FInitializeRoomCallback ends in `jumpstd InitializeEventsScript`,
  -- and that script is where the room's four objects are hidden and the two
  -- decorations the player starts with are marked owned.
  local set = {}
  for _, flag in ipairs(initial.flags or {}) do set[flag] = true end
  for _, row in ipairs(Decorations.OBJECT_SLOTS) do
    check(set[row.flag], ("object flag %d starts SET, i.e. hidden"):format(row.flag))
  end
  check(set[Decorations.attributes(DECO_FEATHERY_BED).flag],
    "and the feathery bed starts owned")
  check(set[Decorations.attributes(DECO_TOWN_MAP).flag],
    "as does the TOWN MAP poster")
end

S.finish()
