import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'

// Mock the decode-gate so the cascade is paced purely by the store's cadence
// in tests (no real <img> load events in happy-dom). Individual tests can
// override per-call to exercise the gating itself.
vi.mock('../src/utils/preloadImage.js', () => ({
  preloadImage: vi.fn(() => Promise.resolve()),
}))
import { preloadImage } from '../src/utils/preloadImage.js'
import { useShowcaseStore } from '../src/stores/showcase.js'

let nextId

function stubShowcase({ empty = false } = {}) {
  globalThis.fetch = vi.fn(async () => {
    const images = empty
      ? []
      : Array.from({ length: 3 }, () => ({
          id: nextId++, sha256: 's', mime: 'image/jpeg',
          width: 1, height: 1, thumbnail_url: '/t',
        }))
    return {
      ok: true,
      status: 200,
      statusText: '200',
      text: async () => JSON.stringify({ images }),
    }
  })
}

describe('showcase store: buffered cascade', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
    nextId = 1
    preloadImage.mockReset()
    preloadImage.mockImplementation(() => Promise.resolve())
    vi.useFakeTimers()
  })
  afterEach(() => {
    vi.useRealTimers()
    vi.restoreAllMocks()
  })

  it('drains buffered items in fire-order at a fixed cadence, no dupes', async () => {
    stubShowcase()
    const s = useShowcaseStore()
    const p = s.loadInitial()
    // Past INITIAL_COUNT (60) × CADENCE (160ms = 9.6s) plus fetch microtasks.
    await vi.advanceTimersByTimeAsync(12000)
    await p
    expect(s.images.length).toBe(60)
    const ids = s.images.map((i) => i.id)
    expect(new Set(ids).size).toBe(60)            // dedup held
    expect(ids).toEqual([...ids].sort((a, b) => a - b))  // shown in fire order
  })

  it('reveals at most one item per cadence tick (decoupled from fetch)', async () => {
    stubShowcase()
    const s = useShowcaseStore()
    const p = s.loadInitial()
    await vi.advanceTimersByTimeAsync(1000)
    const early = s.images.length
    // Even though fetches resolve instantly, the consumer is rate-limited by
    // the cadence — a 1s window must not dump the whole buffer at once.
    expect(early).toBeGreaterThan(0)
    expect(early).toBeLessThan(60)
    await vi.advanceTimersByTimeAsync(12000)
    await p
    expect(s.images.length).toBe(60)
  })

  it('does not reveal a tile until its image has decoded', async () => {
    stubShowcase()
    let release
    // The first tile's preload hangs until we release it; all others resolve.
    preloadImage.mockImplementationOnce(() => new Promise((r) => { release = r }))
    const s = useShowcaseStore()
    const p = s.loadInitial()
    await vi.advanceTimersByTimeAsync(1000)
    expect(s.images.length).toBe(0)   // gated on the un-decoded first image
    release()
    await vi.advanceTimersByTimeAsync(12000)
    await p
    expect(s.images.length).toBe(60)  // cascade proceeds once it decodes
  })

  it('flags an empty library without starting the cascade', async () => {
    stubShowcase({ empty: true })
    const s = useShowcaseStore()
    const p = s.loadInitial()
    await vi.advanceTimersByTimeAsync(1000)
    await p
    expect(s.images.length).toBe(0)
    expect(s.isEmpty).toBe(true)
  })
})
