-- Gen 2 clock setup: the InitClock screens and the RTC base they write.
--
-- The cart does not store "the time": InitClock stores the RTC reading at the
-- moment the player answered (wStartHour / wStartMinute / wStartDay) and every
-- later read goes through it.  src/core/gen2/Clock.lua is that arithmetic and
-- src/ui/gen2/InitClock.lua is both screens that write it.
--   luajit tests/gen2_clock_test.lua
package.path = "./?.lua;./?/init.lua;" .. package.path

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

love = require("tests.love_stub")

-- No font is loaded here (these are layout and arithmetic assertions), so
-- Font.encode would warn once per unknown glyph and bury a real failure.
require("src.core.Logger").warn = function() end

local Clock = require("src.core.gen2.Clock")
local InitClock = require("src.ui.gen2.InitClock")
local Strings = require("src.core.Strings")

-- A stub input the screen drives off, the same shape Input:wasPressed has.
local function fakeInput()
  local pressed = {}
  return {
    press = function(self, button) pressed[button] = true end,
    wasPressed = function(_self, button)
      if pressed[button] then
        pressed[button] = nil
        return true
      end
      return false
    end,
  }
end

-- --------------------------------------------------------------- the offsets
do
  local save = {}
  check(not Clock.isSet(save), "a fresh save has no base")
  eq(Clock.hour(save), math.floor(Clock.hostMinutes() / 60),
    "so it reads the host clock straight through")

  Clock.setTime(save, 10, 0)
  check(Clock.isSet(save), "answering Oak writes the base")
  eq(Clock.hour(save), 10, "and the clock reads back what was set")
  eq(Clock.minute(save), 0, "minutes too")

  Clock.setTime(save, 23, 45)
  eq(Clock.hour(save), 23, "a second answer re-anchors it")
  eq(Clock.minute(save), 45, "minutes and all")

  -- The base is an OFFSET, not a frozen reading: the clock keeps running.
  local base = save.rtc.startMinute
  local moved = (Clock.hostMinutes() + 90 + base) % Clock.MINUTES_PER_DAY
  eq(math.floor(moved / 60), 1,
    "an hour and a half later it is 1:15, not still 23:45")
  eq(moved % 60, 15, "minutes and all")

  Clock.setTime(save, 0, 0)
  eq(Clock.hour(save), 0, "midnight is hour 0")
  Clock.setTime(save, 12, 30)
  eq(Clock.hour(save), 12, "noon is hour 12")
end

do
  local save = {}
  eq(Clock.weekday(save), Clock.hostWeekday(),
    "with no base the weekday is the host's")
  for day = 0, 6 do
    Clock.setWeekday(save, day)
    eq(Clock.weekday(save), day, ("SetDayOfWeek pins day %d"):format(day))
  end
  eq(save.rtc.dayOfWeek, 6,
    "and leaves wCurDay where the daily rollovers read it")
end

-- ------------------------------------------------------------ the hour screen
do
  local save = {}
  local input = fakeInput()
  local done = {}
  local screen = InitClock.new({ input = input }, {
    save = save,
    onDone = function(hour, minute) done = { hour = hour, minute = minute } end,
  })
  eq(screen.phase, "intro", "the screen opens on Oak waking up")
  eq(screen.hour, 10, "with InitClock's own default of 10 AM")
  eq(screen.minute, 0, "and no minutes")

  -- OakTimeWokeUpText is two `para` pages, so A turns the page before it takes
  -- the box down.
  eq(#screen:pages(), 2, "Oak's opening is two pages")
  input:press("a")
  screen:update(0)
  eq(screen.phase, "intro", "the first A turns the page")
  check(screen:pageText():find("clock", 1, true) ~= nil,
    "onto the one that asks about the clock")
  input:press("a")
  screen:update(0)
  eq(screen.phase, "hour", "and the second opens the hour picker")
  -- DisplayHourOClock is PrintHour + String_oclock, so the word rides along
  eq(screen:display(), "DAY 10 o'clock", "which shows the hour DisplayHourOClock does")

  input:press("up")
  screen:update(0)
  eq(screen.hour, 11, "up walks the hour forward")
  input:press("down")
  screen:update(0)
  input:press("down")
  screen:update(0)
  eq(screen.hour, 9, "down walks it back")
  -- .DecreaseThroughMidnight / .AdvanceThroughMidnight: both ends wrap.
  screen.hour = 0
  input:press("down")
  screen:update(0)
  eq(screen.hour, 23, "and midnight wraps to 11 PM")
  input:press("up")
  screen:update(0)
  eq(screen.hour, 0, "and back again")

  screen.hour = 7
  input:press("a")
  screen:update(0)
  eq(screen.phase, "confirm-hour", "A confirms the hour")
  check(screen:question():find("o'clock", 1, true) ~= nil,
    "and the question reads it back")
  -- NO drops back to the picker (`jr c, .loop`).
  screen.yesNo = 2
  input:press("a")
  screen:update(0)
  eq(screen.phase, "hour", "NO goes back to the picker")
  input:press("a")
  screen:update(0)
  input:press("a")
  screen:update(0)
  eq(screen.phase, "minute", "YES moves on to the minutes")

  input:press("up")
  screen:update(0)
  eq(screen.minute, 1, "up walks the minutes")
  screen.minute = 0
  input:press("down")
  screen:update(0)
  eq(screen.minute, 59, "and they wrap at the hour")
  screen.minute = 30
  eq(screen:display(), "30 min.", "DisplayMinutesWithMinString's own string")
  input:press("a")
  screen:update(0)
  eq(screen.phase, "confirm-minute", "A confirms them")
  input:press("a")
  screen:update(0)
  eq(screen.phase, "response", "and Oak answers with the time")
  -- "MORN 7:30", not "7 AM:30": OakText_ResponseToSetTime is PrintHour (the
  -- time-of-day word then the 1-12 hour) then ':' then two-digit minutes, so
  -- the meridiem never appears and cannot land between the hour and them.
  check(screen:question():find("MORN 7:30", 1, true) ~= nil,
    "which is the pair the player just set")
  input:press("a")
  screen:update(0)
  eq(done.hour, 7, "the screen hands the hour back")
  eq(done.minute, 30, "and the minutes")
  eq(Clock.hour(save), 7, "and the save now reads that hour")
  eq(Clock.minute(save), 30, "and those minutes")
end

-- OakText_ResponseToSetTime's own ladder.
eq(InitClock.responseKey(2), "soDark", "before MORN_HOUR it is still dark")
eq(InitClock.responseKey(4), "overslept", "MORN_HOUR is 'I overslept'")
-- DAY_HOUR is 10 (constants/misc_constants.asm:38), and
-- OakText_ResponseToSetTime's `cp DAY_HOUR + 1 / jr c, .morn` keeps the hour
-- ITSELF in the plain "I overslept!" arm; "Yikes!" starts at 11.  This read 9
-- when InitClock did, so both were an hour early together.
eq(InitClock.responseKey(10), "overslept", "and so is DAY_HOUR itself")
eq(InitClock.responseKey(11), "yikes", "past it Oak yikes")
eq(InitClock.responseKey(18), "soDark", "and NITE_HOUR is dark again")

-- PrintHour (engine/rtc/timeset.asm:672) is GetTimeOfDayString + PlaceString
-- and THEN the 1-12 hour, so the cart prints the time-of-day word ahead of the
-- number and no meridiem at all.  These read "12 AM" / "1 PM" while InitClock
-- built the string that way, which is what let InitClock.timeString append
-- ":mm" to a meridiem and have Oak say "5 AM:30".
eq(InitClock.hourString(0), "NITE 12", "PrintHour shows midnight as NITE 12")
eq(InitClock.hourString(12), "DAY 12", "and noon as DAY 12")
eq(InitClock.hourString(13), "DAY 1", "and the afternoon on a 12-hour clock")
eq(InitClock.timeString(5, 30), "MORN 5:30",
   "and OakText_ResponseToSetTime's line is PrintHour, ':', two-digit minutes")

-- --------------------------------------------------------- the weekday wheel
do
  local save = {}
  local input = fakeInput()
  local picked
  local screen = InitClock.new({ input = input }, {
    mode = "day", save = save,
    onDone = function(day) picked = day end,
  })
  eq(screen.phase, "day", "the wheel opens on its picker, with no preamble")
  eq(screen.day, 0, "`xor a / ld [wTempDayOfWeek], a`: SUNDAY")
  eq(screen:display(), "SUNDAY", "which is what the box shows")
  for _ = 1, 2 do
    input:press("up")
    screen:update(0)
  end
  eq(screen:display(), "TUESDAY", "up walks the wheel forward")
  input:press("down")
  screen:update(0)
  input:press("down")
  screen:update(0)
  input:press("down")
  screen:update(0)
  eq(screen:display(), "SATURDAY", "and it wraps past SUNDAY")
  input:press("a")
  screen:update(0)
  eq(screen.phase, "confirm-day", "A confirms it")
  input:press("b")
  screen:update(0)
  eq(screen.phase, "day", "B is NO and drops back to the wheel")
  input:press("a")
  screen:update(0)
  input:press("a")
  screen:update(0)
  eq(picked, 6, "YES hands the day back")
  eq(Clock.weekday(save), 6, "and the save reads it")
end

-- The driven path: a screen this new must not stall a scripted run, so it
-- walks itself to the end on its defaults.
do
  local save = {}
  local finished = false
  local screen = InitClock.new({}, {
    save = save, autoConfirm = true,
    onDone = function() finished = true end,
  })
  for _ = 1, 20 do
    if finished then break end
    screen:update(0)
  end
  check(finished, "autoConfirm reaches the end on its own")
  eq(Clock.hour(save), Clock.DEFAULT_HOUR, "taking InitClock's 10 AM default")
  eq(Clock.minute(save), Clock.DEFAULT_MINUTE, "and no minutes")
end

-- Drawing must not throw with the stub canvas: the layout is transcribed from
-- hlcoord calls, so a bad coordinate is a crash rather than a wrong pixel.
do
  local screen = InitClock.new({}, { save = {} })
  for _, phase in ipairs({ "intro", "hour", "confirm-hour", "minute",
      "confirm-minute", "response", "day", "confirm-day" }) do
    screen.phase = phase
    local ok, err = pcall(function() screen:drawPanel() end)
    check(ok, ("phase %s draws (%s)"):format(phase, tostring(err)))
  end
end

-- Clock.DAY_NAMES / Clock.weekdayName / Clock.daytimeLabel: the single home
-- InitClock, MainMenu and the Pokegear clock card all share, so a weekday
-- cannot be named one way on one screen and another way on the next.
do
  eq(Clock.weekdayName(1), "SUNDAY", "1-based, SUNDAY first")
  eq(Clock.weekdayName(6), "FRIDAY", "and the rest in wCurDay's order")
  check(Clock.weekdayName(0) == nil, "day 0 is out of range")
  check(Clock.weekdayName(8) == nil, "and so is day 8")

  eq(Clock.daytimeLabel(4), "MORN", "daytimeLabel matches clockDaytime's word")
  eq(Clock.daytimeLabel(10), "DAY", "for every hour band")
  eq(Clock.daytimeLabel(20), "NITE", "including the wrap back to NITE")

  local MainMenu = require("src.ui.gen2.MainMenu")
  check(MainMenu.DAYS == Clock.DAY_NAMES,
    "MainMenu reuses the same table InitClock and the Pokegear do")
end

-- ------------------------------------------------- a translation mod's turn
--
-- DAYS, the clockDaytime word and the "o'clock"/"min." suffixes used to
-- bypass Strings entirely, so a translation mod's `strings` registry had no
-- seam to catch them: the picker kept printing the English day name and
-- "o'clock" no matter the catalog (reported from a real Gold build).
do
  Strings.load({
    strings = {
      SUNDAY = "DIMANCHE",
      MORN = "MATIN",
      ["%s o'clock"] = "%s heures",
      ["%d min."] = "%d min",
    },
  })

  local wheel = InitClock.new({ input = fakeInput() }, { mode = "day", save = {} })
  eq(wheel:display(), "DIMANCHE", "a translated catalog reaches the day wheel")

  eq(InitClock.hourString(4), "MATIN 4",
    "and the clockDaytime word, through Clock.daytimeLabel")
  eq(InitClock.oclockString(4), "MATIN 4 heures",
    "and the o'clock suffix, template and all")

  local minutePicker = InitClock.new({ input = fakeInput() }, { save = {} })
  minutePicker.phase = "minute"
  minutePicker.minute = 30
  eq(minutePicker:display(), "30 min", "and the minutes picker's own suffix")

  -- Palettes.clockDaytime itself must stay untranslated even with a catalog
  -- loaded: FORCED_DAYTIME and the rest of Palettes.lua's own lookups
  -- compare against its return value as an internal key, not display text.
  local Palettes = require("src.world.gen2.Palettes")
  eq(Palettes.clockDaytime(4), "MORN",
    "the internal palette key is untouched by the loaded catalog")

  -- Module state is process-global and tests/run_tests.lua runs every suite
  -- in one process (see tests/mod_strings_tests.lua's own note): leaving the
  -- catalog loaded would translate the day/hour of every suite after this
  -- one.
  Strings.load({})
  check(not Strings.active(), "the catalog is unloaded for the suites after this one")
end

S.finish()
