-- Pokemon swarms, WIRED.
--
--   luajit tests/gen2_swarm_test.lua
--
-- src/core/gen2/Roamers.lua has carried the whole swarm model for a while --
-- StoreSwarmMapIndices / SetSwarmFlag / CheckSwarmFlag / ActivateFishingSwarm
-- and the _SwarmWildmonCheck lookup -- with three dead links: the daily clear
-- had no call site, the encounter roll never consulted the tables, and the
-- cache carried no tables to consult.  This suite is those three.
--
--   CheckSwarmFlag            second row of CheckTimeEvents' `.do_daily`
--                             (engine/overworld/events.asm)
--   _SwarmWildmonCheck        the first thing LoadWildMonDataPointer does, for
--                             the grass list and the water one alike
--   SwarmGrassWildMons /      data/wild/swarm_grass.asm, swarm_water.asm,
--   SwarmWaterWildMons        emitted by RomExtractorGen2 as
--                             encounters.swarmGrass / .swarmWater
package.path = "./?.lua;./?/init.lua;" .. package.path

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

local World = require("src.world.gen2.World")
local Roamers = require("src.core.gen2.Roamers")
local Swarm = Roamers.Swarm

local COLL_TALL_GRASS = 0x18

local DATA = {
  items = {},
  moves = { TACKLE = { name = "TACKLE", pp = 35 } },
  pokemon = {
    NIDORAN_M = { name = "NIDORAN", index = 32, types = { "POISON", "POISON" },
      baseStats = { hp = 46, attack = 57, defense = 40, speed = 50,
        specialAttack = 40, specialDefense = 40 },
      growthRate = 0, levelMoves = { { level = 1, move = "TACKLE" } } },
    YANMA = { name = "YANMA", index = 193, types = { "BUG", "FLYING" },
      baseStats = { hp = 65, attack = 65, defense = 45, speed = 95,
        specialAttack = 75, specialDefense = 45 },
      growthRate = 0, levelMoves = { { level = 1, move = "TACKLE" } } },
  },
}

local function slots(species, level)
  local list = {}
  for i = 1, 7 do list[i] = { species = species, level = level } end
  return list
end

-- ROUTE_35's own list against the Yanma swarm's, both always-hit so the only
-- question a step asks is WHICH table was searched.
local ENCOUNTERS = {
  grass = {
    ROUTE_35 = { map = "ROUTE_35", rates = { MORN = 256, DAY = 256, NITE = 256 },
      slots = { MORN = slots("NIDORAN_M", 12), DAY = slots("NIDORAN_M", 12),
        NITE = slots("NIDORAN_M", 12) } },
  },
  swarmGrass = {
    ROUTE_35 = { map = "ROUTE_35", rates = { MORN = 256, DAY = 256, NITE = 256 },
      slots = { MORN = slots("YANMA", 14), DAY = slots("YANMA", 14),
        NITE = slots("YANMA", 14) } },
  },
}

local function swarmWorld()
  local game = {
    data = DATA,
    save = { player = { name = "GOLD", badges = {} },
      party = { { species = "YANMA", level = 5, hp = 20, maxHp = 20 } },
      inventory = {} },
  }
  local world = World.new(game)
  game.world = world
  world.maps = { ROUTE_35 = { id = "ROUTE_35", group = 2, map = 5,
      width = 2, height = 2, blocks = { 1, 2, 3, 4 }, objects = {}, warps = {},
      environment = "ROUTE" },
    ROUTE_36 = { id = "ROUTE_36", group = 2, map = 6, width = 2, height = 2,
      blocks = { 1, 2, 3, 4 }, objects = {}, warps = {}, environment = "ROUTE" } }
  world.map = { id = "ROUTE_35", def = world.maps.ROUTE_35,
    width = 2, height = 2,
    cellCollision = function() return COLL_TALL_GRASS end }
  world.encounters = ENCOUNTERS
  world.player = { cellX = 0, cellY = 0 }
  world.daytime = "DAY"
  world.startBattle = function(self, opts) self.started = opts return true end
  return world, game
end

-- ---- the lookup arm --------------------------------------------------------
do
  local world, game = swarmWorld()
  check(world:tryWildEncounter(), "an ordinary step on Route 35 fights")
  eq(world.started.wild.species, "NIDORAN_M", "the map's own list")

  -- Script_swarm -> StoreSwarmMapIndices, which is World:setSwarm.
  world:setSwarm(2, 5)
  eq(game.save.swarmMap, "ROUTE_35", "the swarm command stores the map pair")
  check(game.save.dailyFlags.swarm, "and falls through into SetSwarmFlag")

  world.started = nil
  check(world:tryWildEncounter(), "the next step still fights")
  eq(world.started.wild.species, "YANMA",
    "but out of the swarm table, which _SwarmWildmonCheck searches first")
  eq(world.started.wild.level, 14, "at the swarm row's own level")

  -- The check is `cp d / cp e` against the CURRENT map: a swarm on Route 35
  -- does nothing to Route 36.
  world.map = { id = "ROUTE_36", def = world.maps.ROUTE_36,
    width = 2, height = 2,
    cellCollision = function() return COLL_TALL_GRASS end }
  world.started = nil
  check(not world:tryWildEncounter(),
    "a map the swarm is not on falls through to its own (absent) table")
  check(world.started == nil, "so nothing is fought there")
end

-- SWEET SCENT reaches the same ChooseWildEncounter, so it sees the swarm too.
do
  local world = swarmWorld()
  world:setSwarm(2, 5)
  check(world:sweetScentEncounter(), "SWEET SCENT turns something up")
  eq(world.started.wild.species, "YANMA", "and it is the swarm's mon")
end

-- ---- the daily clear -------------------------------------------------------
--
-- CheckTimeEvents `.do_daily` is CheckDailyResetTimer, CheckSwarmFlag,
-- CheckPokerusTick, CheckPhoneCall in that order.  The reset takes
-- DAILYFLAGS1_SWARM down; CheckSwarmFlag is what notices and clears the map
-- pair with it, and it is the ONLY thing that ever ends a swarm.
do
  local world, game = swarmWorld()
  world:setSwarm(2, 5)
  -- A day already banked, so the first poll is not the one that starts the
  -- timer (Apricorns.checkDailyResetTimer arms it on a save that has none).
  world:checkTimeEvents()
  check(game.save.swarmMap == "ROUTE_35",
    "a poll on the same day leaves the swarm up")
  check(Swarm.active(game.save), "the flag is still set")

  -- Roll the clock over the way the daily reset timer measures it.
  game.save.dailyReset.day = (game.save.dailyReset.day or 0) - 2
  world:checkTimeEvents()
  check(not Swarm.active(game.save), "the daily reset drops the flag")
  check(game.save.swarmMap == nil, "and CheckSwarmFlag clears the map pair")

  world.started = nil
  check(world:tryWildEncounter(), "the step after that still fights")
  eq(world.started.wild.species, "NIDORAN_M", "out of the map's own list again")
end

-- ActivateFishingSwarm rides the same flag and does NOT touch the map pair,
-- so the clear has to take both down together.
do
  local world, game = swarmWorld()
  world:setSwarm(2, 5)
  Swarm.setFishing(game.save, Swarm.FISH_QWILFISH)
  eq(Swarm.fishing(game.save), Swarm.FISH_QWILFISH, "the fishing swarm is up")
  world:checkTimeEvents() -- arms the timer, the way a save's first step does
  game.save.dailyReset.day = (game.save.dailyReset.day or 0) - 2
  world:checkTimeEvents()
  eq(Swarm.fishing(game.save), Swarm.FISH_NONE,
    "and goes down on the same daily tick")
end

-- ---- the extracted tables --------------------------------------------------
--
-- data/wild/swarm_grass.asm has four rows and swarm_water.asm one; the Yanma
-- swarm on Route 35 is the one the Dunsparce/Yanma phone calls arm.
do
  local cache = os.getenv("GOLD_CACHE")
  local encounters
  if cache then
    local chunk = loadfile(cache .. "/data/generated/encounters.lua")
    encounters = chunk and chunk()
  end
  if not (encounters and encounters.swarmGrass) then
    check(true, "no gold cache: swarm table shape (SKIP)")
  else
    local grass = encounters.swarmGrass
    check(grass.ROUTE_35 ~= nil, "SwarmGrassWildMons carries ROUTE_35")
    check(grass.ROUTE_38 ~= nil, "and ROUTE_38")
    check(grass.DARK_CAVE_VIOLET_ENTRANCE ~= nil,
      "and DARK_CAVE_VIOLET_ENTRANCE, which is the Dunsparce swarm")
    check(grass.MOUNT_MORTAR_1F_OUTSIDE ~= nil, "and MOUNT_MORTAR_1F_OUTSIDE")
    eq(grass.ROUTE_35.rates.DAY, 25, "Route 35's swarm rate is 10 percent")
    eq(grass.ROUTE_35.slots.DAY[3].species, "YANMA",
      "and slot 3 of its day list is the YANMA the swarm exists for")
    local water = encounters.swarmWater or {}
    check(water.MOUNT_MORTAR_1F_OUTSIDE ~= nil,
      "SwarmWaterWildMons carries the one Marill row")
    eq(water.MOUNT_MORTAR_1F_OUTSIDE.slots[2].species, "MARILL",
      "with MARILL in its middle slot")
  end
end

S.finish()
