import { describe, it, expect, vi, beforeEach } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import { setActivePinia, createPinia } from 'pinia'
import UploadView from '@/views/UploadView.vue'
import { useUploadStore } from '@/stores/upload'
import { useDevicesStore } from '@/stores/devices'
import { useImagesStore } from '@/stores/images'
import type { Device } from '@/types'

const routerReplace = vi.fn()

vi.mock('@/components/CropEditor.vue', () => ({
  default: {
    name: 'CropEditor',
    template: '<div class="crop-editor-stub" />',
    props: ['src', 'orientation', 'deviceName', 'initialParams', 'initialOrientation'],
    emits: ['crop'],
  },
}))
vi.mock('@/components/StickerCanvas.vue', () => ({
  default: {
    name: 'StickerCanvas',
    template: '<div class="sticker-canvas-stub" />',
    props: ['croppedUrl', 'orientation', 'stickers'],
    emits: ['add-sticker', 'update-sticker', 'remove-sticker', 'done'],
  },
}))
vi.mock('@/components/DevicePicker.vue', () => ({
  default: {
    name: 'DevicePicker',
    template: '<div class="device-picker-stub" />',
    props: ['modelValue', 'devices', 'selected', 'uploading'],
    emits: ['update:modelValue', 'update:selected', 'confirm'],
  },
}))
vi.mock('@/components/BaseButton.vue', () => ({
  default: {
    name: 'BaseButton',
    template: '<button @click="$emit(\'click\')"><slot /></button>',
    props: ['variant', 'disabled'],
    emits: ['click'],
  },
}))
vi.mock('vue-router', () => ({
  useRouter: () => ({ replace: routerReplace, push: vi.fn() }),
}))

const toastShow = vi.fn()
vi.mock('@/stores/toast', () => ({
  useToastStore: () => ({ show: toastShow }),
}))

const makeDevice = (overrides: Partial<Device> = {}): Device => ({
  id: 1,
  mac: 'AA',
  name: 'Living Room',
  orientation: 'landscape',
  model: 'v1',
  rotationIntervalMinutes: 60,
  wakeTimes: [],
  timezone: 'UTC',
  uniquenessWindow: 30,
  rotationMode: 'oldest_upload',
  prioritizeNeverShown: false,
  linkedAt: '2026-01-01T00:00:00Z',
  lastSeenAt: null,
  nextPollExpectedAt: null,
  lockedImageId: null,
  currentImageId: null,
  ...overrides,
})

function primeUploadStore(file?: File) {
  const upload = useUploadStore()
  upload.originalFile = file ?? new File(['x'], 'orig.jpg', { type: 'image/jpeg' })
  upload.originalUrl = 'blob:original'
  return upload
}

describe('UploadView', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
    routerReplace.mockClear()
    toastShow.mockClear()
    vi.unstubAllGlobals()
    vi.stubGlobal('URL', {
      createObjectURL: vi.fn(() => 'blob:mock'),
      revokeObjectURL: vi.fn(),
    })
  })

  it('redirects to / when no upload is in progress', async () => {
    vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
    mount(UploadView)
    await flushPromises()
    expect(routerReplace).toHaveBeenCalledWith('/')
  })

  it('starts on the crop step with the original URL passed to CropEditor', async () => {
    primeUploadStore()
    vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
    const wrapper = mount(UploadView)
    await flushPromises()
    const crop = wrapper.findComponent({ name: 'CropEditor' })
    expect(crop.exists()).toBe(true)
    expect(crop.props('src')).toBe('blob:original')
    expect(wrapper.text()).toContain('Crop photo')
  })

  it('uses "Edit crop" as the step label when editing', async () => {
    const upload = primeUploadStore()
    upload.editingImageId = 7
    vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
    const wrapper = mount(UploadView)
    await flushPromises()
    expect(wrapper.text()).toContain('Edit crop')
  })

  it('falls back to landscape orientation when no devices are loaded', async () => {
    primeUploadStore()
    vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
    const wrapper = mount(UploadView)
    await flushPromises()
    expect(wrapper.findComponent({ name: 'CropEditor' }).props('orientation')).toBe('landscape')
  })

  it('uses the context device orientation when contextDeviceId matches', async () => {
    const upload = primeUploadStore()
    upload.contextDeviceId = 2
    const devices = useDevicesStore()
    devices.devices = [makeDevice({ id: 1, orientation: 'landscape' }), makeDevice({ id: 2, orientation: 'portrait' })]
    vi.spyOn(devices, 'fetchDevices').mockResolvedValue()

    const wrapper = mount(UploadView)
    await flushPromises()
    expect(wrapper.findComponent({ name: 'CropEditor' }).props('orientation')).toBe('portrait')
    expect(wrapper.findComponent({ name: 'CropEditor' }).props('deviceName')).toBe(devices.devices[1].name)
  })

  it('uses the first device as the context when no contextDeviceId is set', async () => {
    primeUploadStore()
    const devices = useDevicesStore()
    devices.devices = [makeDevice({ id: 1, orientation: 'portrait' })]
    vi.spyOn(devices, 'fetchDevices').mockResolvedValue()
    const wrapper = mount(UploadView)
    await flushPromises()
    expect(wrapper.findComponent({ name: 'CropEditor' }).props('orientation')).toBe('portrait')
  })

  it('crop emit advances to stickers step and stores the crop on the upload store', async () => {
    primeUploadStore()
    vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
    const wrapper = mount(UploadView)
    await flushPromises()

    const blob = new Blob(['x'])
    const params = { natX: 0, natY: 0, natW: 100, natH: 50 }
    await wrapper.findComponent({ name: 'CropEditor' }).vm.$emit('crop', { blob, params, orientation: 'landscape' })
    await flushPromises()
    expect(wrapper.findComponent({ name: 'StickerCanvas' }).exists()).toBe(true)
    expect(wrapper.text()).toContain('Add stickers')
  })

  it('skip on stickers step opens the device picker for new uploads', async () => {
    const upload = primeUploadStore()
    vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
    const wrapper = mount(UploadView)
    await flushPromises()
    const blob = new Blob(['x'])
    await wrapper.findComponent({ name: 'CropEditor' }).vm.$emit('crop', { blob, params: { natX: 0, natY: 0, natW: 1, natH: 1 }, orientation: 'landscape' })
    upload.croppedBlob = blob
    await flushPromises()

    const skip = wrapper.find('.upload-view__skip')
    await skip.trigger('click')
    await flushPromises()
    expect(wrapper.findComponent({ name: 'DevicePicker' }).props('modelValue')).toBe(true)
  })

  it('skip on stickers triggers reprocess directly when editing', async () => {
    const upload = primeUploadStore()
    upload.editingImageId = 11
    const devices = useDevicesStore()
    vi.spyOn(devices, 'fetchDevices').mockResolvedValue()
    const images = useImagesStore()
    const reprocess = vi.spyOn(images, 'reprocessImage').mockResolvedValue({ id: 11 } as any)

    const wrapper = mount(UploadView)
    await flushPromises()
    const blob = new Blob(['x'])
    await wrapper.findComponent({ name: 'CropEditor' }).vm.$emit('crop', { blob, params: { natX: 0, natY: 0, natW: 1, natH: 1 }, orientation: 'landscape' })
    upload.croppedBlob = blob
    await flushPromises()

    await wrapper.find('.upload-view__skip').trigger('click')
    await flushPromises()
    expect(reprocess).toHaveBeenCalled()
    expect(wrapper.text()).toContain('Photo updated!')
  })

  it('skip is a no-op when there is no cropped blob yet', async () => {
    primeUploadStore()
    vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
    const wrapper = mount(UploadView)
    await flushPromises()
    // Force into stickers without having a croppedBlob
    await wrapper.findComponent({ name: 'CropEditor' }).vm.$emit('crop', { blob: null as any, params: null as any, orientation: 'landscape' })
    // Even if we click skip, no DevicePicker should open
    const skip = wrapper.find('.upload-view__skip')
    if (skip.exists()) await skip.trigger('click')
    expect(wrapper.findComponent({ name: 'DevicePicker' }).props('modelValue')).toBe(false)
  })

  it('stickers done opens the picker for new uploads', async () => {
    primeUploadStore()
    vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
    const wrapper = mount(UploadView)
    await flushPromises()
    await wrapper.findComponent({ name: 'CropEditor' }).vm.$emit('crop', {
      blob: new Blob(['x']),
      params: { natX: 0, natY: 0, natW: 1, natH: 1 },
      orientation: 'landscape',
    })
    await flushPromises()
    await wrapper.findComponent({ name: 'StickerCanvas' }).vm.$emit('done', new Blob(['final']))
    await flushPromises()
    expect(wrapper.findComponent({ name: 'DevicePicker' }).props('modelValue')).toBe(true)
  })

  it('stickers done triggers reprocess directly when editing', async () => {
    const upload = primeUploadStore()
    upload.editingImageId = 22
    vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
    const reprocess = vi.spyOn(useImagesStore(), 'reprocessImage').mockResolvedValue({ id: 22 } as any)

    const wrapper = mount(UploadView)
    await flushPromises()
    await wrapper.findComponent({ name: 'CropEditor' }).vm.$emit('crop', {
      blob: new Blob(['x']),
      params: { natX: 0, natY: 0, natW: 1, natH: 1 },
      orientation: 'landscape',
    })
    await flushPromises()
    await wrapper.findComponent({ name: 'StickerCanvas' }).vm.$emit('done', new Blob(['final']))
    await flushPromises()
    expect(reprocess).toHaveBeenCalled()
    expect(wrapper.text()).toContain('Photo updated!')
  })

  it('back from crop cleans up and routes to /library', async () => {
    primeUploadStore()
    vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
    const wrapper = mount(UploadView)
    await flushPromises()
    await wrapper.find('.upload-view__back').trigger('click')
    expect(routerReplace).toHaveBeenCalledWith('/library')
  })

  it('back from stickers returns to the crop step', async () => {
    primeUploadStore()
    vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
    const wrapper = mount(UploadView)
    await flushPromises()
    await wrapper.findComponent({ name: 'CropEditor' }).vm.$emit('crop', {
      blob: new Blob(['x']),
      params: { natX: 0, natY: 0, natW: 1, natH: 1 },
      orientation: 'landscape',
    })
    await flushPromises()
    expect(wrapper.text()).toContain('Add stickers')
    await wrapper.find('.upload-view__back').trigger('click')
    await flushPromises()
    expect(wrapper.text()).toContain('Crop photo')
  })

  it('confirm in the device picker uploads, sets approvals, and shows the done step', async () => {
    const upload = primeUploadStore()
    upload.selectedDeviceIds = [1, 2]
    const images = useImagesStore()
    const uploadSpy = vi.spyOn(images, 'uploadImage').mockResolvedValue({ id: 100 } as any)
    const approvalSpy = vi.spyOn(images, 'setApproval').mockResolvedValue()
    vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()

    const wrapper = mount(UploadView)
    await flushPromises()
    await wrapper.findComponent({ name: 'CropEditor' }).vm.$emit('crop', {
      blob: new Blob(['x']),
      params: { natX: 0, natY: 0, natW: 1, natH: 1 },
      orientation: 'landscape',
    })
    await flushPromises()
    await wrapper.findComponent({ name: 'StickerCanvas' }).vm.$emit('done', new Blob(['final']))
    await flushPromises()
    await wrapper.findComponent({ name: 'DevicePicker' }).vm.$emit('confirm')
    await flushPromises()

    expect(uploadSpy).toHaveBeenCalled()
    expect(approvalSpy).toHaveBeenCalledTimes(2)
    expect(wrapper.text()).toContain('Photo added!')
  })

  it('upload errors surface as a toast', async () => {
    primeUploadStore()
    vi.spyOn(useImagesStore(), 'uploadImage').mockRejectedValue(new Error('disk full'))
    vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()

    const wrapper = mount(UploadView)
    await flushPromises()
    await wrapper.findComponent({ name: 'CropEditor' }).vm.$emit('crop', {
      blob: new Blob(['x']),
      params: { natX: 0, natY: 0, natW: 1, natH: 1 },
      orientation: 'landscape',
    })
    await flushPromises()
    await wrapper.findComponent({ name: 'StickerCanvas' }).vm.$emit('done', new Blob(['final']))
    await flushPromises()
    await wrapper.findComponent({ name: 'DevicePicker' }).vm.$emit('confirm')
    await flushPromises()
    expect(toastShow).toHaveBeenCalledWith('disk full', 'error')
  })

  it('falls back to a generic message for non-Error rejections', async () => {
    primeUploadStore()
    vi.spyOn(useImagesStore(), 'uploadImage').mockRejectedValue('weird')
    vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()

    const wrapper = mount(UploadView)
    await flushPromises()
    await wrapper.findComponent({ name: 'CropEditor' }).vm.$emit('crop', {
      blob: new Blob(['x']),
      params: { natX: 0, natY: 0, natW: 1, natH: 1 },
      orientation: 'landscape',
    })
    await flushPromises()
    await wrapper.findComponent({ name: 'StickerCanvas' }).vm.$emit('done', new Blob(['final']))
    await flushPromises()
    await wrapper.findComponent({ name: 'DevicePicker' }).vm.$emit('confirm')
    await flushPromises()
    expect(toastShow).toHaveBeenCalledWith('Upload failed', 'error')
  })

  it('Done button on the success step routes to /library', async () => {
    primeUploadStore()
    vi.spyOn(useImagesStore(), 'uploadImage').mockResolvedValue({ id: 1 } as any)
    vi.spyOn(useImagesStore(), 'setApproval').mockResolvedValue()
    vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()

    const wrapper = mount(UploadView)
    await flushPromises()
    await wrapper.findComponent({ name: 'CropEditor' }).vm.$emit('crop', {
      blob: new Blob(['x']),
      params: { natX: 0, natY: 0, natW: 1, natH: 1 },
      orientation: 'landscape',
    })
    await flushPromises()
    await wrapper.findComponent({ name: 'StickerCanvas' }).vm.$emit('done', new Blob(['final']))
    await flushPromises()
    await wrapper.findComponent({ name: 'DevicePicker' }).vm.$emit('confirm')
    await flushPromises()

    routerReplace.mockClear()
    await wrapper.find('.upload-view__done-btn').trigger('click')
    expect(routerReplace).toHaveBeenCalledWith('/library')
  })

  it('updates selectedDeviceIds when the picker emits update:selected', async () => {
    const upload = primeUploadStore()
    vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
    const wrapper = mount(UploadView)
    await flushPromises()
    await wrapper.findComponent({ name: 'DevicePicker' }).vm.$emit('update:selected', [3, 4])
    expect(upload.selectedDeviceIds).toEqual([3, 4])
  })

  it('forwards sticker emits to the upload store', async () => {
    const upload = primeUploadStore()
    vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
    const wrapper = mount(UploadView)
    await flushPromises()
    await wrapper.findComponent({ name: 'CropEditor' }).vm.$emit('crop', {
      blob: new Blob(['x']),
      params: { natX: 0, natY: 0, natW: 1, natH: 1 },
      orientation: 'landscape',
    })
    await flushPromises()
    const canvas = wrapper.findComponent({ name: 'StickerCanvas' })
    const sticker = { id: 's1', type: 'emoji', x: 0, y: 0, scale: 1, rotation: 0 } as any
    await canvas.vm.$emit('add-sticker', sticker)
    expect(upload.stickers).toHaveLength(1)
    await canvas.vm.$emit('update-sticker', 's1', { x: 9 })
    expect(upload.stickers[0].x).toBe(9)
    await canvas.vm.$emit('remove-sticker', 's1')
    expect(upload.stickers).toHaveLength(0)
  })

  it('closes the device picker when it emits update:modelValue=false', async () => {
    primeUploadStore()
    vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
    const wrapper = mount(UploadView)
    await flushPromises()
    const picker = wrapper.findComponent({ name: 'DevicePicker' })
    expect(picker.props('modelValue')).toBe(false)
    await picker.vm.$emit('update:modelValue', true)
    await wrapper.vm.$nextTick()
    expect(wrapper.findComponent({ name: 'DevicePicker' }).props('modelValue')).toBe(true)
    await picker.vm.$emit('update:modelValue', false)
    await wrapper.vm.$nextTick()
    expect(wrapper.findComponent({ name: 'DevicePicker' }).props('modelValue')).toBe(false)
  })

  it('confirm fired before any stickers/crop is a no-op (defensive guard)', async () => {
    primeUploadStore()
    const uploadSpy = vi.spyOn(useImagesStore(), 'uploadImage').mockResolvedValue({ id: 1 } as any)
    vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
    const wrapper = mount(UploadView)
    await flushPromises()
    // Picker exists but finalBlob is null — confirm should early-return
    await wrapper.findComponent({ name: 'DevicePicker' }).vm.$emit('confirm')
    await flushPromises()
    expect(uploadSpy).not.toHaveBeenCalled()
  })

  it('reprocess on edit forwards undefined when cropParams/cropOrientation are null', async () => {
    const upload = primeUploadStore()
    upload.editingImageId = 33
    upload.cropParams = null
    upload.cropOrientation = null
    vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
    const reprocess = vi.spyOn(useImagesStore(), 'reprocessImage').mockResolvedValue({ id: 33 } as any)

    const wrapper = mount(UploadView)
    await flushPromises()
    await wrapper.findComponent({ name: 'CropEditor' }).vm.$emit('crop', {
      blob: new Blob(['x']),
      params: { natX: 0, natY: 0, natW: 1, natH: 1 },
      orientation: 'landscape',
    })
    await flushPromises()
    // After crop, upload.cropParams is set by setCrop. Reset to null to exercise null branch.
    upload.cropParams = null
    upload.cropOrientation = null
    await wrapper.findComponent({ name: 'StickerCanvas' }).vm.$emit('done', new Blob(['final']))
    await flushPromises()
    expect(reprocess).toHaveBeenCalledWith(33, expect.any(File), expect.objectContaining({
      cropParams: undefined,
      cropOrientation: undefined,
    }))
  })

  it('uploadImage on a new upload forwards undefined for null optional fields', async () => {
    const upload = primeUploadStore()
    upload.originalFile = null
    upload.cropParams = null
    upload.cropOrientation = null
    vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
    const uploadSpy = vi.spyOn(useImagesStore(), 'uploadImage').mockResolvedValue({ id: 1 } as any)

    // No originalFile → onMounted will redirect; instead set it after the redirect would have run
    upload.originalFile = new File(['x'], 'x.jpg')
    upload.originalUrl = 'blob:x'
    const wrapper = mount(UploadView)
    await flushPromises()
    await wrapper.findComponent({ name: 'CropEditor' }).vm.$emit('crop', {
      blob: new Blob(['x']),
      params: { natX: 0, natY: 0, natW: 1, natH: 1 },
      orientation: 'landscape',
    })
    await flushPromises()
    // Reset to null after onCrop sets these via the store's setCrop
    upload.cropParams = null
    upload.cropOrientation = null
    upload.originalFile = null
    await wrapper.findComponent({ name: 'StickerCanvas' }).vm.$emit('done', new Blob(['final']))
    await flushPromises()
    await wrapper.findComponent({ name: 'DevicePicker' }).vm.$emit('confirm')
    await flushPromises()

    expect(uploadSpy).toHaveBeenCalledWith(expect.any(File), expect.objectContaining({
      original: undefined,
      cropParams: undefined,
      cropOrientation: undefined,
    }))
  })

  it('does not render the device picker when in edit mode', async () => {
    const upload = primeUploadStore()
    upload.editingImageId = 12
    vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
    const wrapper = mount(UploadView)
    await flushPromises()
    expect(wrapper.findComponent({ name: 'DevicePicker' }).exists()).toBe(false)
  })

  it('hides the back button on the done step', async () => {
    primeUploadStore()
    vi.spyOn(useImagesStore(), 'uploadImage').mockResolvedValue({ id: 1 } as any)
    vi.spyOn(useImagesStore(), 'setApproval').mockResolvedValue()
    vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()

    const wrapper = mount(UploadView)
    await flushPromises()
    await wrapper.findComponent({ name: 'CropEditor' }).vm.$emit('crop', {
      blob: new Blob(['x']),
      params: { natX: 0, natY: 0, natW: 1, natH: 1 },
      orientation: 'landscape',
    })
    await flushPromises()
    await wrapper.findComponent({ name: 'StickerCanvas' }).vm.$emit('done', new Blob(['final']))
    await flushPromises()
    await wrapper.findComponent({ name: 'DevicePicker' }).vm.$emit('confirm')
    await flushPromises()

    expect(wrapper.find('.upload-view__back').exists()).toBe(false)
    expect(wrapper.text()).toContain('Added')
  })
})
