-- test_furnishings_migration.lua
-- Phase 1.1 of the Furnishings build (docs/crate-redesign/14-BUILD-PLAN.md).
-- WRITTEN BEFORE THE MIGRATION EXISTS -- this file IS the v6->7 contract.
--
-- Fixture: tests/fixtures/sv_v6_with_crates.lua -- real donor topology
-- (2 houses, 1 version, 15 rooms), synthesized HDGR crates in reducer-verified
-- shape (2 attached w/ counted decor, 1 orphan w/ provenance), and REAL Gen-1
-- fossil crates found at the SV ROOT (HDG_DB.collections) in the wild.
--
-- Contract under test (10-FINAL-MODEL.md + 17-v8-placement-refactor.md):
--   account.schemaVersion 6 -> 8 DIRECTLY (v7 never shipped; no v7 migration)
--   version.rooms        -> account.rooms (persistent, clean room:N IDs)
--                        +  projects.layouts[lid].placements: v8 slot-keyed
--                           records (floor/x/y/rotation/shape) TAGGED with
--                           roomID + carrying capturedID lineage
--   attached crates      -> LOCAL furnishing sets (counts verbatim) equipped
--                           on their rooms
--   orphaned crates      -> unequipped LIBRARY sets named from provenance
--   crate collections    -> removed from account.collections (styles untouched)
--   Gen-1 root fossils   -> untouched at the SV root, never read
--   re-hydration         -> idempotent (no duplicate rooms/sets)
--
-- 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_Format.lua")
dofile("../Core/HDGR_Paths.lua")
dofile("../Core/HDGR_Log.lua")
dofile("../Core/HDGR_ProjectsIDs.lua")   -- parsePath (floor from legacy roomIDs) used by the migration
dofile("../Core/HDGR_StoreFurnishings.lua")
dofile("../Core/HDGR_Store.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

-- ============================================================================
-- Load fixture + derive donor facts (dynamic -- the fixture is real data)
-- ============================================================================

local fixture = dofile("fixtures/sv_v6_with_crates.lua")
assertEq(fixture.schemaVersion, 6, "fixture is v6")

local donorVid, donorVersion
for id, v in pairs(fixture.projects.versions) do donorVid, donorVersion = id, v end
assertTrue(donorVid, "fixture has a version")

local donorRoomIDs = {}
for rid in pairs(donorVersion.rooms) do donorRoomIDs[#donorRoomIDs + 1] = rid end
table.sort(donorRoomIDs)
local N_ROOMS = #donorRoomIDs
assertEq(N_ROOMS, 15, "donor has 15 rooms")

-- Crate facts (synthesized into the fixture; shapes are reducer-verified v6).
local attachedCrates, orphanCrate = {}, nil
for id, c in pairs(fixture.collections) do
    if c.parent then attachedCrates[#attachedCrates + 1] = c else orphanCrate = c end
end
table.sort(attachedCrates, function(a, b) return a.parent < b.parent end)
assertEq(#attachedCrates, 2, "fixture has 2 attached crates")
assertTrue(orphanCrate, "fixture has 1 orphan crate")

-- ============================================================================
-- Hydrate: build HDG_DB exactly as it sits on disk for a v6 user
-- ============================================================================

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

local function buildV6DB()
    local db = {
        account = {
            schemaVersion = 6,
            projects      = deepcopy(fixture.projects),
            collections   = deepcopy(fixture.collections),
        },
        -- Gen-1 fossils live at the SV ROOT in real player data.
        collections = deepcopy(fixture.legacyRootCollections),
    }
    -- A non-crate style entry must survive untouched.
    db.account.collections["style:keepme"] = {
        id = "style:keepme", type = "style", name = "Keep Me",
        items = { 111, 222 }, createdAt = 1780000001,
    }
    return db
end

_G.HDG_DB = buildV6DB()
HDG.Store:LoadFromSavedVariables()
local state = HDG.Store:GetState()
local acct  = state.account

-- ============================================================================
-- 1. Version bump
-- ============================================================================

assertEq(acct.schemaVersion, 8, "schemaVersion bumped straight to 8 (v7 never shipped)")

-- ============================================================================
-- 2. Persistent rooms: clean room:N IDs, one per donor room, fields carried
-- ============================================================================

assertTrue(type(acct.rooms) == "table", "account.rooms exists")
local roomCount = 0
for rid, room in pairs(acct.rooms) do
    roomCount = roomCount + 1
    assertTrue(rid:match("^room:%d+$"), "clean room ID format: " .. tostring(rid))
    assertEq(room.id, rid, "room.id matches key")
    assertTrue(room.shape, "room has shape: " .. rid)
    assertTrue(type(room.furnishingSetIDs) == "table", "room has set list: " .. rid)
    -- v8: capture lineage lives on the PLACEMENT (capturedID), never the room.
    assertNil(room.legacyID, "v8 room carries no legacyID: " .. rid)
end
assertEq(roomCount, N_ROOMS, "all 15 donor rooms became persistent rooms")

-- ============================================================================
-- 3. Layout placements: v8 slot keys, roomID tags, capturedID lineage
-- ============================================================================

local layouts = acct.projects.layouts
assertTrue(type(layouts) == "table", "projects.layouts exists")
local layout = layouts[donorVid]
assertTrue(layout, "donor version became a layout (same id)")
local byLegacy = {}
if layout then
    assertTrue(type(layout.placements) == "table", "layout has placements")
    local placed = 0
    for key, pl in pairs(layout.placements) do
        placed = placed + 1
        assertTrue(key:match("^slot:%d+$"), "v8 placement keys are slot keys: " .. tostring(key))
        assertTrue(pl.roomID and acct.rooms[pl.roomID],
            "placement tagged with a persistent room: " .. tostring(key))
        assertTrue(pl.capturedID, "placement carries capture lineage: " .. tostring(key))
        assertTrue(pl.shape, "placement carries its shape: " .. tostring(key))
        assertTrue(type(pl.floor) == "number", "placement carries floor: " .. tostring(key))
        assertTrue(type(pl.x) == "number" and type(pl.y) == "number",
            "placement carries x/y: " .. tostring(key))
        if pl.capturedID and pl.roomID then byLegacy[pl.capturedID] = acct.rooms[pl.roomID] end
    end
    assertEq(placed, N_ROOMS, "every donor room is placed (tagged) in the migrated layout")
    for _, legacyID in ipairs(donorRoomIDs) do
        local room = byLegacy[legacyID]
        assertTrue(room, "donor room migrated + tagged: " .. legacyID)
        if room then
            assertEq(room.shape, donorVersion.rooms[legacyID].shape,
                "shape carried for " .. legacyID)
        end
    end
    -- Floor + shape must match the legacy roomID encoding / donor record.
    for _, pl in pairs(layout.placements) do
        local donor = donorVersion.rooms[pl.capturedID]
        assertEq(pl.shape, donor and donor.shape, "placement shape stamped: " .. tostring(pl.capturedID))
        local f = pl.capturedID:match(":f(%d+):")
        if f then
            assertEq(pl.floor, tonumber(f), "placement floor matches legacy encoding: " .. pl.capturedID)
        end
    end
end

-- ============================================================================
-- 4. Attached crates -> LOCAL sets, counts verbatim, equipped on their rooms
-- ============================================================================

assertTrue(type(acct.furnishingSets) == "table", "account.furnishingSets exists")
local localSets, librarySets, setCount = {}, {}, 0
for sid, set in pairs(acct.furnishingSets) do
    setCount = setCount + 1
    assertTrue(sid:match("^set:%d+$"), "clean set ID format: " .. tostring(sid))
    if set.isLocal then localSets[#localSets + 1] = set
    else librarySets[#librarySets + 1] = set end
end
assertEq(setCount, 3, "3 crates -> 3 sets (1:1, no dedup)")
assertEq(#localSets, 2, "2 attached crates -> 2 local sets")
assertEq(#librarySets, 1, "1 orphan -> 1 library set")

-- byLegacy correlates crates (keyed by donor room IDs) to migrated rooms via
-- the placements' capturedID lineage built in section 3.
local function itemsMatch(setItems, crateDecor, label)
    assertEq(#setItems, #crateDecor, label .. ": item count")
    for i, d in ipairs(crateDecor) do
        local it = setItems[i]
        assertTrue(it and it.id == d.id and it.count == d.count,
            label .. ": item " .. i .. " {" .. d.id .. " x" .. d.count .. "} verbatim")
    end
end

for _, crate in ipairs(attachedCrates) do
    local room = byLegacy[crate.parent]
    assertTrue(room, "crate parent room migrated: " .. crate.parent)
    if room then
        assertEq(#room.furnishingSetIDs, 1, "room equips exactly its local set: " .. crate.parent)
        local set = acct.furnishingSets[room.furnishingSetIDs[1]]
        assertTrue(set and set.isLocal, "equipped set is local: " .. crate.parent)
        if set then
            assertEq(set.ownerRoom, room.id, "local set ownerRoom backref")
            itemsMatch(set.items, crate.decor, "crate " .. crate.id)
        end
    end
end

-- ============================================================================
-- 5. Orphan -> unequipped library set named from provenance
-- ============================================================================

local lib = librarySets[1]
if lib then
    assertTrue(not lib.isLocal, "orphan set is a library set")
    assertNil(lib.ownerRoom, "library set has no ownerRoom")
    itemsMatch(lib.items, orphanCrate.decor, "orphan crate")
    -- Naming rule: user-given crate name wins; the default "Crate"/"New crate"
    -- falls back to lastKnownRoomName provenance.
    local expectedName = orphanCrate.lastKnownRoomName
    if orphanCrate.name and orphanCrate.name ~= "Crate" and orphanCrate.name ~= "New crate" then
        expectedName = orphanCrate.name
    end
    assertEq(lib.name, expectedName, "orphan set named from provenance")
    -- And nothing equips it.
    for _, room in pairs(acct.rooms) do
        for _, sid in ipairs(room.furnishingSetIDs) do
            assertTrue(acct.furnishingSets[sid] ~= lib, "orphan set is unequipped")
        end
    end
end

-- ============================================================================
-- 6. account.collections: crates gone, styles untouched; root fossils intact
-- ============================================================================

for id, coll in pairs(acct.collections) do
    assertTrue(coll.type ~= "crate", "no crate left in account.collections: " .. id)
end
assertTrue(acct.collections["style:keepme"], "non-crate style survives untouched")
assertEq(acct.collections["style:keepme"].name, "Keep Me", "style fields untouched")

local fossilCount = 0
for id, v in pairs(_G.HDG_DB.collections or {}) do fossilCount = fossilCount + 1 end
local fixtureFossils = 0
for _ in pairs(fixture.legacyRootCollections) do fixtureFossils = fixtureFossils + 1 end
assertEq(fossilCount, fixtureFossils, "Gen-1 root fossils untouched (tolerated, never read)")

-- ============================================================================
-- 7. Idempotency: re-hydration must not duplicate anything
-- ============================================================================

-- Persist (re-link HDG_DB.account), then hydrate again off the migrated DB.
HDG.Store:Flush()
HDG.Store:LoadFromSavedVariables()
local acct2 = HDG.Store:GetState().account
local rooms2, sets2 = 0, 0
for _ in pairs(acct2.rooms) do rooms2 = rooms2 + 1 end
for _ in pairs(acct2.furnishingSets) do sets2 = sets2 + 1 end
assertEq(rooms2, N_ROOMS, "re-hydration: room count stable")
assertEq(sets2, 3, "re-hydration: set count stable")
assertEq(acct2.schemaVersion, 8, "re-hydration: still v8")

-- ============================================================================
-- 8. v7 fossil scrub: a pre-release v7 DB (room-keyed placements, room.legacyID)
--    hydrates clean -- fossils dropped, v8 slots + rooms + sets survive (the
--    live crash class: shapeless fossil records nil-index planDiff).
-- ============================================================================

_G.HDG_DB = {
    account = {
        schemaVersion = 7,
        rooms = {
            ["room:1"] = { id = "room:1", name = "Hall", shape = "hallway",
                           furnishingSetIDs = {}, legacyID = "house:x:f1:r1", createdAt = 0 },
        },
        furnishingSets = {}, roomSeq = 1, furnishingSetSeq = 0,
        projects = {
            houses  = { ["house:x"] = { currentVersionID = "version:1", activeVersionID = "version:1" } },
            layouts = { ["version:1"] = {
                houseID = "house:x", name = "Live", createdAt = 0, slotSeq = 2,
                placements = {
                    ["room:1"] = { floor = 1, x = 0, y = 0, rotation = 0 },   -- v7 fossil (no shape)
                    ["slot:2"] = { floor = 1, x = 4, y = 4, rotation = 0, shape = "square_s",
                                   capturedID = "house:x:f1:r2" },            -- fresh v8 slot
                },
            } },
            versionSeq = 1, schemaVersion = 2,
        },
        collections = {},
    },
}
HDG.Store:LoadFromSavedVariables()
local acct3 = HDG.Store:GetState().account
assertEq(acct3.schemaVersion, 8, "v7 scrub: stamped to 8")
local lay3 = acct3.projects.layouts["version:1"]
assertNil(lay3.placements["room:1"], "v7 scrub: room-keyed fossil placement dropped")
assertTrue(lay3.placements["slot:2"], "v7 scrub: v8 slot placement survives")
assertNil(acct3.rooms["room:1"].legacyID, "v7 scrub: retired room.legacyID stripped")
assertEq(acct3.rooms["room:1"].name, "Hall", "v7 scrub: the room itself survives")

-- The live 2026-06-11 case: schemaVersion ALREADY 8 but fossils present (a
-- fresh-DB version stamp carried fossil layouts along). The scrub must be
-- shape-gated, so version 8 does NOT exempt the fossils.
_G.HDG_DB.account.schemaVersion = 8
_G.HDG_DB.account.projects.layouts["version:1"].placements["room:1"] =
    { floor = 1, x = 0, y = 0, rotation = 0 }
HDG.Store:LoadFromSavedVariables()
local acct4 = HDG.Store:GetState().account
assertNil(acct4.projects.layouts["version:1"].placements["room:1"],
    "shape-gated scrub: fossil dropped even on a v8-stamped DB")
assertTrue(acct4.projects.layouts["version:1"].placements["slot:2"],
    "shape-gated scrub: v8 slots still untouched")
assertEq(acct4.schemaVersion, 8, "shape-gated scrub: version untouched when already 8")

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

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