-- The six Gen 2-only content registries, end to end: held_items,
-- phone_contacts, decorations, apricorns, landmarks and radio_channels.
--
-- Three claims, and the third is the one that makes the other two worth
-- anything:
--
--   1. the routing is symmetrical.  Schemas.GEN2 says where a shared registry
--      lands on Gold; Schemas.GEN1 is its mirror, and these six are gated
--      under GEN ONE -- Red has no phone, no radio and no held items, so a
--      write there is taken, dropped and reported rather than merged into a
--      namespace nothing on Red would read.
--   2. the vanilla records are the literals they replaced, byte for byte.  A
--      registry that seeds a different record than the module used to hold is
--      a behaviour change wearing a refactor's clothes.
--   3. the CONSUMER reads through the registry.  A registry nothing reads is
--      the silent no-op this whole design exists to prevent, so each one is
--      driven from a mod's own registration through to the routine the game
--      calls: Decorations.attributes, Phone.CONTACTS, Apricorns.ballFor,
--      Nests.landmarkId, MapRadio.channelRecord and, for held_items, the
--      write-back onto data.items that src/battle/gen2/Battle.lua:heldEffect
--      reads.
--
-- ROM-free: the generation is injected through the loader (T.sdk.loadMods
-- opts.generation), the way tests/engine/gate_gen2_mod_api.lua does.

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

local T = require("tests.modkit")
local Schemas = require("src.mods.Schemas")
local ItemEffects = require("src.core.gen2.ItemEffects")
local Phone = require("src.core.gen2.Phone")
local Decorations = require("src.core.gen2.Decorations")
local Apricorns = require("src.core.gen2.Apricorns")
local Nests = require("src.core.gen2.Nests")
local MapRadio = require("src.ui.gen2.MapRadio")

local NAMES = { "held_items", "phone_contacts", "decorations", "apricorns",
                "landmarks", "radio_channels" }

local PATHS = {
  held_items = "gen2HeldItems",
  phone_contacts = "gen2PhoneContacts",
  decorations = "gen2Decorations",
  apricorns = "gen2Apricorns",
  landmarks = "gen2Landmarks.landmarks",
  radio_channels = "gen2RadioChannels",
}

-- ------- 1. the mirror of Schemas.GEN2

for _, name in ipairs(NAMES) do
  local spec = Schemas.REGISTRIES[name]
  T.check(spec ~= nil, "the catalog declares it: " .. name)
  T.eq(spec.target, nil,
    "a Gen 2-only registry carries no Gen 1 target: " .. name)
  T.eq(Schemas.targetFor(name, spec, 1), nil,
    "and none is invented for Gen 1: " .. name)
  T.eq(Schemas.gatedFor(name, 1), true, "gated under Gen 1: " .. name)
  T.eq(Schemas.targetFor(name, spec, 2), PATHS[name],
    "routed to its Gen 2 path: " .. name)
  T.eq(Schemas.gatedFor(name, 2), false, "not gated under Gen 2: " .. name)
  T.check(spec.example ~= nil, "and documents a call: " .. name)
end

-- the mirror holds in the other direction too: nothing shared is gated under
-- Gen 1, which is what would break if a routing row were put in the wrong
-- table
for name, spec in pairs(Schemas.REGISTRIES) do
  if Schemas.GEN1[name] == nil then
    T.eq(Schemas.targetFor(name, spec, 1), spec.target,
      "a registry with no Gen 1 routing row keeps its target: " .. name)
    T.eq(Schemas.gatedFor(name, 1), false,
      "and is not gated under Gen 1: " .. name)
  end
end

for name in pairs(Schemas.GEN1) do
  T.check(Schemas.REGISTRIES[name] ~= nil,
    "Schemas.GEN1 names a real registry: " .. name)
end

-- ------- the Gen 2 dataset these merge into
--
-- data.gen2Constants.phoneContactOrder is the phone id space, verbatim from
-- the ROM manifest (the four PHONE_UNUSED rows are the const_skip holes).
local PHONE_ORDER = {
  "PHONE_00", "PHONE_MOM", "PHONE_OAK", "PHONE_BILL", "PHONE_ELM",
  "PHONE_SCHOOLBOY_JACK", "PHONE_POKEFAN_BEVERLY", "PHONE_SAILOR_HUEY",
  "PHONE_UNUSED", "PHONE_UNUSED", "PHONE_UNUSED",
  "PHONE_COOLTRAINERM_GAVEN", "PHONE_COOLTRAINERF_BETH",
  "PHONE_BIRDKEEPER_JOSE", "PHONE_COOLTRAINERF_REENA", "PHONE_YOUNGSTER_JOEY",
  "PHONE_BUG_CATCHER_WADE", "PHONE_FISHER_RALPH", "PHONE_PICNICKER_LIZ",
  "PHONE_HIKER_ANTHONY", "PHONE_CAMPER_TODD", "PHONE_PICNICKER_GINA",
  "PHONE_JUGGLER_IRWIN", "PHONE_BUG_CATCHER_ARNIE", "PHONE_SCHOOLBOY_ALAN",
  "PHONE_UNUSED", "PHONE_LASS_DANA", "PHONE_SCHOOLBOY_CHAD",
  "PHONE_POKEFANM_DEREK", "PHONE_FISHER_CHRIS", "PHONE_POKEMANIAC_BRENT",
  "PHONE_PICNICKER_TIFFANY", "PHONE_BIRDKEEPER_VANCE", "PHONE_FISHER_WILTON",
  "PHONE_BLACKBELT_KENJI", "PHONE_HIKER_PARRY", "PHONE_PICNICKER_ERIN",
}

-- two landmark records in the cache's own shape (index = the map header byte)
local function landmarkTable()
  return {
    order = { "LANDMARK_SPECIAL", "LANDMARK_FIX_TOWN" },
    landmarks = {
      LANDMARK_SPECIAL = { id = "LANDMARK_SPECIAL", name = "SPECIAL",
                           x = 0, y = 0, index = 0 },
      LANDMARK_FIX_TOWN = { id = "LANDMARK_FIX_TOWN", name = "FIX\nTOWN",
                            x = 4, y = 5, index = 1 },
    },
  }
end

local function goldData()
  local data = T.fixtures.fresh()
  -- an item that holds something, so held_items has a row to seed from
  data.items.FIX_LEFTOVERS = {
    id = "FIX_LEFTOVERS", index = 90, name = "FIX LEFTOVERS", price = 0,
    heldEffect = "HELD_LEFTOVERS", heldParameter = 0,
  }
  data.items.RED_APRICORN = { id = "RED_APRICORN", index = 91,
                              name = "RED APRICORN", price = 0 }
  data.items.LEVEL_BALL = { id = "LEVEL_BALL", index = 92, ball = true,
                            name = "LEVEL BALL", price = 0 }
  data.items.ULTRA_BALL = { id = "ULTRA_BALL", index = 93, ball = true,
                            name = "ULTRA BALL", price = 0 }
  -- the fixture maps under the key Gold keeps them at, so a contact's `map`
  -- resolves against the same id space src/world/gen2/World.lua walks
  data.gen2Maps = data.maps
  data.gen2Constants = { phoneContactOrder = PHONE_ORDER }
  data.gen2Landmarks = landmarkTable()
  -- src/core/Game2.lua:load builds this before mods:load; the harness
  -- stands in for that boot step
  data.gen2HeldItems = ItemEffects.heldItemsFrom(data.items)
  return data
end

local function memfsFor(body, extra)
  local manifest = [[{
    "id": "fix_gen2_content",
    "name": "Fixture Gen 2 Content",
    "version": "1.0.0",
    "entry": "main.lua",
    "api": 2,
    "gen2compat": true
  }]]
  local files = { ["mods/fix_gen2_content/manifest.json"] = manifest,
                  ["mods/fix_gen2_content/main.lua"] = body }
  for path, text in pairs(extra or {}) do files[path] = text end
  return T.sdk.memfs(files)
end

-- ------- 2. parity: the seeded record IS the literal
--
-- Compared field by field against the module's own table rather than against
-- a copy of it, so a record that gained or lost a key fails here.
local function sameRecord(got, want, label)
  if type(got) ~= "table" or type(want) ~= "table" then
    return T.eq(got, want, label)
  end
  local ok = true
  for key, value in pairs(want) do
    if got[key] ~= value then ok = false end
  end
  for key in pairs(got) do
    if want[key] == nil then ok = false end
  end
  return T.check(ok, label)
end

do
  local run = T.sdk.loadNone({ data = goldData(), generation = 2 })
  T.eq(#run.errors, 0, "a zero-mod Gold load reports no errors")

  local decorations = run.data.gen2Decorations
  T.check(decorations ~= nil, "the decorations merge target appears")
  local rows = 0
  for decoId, attr in pairs(Decorations.ATTRIBUTES) do
    rows = rows + 1
    sameRecord(decorations[Decorations.idFor(decoId)], attr,
      "vanilla decoration is the attribute row: deco:" .. decoId)
  end
  T.eq(rows, 53, "every attribute row is registered")

  local contacts = run.data.gen2PhoneContacts
  T.check(contacts ~= nil, "the phone_contacts merge target appears")
  sameRecord(contacts.PHONE_YOUNGSTER_JOEY, Phone.CONTACTS[15],
    "vanilla contact is the PhoneContacts row: PHONE_YOUNGSTER_JOEY")
  sameRecord(contacts.PHONE_MOM, Phone.CONTACTS[1],
    "and the non-trainer rows too: PHONE_MOM")
  T.eq(contacts.PHONE_UNUSED, nil,
    "the const_skip holes are not registered under one shared id")

  local apricorns = run.data.gen2Apricorns
  T.check(apricorns ~= nil, "the apricorns merge target appears")
  sameRecord(apricorns.RED_APRICORN, Apricorns.row("RED_APRICORN"),
    "vanilla apricorn is the ApricornBalls row: RED_APRICORN")
  T.eq(apricorns.RED_APRICORN.ball, "LEVEL_BALL",
    "and it still hands back the LEVEL BALL")

  local channels = run.data.gen2RadioChannels
  T.check(channels ~= nil, "the radio_channels merge target appears")
  T.eq(channels.OAKS_POKEMON_TALK and channels.OAKS_POKEMON_TALK.channel, 1,
    "OAK's POKEMON TALK keeps its dial position")
  T.eq(channels.ROCKET_RADIO and channels.ROCKET_RADIO.channel, 8,
    "and ROCKET RADIO keeps its own")

  -- held_items and landmarks merge onto a table that already existed, so the
  -- claim there is that the merge left it exactly as it found it
  T.eq(run.data.gen2HeldItems.FIX_LEFTOVERS.heldEffect, "HELD_LEFTOVERS",
    "the held_items view still holds the item's own effect")
  T.eq(ItemEffects.applyHeldItems(run.data,
         ItemEffects.heldSnapshot(run.data.gen2HeldItems)), 0,
    "and a mod-free merge writes nothing back onto data.items")
  T.eq(run.data.gen2Landmarks.landmarks.LANDMARK_FIX_TOWN.x, 4,
    "the landmark records are the cache's own")

  run.release()
end

-- ------- 3. the consumers, driven from a mod's registration

do
  local data = goldData()
  local run = T.sdk.loadMods({ "mods/fix_gen2_content" }, {
    fs = memfsFor([[
      local mod = ...
      -- an existing row edited, and a new one registered, for each registry
      mod.content.decorations:patch("deco:2", { name = "COZY" })
      mod.content.phone_contacts:patch("PHONE_YOUNGSTER_JOEY",
        { map = "FIX_ROUTE" })
      mod.content.apricorns:override("RED_APRICORN",
        { apricorn = "RED_APRICORN", ball = "ULTRA_BALL", event = 600,
          index = 1 })
      mod.content.landmarks:patch("LANDMARK_FIX_TOWN", { x = 9 })
      mod.content.landmarks:register("LANDMARK_MOD_ISLE",
        { id = "LANDMARK_MOD_ISLE", name = "MOD\nISLE", x = 1, y = 2,
          index = 7 })
      mod.content.radio_channels:register("PIRATE_RADIO",
        { channel = 9, name = "PIRATE RADIO" })
      mod.content.held_items:patch("FIX_LEFTOVERS", { heldParameter = 7 })
    ]]),
    data = data,
    generation = 2,
  })
  T.eq(#run.errors, 0,
    "the mod loads clean (" .. table.concat(run.errors, "; ") .. ")")

  -- decorations: Decorations.attributes is the one read point every caller
  -- (and src/ui/gen2/DecorationMenu.lua) comes through
  Decorations.useRegistry(run.data)
  T.eq(Decorations.attributes(2).name, "COZY",
    "decorations: the merged row is what attributes() answers")
  T.eq(Decorations.attributes(2).flag, Decorations.ATTRIBUTES[2].flag,
    "and the fields the patch left alone are the cart's own")
  T.eq(Decorations.name(2, nil), "COZY BED",
    "so GetDecoName's port spells the merged row")

  -- phone: the merged rows are folded onto the contact table every lookup in
  -- src/core/gen2/Phone.lua keys by
  Phone.useRegistry(run.data)
  T.eq(Phone.CONTACTS[15].map, "FIX_ROUTE",
    "phone_contacts: the merged row reaches the contact table")
  T.eq(Phone.CONTACTS[15].class, "YOUNGSTER",
    "and the untouched fields survive the patch")
  -- the cache overlay runs from src/world/gen2/World.lua AFTER this, and must
  -- not undo it
  Phone.useExtracted({ phone = { [15] = { map = "ROUTE_30",
                                          calleeTime = 7, callerTime = 7,
                                          callee = "41:0001",
                                          caller = "41:0002" } } })
  T.eq(Phone.CONTACTS[15].map, "FIX_ROUTE",
    "and the cache overlay does not undo it")

  -- apricorns: Kurt hands back what the registry says he does
  Apricorns.useRegistry(run.data)
  T.eq(Apricorns.ballFor("RED_APRICORN"), "ULTRA_BALL",
    "apricorns: the merged row is the ball Kurt makes")
  T.eq(Apricorns.apricornFor("ULTRA_BALL"), "RED_APRICORN",
    "and the reverse lookup follows it")
  T.eq(#Apricorns.BALLS, 7, "the table is still the seven rows")
  T.eq(Apricorns.BALLS[1].apricorn, "RED_APRICORN",
    "in the ApricornBalls order the menu walks")

  -- landmarks: the registry answers the map header's byte, including for an
  -- index the extractor's `order` list has never heard of
  T.eq(Nests.landmarkId(run.data, 1), "LANDMARK_FIX_TOWN",
    "landmarks: a vanilla index still resolves")
  T.eq(Nests.landmark(run.data, 1).x, 9,
    "and the patch reaches the record the town map draws")
  T.eq(Nests.landmarkId(run.data, 7), "LANDMARK_MOD_ISLE",
    "a registered landmark resolves at its own index")
  -- and a registered landmark cannot shadow a vanilla one by claiming its
  -- byte: the cache's own row keeps its slot, so the answer does not depend
  -- on pairs() order.  A fresh table, because the index map is memoized per
  -- landmarks table and the run's is already built.
  local shadowed = landmarkTable()
  shadowed.landmarks.LANDMARK_AAA_SHADOW = {
    id = "LANDMARK_AAA_SHADOW", name = "AAA", x = 0, y = 0, index = 1,
  }
  T.eq(Nests.landmarkId({ gen2Landmarks = shadowed }, 1), "LANDMARK_FIX_TOWN",
    "a second record at a taken index does not displace the cache's own")
  local twoNew = landmarkTable()
  twoNew.landmarks.LANDMARK_MOD_B = { id = "LANDMARK_MOD_B", name = "B",
                                      x = 0, y = 0, index = 7 }
  twoNew.landmarks.LANDMARK_MOD_A = { id = "LANDMARK_MOD_A", name = "A",
                                      x = 0, y = 0, index = 7 }
  T.eq(Nests.landmarkId({ gen2Landmarks = twoNew }, 7), "LANDMARK_MOD_A",
    "and two registered records at one index resolve the same way every boot")

  -- radio: a registered station is on the dial
  local record, station = MapRadio.channelRecord(run.data, 9)
  T.eq(station, "PIRATE_RADIO", "radio_channels: the new station is on the dial")
  T.eq(record and record.name, "PIRATE RADIO", "with its own name")
  T.eq(select(2, MapRadio.channelRecord(run.data, 8)), "ROCKET_RADIO",
    "and the vanilla positions are unmoved")

  -- held items: the merged row is written back onto the item record, which is
  -- what src/battle/gen2/Battle.lua:itemDef reads
  local applied = ItemEffects.applyHeldItems(run.data,
    ItemEffects.heldSnapshot({ FIX_LEFTOVERS = { heldEffect = "HELD_LEFTOVERS",
                                                 heldParameter = 0 } }))
  T.eq(applied, 1, "held_items: one item record changed")
  T.eq(run.data.items.FIX_LEFTOVERS.heldParameter, 7,
    "and the battle's own read sees the merged parameter")
  T.eq(run.data.items.FIX_LEFTOVERS.heldEffect, "HELD_LEFTOVERS",
    "with the effect the patch left alone")
  T.eq(ItemEffects.heldItemFor("FIX_LEFTOVERS", run.data).heldParameter, 7,
    "and heldItemFor answers from the merged table")

  run.release()
  -- module statics are process-wide; put them back before the next case
  Phone.useRegistry(nil)
  Decorations.useRegistry(nil)
end

-- ------- 4. the mirror case, through a real load
--
-- A Red boot takes the write, drops it and says so.  Not fatal: a mod that
-- supports both games registers its Gold content unconditionally and should
-- still load the half that applies.

do
  local run = T.sdk.loadMods({ "mods/fix_gen2_content" }, {
    fs = memfsFor([[
      local mod = ...
      mod.content.pokemon:patch("FIXMON_A", { catchRate = 77 })
      mod.content.decorations:patch("deco:2", { name = "COZY" })
      mod.content.radio_channels:register("PIRATE_RADIO", { channel = 9 })
    ]]),
    generation = 1,
  })
  T.eq(run.data.pokemon.FIXMON_A.catchRate, 77,
    "Gen 1: the shared registry still merged")
  T.eq(run.data.gen2Decorations, nil,
    "Gen 1: a Gen 2-only registry merges nothing")
  T.eq(run.data.decorations, nil,
    "Gen 1: and invents no namespace of its own")
  local told = {}
  for _, message in ipairs(run.errors) do
    if message:match("decorations") then told.decorations = true end
    if message:match("radio_channels") then told.radio = true end
  end
  T.check(told.decorations and told.radio,
    "Gen 1: both dropped registrations are reported, not silent")
  run.release()
end

T.finish("gen2_content_registries")
