-- Loopback + lockstep helpers (21-testing-and-ci "link desync suite"),
-- generalized out of the hand-rolled harness in tests/run_link_tests.lua
-- so a modded battle can be driven the same way an unmodded one is.
--
-- Both sides run the whole engine locally off a shared seed; the only
-- thing crossing the wire is the turn's choice and a per-turn state hash.
-- If a mod changes battle math on one side only, the hashes diverge -- so
-- "the hashes agreed every turn" is the desync assertion, and the
-- handshake fingerprint is what is supposed to stop that battle starting.

local Net = require("src.link.Net")
local Protocol = require("src.link.Protocol")
local Pokemon = require("src.pokemon.Pokemon")
local SaveData = require("src.core.SaveData")

local Link = {}

-- the 15-line stack pattern from tests/run_link_tests.lua:171-186
function Link.fakeGame(data, leadSpecies, opts)
  opts = opts or {}
  local save = SaveData.newGame()
  local level = opts.level or 50
  for _, species in ipairs(type(leadSpecies) == "table" and leadSpecies or { leadSpecies }) do
    table.insert(save.party, Pokemon.new(data, species, level))
  end
  if opts.name then save.player.name = opts.name end

  local stack = { list = {} }
  function stack:push(state, ...)
    table.insert(self.list, state)
    if state.enter then state:enter(...) end
  end
  function stack:pop() table.remove(self.list) end
  function stack:top() return self.list[#self.list] end
  function stack:update(dt)
    local top = self:top()
    if top and top.update then top:update(dt) end
  end

  local Input = require("src.core.Input")
  return { data = data, input = Input, stack = stack, save = save }
end

-- the engine bits a battle needs before any of this runs
function Link.prepare(data)
  local Input = require("src.core.Input")
  Input:init()
  require("src.render.Font").load(data)
  return Input
end

local function steerReplacement(game)
  local top = game.stack:top()
  if not (top and top.forceSwitch and top.party) then return end
  for i, mon in ipairs(top.party) do
    if mon.hp > 0 then top.index = i return end
  end
end

-- run a full lockstep battle over a loopback pair, mashing A on both
-- sides, and report whether any turn's hashes disagreed
function Link.lockstep(gameA, gameB, opts)
  opts = opts or {}
  local LinkBattle = require("src.link.LinkBattle")
  local Input = require("src.core.Input")

  local netA, netB = Net.loopbackPair()
  local packedA = Protocol.packParty(gameA.save.party)
  local packedB = Protocol.packParty(gameB.save.party)
  local seed = opts.seed or 987654321

  local battleA = LinkBattle.newHost(gameA, netA, {
    myParty = packedA, theirParty = packedB,
    theirName = gameB.save.player.name, seed = seed,
  })
  local battleB = LinkBattle.newGuest(gameB, netB, {
    myParty = packedB, theirParty = packedA,
    theirName = gameA.save.player.name, seed = seed,
  })

  local resA, resB
  battleA.onFinish = function(r) resA = r end
  battleB.onFinish = function(r) resB = r end
  gameA.stack:push(battleA)
  gameB.stack:push(battleB)

  local guard, limit = 0, opts.maxFrames or 60000
  while (resA == nil or resB == nil) and guard < limit do
    guard = guard + 1
    Input.pressed = { a = true }
    steerReplacement(gameA)
    gameA.stack:update(1 / 60)
    steerReplacement(gameB)
    gameB.stack:update(1 / 60)
  end

  -- a turn present on both sides with different hashes is the desync the
  -- suite exists to catch
  local mismatch
  for turn, hash in pairs(battleA.localHashes) do
    local other = battleB.localHashes[turn]
    if other and other ~= hash then mismatch = mismatch or turn end
  end

  return {
    battleA = battleA, battleB = battleB,
    resultA = resA, resultB = resB,
    frames = guard, completed = resA ~= nil and resB ~= nil,
    desyncTurn = mismatch,
    agreed = mismatch == nil,
  }
end

-- convenience: build both sides and run, for the common symmetric case
function Link.pair(data, leadA, leadB, opts)
  opts = opts or {}
  Link.prepare(data)
  local gameA = Link.fakeGame(data, leadA, { name = opts.nameA or "RED", level = opts.level })
  local gameB = Link.fakeGame(data, leadB, { name = opts.nameB or "BLUE", level = opts.level })
  return gameA, gameB
end

-- the two hellos the handshake compares.  A one-sided mod moves the
-- fingerprint, which must land as "subset" -- and subset is not
-- battleAllowed, so the lockstep battle never starts instead of desyncing
-- into an unexplained draw.
function Link.handshake(gameA, gameB, modeA, modeB)
  local Handshake = require("src.link.Handshake")
  local helloA = Handshake.hello(gameA, modeA or "battle")
  local helloB = Handshake.hello(gameB, modeB)
  local verdict, reason = Handshake.checkCompat(helloA, helloB)
  return {
    helloA = helloA, helloB = helloB,
    verdict = verdict, reason = reason,
    match = helloA.fingerprint == helloB.fingerprint,
    battleAllowed = Handshake.battleAllowed(verdict),
    tradeAllowed = Handshake.tradeAllowed(verdict),
  }
end

return Link
