import XCTest
@testable import Physarum
final class ThumbnailStorageTests: XCTestCase {
private func tmpStore() -> PresetStore {
let dir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
return PresetStore(baseDir: dir)
}
func testWriteAndResolveCustomThumbnail() throws {
let store = tmpStore()
let png = Data([0x89, 0x50, 0x4E, 0x47])
try store.writeThumbnail(png, for: "custom-x")
let url = try XCTUnwrap(store.thumbnailURL(for: "custom-x"))
XCTAssertTrue(FileManager.default.fileExists(atPath: url.path))
}
func testCatalogPrefersCustomOverBundle() throws {
let store = tmpStore()
try store.writeThumbnail(Data([1, 2, 3]), for: "custom-y")
let url = PresetCatalog.thumbnailURL(for: "custom-y", store: store)
XCTAssertEqual(url?.lastPathComponent, "custom-y.png")
}
func testPresetsMissingThumbnail() throws {
let store = tmpStore()
try store.writeThumbnail(Data([1]), for: "has")
let missing = store.presetsMissingThumbnail(ids: ["has", "nope"])
XCTAssertEqual(missing, ["nope"])
}
func testBackfillTargetsOnlyMissing() throws {
let store = tmpStore()
try store.writeThumbnail(Data([1]), for: "a")
XCTAssertEqual(store.presetsMissingThumbnail(ids: ["a", "b", "c"]), ["b", "c"])
}
// A factory slot's preset id is `slot-NN` after the cutover, but it carries the original content
// id in `thumbnail`, so the curated bundled art still resolves (no slot-NN.png in the bundle).
func testFactorySlotResolvesBundledArtViaThumbnailKey() {
var p = PresetCatalog.fallbackPreset
p.id = "slot-00"; p.thumbnail = "aurora-drift"
let url = PresetCatalog.thumbnailURL(for: p, store: tmpStore()) // empty store, no slot-00.png
XCTAssertEqual(url?.lastPathComponent, "aurora-drift.png")
}
// Once the user saves/edits a slot, the local render (keyed by content hash) wins over the bundled key.
func testUserSlotRenderWinsOverBundledKey() throws {
let store = tmpStore()
var p = PresetCatalog.fallbackPreset
p.id = "slot-00"; p.thumbnail = "aurora-drift"
let key = PresetCatalog.thumbnailKey(for: p)
try store.writeThumbnail(Data([0x89, 0x50]), for: key)
let url = PresetCatalog.thumbnailURL(for: p, store: store)
XCTAssertEqual(url?.lastPathComponent, "\(key).png") // content-hash render wins
}
func testThumbnailKeyIsStableAndContentSensitive() {
let a = PresetCatalog.fallbackPreset.params
var b = a; b.bloomIntensity += 0.5
XCTAssertEqual(PresetCatalog.thumbnailKey(forParams: a), PresetCatalog.thumbnailKey(forParams: a)) // stable
XCTAssertNotEqual(PresetCatalog.thumbnailKey(forParams: a), PresetCatalog.thumbnailKey(forParams: b)) // any change → new key
}
// The core fix: a changed look invalidates the old render (content-addressed), so no stale preview.
func testChangedParamsInvalidateThumbnail() throws {
let store = tmpStore()
var p = PresetCatalog.fallbackPreset; p.id = "slot-00"; p.thumbnail = nil // a user slot
try store.writeThumbnail(Data([1]), for: PresetCatalog.thumbnailKey(for: p))
XCTAssertNotNil(PresetCatalog.thumbnailURL(for: p, store: store)) // resolves to its render
p.params.bloomIntensity += 0.5 // change the look
XCTAssertNil(PresetCatalog.thumbnailURL(for: p, store: store)) // stale render no longer resolves
}
func testPruneThumbnailsDropsOrphans() throws {
let store = tmpStore()
try store.writeThumbnail(Data([1]), for: "h-keep")
try store.writeThumbnail(Data([2]), for: "h-orphan")
store.pruneThumbnails(keeping: ["h-keep"])
XCTAssertNotNil(store.thumbnailURL(for: "h-keep"))
XCTAssertNil(store.thumbnailURL(for: "h-orphan"))
}
}