import { act, render, renderHook, waitFor } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'

const harness = vi.hoisted(() => {
  const apiMock = vi.fn()
  const uploadState = {
    mode: 'success' as 'success' | 'error',
    dispatchBeforeUnload: false,
    lastBeforeUnloadEvent: undefined as BeforeUnloadEvent | undefined,
    pauseUpload: false,
    finishUpload: undefined as (() => void) | undefined,
  }

  class UploadMock {
    opts: any
    file: File

    constructor(file: File, opts: any) {
      this.file = file
      this.opts = opts
    }

    start() {
      if (uploadState.mode === 'error') {
        this.opts.onError?.({ originalRequest: { status: 503, statusText: 'Service Unavailable' } })
        return
      }
      if (uploadState.pauseUpload) {
        uploadState.finishUpload = () => {
          this.opts.onProgress?.(5, 10)
          this.opts.onSuccess?.()
        }
        return
      }
      if (uploadState.dispatchBeforeUnload) {
        const event = new Event('beforeunload', { cancelable: true }) as BeforeUnloadEvent
        uploadState.lastBeforeUnloadEvent = event
        window.dispatchEvent(event)
      }
      this.opts.onProgress?.(5, 10)
      this.opts.onSuccess?.()
    }
  }

  return { apiMock, uploadState, UploadMock }
})

vi.mock('./api', () => ({
  api: harness.apiMock,
}))

vi.mock('tus-js-client', () => ({
  Upload: harness.UploadMock,
}))

import useUploaderController from './uploader-controller'

function makeFile(name: string, type: string) {
  return new File(['x'], name, { type })
}

function installGlobals() {
  vi.stubGlobal('alert', vi.fn())
  vi.stubGlobal('confirm', vi.fn(() => true))
  vi.stubGlobal('prompt', vi.fn(() => 'renamed'))
  vi.stubGlobal('location', { replace: vi.fn() } as any)
}

function installApi() {
  harness.apiMock.mockImplementation(async (path: string) => {
    if (path === '/api/whoami') {
      return { username: 'brad', roots: ['alpha', 'beta'] }
    }
    if (path.startsWith('/api/list?')) {
      const params = new URLSearchParams(path.split('?')[1])
      if (params.get('path')) {
        return [
          { name: 'nested', path: 'nested', is_dir: true, size: 0, mtime: 0 },
          { name: 'nested.mp4', path: 'nested/nested.mp4', is_dir: false, size: 4096, mtime: 1713000000 },
        ]
      }
      return [
        { name: 'videos', path: 'videos', is_dir: true, size: 0, mtime: 0 },
        { name: 'archive', path: 'archive', is_dir: true, size: 0, mtime: 0 },
        { name: 'clip.mp4', path: 'clip.mp4', is_dir: false, size: 2048, mtime: 1713000000 },
      ]
    }
    if (path === '/api/mkdir' || path === '/api/rename' || path.startsWith('/api/file?') || path === '/api/logout') {
      return {}
    }
    throw new Error(`unexpected api path: ${path}`)
  })
}

beforeEach(() => {
  harness.apiMock.mockReset()
  harness.uploadState.mode = 'success'
  harness.uploadState.dispatchBeforeUnload = false
  harness.uploadState.lastBeforeUnloadEvent = undefined
  harness.uploadState.pauseUpload = false
  harness.uploadState.finishUpload = undefined
  installGlobals()
  installApi()
})

afterEach(() => {
  vi.unstubAllGlobals()
})

describe('useUploaderController', () => {
  it('syncs folder input attributes for desktop and mobile UAs', async () => {
    vi.stubGlobal('navigator', { userAgent: 'iPhone' } as any)

    function Harness() {
      const controller = useUploaderController()
      return <input data-testid="folder" ref={controller.folderInputRef} {...({ webkitdirectory: '', directory: '' } as any)} />
    }

    const { getByTestId } = render(<Harness />)
    const input = getByTestId('folder') as HTMLInputElement

    await waitFor(() => {
      expect(input.hasAttribute('webkitdirectory')).toBe(false)
      expect(input.hasAttribute('directory')).toBe(false)
    })
  })

  it('returns early before the profile loads', async () => {
    const { result } = renderHook(() => useUploaderController())

    await waitFor(() => expect(result.current.libs).toEqual(['alpha', 'beta']))

    act(() => {
      result.current.setMe(undefined)
    })

    await waitFor(() => expect(result.current.me).toBeUndefined())
    await act(async () => {
      await result.current.doUpload()
    })
    await waitFor(() => expect(result.current.status).toBe('Not signed in'))
  })

  it('rejects uploads that still need video descriptions', async () => {
    const { result } = renderHook(() => useUploaderController())

    await waitFor(() => expect(result.current.libs).toEqual(['alpha', 'beta']))

    act(() => {
      result.current.setLib('alpha')
      result.current.handleChoose([makeFile('clip.mp4', 'video/mp4')] as any)
    })
    await waitFor(() => expect(result.current.sel).toHaveLength(1))

    await act(async () => {
      await result.current.doUpload()
    })

    expect((globalThis as any).alert).toHaveBeenCalledWith(
      expect.stringContaining('Please add a short description for all videos')
    )
  })

  it('loads data, supports folder ops, and uploads successfully', async () => {
    const { result } = renderHook(() => useUploaderController())

    await waitFor(() => expect(result.current.libs).toEqual(['alpha', 'beta']))

    act(() => {
      result.current.setLib('alpha')
    })
    await waitFor(() => expect(result.current.rootDirs).toEqual(['archive', 'videos']))
    expect(result.current.destPath).toBe('/alpha')

    act(() => {
      result.current.handleChoose([makeFile('clip.mp4', 'video/mp4'), makeFile('photo.jpg', 'image/jpeg')] as any)
    })
    await waitFor(() => expect(result.current.sel).toHaveLength(2))

    act(() => {
      result.current.setBulkDesc('family trip')
    })
    await waitFor(() => expect(result.current.bulkDesc).toBe('family trip'))

    act(() => {
      result.current.applyDescToAllVideos()
    })
    await waitFor(() => expect(result.current.sel[0].desc).toBe('family trip'))

    act(() => {
      result.current.setGlobalDate('2026-04-11')
    })
    await waitFor(() => expect(result.current.sel[0].date).toBe('2026-04-11'))

    await act(async () => {
      await result.current.createSubfolder('new folder')
    })
    await act(async () => {
      await result.current.renameFolder('videos')
    })
    await act(async () => {
      await result.current.deleteFolder('archive')
    })
    await act(async () => {
      await result.current.renamePath('clip.mp4')
    })
    await act(async () => {
      await result.current.deletePath('clip.mp4', false)
    })

    await act(async () => {
      await result.current.refresh('alpha', 'videos')
    })
    expect(result.current.sortedRows[0].name).toBe('nested')
    expect(result.current.existingNames.has('nested.mp4')).toBe(true)

    await act(async () => {
      await result.current.doUpload()
    })

    expect(harness.apiMock).toHaveBeenCalledWith('/api/mkdir', expect.any(Object))
    expect(harness.apiMock).toHaveBeenCalledWith('/api/rename', expect.any(Object))
    expect(harness.apiMock).toHaveBeenCalledWith(expect.stringContaining('/api/file?'), expect.any(Object))
    expect(result.current.sel).toEqual([])
    expect(result.current.status).toContain('Ready')
  })

  it('surfaces upload failures and the not-signed-in guard', async () => {
    harness.uploadState.mode = 'error'
    const { result } = renderHook(() => useUploaderController())

    await waitFor(() => expect(result.current.libs).toEqual(['alpha', 'beta']))

    await act(async () => {
      await result.current.doUpload()
    })
    expect((globalThis as any).alert).toHaveBeenCalledWith('Please select a Library to upload into.')

    act(() => {
      result.current.setLib('alpha')
      result.current.setSel([
        { file: makeFile('clip.mp4', 'video/mp4'), desc: 'clip', date: '2026-04-10', finalName: '2026.04.10.clip.clip.mp4', progress: 0 },
      ])
    })

    await waitFor(() => expect(result.current.sel).toHaveLength(1))

    harness.apiMock.mockImplementationOnce(async () => {
      throw new Error('mkdir failed')
    })
    await act(async () => {
      await result.current.createSubfolder('broken folder')
    })

    harness.apiMock.mockImplementationOnce(async () => {
      throw new Error('rename folder failed')
    })
    await act(async () => {
      await result.current.renameFolder('videos')
    })

    harness.apiMock.mockImplementationOnce(async () => {
      throw new Error('delete folder failed')
    })
    await act(async () => {
      await result.current.deleteFolder('archive')
    })

    harness.apiMock.mockImplementationOnce(async () => {
      throw new Error('rename failed')
    })
    await act(async () => {
      await result.current.renamePath('clip.mp4')
    })

    harness.apiMock.mockImplementationOnce(async () => {
      throw new Error('delete failed')
    })
    await act(async () => {
      await result.current.deletePath('clip.mp4', false)
    })

    await act(async () => {
      await result.current.doUpload().catch(() => {})
    })

    expect((globalThis as any).alert).toHaveBeenCalledWith(expect.stringContaining('Upload failed'))
  })

  it('surfaces profile errors and logs out on missing mappings', async () => {
    harness.apiMock.mockImplementation(async (path: string) => {
      if (path === '/api/whoami') {
        throw new Error('no mapping found')
      }
      if (path === '/api/logout') {
        return {}
      }
      if (path.startsWith('/api/list?') || path === '/api/mkdir' || path === '/api/rename' || path.startsWith('/api/file?')) {
        return {}
      }
      throw new Error(`unexpected api path: ${path}`)
    })

    const { result } = renderHook(() => useUploaderController())

    await waitFor(() => expect(result.current.status).toMatch(/^Profile error:/))
    expect((globalThis as any).alert).toHaveBeenCalledWith(
      'Your account is not linked to any upload library yet. Please contact the admin to be granted access.'
    )
    expect((globalThis as any).location.replace).toHaveBeenCalledWith('/')
    expect(harness.apiMock).toHaveBeenCalledWith('/api/logout', { method: 'POST' })
  })

  it('blocks unload while an upload is in flight', async () => {
    harness.uploadState.pauseUpload = true
    const { result } = renderHook(() => useUploaderController())

    await waitFor(() => expect(result.current.libs).toEqual(['alpha', 'beta']))

    act(() => {
      result.current.setLib('alpha')
      result.current.setSel([
        { file: makeFile('clip.mp4', 'video/mp4'), desc: 'clip', date: '2026-04-10', finalName: '2026.04.10.clip.clip.mp4', progress: 0 },
      ])
    })

    await waitFor(() => expect(result.current.sel).toHaveLength(1))

    const uploadPromise = result.current.doUpload()
    await waitFor(() => expect(result.current.uploading).toBe(true))

    const event = new Event('beforeunload', { cancelable: true }) as BeforeUnloadEvent
    const preventDefault = vi.spyOn(event, 'preventDefault')
    await act(async () => {
      window.dispatchEvent(event)
    })

    expect(preventDefault).toHaveBeenCalled()
    expect(event.defaultPrevented).toBe(true)

    await act(async () => {
      harness.uploadState.finishUpload?.()
      await uploadPromise
    })

    expect(result.current.status).toContain('Ready')
  })
})
