import XCTest
import PhysarumEngine
@testable import Physarum
final class SlotMigrationTests: XCTestCase {
private func preset(_ name: String) -> Preset {
var p = PresetCatalog.fallbackPreset; p.name = name; p.builtIn = false; return p
}
func testMigrateFillsEmptySlotsAndMarksUserOwned() {
var s = PresetSlots()
s.seed(.init(presets: [preset("Factory0"), preset("Factory1")], version: 1))
let overflow = s.migrate(customs: [preset("Old A"), preset("Old B")])
XCTAssertEqual(s.preset(at: 2)?.name, "Old A") // first empty after the 2 factory slots
XCTAssertEqual(s.preset(at: 3)?.name, "Old B")
XCTAssertTrue(s.isUserOwned(2)) // migrated customs are the user's
XCTAssertTrue(overflow.isEmpty)
}
func testMigrateDisplacesFactoryDefaultWhenNoEmptySlot() {
var s = PresetSlots()
// The factory fills all 24 slots (none userOwned) — the real upgrade situation.
for i in 0..<PresetSlots.count { s.fill(index: i, preset: preset("F\(i)"), userOwned: false) }
let overflow = s.migrate(customs: [preset("Old A"), preset("Old B")])
XCTAssertEqual(s.preset(at: 0)?.name, "Old A") // displaces the lowest factory default
XCTAssertEqual(s.preset(at: 1)?.name, "Old B")
XCTAssertTrue(s.isUserOwned(0)) // and they become the user's
XCTAssertEqual(s.preset(at: 2)?.name, "F2") // higher factory slots survive
XCTAssertTrue(overflow.isEmpty) // nothing lost
}
func testMigrateReturnsOverflowOnlyWhenEverySlotIsUserOwned() {
var s = PresetSlots()
// Every slot is the user's own → no factory default to displace → genuine overflow.
for i in 0..<PresetSlots.count { s.fill(index: i, preset: preset("Mine\(i)"), userOwned: true) }
let overflow = s.migrate(customs: [preset("Homeless")])
XCTAssertEqual(overflow.map { $0.name }, ["Homeless"]) // returned, not lost
}
/// A migrated custom is userOwned, so a LATER factory update must fill the untouched/empty slots
/// around it while leaving the migrated slot intact. (Closes the review's seed→migrate→seed test gap.)
func testMigratedCustomSurvivesLaterFactoryUpdate() {
var s = PresetSlots()
s.seed(.init(presets: [preset("F0"), preset("F1")], version: 1)) // slots 0,1 (not userOwned)
_ = s.migrate(customs: [preset("Mine")]) // → slot 2 (userOwned)
s.seed(.init(presets: [preset("G0"), preset("G1"), preset("G2"), preset("G3")], version: 2))
XCTAssertEqual(s.preset(at: 0)?.name, "G0") // untouched factory slot takes the new default
XCTAssertEqual(s.preset(at: 1)?.name, "G1")
XCTAssertEqual(s.preset(at: 2)?.name, "Mine") // migrated custom (userOwned) untouched
XCTAssertEqual(s.preset(at: 3)?.name, "G3") // new default fills the empty slot
}
}