/**
 * Lane J — the demo cannot touch the user's work.
 *
 * demos/lane-j.html used to run against the SAME database as the real editor.
 * Its scripted `save()` handed `pruneOrphans` a manifest containing only the
 * demo's own slot, and pruneOrphans deletes every stored file the manifest given
 * to it does not mention — so opening the demo deleted every custom model the
 * user had saved, and a run interrupted between save and clear left a fake
 * override that the LIVE game would then load.
 *
 * This reproduces both, against the real store/idb code, with the database name
 * being the only thing that changed.
 *
 *   node --experimental-strip-types --import ./src/editor/node-ts-resolve.mjs \
 *        src/editor/demo-isolation.test.ts
 */
export {} // everything is loaded with await import(), so say "module" explicitly

type Store = Map<string, unknown>
const dbs = new Map<string, Map<string, Store>>()

const fakeIndexedDb = {
  open(name: string) {
    const req: Record<string, unknown> = {
      result: null, onsuccess: null, onerror: null, onupgradeneeded: null, onblocked: null,
    }
    const stores = dbs.get(name) ?? new Map<string, Store>()
    dbs.set(name, stores)
    const db = {
      objectStoreNames: { contains: (s: string) => stores.has(s) },
      createObjectStore: (s: string) => void stores.set(s, new Map()),
      transaction: (s: string) => ({
        objectStore: () => {
          const store = stores.get(s)!
          const wrap = <T>(result: T): Record<string, unknown> => {
            const r: Record<string, unknown> = { result, onsuccess: null, onerror: null }
            queueMicrotask(() => void (r.onsuccess as (() => void) | null)?.())
            return r
          }
          return {
            get: (k: string) => wrap(store.get(k)),
            put: (v: unknown, k: string) => { store.set(k, v); return wrap(undefined) },
            delete: (k: string) => { store.delete(k); return wrap(undefined) },
            getAllKeys: () => wrap([...store.keys()]),
            clear: () => { store.clear(); return wrap(undefined) },
          }
        },
      }),
    }
    req.result = db
    queueMicrotask(() => {
      ;(req.onupgradeneeded as (() => void) | null)?.()
      ;(req.onsuccess as (() => void) | null)?.()
    })
    return req
  },
  deleteDatabase(name: string) {
    const req: Record<string, unknown> = { onsuccess: null, onerror: null, onblocked: null }
    dbs.delete(name)
    queueMicrotask(() => void (req.onsuccess as (() => void) | null)?.())
    return req
  },
}
;(globalThis as unknown as { indexedDB: unknown }).indexedDB = fakeIndexedDb

const {
  BLOB_STORE, DEFAULT_DB_NAME, deleteDb, idbKeys, setDbName,
} = await import('./idb')
const {
  loadOverrideManifest, saveOverrideManifest, putGlb, pruneOrphans,
} = await import('./store')

const DEMO_DB = 'blobbo-workshop-demo'

let passed = 0
function ok(cond: boolean, msg: string): void {
  if (!cond) throw new Error('FAIL: ' + msg)
  passed++
}

const fingerprint = async (): Promise<string> => JSON.stringify({
  manifest: await loadOverrideManifest(),
  keys: (await idbKeys(BLOB_STORE)).map(String).sort(),
})

// ---- the user's real, hard-won session --------------------------------------
setDbName(DEFAULT_DB_NAME)
await putGlb('blob.body-my-blob.glb', 'my-blob.glb', new ArrayBuffer(2048))
await putGlb('machine.bucket-my-bucket.glb', 'my-bucket.glb', new ArrayBuffer(4096))
await saveOverrideManifest({
  'blob.body': { url: 'idb:blob.body-my-blob.glb', scale: 1.2 },
  'machine.bucket': { url: 'idb:machine.bucket-my-bucket.glb' },
})
const before = await fingerprint()
ok(Object.keys(JSON.parse(before).manifest).length === 2, 'the user has two saved swaps')

// ---- the demo runs, in its own database -------------------------------------
setDbName(DEMO_DB)
await putGlb('machine.boot-prop-spring-boot.glb', 'prop-spring-boot.glb', new ArrayBuffer(512))
const demoManifest = { 'machine.boot': { url: 'idb:machine.boot-prop-spring-boot.glb' } }
await saveOverrideManifest(demoManifest)
// The destructive call: with a shared database this deleted BOTH of the user's
// files, because neither appears in the demo's manifest.
const pruned = await pruneOrphans(demoManifest)
ok(pruned === 0, 'pruning inside the demo database finds nothing of the user\'s to delete')
ok(Object.keys(await loadOverrideManifest()).length === 1, 'the demo has its own manifest')

// ---- the demo is interrupted, then cleans up in its finally ------------------
setDbName(DEMO_DB)
await deleteDb(DEMO_DB)
setDbName(DEMO_DB)
ok(Object.keys(await loadOverrideManifest()).length === 0,
  'after cleanup the demo leaves no override behind — not even for its own database')

// ---- the user's session is untouched ----------------------------------------
setDbName(DEFAULT_DB_NAME)
const after = await fingerprint()
ok(after === before, 'the real store is byte-identical before and after the demo')
const m = JSON.parse(after).manifest as Record<string, { url: string }>
ok(m['blob.body']?.url === 'idb:blob.body-my-blob.glb', 'the custom blob body survived')
ok(m['machine.bucket']?.url === 'idb:machine.bucket-my-bucket.glb', 'the custom bucket survived')
ok((JSON.parse(after).keys as string[]).length === 2, 'both stored .glb files survived')
ok(!(JSON.parse(after).manifest as Record<string, unknown>)['machine.boot'],
  "the demo's fake spring-boot override never reached the game's database")

console.log(`editor demo-isolation.test: ${passed} assertions passed ✓`)
