import { describe, it, expect, beforeEach, vi } from 'vitest'

// apps.ts holds only app metadata + a reactive registry. `component` is a
// dynamic-import loader, not the page itself, so importing apps.ts pulls no
// page modules. The install/uninstall tests touch localStorage and the
// module-scoped installedIds store, so each re-imports the module fresh (see
// docked.test.ts for the same pattern).
import { builtinApps, appWindowId, appIdFromWindowId } from './apps'

describe('builtinApps registry', () => {
  it('has unique, non-empty ids', () => {
    const ids = builtinApps.map((a) => a.id)
    expect(ids.length).toBeGreaterThan(0)
    expect(new Set(ids).size).toBe(ids.length)
    for (const id of ids) expect(id).not.toBe('')
  })

  it('component is a loader function, not the component itself', () => {
    for (const app of builtinApps) {
      expect(typeof app.component).toBe('function')
    }
  })

  it('every built-in is source: builtin', () => {
    for (const app of builtinApps) expect(app.source).toBe('builtin')
  })

  it('windowed apps have positive default geometry', () => {
    for (const app of builtinApps.filter((a) => !a.docked)) {
      expect(app.width).toBeGreaterThan(0)
      expect(app.height).toBeGreaterThan(0)
    }
  })

  it('docked apps forbid window geometry', () => {
    for (const app of builtinApps.filter((a) => a.docked)) {
      expect(app.width).toBeUndefined()
      expect(app.height).toBeUndefined()
      expect(app.minWidth).toBeUndefined()
      expect(app.minHeight).toBeUndefined()
    }
  })

  it('includes the App Store and mascot as built-ins', () => {
    expect(builtinApps.find((a) => a.id === 'app-store')).toBeTruthy()
    const mascot = builtinApps.find((a) => a.id === 'mascot')
    expect(mascot?.docked).toBe(true)
  })
})

describe('appWindowId / appIdFromWindowId', () => {
  it('round-trips an app id through its window id', () => {
    for (const app of builtinApps) {
      expect(appIdFromWindowId(appWindowId(app.id))).toBe(app.id)
    }
  })

  it('returns null for ids that are not app windows', () => {
    expect(appIdFromWindowId('session:abc-123')).toBeNull()
    expect(appIdFromWindowId('host:strong')).toBeNull()
    expect(appIdFromWindowId('new-task')).toBeNull()
  })

  it('namespaces window ids so they cannot collide with entity slugs', () => {
    for (const app of builtinApps) {
      expect(appWindowId(app.id).startsWith('app:')).toBe(true)
    }
  })
})

// install/uninstall lifecycle — each test re-imports fresh so the
// module-scoped installedIds store starts empty and localStorage is clean.
describe('install / uninstall', () => {
  beforeEach(() => {
    localStorage.clear()
    vi.resetModules()
  })

  it('installApp adds a catalog app to the installed set', async () => {
    const fresh = await import('./apps')
    fresh.installApp('notes')
    let snap: string[] = []
    const unsub = fresh.installedAppIds.subscribe((v) => (snap = v))
    expect(snap).toContain('notes')
    unsub()
  })

  it('install is idempotent', async () => {
    const fresh = await import('./apps')
    fresh.installApp('notes')
    fresh.installApp('notes')
    let snap: string[] = []
    fresh.installedAppIds.subscribe((v) => (snap = v))
    expect(snap.filter((id) => id === 'notes')).toHaveLength(1)
  })

  it('installing an unknown manifest id is a no-op', async () => {
    const fresh = await import('./apps')
    fresh.installApp('does-not-exist')
    let snap: string[] = []
    fresh.installedAppIds.subscribe((v) => (snap = v))
    expect(snap).not.toContain('does-not-exist')
  })

  it('uninstall removes the app', async () => {
    const fresh = await import('./apps')
    fresh.installApp('notes')
    fresh.uninstallApp('notes')
    let snap: string[] = []
    fresh.installedAppIds.subscribe((v) => (snap = v))
    expect(snap).not.toContain('notes')
  })

  it('uninstall is idempotent', async () => {
    const fresh = await import('./apps')
    expect(() => fresh.uninstallApp('notes')).not.toThrow()
  })

  it('persists the installed set to localStorage', async () => {
    const fresh = await import('./apps')
    fresh.installApp('notes')
    const raw = localStorage.getItem('oikos-installed-apps')
    expect(raw).toBeTruthy()
    expect(JSON.parse(raw!)).toContain('notes')
  })

  it('drops persisted ids that no longer resolve to a catalog entry', async () => {
    localStorage.setItem('oikos-installed-apps', JSON.stringify(['notes', 'removed-app']))
    const fresh = await import('./apps')
    let snap: string[] = []
    fresh.installedAppIds.subscribe((v) => (snap = v))
    expect(snap).toContain('notes')
    expect(snap).not.toContain('removed-app')
  })
})
