Zum Inhalt

apple/PhysarumApp/Tests/SlotMigrationTests.swift

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
    }
}