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

const routerPush = vi.fn()

// Stub heavy child components so tests focus on HomeView logic
vi.mock('@/components/FrameCard.vue', () => ({
  default: {
    name: 'FrameCard',
    template: '<div class="frame-card-stub" :data-device-id="deviceId" :data-name="name" />',
    props: ['deviceId', 'name', 'size', 'status', 'orientation', 'thumbnailUrl', 'lastSync', 'nextSync'],
    emits: ['add-photo', 'edit'],
  },
}))
vi.mock('@/components/BaseBottomSheet.vue', () => ({
  default: {
    name: 'BaseBottomSheet',
    template: '<div><slot /></div>',
    props: ['modelValue', 'label'],
    emits: ['update:modelValue'],
  },
}))
vi.mock('@/components/BaseButton.vue', () => ({
  default: {
    name: 'BaseButton',
    template: '<button><slot /></button>',
    props: ['variant', 'disabled'],
  },
}))
vi.mock('@/components/BaseInput.vue', () => ({
  default: {
    name: 'BaseInput',
    template: '<input />',
    props: ['modelValue', 'label', 'maxlength'],
    emits: ['update:modelValue'],
  },
}))
vi.mock('@/components/OrientationPicker.vue', () => ({
  default: {
    name: 'OrientationPicker',
    template: '<div />',
    props: ['modelValue'],
    emits: ['update:modelValue'],
  },
}))

// Stub vue-router so HomeView can call useRouter() without a real router
vi.mock('vue-router', () => ({
  useRouter: () => ({ push: routerPush }),
}))

// Stub URL.createObjectURL used by upload store
vi.stubGlobal('URL', {
  createObjectURL: vi.fn(() => 'blob:mock-url'),
  revokeObjectURL: vi.fn(),
})

const makeDevice = (overrides: Partial<Device> = {}): Device => ({
  id: 1,
  mac: 'AA:BB:CC:DD:EE:FF',
  name: 'Living Room',
  orientation: 'landscape',
  rotationIntervalMinutes: 60,
  wakeHour: null,
  timezone: 'America/Chicago',
  uniquenessWindow: 30,
  linkedAt: '2026-01-01T00:00:00Z',
  lastSeenAt: null,
  lockedImageId: null,
  currentImageId: null,
  ...overrides,
})

describe('HomeView', () => {
  let pinia: ReturnType<typeof createPinia>

  beforeEach(() => {
    vi.unstubAllGlobals()
    vi.restoreAllMocks()
    pinia = createPinia()
    setActivePinia(pinia)

    // Re-stub URL after unstubAllGlobals
    vi.stubGlobal('URL', {
      createObjectURL: vi.fn(() => 'blob:mock-url'),
      revokeObjectURL: vi.fn(),
    })

    // Stub fetch so onMounted fetchDevices doesn't fail
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve([]),
    }))
  })

  function mountView() {
    return mount(HomeView, {
      global: {
        plugins: [pinia],
      },
    })
  }

  // HV-01: N devices renders a carousel of N large FrameCard stubs + N dots
  it('renders one FrameCard per device in a carousel when multiple devices present', async () => {
    const devicesStore = useDevicesStore()
    devicesStore.devices = [
      makeDevice({ id: 1, name: 'Frame A' }),
      makeDevice({ id: 2, name: 'Frame B' }),
      makeDevice({ id: 3, name: 'Frame C' }),
    ]
    vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue()

    const wrapper = mountView()
    await wrapper.vm.$nextTick()

    expect(wrapper.find('.home-view__carousel').exists()).toBe(true)
    expect(wrapper.findAll('.frame-card-stub')).toHaveLength(3)
    // All cards should be the large variant (no more compact stack)
    const cards = wrapper.findAllComponents({ name: 'FrameCard' })
    for (const c of cards) expect(c.props('size')).toBe('large')
    // One navigation dot per device
    expect(wrapper.findAll('.home-view__dot')).toHaveLength(3)
  })

  it('marks the first dot active by default and updates active dot on scroll', async () => {
    const devicesStore = useDevicesStore()
    devicesStore.devices = [
      makeDevice({ id: 1, name: 'A' }),
      makeDevice({ id: 2, name: 'B' }),
    ]
    vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue()

    const wrapper = mountView()
    await wrapper.vm.$nextTick()
    let dots = wrapper.findAll('.home-view__dot')
    expect(dots[0].classes()).toContain('home-view__dot--active')
    expect(dots[1].classes()).not.toContain('home-view__dot--active')

    // Simulate the carousel having scrolled to the second slide
    const carousel = wrapper.find('.home-view__carousel').element as HTMLElement
    Object.defineProperty(carousel, 'clientWidth', { configurable: true, value: 360 })
    Object.defineProperty(carousel, 'scrollLeft',  { configurable: true, value: 360 })
    await wrapper.find('.home-view__carousel').trigger('scroll')
    await wrapper.vm.$nextTick()

    dots = wrapper.findAll('.home-view__dot')
    expect(dots[1].classes()).toContain('home-view__dot--active')
    expect(dots[0].classes()).not.toContain('home-view__dot--active')
  })

  it('clicking a dot scrolls the carousel to that slide', async () => {
    const devicesStore = useDevicesStore()
    devicesStore.devices = [makeDevice({ id: 1 }), makeDevice({ id: 2 }), makeDevice({ id: 3 })]
    vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue()

    const wrapper = mountView()
    await wrapper.vm.$nextTick()

    const carousel = wrapper.find('.home-view__carousel').element as HTMLElement
    Object.defineProperty(carousel, 'clientWidth', { configurable: true, value: 360 })
    const scrollToSpy = vi.fn()
    ;(carousel as any).scrollTo = scrollToSpy

    await wrapper.findAll('.home-view__dot')[2].trigger('click')
    await wrapper.vm.$nextTick()

    expect(scrollToSpy).toHaveBeenCalledWith(expect.objectContaining({ left: 720, behavior: 'smooth' }))
  })

  // HV-01b: single device still renders one FrameCard (large variant branch)
  it('renders one FrameCard for a single device', async () => {
    const devicesStore = useDevicesStore()
    devicesStore.devices = [makeDevice({ id: 1, name: 'Solo Frame' })]
    vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue()

    const wrapper = mountView()
    await wrapper.vm.$nextTick()

    const cards = wrapper.findAll('.frame-card-stub')
    expect(cards).toHaveLength(1)
  })

  // HV-02: empty state shown when no devices
  it('shows empty state when devices list is empty', async () => {
    const devicesStore = useDevicesStore()
    devicesStore.devices = []
    devicesStore.loading = false
    vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue()

    const wrapper = mountView()
    await wrapper.vm.$nextTick()

    expect(wrapper.find('.home-view__empty').exists()).toBe(true)
    expect(wrapper.text()).toContain('Set up your first frame')
  })

  // HV-03: loading state shown while fetching
  it('shows loading indicator when store is loading', async () => {
    const devicesStore = useDevicesStore()
    devicesStore.loading = true
    // Keep fetchDevices pending so loading stays true
    vi.spyOn(devicesStore, 'fetchDevices').mockReturnValue(new Promise(() => {}))

    const wrapper = mountView()
    await wrapper.vm.$nextTick()

    expect(wrapper.find('.home-view__loading').exists()).toBe(true)
    expect(wrapper.text()).toContain('Loading')
  })

  // HV-04: add-photo opens a file picker, primes the upload store, and navigates
  it('add-photo from a FrameCard primes upload state and routes to /upload', async () => {
    const devicesStore = useDevicesStore()
    devicesStore.devices = [makeDevice({ id: 7, name: 'Hall' })]
    vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue()
    routerPush.mockClear()

    // Spy on createElement so we can intercept the synthetic file input
    const realCreate = document.createElement.bind(document)
    let capturedInput: HTMLInputElement | null = null
    vi.spyOn(document, 'createElement').mockImplementation((tag: string) => {
      const el = realCreate(tag)
      if (tag === 'input') {
        capturedInput = el as HTMLInputElement
        // Don't actually open a file dialog
        ;(el as HTMLInputElement).click = vi.fn()
      }
      return el
    })

    const wrapper = mountView()
    await flushPromises()

    const card = wrapper.findComponent({ name: 'FrameCard' })
    await card.vm.$emit('add-photo', 7)

    expect(capturedInput).not.toBeNull()
    expect(capturedInput!.type).toBe('file')

    const file = new File(['x'], 'photo.jpg', { type: 'image/jpeg' })
    Object.defineProperty(capturedInput, 'files', { value: [file], configurable: true })
    capturedInput!.onchange?.(new Event('change'))

    const upload = useUploadStore()
    expect(upload.originalFile).toStrictEqual(file)
    expect(upload.contextDeviceId).toBe(7)
    expect(routerPush).toHaveBeenCalledWith('/upload')
  })

  it('add-photo without a chosen file does not navigate', async () => {
    const devicesStore = useDevicesStore()
    devicesStore.devices = [makeDevice({ id: 7 })]
    vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue()
    routerPush.mockClear()

    const realCreate = document.createElement.bind(document)
    let capturedInput: HTMLInputElement | null = null
    vi.spyOn(document, 'createElement').mockImplementation((tag: string) => {
      const el = realCreate(tag)
      if (tag === 'input') {
        capturedInput = el as HTMLInputElement
        ;(el as HTMLInputElement).click = vi.fn()
      }
      return el
    })

    const wrapper = mountView()
    await flushPromises()
    const card = wrapper.findComponent({ name: 'FrameCard' })
    await card.vm.$emit('add-photo', 7)

    Object.defineProperty(capturedInput, 'files', { value: [], configurable: true })
    capturedInput!.onchange?.(new Event('change'))

    expect(routerPush).not.toHaveBeenCalled()
  })

  // HV-05: edit opens the settings sheet pre-filled from the device record
  it('edit emits open the settings sheet pre-populated from the device', async () => {
    const devicesStore = useDevicesStore()
    devicesStore.devices = [makeDevice({ id: 9, name: 'Den', wakeHour: 22, timezone: 'America/Chicago' })]
    vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue()

    const wrapper = mountView()
    await flushPromises()

    const card = wrapper.findComponent({ name: 'FrameCard' })
    await card.vm.$emit('edit', 9)
    await flushPromises()

    const sheet = wrapper.findComponent({ name: 'BaseBottomSheet' })
    expect(sheet.props('modelValue')).toBe(true)
    // The name input is the first BaseInput stub
    const nameInput = wrapper.findComponent({ name: 'BaseInput' })
    expect(nameInput.props('modelValue')).toBe('Den')
  })

  it('edit for an unknown device id is a no-op', async () => {
    const devicesStore = useDevicesStore()
    devicesStore.devices = [makeDevice({ id: 1 })]
    vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue()

    const wrapper = mountView()
    await flushPromises()
    const card = wrapper.findComponent({ name: 'FrameCard' })
    await card.vm.$emit('edit', 999)
    await flushPromises()
    const sheet = wrapper.findComponent({ name: 'BaseBottomSheet' })
    expect(sheet.props('modelValue')).toBe(false)
  })

  // HV-06: saving the sheet calls updateDevice and closes it
  it('saving the settings sheet PATCHes via the store and closes', async () => {
    const devicesStore = useDevicesStore()
    devicesStore.devices = [makeDevice({ id: 5, name: 'Old', wakeHour: 4, timezone: 'UTC' })]
    vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue()
    const updateSpy = vi.spyOn(devicesStore, 'updateDevice').mockResolvedValue(makeDevice({ id: 5 }))

    const wrapper = mountView()
    await flushPromises()
    await wrapper.findComponent({ name: 'FrameCard' }).vm.$emit('edit', 5)
    await flushPromises()

    // Click save (the only button in the sheet stub for now)
    const saveBtn = wrapper.findAllComponents({ name: 'BaseButton' })
      .find(b => b.text().toLowerCase().includes('sav'))!
    await saveBtn.trigger('click')
    await flushPromises()

    expect(updateSpy).toHaveBeenCalledWith(5, expect.objectContaining({
      orientation: 'landscape',
      wakeHour: 4,
      timezone: 'UTC',
    }))
    const sheet = wrapper.findComponent({ name: 'BaseBottomSheet' })
    expect(sheet.props('modelValue')).toBe(false)
  })

  it('passes status="ok" to the FrameCard when lastSeenAt is recent', async () => {
    const devicesStore = useDevicesStore()
    devicesStore.devices = [makeDevice({ id: 1, lastSeenAt: new Date().toISOString() })]
    vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue()
    const wrapper = mountView()
    await flushPromises()
    expect(wrapper.findComponent({ name: 'FrameCard' }).props('status')).toBe('ok')
  })

  it('passes status="offline" when lastSeenAt is older than the rotation window', async () => {
    const devicesStore = useDevicesStore()
    devicesStore.devices = [makeDevice({
      id: 1,
      rotationIntervalMinutes: 60,
      lastSeenAt: new Date(Date.now() - 1000 * 60 * 60 * 24).toISOString(),
    })]
    vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue()
    const wrapper = mountView()
    await flushPromises()
    expect(wrapper.findComponent({ name: 'FrameCard' }).props('status')).toBe('offline')
  })

  it('passes status="sync-fail" when one sync window has been missed but not two', async () => {
    const devicesStore = useDevicesStore()
    // 90 minutes since last seen, interval = 60 — between 1× and 2× → sync-fail
    devicesStore.devices = [makeDevice({
      id: 1,
      rotationIntervalMinutes: 60,
      lastSeenAt: new Date(Date.now() - 1000 * 60 * 90).toISOString(),
    })]
    vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue()
    const wrapper = mountView()
    await flushPromises()
    expect(wrapper.findComponent({ name: 'FrameCard' }).props('status')).toBe('sync-fail')
  })

  it('uses a 24h window for devices configured with a daily wakeHour', async () => {
    const devicesStore = useDevicesStore()
    // wakeHour set, last seen 30h ago — between 1×24h and 2×24h → sync-fail
    devicesStore.devices = [makeDevice({
      id: 1,
      wakeHour: 4,
      rotationIntervalMinutes: 5, // ignored when wakeHour is set
      lastSeenAt: new Date(Date.now() - 1000 * 60 * 60 * 30).toISOString(),
    })]
    vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue()
    const wrapper = mountView()
    await flushPromises()
    expect(wrapper.findComponent({ name: 'FrameCard' }).props('status')).toBe('sync-fail')
  })

  it('passes a relative lastSync label and a nextSync label to the FrameCard', async () => {
    const devicesStore = useDevicesStore()
    devicesStore.devices = [makeDevice({
      id: 1,
      rotationIntervalMinutes: 60,
      lastSeenAt: new Date(Date.now() - 1000 * 60 * 30).toISOString(), // 30m ago
    })]
    vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue()
    const wrapper = mountView()
    await flushPromises()
    const props = wrapper.findComponent({ name: 'FrameCard' }).props()
    expect(props.lastSync).toMatch(/m ago/)
    expect(props.nextSync).toMatch(/next sync in/)
  })

  it('passes a wakeHour-based nextSync label when the device wakes daily', async () => {
    const devicesStore = useDevicesStore()
    devicesStore.devices = [makeDevice({
      id: 1,
      wakeHour: 4,
      timezone: 'UTC',
      lastSeenAt: new Date(Date.now() - 1000 * 60 * 60).toISOString(),
    })]
    vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue()
    const wrapper = mountView()
    await flushPromises()
    expect(wrapper.findComponent({ name: 'FrameCard' }).props('nextSync')).toMatch(/4 AM/)
  })

  it('formats lastSync as "yesterday" / "N days ago" / "just now"', async () => {
    const devicesStore = useDevicesStore()
    devicesStore.devices = [makeDevice({
      id: 1,
      lastSeenAt: new Date(Date.now() - 1000 * 60 * 60 * 26).toISOString(), // ~26h
    })]
    vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue()
    let wrapper = mountView()
    await flushPromises()
    expect(wrapper.findComponent({ name: 'FrameCard' }).props('lastSync')).toBe('yesterday')

    devicesStore.devices = [makeDevice({
      id: 1,
      lastSeenAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 4).toISOString(), // ~4 days
    })]
    wrapper = mountView()
    await flushPromises()
    expect(wrapper.findComponent({ name: 'FrameCard' }).props('lastSync')).toMatch(/4 days ago/)

    devicesStore.devices = [makeDevice({
      id: 1,
      lastSeenAt: new Date(Date.now() - 5_000).toISOString(), // 5 seconds
    })]
    wrapper = mountView()
    await flushPromises()
    expect(wrapper.findComponent({ name: 'FrameCard' }).props('lastSync')).toBe('just now')
  })

  it('omits nextSync when an interval-based device is already past its next expected sync', async () => {
    const devicesStore = useDevicesStore()
    devicesStore.devices = [makeDevice({
      id: 1,
      rotationIntervalMinutes: 60,
      lastSeenAt: new Date(Date.now() - 1000 * 60 * 90).toISOString(), // 90m ago, already late
    })]
    vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue()
    const wrapper = mountView()
    await flushPromises()
    expect(wrapper.findComponent({ name: 'FrameCard' }).props('nextSync')).toBeNull()
  })

  it('formats wakeHour 12 PM, 12 AM, and 8 PM correctly', async () => {
    const devicesStore = useDevicesStore()
    vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue()

    devicesStore.devices = [makeDevice({ id: 1, wakeHour: 12, timezone: 'UTC' })]
    let wrapper = mountView()
    await flushPromises()
    expect(wrapper.findComponent({ name: 'FrameCard' }).props('nextSync')).toMatch(/12 PM/)

    devicesStore.devices = [makeDevice({ id: 1, wakeHour: 0, timezone: 'UTC' })]
    wrapper = mountView()
    await flushPromises()
    expect(wrapper.findComponent({ name: 'FrameCard' }).props('nextSync')).toMatch(/12 AM/)

    devicesStore.devices = [makeDevice({ id: 1, wakeHour: 20, timezone: 'UTC' })]
    wrapper = mountView()
    await flushPromises()
    expect(wrapper.findComponent({ name: 'FrameCard' }).props('nextSync')).toMatch(/8 PM/)
  })

  it('returns null lastSync when the device has no recorded last-seen time', async () => {
    const devicesStore = useDevicesStore()
    devicesStore.devices = [makeDevice({ id: 1, lastSeenAt: null })]
    vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue()
    const wrapper = mountView()
    await flushPromises()
    const props = wrapper.findComponent({ name: 'FrameCard' }).props()
    expect(props.lastSync).toBeNull()
    expect(props.nextSync).toBeNull()
  })

  it('builds a thumbnail URL when the device has a current image', async () => {
    const devicesStore = useDevicesStore()
    devicesStore.devices = [makeDevice({ id: 1, currentImageId: 42 })]
    vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue()
    const wrapper = mountView()
    await flushPromises()
    expect(wrapper.findComponent({ name: 'FrameCard' }).props('thumbnailUrl')).toBe('/api/devices/1/preview?v=42')
  })

  it('prefers lockedImageId over currentImageId for the thumbnail', async () => {
    const devicesStore = useDevicesStore()
    devicesStore.devices = [makeDevice({ id: 1, lockedImageId: 99, currentImageId: 42 })]
    vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue()
    const wrapper = mountView()
    await flushPromises()
    expect(wrapper.findComponent({ name: 'FrameCard' }).props('thumbnailUrl')).toBe('/api/devices/1/preview?v=99')
  })

  it('updates editWakeHour when the user picks a different hour chip', async () => {
    const devicesStore = useDevicesStore()
    devicesStore.devices = [makeDevice({ id: 5, wakeHour: 4 })]
    vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue()
    vi.spyOn(devicesStore, 'updateDevice').mockResolvedValue(makeDevice({ id: 5 }))

    const wrapper = mountView()
    await flushPromises()
    await wrapper.findComponent({ name: 'FrameCard' }).vm.$emit('edit', 5)
    await flushPromises()

    const chips = wrapper.findAll('.home-view__interval-chip')
    const chip8pm = chips.find(c => c.text() === '8 PM')!
    await chip8pm.trigger('click')
    expect(chip8pm.classes()).toContain('home-view__interval-chip--on')

    const saveBtn = wrapper.findAllComponents({ name: 'BaseButton' })
      .find(b => b.text().toLowerCase().includes('sav'))!
    await saveBtn.trigger('click')
    await flushPromises()
    expect(devicesStore.updateDevice).toHaveBeenCalledWith(5, expect.objectContaining({ wakeHour: 20 }))
  })

  it('saving while no device is being edited is a no-op (defensive guard)', async () => {
    const devicesStore = useDevicesStore()
    devicesStore.devices = [makeDevice({ id: 1 })]
    vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue()
    const updateSpy = vi.spyOn(devicesStore, 'updateDevice').mockResolvedValue(makeDevice())

    const wrapper = mountView()
    await flushPromises()
    // The BaseBottomSheet stub always renders its slot, so the Save button is in
    // the DOM even before onEdit is called. Clicking it now exercises the
    // editingDevice null-guard.
    const saveBtn = wrapper.findAllComponents({ name: 'BaseButton' })
      .find(b => b.text().toLowerCase().includes('sav'))!
    await saveBtn.trigger('click')
    await flushPromises()
    expect(updateSpy).not.toHaveBeenCalled()
  })

  it('updates editName/orientation/timezone when their components emit changes', async () => {
    const devicesStore = useDevicesStore()
    devicesStore.devices = [makeDevice({ id: 5, name: 'Original', wakeHour: 4, timezone: 'UTC' })]
    vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue()
    const updateSpy = vi.spyOn(devicesStore, 'updateDevice').mockResolvedValue(makeDevice({ id: 5 }))

    const wrapper = mountView()
    await flushPromises()
    await wrapper.findComponent({ name: 'FrameCard' }).vm.$emit('edit', 5)
    await flushPromises()

    await wrapper.findComponent({ name: 'BaseInput' }).vm.$emit('update:modelValue', 'New Name')
    await wrapper.findComponent({ name: 'OrientationPicker' }).vm.$emit('update:modelValue', 'portrait')
    const select = wrapper.find('select.home-view__tz-select')
    ;(select.element as HTMLSelectElement).value = 'America/New_York'
    await select.trigger('change')

    const saveBtn = wrapper.findAllComponents({ name: 'BaseButton' })
      .find(b => b.text().toLowerCase().includes('sav'))!
    await saveBtn.trigger('click')
    await flushPromises()
    expect(updateSpy).toHaveBeenCalledWith(5, expect.objectContaining({
      name: 'New Name',
      orientation: 'portrait',
      timezone: 'America/New_York',
    }))
  })

  it('edit defaults wakeHour to 4 and timezone to UTC when the device has neither', async () => {
    const devicesStore = useDevicesStore()
    devicesStore.devices = [makeDevice({
      id: 5,
      name: 'Den',
      wakeHour: null,
      timezone: null as any,
    })]
    vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue()
    const updateSpy = vi.spyOn(devicesStore, 'updateDevice').mockResolvedValue(makeDevice({ id: 5 }))

    const wrapper = mountView()
    await flushPromises()
    await wrapper.findComponent({ name: 'FrameCard' }).vm.$emit('edit', 5)
    await flushPromises()

    const saveBtn = wrapper.findAllComponents({ name: 'BaseButton' })
      .find(b => b.text().toLowerCase().includes('sav'))!
    await saveBtn.trigger('click')
    await flushPromises()
    expect(updateSpy).toHaveBeenCalledWith(5, expect.objectContaining({
      wakeHour: 4,
      timezone: 'UTC',
    }))
  })

  it('the settings sheet closes when the underlying bottom-sheet emits close', async () => {
    const devicesStore = useDevicesStore()
    devicesStore.devices = [makeDevice({ id: 5 })]
    vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue()

    const wrapper = mountView()
    await flushPromises()
    await wrapper.findComponent({ name: 'FrameCard' }).vm.$emit('edit', 5)
    await flushPromises()
    const sheet = wrapper.findComponent({ name: 'BaseBottomSheet' })
    expect(sheet.props('modelValue')).toBe(true)
    await sheet.vm.$emit('update:modelValue', false)
    await wrapper.vm.$nextTick()
    expect(wrapper.findComponent({ name: 'BaseBottomSheet' }).props('modelValue')).toBe(false)
  })

  it('saving falls back to the original name when the user clears the field', async () => {
    const devicesStore = useDevicesStore()
    devicesStore.devices = [makeDevice({ id: 5, name: 'Original' })]
    vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue()
    const updateSpy = vi.spyOn(devicesStore, 'updateDevice').mockResolvedValue(makeDevice({ id: 5 }))

    const wrapper = mountView()
    await flushPromises()
    await wrapper.findComponent({ name: 'FrameCard' }).vm.$emit('edit', 5)
    await flushPromises()
    await wrapper.findComponent({ name: 'BaseInput' }).vm.$emit('update:modelValue', '   ')

    const saveBtn = wrapper.findAllComponents({ name: 'BaseButton' })
      .find(b => b.text().toLowerCase().includes('sav'))!
    await saveBtn.trigger('click')
    await flushPromises()

    expect(updateSpy).toHaveBeenCalledWith(5, expect.objectContaining({ name: 'Original' }))
  })
})
