-- test_furnishings_capture.lua
-- Phase 1.4 of the Furnishings build: THE data-critical regression test
-- (12-final-stress.md blocking condition / 11 F-NEW-2), rewritten for the v8
-- tag model (17-v8-placement-refactor.md). Capture writes placements ONLY --
-- a recapture can NEVER destroy rooms, their equipped sets, or their local
-- sets. v8 specifics under test: roomID tags SURVIVE recapture (in-place
-- capturedID match), new captured rooms arrive as unassigned slots whose
-- curation sticks, demolition removes the placement but never the room, and
-- CLEAR_HOUSE prunes only capture-owned placements above maxFloor.
--
-- NOTE: print() is a no-op in wow_mock.lua; use io.write() for output.

dofile("wow_mock.lua")
dofile("../Core/HDGR_SchemeConstants.lua")
dofile("../Core/HDGR_Constants.lua")
dofile("../Core/HDGR_Actions.lua")
dofile("../Core/HDGR_Resolvers.lua")
dofile("../Core/HDGR_Format.lua")
dofile("../Core/HDGR_Paths.lua")
dofile("../Core/HDGR_Log.lua")
dofile("../Core/HDGR_ProjectsIDs.lua")
dofile("../Core/HDGR_Memo.lua")
dofile("../Core/HDGR_TableUtils.lua")
dofile("../Core/HDGR_StoreFurnishings.lua")
dofile("../Core/HDGR_Store.lua")
dofile("../UI/HDGR_Selectors.lua")

local failures = 0
local function fail(msg) failures = failures + 1; io.write("FAIL: " .. msg .. "\n") end
local function assertEq(actual, expected, label)
    if actual ~= expected then
        fail(label .. ": expected " .. tostring(expected) .. " got " .. tostring(actual))
    end
end
local function assertTrue(v, label) if not v then fail(label) end end
local function assertNil(v, label)
    if v ~= nil then fail(label .. " (expected nil, got " .. tostring(v) .. ")") end
end

local function deepcopy(t)
    if type(t) ~= "table" then return t end
    local out = {}
    for k, v in pairs(t) do out[k] = deepcopy(v) end
    return out
end

-- ===== Boot from the migrated real-donor fixture ============================

local fixture = dofile("fixtures/sv_v6_with_crates.lua")
local donorVid, donorVersion
for id, v in pairs(fixture.projects.versions) do donorVid, donorVersion = id, v end
local houseID
for hid, h in pairs(fixture.projects.houses) do
    if h.currentVersionID == donorVid then houseID = hid end
end
assertTrue(houseID, "fixture house with current version found")

_G.HDG_DB = {
    account = {
        schemaVersion = 6,
        projects      = deepcopy(fixture.projects),
        collections   = deepcopy(fixture.collections),
    },
}
HDG.Store:LoadFromSavedVariables()
local A = HDG.Constants.ACTIONS
local function d(t, payload) HDG.Store:_RawDispatch({ type = t, payload = payload or {} }) end
local function st() return HDG.Store:GetState() end

-- Snapshot the furnishings ground truth post-migration.
local function snapshotFurnishings()
    local snap = { sets = {}, equips = {} }
    for sid, set in pairs(st().account.furnishingSets) do
        local items = {}
        for i, it in ipairs(set.items) do items[i] = it.id .. "x" .. it.count end
        snap.sets[sid] = table.concat(items, ",")
    end
    for rid, room in pairs(st().account.rooms) do
        snap.equips[rid] = table.concat(room.furnishingSetIDs, ",")
    end
    return snap
end
local function assertFurnishingsUnchanged(before, label)
    local after = snapshotFurnishings()
    for sid, sig in pairs(before.sets) do
        assertEq(after.sets[sid], sig, label .. ": set " .. sid .. " intact")
    end
    for rid, sig in pairs(before.equips) do
        assertEq(after.equips[rid], sig, label .. ": equips of " .. rid .. " intact")
    end
end

-- v8 counts: assigned vs unassigned is the roomID TAG, never the key shape.
local function countPlacements(lay)
    local tagged, bare = 0, 0
    for _, pl in pairs(lay.placements) do
        if pl.roomID then tagged = tagged + 1 else bare = bare + 1 end
    end
    return tagged, bare
end
local function keyForCaptured(lay, capturedID)
    for k, pl in pairs(lay.placements) do
        if pl.capturedID == capturedID then return k end
    end
end

local N_ROOMS = 0
for _ in pairs(st().account.rooms) do N_ROOMS = N_ROOMS + 1 end
local before = snapshotFurnishings()

-- Captured records mirror the donor's physical rooms (shape/name/cell),
-- keyed by the SAME deterministic captured IDs.
local function capturePayloadRooms()
    local rooms = {}
    for legacyID, rec in pairs(donorVersion.rooms) do
        rooms[legacyID] = { shape = rec.shape, name = rec.name, cell = deepcopy(rec.cell) }
    end
    return rooms
end

-- ===== Scenario 1: clean recapture (house unchanged) ========================

d(A.PROJECTS_CLEAR_HOUSE,   { houseID = houseID, clearedAt = 100 })
d(A.PROJECTS_CAPTURE_COMMIT, { houseID = houseID, lastCapturedAt = 101, rooms = capturePayloadRooms() })

local layout = st().account.projects.layouts[donorVid]
assertTrue(layout, "current layout exists")
local tagged, bare = countPlacements(layout)
assertEq(tagged, N_ROOMS, "clean recapture: every tag survives (in-place match)")
assertEq(bare, 0, "clean recapture: no unassigned slots")
assertFurnishingsUnchanged(before, "clean recapture")
assertEq(st().session.furn.lastCapture.matched, N_ROOMS, "capture summary: matched count")

-- ===== Scenario 2: a room changed shape in-game -> tag still survives =======
-- v8 matches capturedID 1:1 with no shape disambiguation: the spot is the
-- same physical room, renovated. Geometry/shape update in place.

local changedID
for legacyID in pairs(donorVersion.rooms) do changedID = legacyID break end
local rooms2 = capturePayloadRooms()
rooms2[changedID].shape = "___different_shape___"

local keyChanged  = keyForCaptured(layout, changedID)
local tagOnChanged = layout.placements[keyChanged].roomID
assertTrue(tagOnChanged, "precondition: the renovated spot is tagged")

d(A.PROJECTS_CLEAR_HOUSE,    { houseID = houseID, clearedAt = 200 })
d(A.PROJECTS_CAPTURE_COMMIT, { houseID = houseID, lastCapturedAt = 201, rooms = rooms2 })

tagged, bare = countPlacements(layout)
assertEq(tagged, N_ROOMS, "shape change: every tag survives")
assertEq(bare, 0, "shape change: no slot minted (1:1 capturedID match)")
assertEq(keyForCaptured(layout, changedID), keyChanged, "slot key stable across recapture")
assertEq(layout.placements[keyChanged].shape, "___different_shape___", "placement shape updated in place")
assertEq(layout.placements[keyChanged].roomID, tagOnChanged, "the tag rode through the renovation")
assertFurnishingsUnchanged(before, "shape change")

-- ===== Scenario 3: NEW physical room -> slot; curation sticks ===============

local newID = changedID .. "_annex"
local rooms3 = capturePayloadRooms()
rooms3[newID] = { shape = "squareS", name = "New Annex", cell = { x = 30, y = 30, rotation = 0 } }

d(A.PROJECTS_CLEAR_HOUSE,    { houseID = houseID, clearedAt = 300 })
d(A.PROJECTS_CAPTURE_COMMIT, { houseID = houseID, lastCapturedAt = 301, rooms = rooms3 })
tagged, bare = countPlacements(layout)
assertEq(tagged, N_ROOMS, "new room: existing tags untouched")
assertEq(bare, 1, "new room: arrives as ONE unassigned slot")
local theSlotKey = keyForCaptured(layout, newID)
assertTrue(theSlotKey, "slot carries capture lineage")
assertEq(st().session.furn.lastCapture.slots, 1, "capture summary: to-assign count")

-- Curate the slot in place: the slot key is stable, the tag is the curation.
d(A.FURN_ROOM_CREATE, { shape = "squareS", name = "Annex",
                        layoutID = donorVid, slotKey = theSlotKey })
local newRoomID = st().session.furn.lastRoomID
assertEq(layout.placements[theSlotKey].roomID, newRoomID, "create-in-place TAGS the slot (key survives)")
assertEq(layout.placements[theSlotKey].capturedID, newID, "lineage stays on the placement")

-- Recapture: the curated tag re-matches by capturedID -- no duplicate slot.
d(A.PROJECTS_CLEAR_HOUSE,    { houseID = houseID, clearedAt = 400 })
d(A.PROJECTS_CAPTURE_COMMIT, { houseID = houseID, lastCapturedAt = 401, rooms = rooms3 })
tagged, bare = countPlacements(layout)
assertEq(bare, 0, "post-curation recapture: lineage re-matches, no duplicate slot")
assertEq(tagged, N_ROOMS + 1, "post-curation recapture: all tagged (incl. the annex)")
assertEq(layout.placements[theSlotKey].roomID, newRoomID, "curated tag survives recapture")

-- ===== Scenario 4: demolished room ==========================================
-- v8: placements persist across CLEAR, so removal is the explicit per-floor
-- diff (deleteRoomIDs) the observer computes -- placement gone, room intact.

d(A.PROJECTS_CLEAR_HOUSE,    { houseID = houseID, clearedAt = 500 })
d(A.PROJECTS_CAPTURE_COMMIT, { houseID = houseID, lastCapturedAt = 501,
                               rooms = capturePayloadRooms(), deleteRoomIDs = { newID } })
tagged, bare = countPlacements(layout)
assertEq(tagged, N_ROOMS, "demolished: placement gone")
assertEq(bare, 0, "demolished: nothing left behind")
assertNil(keyForCaptured(layout, newID), "the demolished spot's placement is removed")
assertTrue(st().account.rooms[newRoomID], "the curated ROOM survives demolition (library)")
assertEq(st().session.furn.lastCapture.removed, 1, "capture summary: removed count")
assertFurnishingsUnchanged(before, "demolished room: furnishings fully intact")
local survivors = 0
for _ in pairs(st().account.rooms) do survivors = survivors + 1 end
assertEq(survivors, N_ROOMS + 1, "no room destroyed by any capture flow (donor rooms + annex)")

-- ===== Scenario 5: deleted floor -> CLEAR prunes above maxFloor =============
-- A floor that no longer exists never gets swept, so its per-floor diff never
-- runs; CLEAR_HOUSE prunes capture-owned placements above payload.maxFloor.
-- The fixture is single-floor: synthesize an upstairs room, curate it, then
-- delete the floor.

local upID   = changedID:gsub(":f1:", ":f2:")
local rooms4 = capturePayloadRooms()
rooms4[upID] = { shape = "squareM", name = "Loft", cell = { x = 2, y = 2, rotation = 0 } }
d(A.PROJECTS_CLEAR_HOUSE,    { houseID = houseID, clearedAt = 600 })
d(A.PROJECTS_CAPTURE_COMMIT, { houseID = houseID, lastCapturedAt = 601, rooms = rooms4 })
local upKey = keyForCaptured(layout, upID)
assertEq(layout.placements[upKey].floor, 2, "upstairs capture lands on floor 2")
d(A.FURN_ROOM_CREATE, { shape = "squareM", name = "Loft",
                        layoutID = donorVid, slotKey = upKey })
local loftRoomID = st().session.furn.lastRoomID
assertEq(layout.placements[upKey].roomID, loftRoomID, "upstairs slot curated (tagged)")

-- A user doodle (no capturedID) above maxFloor must NOT be pruned.
d(A.LAYOUT_PLACE, { layoutID = donorVid, shape = "squareL", floor = 9, x = 1, y = 1 })

d(A.PROJECTS_CLEAR_HOUSE, { houseID = houseID, clearedAt = 700, maxFloor = 1 })
local total, doodleSurvives = 0, false
for _, pl in pairs(layout.placements) do
    total = total + 1
    if not pl.capturedID and pl.shape == "squareL" then doodleSurvives = true end
    if pl.capturedID and pl.floor > 1 then
        fail("capture-owned placement above maxFloor survived the prune (floor " .. pl.floor .. ")")
    end
end
assertEq(total, N_ROOMS + 1, "prune removed exactly the upstairs capture-owned placement")
assertTrue(doodleSurvives, "user doodle above maxFloor is never pruned")
assertEq(st().session.furn.lastCapture.removed, 1, "echo seeds removed with pruned TAGGED spots")
assertTrue(st().account.rooms[loftRoomID], "the curated ROOM survives the floor delete (library)")
assertFurnishingsUnchanged(before, "floor prune: furnishings fully intact")

-- ============================================================================

if failures == 0 then
    io.write("test_furnishings_capture.lua passed\n")
else
    io.write(("test_furnishings_capture.lua FAILED (%d)\n"):format(failures))
    os.exit(1)
end
