import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useDevicesStore } from '@/stores/devices'
import type { Device } from '@/types'

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

describe('devices 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
  })

  // DS-01
  it('fetchDevices success populates devices and clears loading', async () => {
    const mockDevices = [makeDevice()]
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve(mockDevices),
    }))

    const store = useDevicesStore()
    await store.fetchDevices()

    expect(store.devices).toEqual(mockDevices)
    expect(store.loading).toBe(false)
    expect(store.error).toBeNull()
  })

  // DS-02
  it('fetchDevices network error sets error state', async () => {
    vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('Network failure')))

    const store = useDevicesStore()
    await store.fetchDevices()

    expect(store.devices).toEqual([])
    expect(store.loading).toBe(false)
    expect(store.error).toBe('Network failure')
  })

  // DS-02b — non-ok response
  it('fetchDevices non-ok response sets error state', async () => {
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false }))

    const store = useDevicesStore()
    await store.fetchDevices()

    expect(store.error).toBe('Failed to load devices')
    expect(store.loading).toBe(false)
  })

  // DS-03
  it('updateDevice patches local array entry', async () => {
    const original = makeDevice({ id: 1, name: 'Old Name' })
    const updated = makeDevice({ id: 1, name: 'New Name' })

    const store = useDevicesStore()
    store.devices = [original]

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

    const result = await store.updateDevice(1, { name: 'New Name' })

    expect(result.name).toBe('New Name')
    expect(store.devices[0].name).toBe('New Name')
  })

  // DS-03b — updateDevice throws on failure
  it('updateDevice throws on non-ok response', async () => {
    const store = useDevicesStore()
    store.devices = [makeDevice()]

    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false }))

    await expect(store.updateDevice(1, { name: 'x' })).rejects.toThrow('Failed to update device')
  })

  // DS-04
  it('lockImage sets lockedImageId on local device', async () => {
    const device = makeDevice({ id: 1, lockedImageId: null })
    const locked = makeDevice({ id: 1, lockedImageId: 42 })

    const store = useDevicesStore()
    store.devices = [device]

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

    const result = await store.lockImage(1, 42)

    expect(result.lockedImageId).toBe(42)
    expect(store.devices[0].lockedImageId).toBe(42)
  })

  // DS-05
  it('unlockImage clears lockedImageId', async () => {
    const device = makeDevice({ id: 1, lockedImageId: 42 })
    const unlocked = makeDevice({ id: 1, lockedImageId: null })

    const store = useDevicesStore()
    store.devices = [device]

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

    const result = await store.unlockImage(1)

    expect(result.lockedImageId).toBeNull()
    expect(store.devices[0].lockedImageId).toBeNull()
  })

  // DS-05b — lockImage throws on failure
  it('lockImage throws on non-ok response', async () => {
    const store = useDevicesStore()
    store.devices = [makeDevice()]

    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false }))

    await expect(store.lockImage(1, 42)).rejects.toThrow('Failed to lock image')
  })

  // DS-05c — unlockImage throws on failure
  it('unlockImage throws on non-ok response', async () => {
    const store = useDevicesStore()
    store.devices = [makeDevice()]

    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false }))

    await expect(store.unlockImage(1)).rejects.toThrow('Failed to unlock')
  })

  // DS-06: non-Error rejections fall back to "Unknown error"
  it('fetchDevices stores "Unknown error" when the rejection is not an Error', async () => {
    vi.stubGlobal('fetch', vi.fn().mockRejectedValue('boom'))
    const store = useDevicesStore()
    await store.fetchDevices()
    expect(store.error).toBe('Unknown error')
  })

  // DS-07: returns the patched device even when it isn't in the local list
  it('updateDevice no-ops the list when the id is not present', async () => {
    const updated = makeDevice({ id: 99, name: 'New' })
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve(updated),
    }))
    const store = useDevicesStore()
    store.devices = [makeDevice({ id: 1 })]
    const result = await store.updateDevice(99, { name: 'New' })
    expect(result.name).toBe('New')
    expect(store.devices.find(d => d.id === 99)).toBeUndefined()
  })

  it('lockImage no-ops the list when the device id is not present', async () => {
    const locked = makeDevice({ id: 99, lockedImageId: 42 })
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve(locked),
    }))
    const store = useDevicesStore()
    store.devices = [makeDevice({ id: 1 })]
    await store.lockImage(99, 42)
    expect(store.devices.find(d => d.id === 99)).toBeUndefined()
  })

  it('unlockImage no-ops the list when the device id is not present', async () => {
    const unlocked = makeDevice({ id: 99, lockedImageId: null })
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve(unlocked),
    }))
    const store = useDevicesStore()
    store.devices = [makeDevice({ id: 1 })]
    await store.unlockImage(99)
    expect(store.devices.find(d => d.id === 99)).toBeUndefined()
  })
})
