-- Talking to overworld objects: the Route 30 roadblock's Rattata turn to face
-- the player (ObjectEvent is `jumptextfaceplayer`, home/map.asm, and
-- SPRITE_MONSTER is a WALKING_SPRITE so ApplyObjectFacing really turns it),
-- and the talked-to / engaged object holds still for the conversation the way
-- FreezeAllOtherObjects + EndScript's UnfreezeAllObjects bracket it on the
-- cart -- a SPINRANDOM trainer must not roll a new facing under his own
-- sighting text.
--
--   luajit tests/gen2_npc_interact_test.lua   (ROM-free)
package.path = "./?.lua;./?/init.lua;" .. package.path

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

love = require("tests.love_stub")

local World = require("src.world.gen2.World")
local NPC = require("src.world.gen2.Npc")
local Vm = require("src.script.gen2.Vm")
local Permissions = require("src.world.gen2.Permissions")

local COLL_FLOOR = 0x00

-- The gen2_world_test rig, trimmed: a flat map, a fake player, and NPCs built
-- as NPC-shaped tables (a real NPC.new needs a sprite sheet and a graphics
-- device) so facePlayer / update / scriptFace are the shipped methods.
local MAP_W, MAP_H = 10, 10

local function fakeMap()
  local map
  map = {
    id = "TEST_MAP",
    width = MAP_W, height = MAP_H,
    def = { bgEvents = {}, objects = {}, width = MAP_W, height = MAP_H },
    cellCollision = function() return COLL_FLOOR end,
    inBounds = function(_, x, y)
      return x >= 0 and y >= 0 and x < MAP_W * 2 and y < MAP_H * 2
    end,
    isWalkable = function() return true end,
    warpAt = function() return nil end,
  }
  return map
end

local function fakePlayer(x, y, facing)
  return {
    cellX = x, cellY = y, px = x * 16, py = y * 16,
    facing = facing, moving = false, turnArmed = true,
    update = function() return false end,
    setSprite = function() end,
  }
end

local function npcAt(def, x, y, facing, kind)
  return setmetatable({
    def = def, id = "npc_" .. tostring(def.index),
    cellX = x, cellY = y, homeX = x, homeY = y,
    px = x * 16, py = y * 16,
    facing = facing, moving = false, progress = 0, stepFlip = false,
    frozen = false, kind = kind or "stand",
    roamDirs = { "up", "down", "left", "right" },
    radiusX = 0, radiusY = 0, spinLo = 1, spinHi = 1, timer = 1,
  }, NPC)
end

local function talkWorld(opts)
  opts = opts or {}
  local game = {
    data = {},
    save = { player = { name = "GOLD" }, party = opts.party or {},
      inventory = {} },
  }
  local world = World.new(game)
  game.world = world
  world.map = fakeMap()
  world.maps = { TEST_MAP = world.map.def }
  world.player = fakePlayer(opts.px or 4, opts.py or 5, opts.facing or "right")
  world.pollTimeOfDay = function() end
  local log = {}
  world.log = log
  world.showText = function(self, body, onDone)
    log[#log + 1] = body
    self.textbox = true
    self.pendingText = function()
      self.textbox = nil
      if onDone then onDone() end
    end
  end
  world.vm = Vm.new(opts.scripts or {}, opts.texts or {}, world.events, {
    showText = function(body, onDone) world:showText(body, onDone) end,
    -- The shipped hook body from World:load, restated over this rig: the
    -- talked-to NPC turns through the real NPC:facePlayer.
    facePlayer = function()
      if world.talkNpc and world.player then
        world.talkNpc:facePlayer(world.player)
      end
    end,
    showEmote = function() end,
    encounterMusic = function() end,
    trainerApproach = function(onDone) world:trainerApproach(onDone) end,
    lookupTrainer = function()
      return { name = "DON", class = 36, member = 1 }
    end,
    startBattle = function(_, _, onDone) onDone("win") end,
    reloadMap = function() end,
  })
  return world
end

local function advanceText(world)
  local fn = world.pendingText
  world.pendingText = nil
  if fn then fn() end
end

-- ---- the roadblock monsters turn to the player -----------------------------
-- maps/Route30.asm: both SPRITE_MONSTER rows point at ObjectEvent, one
-- STANDING_DOWN and one STANDING_UP -- they face each other until spoken to.
do
  local OBJECT_EVENT = "00:2812"
  local monster = { index = 7, sprite = "SPRITE_MONSTER", movement = 7,
    scriptKey = OBJECT_EVENT }
  local world = talkWorld({
    scripts = { [OBJECT_EVENT] = {
      { op = "jumptextfaceplayer", text = "00:2815" },
    } },
    texts = { ["00:2815"] = "Object event." },
  })
  eq(NPC.new and select(1, "x"), "x", "NPC module loaded")
  local rattata = npcAt(monster, 5, 5, "up")
  world.npcs = { rattata }
  world.entities = { world.player, rattata }
  check(world:interact(), "A from the west starts the ObjectEvent script")
  eq(rattata.facing, "left", "the spoken-to Rattata turns to face the player")
  eq(world.log[1], "Object event.", "and its line prints")
  check(rattata.frozen, "it holds still while its text is up")
  advanceText(world)
  for _ = 1, 3 do world:step() end
  check(not rattata.frozen, "and is released when the interaction ends")
end

-- ---- a spinning trainer cannot spin under his own sighting text ------------
local BEAT_FLAG = 1000
local record = { event = BEAT_FLAG, class = 36, member = 1,
  seenText = "t:seen", winText = "t:win", scriptKey = "s:after" }

local function trainerWorld(px, py, facing, npcFacing)
  local world = talkWorld({
    px = px, py = py, facing = facing,
    party = { { species = "RATTATA", hp = 10 } },
    scripts = { ["s:after"] = { { op = "end" } } },
    texts = {
      ["t:seen"] = "Instead of a bug\nPOKéMON, I found\va trainer!",
      ["t:win"] = "Deary me.",
    },
  })
  local don = { index = 4, sprite = "SPRITE_BUG_CATCHER", movement = 10,
    trainer = record, sight = 3 }
  local npc = npcAt(don, 4, 7, npcFacing, "spin")
  world.npcs = { npc }
  world.entities = { world.player, npc }
  return world, npc
end

do
  -- Player one cell below the trainer, trainer facing down: engagement.
  local world, npc = trainerWorld(4, 8, "up", "down")
  check(world:checkTrainerBattle(), "inside the cone the trainer engages")
  check(npc.frozen, "and freezes for the whole engagement")
  local before = npc.facing
  npc.timer = 1
  for _ = 1, 30 do world:updatePeople() end
  eq(npc.facing, before, "his spin cannot roll a new facing mid-script")
  -- Drain the engagement: the emote's 30-frame hold, the seen text, the
  -- battle, the after script.  updatePeople keeps running under it, which is
  -- exactly when an unfrozen spinner would drift.
  local drifted = false
  for _ = 1, 60 do
    world:step()
    world:updatePeople()
    if world.vm:running() and npc.facing ~= before then drifted = true end
    advanceText(world)
  end
  eq(world.log[1], "Instead of a bug\nPOKéMON, I found\va trainer!",
    "the sighting text is the struct's seen text, verbatim")
  check(not drifted, "and the spin never rolled while the script ran")
  check(not world.vm:running(), "the engagement script finished")
  check(world.events:get(BEAT_FLAG), "and the trainer is marked beaten")
  for _ = 1, 3 do world:step() end
  check(not npc.frozen, "the freeze lifts once everything settles")
end

do
  -- Player BEHIND the trainer (he faces down, player stands above): nothing.
  local world = trainerWorld(4, 6, "down", "down")
  check(not world:checkTrainerBattle(),
    "standing behind his facing never engages")
  eq(#world.log, 0, "and no sighting text prints")
end

do
  -- Off his column entirely: nothing, whatever the range.
  local world = trainerWorld(6, 8, "up", "down")
  check(not world:checkTrainerBattle(), "off the sight line never engages")
end

do
  -- Facing him but beyond sight 3: nothing.
  local world, npc = trainerWorld(4, 11, "up", "down")
  npc.cellY = 7
  check(not world:checkTrainerBattle(), "past the sight range never engages")
end

do
  -- CheckTrainerEvent is PlayerEvents' FIRST test (engine/overworld/events.asm:
  -- 245) and, unlike every arm of CheckTileEvent, it is NOT behind
  -- wEnabledPlayerEvents -- MapEvents clears that byte every pass
  -- (events.asm:168) and CheckPlayerState only re-sets it on a step that
  -- landed (events.asm:210-221).  So the sight cone is polled on every
  -- overworld frame, not only on the frame a step lands: a spinner that
  -- rotates onto a STANDING player engages there and then.  Nothing is called
  -- directly here -- that is the point, the call SITE is what was wrong.
  local world, npc = trainerWorld(4, 8, "up", "up")
  world.updatePeople = function() end
  world:step()
  check(not world.vm:running(),
    "a trainer facing away leaves the standing player alone")
  npc.facing = "down"
  for _ = 1, 3 do world:step() end
  check(world.vm:running(),
    "and the frame he turns onto the player's line, he engages -- no step needed")
  check(npc.frozen, "the engagement freezes him the same way")
end

S.finish()
