-- Projects topology pipeline (pure): Capture -> Fingerprint -> AutoLayout (grid-pack).
dofile("wow_mock.lua")
dofile("../Core/HDGR_ProjectsShapeAtlas.lua")
dofile("../Core/HDGR_ProjectsIDs.lua")
dofile("../Core/HDGR_ProjectsFloorMap.lua")
dofile("../Core/HDGR_ProjectsFingerprint.lua")
dofile("../Core/HDGR_ProjectsAutoLayout.lua")
dofile("../Core/HDGR_ProjectsCapture.lua")

local FP  = HDG.Projects.Fingerprint
local AL  = HDG.Projects.AutoLayout
local Cap = HDG.Projects.Capture

local function assertEq(a, b, m) if a ~= b then error((m or "") .. ": expected " .. tostring(b) .. ", got " .. tostring(a), 2) end end
local function assertTrue(c, m) if not c then error(m or "assert", 2) end end

-- Door-facing radians (data compass) -> player-cardinal after the N<->S flip:
local PI = math.pi
local DATA_S, DATA_N, DATA_E, DATA_W = PI, 0, PI / 2, 3 * PI / 2  -- -> player N, S, E, W

-- ===== Fingerprint: the N<->S flip =====
assertEq(FP.facingToCardinal(DATA_S), "N", "data-S facing -> player N")
assertEq(FP.facingToCardinal(DATA_N), "S", "data-N facing -> player S")
assertEq(FP.facingToCardinal(DATA_E), "E", "data-E facing -> player E")
assertEq(FP.facingToCardinal(DATA_W), "W", "data-W facing -> player W")

-- ===== Capture.buildRoomRecord: doorCardinals only (occupancy/doors retired) =====
local rawA = { roomGUID = "gA", shape = "square_s", name = "A",
    doors = { { doorID = 1, connectionType = 7, facing = DATA_S, occupied = true } } }   -- player-N door
local recA = Cap.buildRoomRecord(rawA)
assertEq(recA.doorCardinals[1], "N", "Capture: door facing -> player N cardinal")
assertEq(recA.occupiedCardinals, nil, "Capture: occupiedCardinals retired (grid-pack SSoT)")
assertEq(recA.doors, nil, "Capture: raw doors retired (grid-pack SSoT)")
assertEq(recA.shape, "square_s", "Capture: shape preserved")

-- Multiple doors on the same cardinal dedup to a single sorted-unique entry.
local recDup = Cap.buildRoomRecord({ shape = "hallway", name = "D",
    doors = { { facing = DATA_S }, { facing = DATA_S }, { facing = DATA_N } } })  -- N, N, S
assertEq(#recDup.doorCardinals, 2, "Capture: same-cardinal doors dedup")
assertEq(recDup.doorCardinals[1], "N", "Capture: doorCardinals sorted (N first)")
assertEq(recDup.doorCardinals[2], "S", "Capture: doorCardinals sorted (S second)")

-- ===== AutoLayout grid-pack: rooms pack in a row, never overlapping =====
-- square_s is a 4x4 footprint (half-module grid); a 1-cell gutter sits between rooms.
local two = AL.compute({
    rooms = { A = { shape = "square_s", captureIndex = 1 },
              B = { shape = "square_s", captureIndex = 2 } },
}).layout
assertEq(two.A.cell.x, 0, "A first in row at x=0"); assertEq(two.A.cell.y, 0, "A at y=0")
assertEq(two.A.w, 4, "square_s footprint width = 4"); assertEq(two.A.d, 4, "square_s footprint depth = 4")
assertEq(two.B.cell.x, 5, "B next in row (4 wide + 1 gutter)"); assertEq(two.B.cell.y, 0, "B same row")

-- ===== AutoLayout: rotation inferred from captured door cardinals =====
local rot = AL.compute({
    rooms = { H = { shape = "hallway", doorCardinals = { "N", "S" }, captureIndex = 1 } },
}).layout
assertEq(rot.H.rotation, 1, "hallway captured N/S -> rotation 1")
assertEq(rot.H.w, 2, "rotated hallway width = 2"); assertEq(rot.H.d, 4, "rotated hallway depth = 4")

-- ===== AutoLayout: the ENTRY (anchor) room goes bottom-centre =====
-- isBase marks the entry; it's excluded from the row pack and placed below, centred.
local withEntry = AL.compute({
    rooms = { A = { shape = "square_s", captureIndex = 1 },
              B = { shape = "square_s", captureIndex = 2 },
              E = { shape = "square_s", isBase = true } },
}).layout
assertEq(withEntry.A.cell.x, 0, "non-entry A packs first"); assertEq(withEntry.B.cell.x, 5, "non-entry B second")
-- packed row spans x[0..8] (maxRight 9); entry (w4) centred -> floor((9-4)/2)=2, below row (y=5)
assertEq(withEntry.E.cell.x, 2, "entry centred under the packed row")
assertEq(withEntry.E.cell.y, 5, "entry below the packed row (bottom)")

-- ===== AutoLayout: rows wrap past MAX_ROW_W (22 cells) =====
-- Five square_s (each 4 wide + 1 gutter = 5): four fit a row (x 0,5,10,15), the fifth wraps.
local wrapRooms = {}
for i = 1, 5 do wrapRooms["r" .. i] = { shape = "square_s", captureIndex = i } end
local wrap = AL.compute({ rooms = wrapRooms }).layout
assertEq(wrap.r1.cell.x, 0, "r1 row 0 x=0"); assertEq(wrap.r4.cell.x, 15, "r4 still row 0")
assertEq(wrap.r4.cell.y, 0, "r4 on row 0"); assertEq(wrap.r5.cell.x, 0, "r5 wraps to next row x=0")
assertEq(wrap.r5.cell.y, 5, "r5 wraps below (depth 4 + gutter 1)")

-- ===== AutoLayout: determinism -- same input, same output =====
local a = AL.compute({ rooms = wrapRooms }).layout
local b = AL.compute({ rooms = wrapRooms }).layout
for id in pairs(a) do
    assertEq(b[id].cell.x, a[id].cell.x, "deterministic x for " .. id)
    assertEq(b[id].cell.y, a[id].cell.y, "deterministic y for " .. id)
end

-- ===== AutoLayout: footprints never overlap (entry + row + rotated hallway) =====
do
    local res = AL.compute({
        rooms = { E = { shape = "square_s", isBase = true },                       -- entry
                  A = { shape = "square_s", captureIndex = 1 },                     -- 4x4
                  H = { shape = "hallway", doorCardinals = { "N", "S" }, captureIndex = 2 } }, -- 2x4 rotated
    })
    local seen = {}
    for id, placed in pairs(res.layout) do
        for _, mc in ipairs(placed.mask) do
            local k = (placed.cell.x + mc[1]) .. "," .. (placed.cell.y + mc[2])
            assertTrue(not seen[k], "no two rooms share footprint cell " .. k)
            seen[k] = id
        end
    end
end

-- ===== FloorMap.FloatingDoorCardinal: garden's single door floats to nearest neighbour =====
do
    local FM = HDG.Projects.FloorMap
    local G  = "house:t:f1:raa"   -- 8x8 garden at origin
    local function card(extra)
        local rooms = { [G] = { shape = "circle_evening", cell = { x = 0, y = 0, rotation = 0 } } }
        for id, r in pairs(extra) do rooms[id] = r end
        return FM.FloatingDoorCardinal(rooms, G)
    end
    assertEq(card({ ["house:t:f1:rbb"] = { shape = "square_s", cell = { x = 2, y = 8 } } }), "S", "garden faces S neighbour")
    assertEq(card({ ["house:t:f1:rcc"] = { shape = "square_s", cell = { x = 8, y = 2 } } }), "E", "garden faces E neighbour")
    assertEq(card({ ["house:t:f1:rdd"] = { shape = "square_s", cell = { x = -4, y = 2 } } }), "W", "garden faces W neighbour")
    assertEq(card({ ["house:t:f1:ree"] = { shape = "square_s", cell = { x = 2, y = -4 } } }), "N", "garden faces N neighbour")
    -- closest wins: near E (gap 1) beats far N (gap 9)
    assertEq(card({ ["house:t:f1:ree"] = { shape = "square_s", cell = { x = 2, y = -9 } },
                    ["house:t:f1:rcc"] = { shape = "square_s", cell = { x = 8, y = 2 } } }), "E", "garden faces the CLOSER neighbour")
    assertEq(card({}), "N", "island garden falls back to its default door (N)")
    -- GROUND-LEVEL ONLY: a room on the floor ABOVE (f2) does NOT orient the ground door.
    assertEq(card({ ["house:t:f2:rff"] = { shape = "square_s", cell = { x = 2, y = 8 } } }), "N",
        "upper-floor neighbour ignored (garden door is ground-level only)")
end

-- ===== Stairwells share FloatingDoorCardinal: any cardinal, ground-floor connection =====
-- (Was W/E-only; now floats to whichever side the ground-floor room connects on. Island
-- falls back to the shape default -- staircase W, mirror E.)
do
    local FM = HDG.Projects.FloorMap
    local S  = "house:t:f1:raa"   -- 4x4 staircase at origin
    local function stairCard(shape, extra)
        local rooms = { [S] = { shape = shape, cell = { x = 0, y = 0, rotation = 0 } } }
        for id, r in pairs(extra or {}) do rooms[id] = r end
        return FM.FloatingDoorCardinal(rooms, S)
    end
    assertEq(stairCard("staircase", {}), "W", "island staircase -> default W")
    assertEq(stairCard("staircase_mirror", {}), "E", "island mirror staircase -> default E")
    assertEq(stairCard("staircase", { ["house:t:f1:rbb"] = { shape = "square_s", cell = { x = 0, y = -4 } } }), "N",
        "staircase floats to its N neighbour (any cardinal, not just W/E)")
    assertEq(stairCard("staircase", { ["house:t:f1:rcc"] = { shape = "square_s", cell = { x = 0, y = 4 } } }), "S",
        "staircase floats to its S neighbour")
end

-- ===== Fingerprint.facingToCardinal: quadrant boundary rounding + nil =====
do
    local Q = math.pi / 2
    assertEq(FP.facingToCardinal(Q / 2), "E", "exactly pi/4 buckets to E")
    assertEq(FP.facingToCardinal(Q / 2 - 0.001), "S", "just below pi/4 stays in the data-N (player S) bucket")
    assertEq(FP.facingToCardinal(-0.001), "S", "small negative wraps to data-N (player S)")
    assertEq(FP.facingToCardinal("bad"), nil, "non-number facing returns nil")
end

-- ===== ShapeAtlas footprint + rotation primitives =====
do
    local A = HDG.Projects.ShapeAtlas
    -- RotateCardinal: clockwise N->E->S->W
    assertEq(A.RotateCardinal("N", 1), "E", "N rotated 1 CW -> E")
    assertEq(A.RotateCardinal("N", 2), "S", "N rotated 2 CW -> S")
    assertEq(A.RotateCardinal("W", 1), "N", "W rotated 1 CW -> N")
    -- RotateCells: odd turns swap axes; even keep
    local hc = A.GetCells("hallway")   -- canonical {4,2} (half-module grid)
    assertEq(hc[1], 4, "hallway canonical w=4"); assertEq(hc[2], 2, "hallway canonical d=2")
    local hr = A.RotateCells(hc, 1)
    assertEq(hr[1], 2, "hallway rotated w=2"); assertEq(hr[2], 4, "hallway rotated d=4")
    assertEq(A.RotateCells(hc, 2)[1], 4, "180 keeps axes")
    -- GetMask: convex derives full bbox; non-convex shapes carry explicit masks
    assertEq(#A.GetMask("square_s"), 16, "square_s mask = full 4x4 (16 cells)")
    assertEq(#A.GetMask("cross_shape"), 12, "cross mask = 4x4 plus (12 cells)")
    assertEq(#A.GetMask("l_shape"), 8, "L mask = 3x3 L (8 cells)")
    assertEq(#A.GetMask("t_shape"), 10, "T mask = 4x3 T (10 cells)")
    -- RotateMask: T (4x3) rotated CW once -> 10 cells inside the rotated 3x4 bbox
    local tm = A.RotateMask(A.GetMask("t_shape"), 1, 4, 3)
    assertEq(#tm, 10, "rotated T keeps 10 cells")
    local maxx, maxy = 0, 0
    for _, c in ipairs(tm) do if c[1] > maxx then maxx = c[1] end; if c[2] > maxy then maxy = c[2] end end
    assertTrue(maxx <= 2 and maxy <= 3, "rotated T fits the 3-wide x 4-deep bbox")
    -- InferRotation from captured door cardinals
    assertEq(A.InferRotation("hallway", { "E", "W" }), 0, "hallway E/W -> rotation 0")
    assertEq(A.InferRotation("hallway", { "N", "S" }), 1, "hallway captured N/S -> rotated 1")
    assertEq(A.InferRotation("square_s", { "N", "E", "S", "W" }), 0, "symmetric square -> 0")
end

print("test_projects_topology.lua passed")
