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

import { downscaleDataUrlForPreview } from './image-resize'

// A minimal valid 1x1 red PNG (67 bytes) for testing
const TINY_PNG_B64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+GkZcAAAAASUVORK5CYII='

const TINY_PNG_DATA_URL = `data:image/png;base64,${TINY_PNG_B64}`

// A 1×1 transparent PNG used as the fallback placeholder
const FALLBACK_PLACEHOLDER =
  'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+GkZcAAAAASUVORK5CYII='

describe('downscaleDataUrlForPreview', () => {
  afterEach(() => {
    vi.restoreAllMocks()
    vi.unstubAllGlobals()
  })

  describe('without createImageBitmap (jsdom default)', () => {
    it('fails closed when createImageBitmap is unavailable', async () => {
      await expect(downscaleDataUrlForPreview('data:image/png;base64,ZmFrZQ==')).resolves.toBe(FALLBACK_PLACEHOLDER)
    })

    it('preserves an external source when resize APIs are unavailable', async () => {
      await expect(downscaleDataUrlForPreview('not-a-data-url')).resolves.toBe('not-a-data-url')
    })

    it('preserves a non-image data URL when resize APIs are unavailable', async () => {
      await expect(downscaleDataUrlForPreview('data:text/plain')).resolves.toBe('data:text/plain')
    })
  })

  describe('with mocked createImageBitmap + OffscreenCanvas', () => {
    /**
     * Set up the full mock chain: fetch → createImageBitmap → OffscreenCanvas → FileReader.
     * Mocks a bitmap of the given dimensions so the downscaling logic is exercised.
     */
    function setupMocks(bitmapWidth: number, bitmapHeight: number) {
      let activeBitmaps = 0
      let maxActiveBitmaps = 0

      const close = vi.fn(() => {
        activeBitmaps -= 1
      })

      const drawImage = vi.fn()
      const convertToBlob = vi.fn(async () => new Blob(['x'], { type: 'image/png' }))
      const canvasSizes: [number, number][] = []

      const bitmap = { width: bitmapWidth, height: bitmapHeight, close }
      const ctx = { drawImage }

      class MockOffscreenCanvas {
        getContext = vi.fn(() => ctx)
        convertToBlob = convertToBlob
        constructor(width: number, height: number) {
          canvasSizes.push([width, height])
        }
      }

      // Mock fetch to return a Blob from the data URL
      vi.stubGlobal(
        'fetch',
        vi.fn(async () => ({
          blob: async () => new Blob([new Uint8Array([0])], { type: 'image/png' })
        }))
      )
      vi.stubGlobal(
        'createImageBitmap',
        vi.fn(async () => {
          activeBitmaps += 1
          maxActiveBitmaps = Math.max(maxActiveBitmaps, activeBitmaps)

          return bitmap
        })
      )
      vi.stubGlobal('OffscreenCanvas', MockOffscreenCanvas)

      return { bitmap, close, drawImage, convertToBlob, ctx, canvasSizes, maxActiveBitmaps: () => maxActiveBitmaps }
    }

    it('returns the original when image is smaller than maxLongEdge', async () => {
      setupMocks(500, 300)

      const result = await downscaleDataUrlForPreview(TINY_PNG_DATA_URL, 2048)
      expect(result).toBe(TINY_PNG_DATA_URL)
    })

    it('downscales when image exceeds the default preview edge', async () => {
      const { drawImage, close } = setupMocks(4000, 3000)

      // In jsdom, FileReader.readAsDataURL won't actually produce a data URL,
      // so the function falls through to the fallback placeholder. But the
      // important thing is that drawImage was called with scaled dimensions
      // and the bitmap was closed.
      const result = await downscaleDataUrlForPreview(TINY_PNG_DATA_URL)

      // scale = 512 / 4000 = 0.128 → width=512, height=384
      expect(drawImage).toHaveBeenCalledWith(expect.anything(), 0, 0, 512, 384)
      expect(close).toHaveBeenCalled()

      // Result should be the downscaled data URL (from our mocked blob),
      // NOT the original tiny PNG — the function attempted downscaling.
      expect(result).not.toBe(TINY_PNG_DATA_URL)
      // The drawImage was called with correct dimensions and bitmap was cleaned up
      expect(drawImage).toHaveBeenCalled()
      expect(close).toHaveBeenCalled()
    })

    it('keeps every thumbnail bounded for a 72-image composer prompt', async () => {
      const { canvasSizes, drawImage, maxActiveBitmaps } = setupMocks(6000, 6000)

      await Promise.all(Array.from({ length: 72 }, () => downscaleDataUrlForPreview(TINY_PNG_DATA_URL)))

      expect(canvasSizes).toHaveLength(72)
      expect(canvasSizes.every(([width, height]) => width === 512 && height === 512)).toBe(true)
      expect(drawImage).toHaveBeenCalledTimes(72)
      expect(maxActiveBitmaps()).toBe(1)
    })

    it('returns placeholder when createImageBitmap throws', async () => {
      vi.stubGlobal(
        'fetch',
        vi.fn(async () => ({
          blob: async () => new Blob([new Uint8Array([0])], { type: 'image/png' })
        }))
      )
      vi.stubGlobal(
        'createImageBitmap',
        vi.fn(async () => {
          throw new Error('decode failed')
        })
      )
      vi.stubGlobal(
        'OffscreenCanvas',
        vi.fn(() => ({}))
      )

      const result = await downscaleDataUrlForPreview(TINY_PNG_DATA_URL, 2048)
      expect(result).toBe(FALLBACK_PLACEHOLDER)
    })

    it('closes bitmap even when canvas getContext returns null', async () => {
      const close = vi.fn()
      const bitmap = { width: 4000, height: 3000, close }

      class NullCanvas {
        getContext = () => null
        constructor(_w: number, _h: number) {}
      }

      vi.stubGlobal(
        'fetch',
        vi.fn(async () => ({
          blob: async () => new Blob([new Uint8Array([0])], { type: 'image/png' })
        }))
      )
      vi.stubGlobal(
        'createImageBitmap',
        vi.fn(async () => bitmap)
      )
      vi.stubGlobal('OffscreenCanvas', NullCanvas)

      const result = await downscaleDataUrlForPreview(TINY_PNG_DATA_URL, 2048)
      expect(result).toBe(FALLBACK_PLACEHOLDER)
      expect(close).toHaveBeenCalled()
    })

    it('uses placeholder instead of original on convertToBlob failure', async () => {
      const close = vi.fn()
      const bitmap = { width: 4000, height: 3000, close }

      class BrokenCanvas {
        getContext = () => ({ drawImage: vi.fn() })
        convertToBlob = vi.fn(async () => {
          throw new Error('blob failed')
        })
        constructor(_w: number, _h: number) {}
      }

      vi.stubGlobal(
        'fetch',
        vi.fn(async () => ({
          blob: async () => new Blob([new Uint8Array([0])], { type: 'image/png' })
        }))
      )
      vi.stubGlobal(
        'createImageBitmap',
        vi.fn(async () => bitmap)
      )
      vi.stubGlobal('OffscreenCanvas', BrokenCanvas)

      const result = await downscaleDataUrlForPreview(TINY_PNG_DATA_URL, 2048)
      expect(result).toBe(FALLBACK_PLACEHOLDER)
      expect(close).toHaveBeenCalled()
    })
  })
})
