import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useImagesStore } from '@/stores/images'
import type { Image } from '@/types'

const makeImage = (overrides: Partial<Image> = {}): Image => ({
  id: 1,
  originalFilename: 'photo.jpg',
  thumbnailUrl: '/thumb/1.jpg',
  originalUrl: '/orig/1.jpg',
  uploadedAt: '2026-01-01T00:00:00Z',
  approvedDeviceIds: [],
  cropParams: null,
  stickerState: null,
  cropOrientation: null,
  ...overrides,
})

describe('images store', () => {
  beforeEach(() => {
    vi.unstubAllGlobals()
    vi.restoreAllMocks()
    setActivePinia(createPinia())
  })

  afterEach(() => {
    // unstubAllGlobals is handled in beforeEach so stubs don't leak
    // even if a test throws before afterEach runs
  })

  it('fetchImages success populates images and clears loading', async () => {
    const mockImages = [makeImage(), makeImage({ id: 2 })]
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve(mockImages),
    }))

    const store = useImagesStore()
    await store.fetchImages()

    expect(store.images).toEqual(mockImages)
    expect(store.loading).toBe(false)
    expect(store.error).toBeNull()
  })

  it('fetchImages network error sets error state', async () => {
    vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('Net error')))

    const store = useImagesStore()
    await store.fetchImages()

    expect(store.images).toEqual([])
    expect(store.error).toBe('Net error')
    expect(store.loading).toBe(false)
  })

  it('uploadImage prepends to images list on success', async () => {
    const existing = makeImage({ id: 1 })
    const newImage = makeImage({ id: 2 })

    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve(newImage),
    }))

    const store = useImagesStore()
    store.images = [existing]

    const file = new File(['data'], 'photo.jpg', { type: 'image/jpeg' })
    const result = await store.uploadImage(file)

    expect(result).toEqual(newImage)
    expect(store.images[0]).toEqual(newImage)
    expect(store.images).toHaveLength(2)
  })

  it('uploadImage throws with error message on failure', async () => {
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
      ok: false,
      json: () => Promise.resolve({ error: 'File too large' }),
    }))

    const store = useImagesStore()
    const file = new File(['data'], 'photo.jpg', { type: 'image/jpeg' })

    await expect(store.uploadImage(file)).rejects.toThrow('File too large')
  })

  it('deleteImage removes image from list', async () => {
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true }))

    const store = useImagesStore()
    store.images = [makeImage({ id: 1 }), makeImage({ id: 2 })]

    await store.deleteImage(1)

    expect(store.images).toHaveLength(1)
    expect(store.images[0].id).toBe(2)
  })

  it('deleteImage throws on failure', async () => {
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false }))

    const store = useImagesStore()
    store.images = [makeImage()]

    await expect(store.deleteImage(1)).rejects.toThrow('Delete failed')
  })

  it('setApproval updates image in list', async () => {
    const original = makeImage({ id: 1, approvedDeviceIds: [] })
    const updated = makeImage({ id: 1, approvedDeviceIds: [42] })

    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve(updated),
    }))

    const store = useImagesStore()
    store.images = [original]

    await store.setApproval(1, 42, true)

    expect(store.images[0].approvedDeviceIds).toEqual([42])
  })

  it('fetchPendingCount stores the count', async () => {
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve({ count: 5 }),
    }))

    const store = useImagesStore()
    await store.fetchPendingCount()

    expect(store.pendingCount).toBe(5)
  })

  it('approveShared decrements pendingCount', async () => {
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve({ id: 1, status: 'approved' }),
    }))

    const store = useImagesStore()
    store.pendingCount = 3

    await store.approveShared(1, [42])

    expect(store.pendingCount).toBe(2)
  })

  it('declineShared decrements pendingCount', async () => {
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve({ id: 1, status: 'declined' }),
    }))

    const store = useImagesStore()
    store.pendingCount = 2

    await store.declineShared(1)

    expect(store.pendingCount).toBe(1)
  })

  it('fetchImages records "Unknown error" when the rejection is not an Error', async () => {
    vi.stubGlobal('fetch', vi.fn().mockRejectedValue('boom'))
    const store = useImagesStore()
    await store.fetchImages()
    expect(store.error).toBe('Unknown error')
  })

  it('fetchImages sets error when the response is not ok', async () => {
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false }))
    const store = useImagesStore()
    await store.fetchImages()
    expect(store.error).toBe('Failed to load images')
    expect(store.loading).toBe(false)
  })

  it('uploadImage forwards optional extras as form fields', async () => {
    const newImage = makeImage({ id: 5 })
    const fetchMock = vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve(newImage),
    })
    vi.stubGlobal('fetch', fetchMock)

    const store = useImagesStore()
    const file = new File(['x'], 'x.jpg')
    const original = new File(['y'], 'orig.jpg')
    await store.uploadImage(file, {
      original,
      cropParams: { natX: 0, natY: 0, natW: 50, natH: 50 },
      stickerState: [{ id: 's', type: 'emoji', x: 1, y: 2, scale: 1, rotation: 0 } as any],
      cropOrientation: 'portrait',
    })

    const sent = fetchMock.mock.calls[0][1].body as FormData
    expect(sent.get('file')).toBeInstanceOf(File)
    expect(sent.get('original')).toBeInstanceOf(File)
    expect(sent.get('cropParams')).toBe(JSON.stringify({ natX: 0, natY: 0, natW: 50, natH: 50 }))
    expect(sent.get('cropOrientation')).toBe('portrait')
  })

  it('uploadImage falls back to "Upload failed" when the server returns no error message', async () => {
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
      ok: false,
      json: () => Promise.reject(new Error('not json')),
    }))
    const store = useImagesStore()
    await expect(store.uploadImage(new File(['x'], 'x.jpg'))).rejects.toThrow('Upload failed')
  })

  it('reprocessImage replaces the matching image with the server response', async () => {
    const updated = makeImage({ id: 9, approvedDeviceIds: [1] })
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve(updated),
    }))
    const store = useImagesStore()
    store.images = [makeImage({ id: 9 })]
    const result = await store.reprocessImage(9, new File(['x'], 'x.jpg'), {
      cropParams: { natX: 0, natY: 0, natW: 1, natH: 1 },
      stickerState: [],
      cropOrientation: 'landscape',
    })
    expect(result).toEqual(updated)
    expect(store.images[0].approvedDeviceIds).toEqual([1])
  })

  it('reprocessImage no-ops the list when the image id is not present', async () => {
    const updated = makeImage({ id: 99 })
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve(updated),
    }))
    const store = useImagesStore()
    store.images = [makeImage({ id: 1 })]
    await store.reprocessImage(99, new File(['x'], 'x.jpg'))
    expect(store.images.find(i => i.id === 99)).toBeUndefined()
  })

  it('reprocessImage throws Reprocess failed when the server omits an error message', async () => {
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
      ok: false,
      json: () => Promise.reject(new Error('not json')),
    }))
    const store = useImagesStore()
    await expect(store.reprocessImage(1, new File(['x'], 'x.jpg'))).rejects.toThrow('Reprocess failed')
  })

  it('reprocessImage surfaces server-provided error messages', async () => {
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
      ok: false,
      json: () => Promise.resolve({ error: 'too big' }),
    }))
    const store = useImagesStore()
    await expect(store.reprocessImage(1, new File(['x'], 'x.jpg'))).rejects.toThrow('too big')
  })

  it('setApproval uses DELETE when approved=false', async () => {
    const updated = makeImage({ id: 1, approvedDeviceIds: [] })
    const fetchMock = vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve(updated),
    })
    vi.stubGlobal('fetch', fetchMock)
    const store = useImagesStore()
    store.images = [makeImage({ id: 1, approvedDeviceIds: [42] })]
    await store.setApproval(1, 42, false)
    expect(fetchMock.mock.calls[0][1].method).toBe('DELETE')
    expect(store.images[0].approvedDeviceIds).toEqual([])
  })

  it('setApproval throws on failure', async () => {
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false }))
    const store = useImagesStore()
    await expect(store.setApproval(1, 1, true)).rejects.toThrow('Failed to update approval')
  })

  it('setApproval does nothing to the list when the image id is not present', async () => {
    const updated = makeImage({ id: 100, approvedDeviceIds: [42] })
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve(updated),
    }))
    const store = useImagesStore()
    store.images = [makeImage({ id: 1 })]
    await store.setApproval(100, 42, true)
    expect(store.images.find(i => i.id === 100)).toBeUndefined()
  })

  it('fetchSharedImages includes status in the query when provided', async () => {
    const fetchMock = vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve({ items: [], total: 0, page: 1, limit: 20, totalPages: 1 }),
    })
    vi.stubGlobal('fetch', fetchMock)
    const store = useImagesStore()
    await store.fetchSharedImages('approved', 2, 10)
    const url = String(fetchMock.mock.calls[0][0])
    expect(url).toContain('status=approved')
    expect(url).toContain('page=2')
    expect(url).toContain('limit=10')
  })

  it('fetchSharedImages omits status when not provided', async () => {
    const fetchMock = vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve({ items: [], total: 0, page: 1, limit: 20, totalPages: 1 }),
    })
    vi.stubGlobal('fetch', fetchMock)
    const store = useImagesStore()
    await store.fetchSharedImages()
    expect(String(fetchMock.mock.calls[0][0])).not.toContain('status=')
  })

  it('fetchSharedImages throws on non-ok response', async () => {
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false }))
    const store = useImagesStore()
    await expect(store.fetchSharedImages()).rejects.toThrow('Failed to load shared images')
  })

  it('fetchPendingCount silently no-ops on a non-ok response', async () => {
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false }))
    const store = useImagesStore()
    store.pendingCount = 5
    await store.fetchPendingCount()
    expect(store.pendingCount).toBe(5)
  })

  it('approveShared does not go negative when pendingCount is already zero', async () => {
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve({ id: 1, status: 'approved' }),
    }))
    const store = useImagesStore()
    store.pendingCount = 0
    await store.approveShared(1, [42])
    expect(store.pendingCount).toBe(0)
  })

  it('approveShared throws on failure', async () => {
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false }))
    const store = useImagesStore()
    await expect(store.approveShared(1, [42])).rejects.toThrow('Failed to approve')
  })

  it('declineShared does not go negative when pendingCount is already zero', async () => {
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve({ id: 1, status: 'declined' }),
    }))
    const store = useImagesStore()
    store.pendingCount = 0
    await store.declineShared(1)
    expect(store.pendingCount).toBe(0)
  })

  it('declineShared throws on failure', async () => {
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false }))
    const store = useImagesStore()
    await expect(store.declineShared(1)).rejects.toThrow('Failed to decline')
  })

  it('shareImage POSTs to the share endpoint with the recipient email', async () => {
    const fetchMock = vi.fn().mockResolvedValue({ ok: true })
    vi.stubGlobal('fetch', fetchMock)
    const store = useImagesStore()
    await store.shareImage(7, 'a@b.com')
    expect(fetchMock).toHaveBeenCalledWith(
      '/api/images/7/share',
      expect.objectContaining({
        method: 'POST',
        body: JSON.stringify({ recipientEmail: 'a@b.com' }),
      }),
    )
  })

  it('shareImage surfaces server-provided error messages', async () => {
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
      ok: false,
      json: () => Promise.resolve({ error: 'rate limited' }),
    }))
    const store = useImagesStore()
    await expect(store.shareImage(1, 'a@b')).rejects.toThrow('rate limited')
  })

  it('shareImage falls back to a generic message when no body is parseable', async () => {
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
      ok: false,
      json: () => Promise.reject(new Error('not json')),
    }))
    const store = useImagesStore()
    await expect(store.shareImage(1, 'a@b')).rejects.toThrow('Failed to share')
  })
})
