-- NX Yellow boot, headless: the runtime complement to
-- tests/engine/nx_generated_guard_test.lua.  The guard only sees literal
-- loader calls; this suite drives the REAL boot states (TitleState,
-- YellowIntro/IntroMovie, Sound.playPikaCry) against a broken-mount NX
-- filesystem where generated art exists ONLY under the versioned save-dir
-- prefix (yellow|blue/), and records every path that reaches the raw love
-- loaders AFTER NxAssetOverlay's rewrite.  Any data-driven or formatted
-- assets/generated path the overlay misses shows up here as a bare path
-- the recorder saw.  Self-contained:
--   luajit tests/engine/nx_yellow_boot_test.lua
package.path = "./?.lua;./?/init.lua;" .. package.path
if not _G.love then _G.love = require("tests.love_stub") end

local T = require("tests.harness")
local check = T.check
local eq = T.eq

local GameVersion = require("src.core.GameVersion")
local Platform = require("src.core.Platform")
local Overlay = require("src.core.NxAssetOverlay")
local TitleState = require("src.ui.TitleState")
local YellowIntro = require("src.ui.YellowIntro")
local Sound = require("src.core.Sound")

local savedVersion = GameVersion.get()

local GENERATED = "assets/generated/"

-- --- fixtures ----------------------------------------------------------
-- Seeded ONLY under the versioned prefix, mirroring the fused love-nx bug
-- where yellow|blue/assets/generated is never mounted over assets/generated.
local seeded = {}
local function seed(path, bytes)
  love.filesystem.write(path, bytes or "fake-asset-bytes")
  seeded[#seeded + 1] = path
end

local Y_TITLE = {
  "pikachu.png", "pika_bubble.png", "eyes_half.png", "eyes_closed.png",
  "player.png", "copyright.png", "gamefreak_inc.png", "nine.png",
  "yellow_version.png",
}
for _, name in ipairs(Y_TITLE) do
  seed("yellow/assets/generated/title/" .. name)
end
local Y_INTRO = {
  "yellow_intro_1.png", "yellow_intro_2.png", "clouds.png",
  -- data-driven manifest entries (the paths a literal scan cannot see)
  "gf_logo.png", "gf_text.png", "big_star.png",
  "falling_star.png", "falling_star_blink.png", "studio_logo.png",
  "gengar_1.png", "gengar_2.png", "gengar_3.png",
  "nidorino_1.png", "nidorino_2.png", "nidorino_3.png",
}
for _, name in ipairs(Y_INTRO) do
  seed("yellow/assets/generated/intro/" .. name)
end
seed("yellow/assets/generated/audio/pika_cries/cry_01.wav", "RIFF-fake-wav")

-- --- recorder, installed BEFORE Overlay.install so it sees the final
-- resolved path (the overlay wraps whatever is in place at install time)
local recorded = {}
local function record(kind, path)
  if type(path) == "string" then
    recorded[#recorded + 1] = { kind = kind, path = path }
  end
end

local rawNewImage = love.graphics.newImage
love.graphics.newImage = function(path, ...)
  record("image", path)
  return rawNewImage(path, ...)
end

local rawRead = love.filesystem.read
love.filesystem.read = function(path, ...)
  record("read", path)
  return rawRead(path, ...)
end

-- widenMono re-reads the caller's bare path via newSoundData; without this
-- recorder the boot suite only saw newSource and could not catch a missing
-- sound.newSoundData wrap (the silent hole that motivated the full-surface
-- overlay).  Installed before Overlay.install so the overlay wraps us.
local rawNewSoundData = love.sound.newSoundData
love.sound.newSoundData = function(samples, ...)
  record("sounddata", samples)
  return rawNewSoundData(samples, ...)
end

local Source = {}
Source.__index = Source
function Source:play() self.playing = true end
function Source:stop() self.playing = false end
function Source:setVolume() end
function Source:isPlaying() return self.playing end
function Source:getChannelCount() return self.channels or 1 end

love.audio = {
  newSource = function(pathOrData, mode)
    record("source", pathOrData)
    local channels = 1
    if type(pathOrData) == "table" and pathOrData.getChannelCount then
      channels = pathOrData:getChannelCount()
    end
    return setmetatable({
      path = pathOrData, mode = mode, channels = channels,
    }, Source)
  end,
}

Overlay.install()
check(Overlay.isInstalled(), "overlay installs over the recorders")

-- Every recorded path under assets/generated/ must already carry the
-- active version prefix: a bare generated path here is a dynamic-path
-- regression the static guard cannot catch.
local function assertNoBareGenerated(prefix, label)
  local bare = {}
  for _, r in ipairs(recorded) do
    if r.path:sub(1, #GENERATED) == GENERATED then
      bare[#bare + 1] = r.kind .. " " .. r.path
    end
  end
  check(#bare == 0,
    label .. ": every generated path reached the raw loader " .. prefix
      .. "-prefixed"
      .. (#bare > 0 and (" (bare: " .. table.concat(bare, ", ") .. ")") or ""))
end

local function countRecorded(prefix)
  local n = 0
  for _, r in ipairs(recorded) do
    if r.path:sub(1, #prefix) == prefix then n = n + 1 end
  end
  return n
end

-- --- Yellow boot -------------------------------------------------------
GameVersion.set("yellow")

local yellowTitleManifest = {
  layout = "yellow_pikachu",
  pikachu = { path = "assets/generated/title/pikachu.png" },
  pikaBubble = { path = "assets/generated/title/pika_bubble.png" },
  version = { path = "assets/generated/title/yellow_version.png" },
}
local yellowIntroManifest = {
  studio = { logo = "assets/generated/intro/studio_logo.png" },
  gamefreakLogo = { path = "assets/generated/intro/gf_logo.png" },
  gamefreakText = { path = "assets/generated/intro/gf_text.png" },
  bigStar = { path = "assets/generated/intro/big_star.png" },
  fallingStar = { path = "assets/generated/intro/falling_star.png" },
  fallingStarBlink = { path = "assets/generated/intro/falling_star_blink.png" },
  gengar = {
    frame1 = { path = "assets/generated/intro/gengar_1.png" },
    frame2 = { path = "assets/generated/intro/gengar_2.png" },
    frame3 = { path = "assets/generated/intro/gengar_3.png" },
  },
  nidorino = {
    frame1 = { path = "assets/generated/intro/nidorino_1.png" },
    frame2 = { path = "assets/generated/intro/nidorino_2.png" },
    frame3 = { path = "assets/generated/intro/nidorino_3.png" },
  },
}
local game = { data = { field = {
  title = yellowTitleManifest,
  intro = yellowIntroManifest,
} } }

local titleState = TitleState.new(game, {})
check(titleState.yellowLayout, "the Yellow manifest selects the Pikachu layout")
check(titleState.yellowPikachu ~= nil, "title Pikachu art loaded")
eq(titleState.yellowPikachu and titleState.yellowPikachu.path,
  "yellow/assets/generated/title/pikachu.png",
  "title Pikachu resolves to the yellow/ copy")
check(titleState.yellowBubble ~= nil, "title speech bubble loaded")
eq(titleState.yellowBubble and titleState.yellowBubble.path,
  "yellow/assets/generated/title/pika_bubble.png",
  "title bubble resolves to the yellow/ copy")
eq(titleState.eyesHalf and titleState.eyesHalf.path,
  "yellow/assets/generated/title/eyes_half.png",
  "blink overlay (eyes_half) resolves to the yellow/ copy")
eq(titleState.eyesClosed and titleState.eyesClosed.path,
  "yellow/assets/generated/title/eyes_closed.png",
  "blink overlay (eyes_closed) resolves to the yellow/ copy")
eq(titleState.player and titleState.player.path,
  "yellow/assets/generated/title/player.png",
  "title player resolves to the yellow/ copy")
eq(titleState.version and titleState.version.path,
  "yellow/assets/generated/title/yellow_version.png",
  "version ribbon resolves to the yellow/ copy")

-- YellowIntro.new internally builds IntroMovie.new as its pre-roll, so one
-- constructor covers the copyright card, the GAME FREAK splash and the
-- Yellow attract atlases.
local yellowIntro = YellowIntro.new(game, function() end)
eq(yellowIntro.atlas1 and yellowIntro.atlas1.path,
  "yellow/assets/generated/intro/yellow_intro_1.png",
  "YellowIntro atlas1 resolves to the yellow/ copy")
eq(yellowIntro.atlas2 and yellowIntro.atlas2.path,
  "yellow/assets/generated/intro/yellow_intro_2.png",
  "YellowIntro atlas2 resolves to the yellow/ copy")
eq(yellowIntro.clouds and yellowIntro.clouds.path,
  "yellow/assets/generated/intro/clouds.png",
  "YellowIntro clouds resolve to the yellow/ copy")
local pre = yellowIntro.pre
check(pre ~= nil, "the IntroMovie pre-roll was constructed")
eq(pre and pre.copyright and pre.copyright.path,
  "yellow/assets/generated/title/copyright.png",
  "copyright card resolves to the yellow/ copy")
eq(pre and pre.nineImg and pre.nineImg.path,
  "yellow/assets/generated/title/nine.png",
  "copyright NineTile resolves to the yellow/ copy")
check(pre and pre.yellowCopy == true,
  "IntroMovie selects the Yellow (c)1995-1999 tile sequence")
eq(pre and pre.studioLogo and pre.studioLogo.path,
  "yellow/assets/generated/intro/studio_logo.png",
  "data-driven studio logo resolves to the yellow/ copy")
eq(pre and pre.logo and pre.logo.path,
  "yellow/assets/generated/intro/gf_logo.png",
  "data-driven gamefreakLogo resolves to the yellow/ copy")
eq(pre and pre.gfText and pre.gfText.path,
  "yellow/assets/generated/intro/gf_text.png",
  "data-driven gamefreakText resolves to the yellow/ copy")
eq(pre and pre.bigStar and pre.bigStar.path,
  "yellow/assets/generated/intro/big_star.png",
  "data-driven bigStar resolves to the yellow/ copy")
eq(pre and pre.gengarFrames and pre.gengarFrames[2]
    and pre.gengarFrames[2].path,
  "yellow/assets/generated/intro/gengar_2.png",
  "data-driven gengar frame resolves to the yellow/ copy")
eq(pre and pre.nidoFrames and pre.nidoFrames[3]
    and pre.nidoFrames[3].path,
  "yellow/assets/generated/intro/nidorino_3.png",
  "data-driven nidorino frame resolves to the yellow/ copy")

-- Yellow's voiced Pikachu clip: a FORMATTED path (cry_%02d.wav), invisible
-- to the static guard.  playPikaCry also runs widenMono, which re-reads the
-- same bare path via newSoundData and must emit a 16-bit STEREO Source
-- (#626) -- path rewrite alone is not enough on Switch audren.
local cry = Sound.playPikaCry({ audio = { pikaCries = 1 } }, 1)
check(cry ~= nil, "playPikaCry returns a source on NX Yellow")
eq(cry and cry:getChannelCount(), 2,
  "playPikaCry widens the mono PCM clip to stereo")
local sawCrySoundData = false
local sawCrySource = false
for _, r in ipairs(recorded) do
  if r.kind == "sounddata"
      and r.path == "yellow/assets/generated/audio/pika_cries/cry_01.wav" then
    sawCrySoundData = true
  end
  if r.kind == "source"
      and r.path == "yellow/assets/generated/audio/pika_cries/cry_01.wav" then
    sawCrySource = true
  end
end
check(sawCrySource,
  "newSource loaded the cry through the yellow/ prefix")
check(sawCrySoundData,
  "widenMono re-read the cry via newSoundData with the yellow/ path")

check(countRecorded("yellow/assets/generated/") >= 20,
  "the boot pulled its generated art through the yellow/ prefix")
assertNoBareGenerated("yellow/", "Yellow boot")

-- --- Blue boot: parity with Yellow --------------------------------------
-- Blue's real boot is IntroMovie (the Red/Blue attract movie) straight onto
-- the stack, then TitleState with cycling mons -- no Pikachu layout.  Same
-- broken-mount setup, same assertions, blue/ prefix.
recorded = {}
GameVersion.set("blue")
seed("blue/assets/generated/title/blue_version.png", "blue-version-bytes")
seed("blue/assets/generated/title/player.png")
seed("blue/assets/generated/title/copyright.png")
seed("blue/assets/generated/title/gamefreak_inc.png")
local B_INTRO = {
  "gf_logo.png", "gf_text.png", "big_star.png",
  "falling_star.png", "falling_star_blink.png", "studio_logo.png",
  "gengar_1.png", "gengar_2.png", "gengar_3.png",
  "nidorino_1.png", "nidorino_2.png", "nidorino_3.png",
}
for _, name in ipairs(B_INTRO) do
  seed("blue/assets/generated/intro/" .. name)
end

local blueIntroManifest = {
  studio = { logo = "assets/generated/intro/studio_logo.png" },
  gamefreakLogo = { path = "assets/generated/intro/gf_logo.png" },
  gamefreakText = { path = "assets/generated/intro/gf_text.png" },
  bigStar = { path = "assets/generated/intro/big_star.png" },
  fallingStar = { path = "assets/generated/intro/falling_star.png" },
  fallingStarBlink = { path = "assets/generated/intro/falling_star_blink.png" },
  gengar = {
    frame1 = { path = "assets/generated/intro/gengar_1.png" },
    frame2 = { path = "assets/generated/intro/gengar_2.png" },
    frame3 = { path = "assets/generated/intro/gengar_3.png" },
  },
  nidorino = {
    frame1 = { path = "assets/generated/intro/nidorino_1.png" },
    frame2 = { path = "assets/generated/intro/nidorino_2.png" },
    frame3 = { path = "assets/generated/intro/nidorino_3.png" },
  },
}
local blueGame = { data = { field = {
  title = { version = { path = "assets/generated/title/blue_version.png" } },
  intro = blueIntroManifest,
} } }

local IntroMovie = require("src.ui.IntroMovie")
local blueIntro = IntroMovie.new(blueGame, function() end)
eq(blueIntro.copyright and blueIntro.copyright.path,
  "blue/assets/generated/title/copyright.png",
  "Blue copyright card resolves to the blue/ copy")
eq(blueIntro.studioLogo and blueIntro.studioLogo.path,
  "blue/assets/generated/intro/studio_logo.png",
  "Blue data-driven studio logo resolves to the blue/ copy")
eq(blueIntro.logo and blueIntro.logo.path,
  "blue/assets/generated/intro/gf_logo.png",
  "Blue data-driven gamefreakLogo resolves to the blue/ copy")
eq(blueIntro.gfText and blueIntro.gfText.path,
  "blue/assets/generated/intro/gf_text.png",
  "Blue data-driven gamefreakText resolves to the blue/ copy")
eq(blueIntro.bigStar and blueIntro.bigStar.path,
  "blue/assets/generated/intro/big_star.png",
  "Blue data-driven bigStar resolves to the blue/ copy")
eq(blueIntro.gengarFrames and blueIntro.gengarFrames[2]
    and blueIntro.gengarFrames[2].path,
  "blue/assets/generated/intro/gengar_2.png",
  "Blue data-driven gengar frame resolves to the blue/ copy")
eq(blueIntro.nidoFrames and blueIntro.nidoFrames[3]
    and blueIntro.nidoFrames[3].path,
  "blue/assets/generated/intro/nidorino_3.png",
  "Blue data-driven nidorino frame resolves to the blue/ copy")

local blueTitle = TitleState.new(blueGame, {})
check(not blueTitle.yellowLayout,
  "the Blue manifest keeps the cycling-mons layout")
eq(blueTitle.version and blueTitle.version.path,
  "blue/assets/generated/title/blue_version.png",
  "Blue title ribbon resolves to the blue/ copy")
eq(blueTitle.player and blueTitle.player.path,
  "blue/assets/generated/title/player.png",
  "Blue title player resolves to the blue/ copy")
eq(love.filesystem.read("assets/generated/title/blue_version.png"),
  "blue-version-bytes",
  "a generated filesystem.read resolves to the blue/ bytes")

check(countRecorded("blue/assets/generated/") >= 12,
  "the boot pulled its generated art through the blue/ prefix")
assertNoBareGenerated("blue/", "Blue boot")

-- --- cleanup ------------------------------------------------------------
Overlay.uninstall()
check(not Overlay.isInstalled(), "overlay uninstalls")
love.graphics.newImage = rawNewImage
love.filesystem.read = rawRead
love.sound.newSoundData = rawNewSoundData
love.audio = nil
for _, path in ipairs(seeded) do
  love.filesystem.remove(path)
end
Platform._resetForTests()
GameVersion.set(savedVersion)

T.finish()
