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

const mockApi = jest.fn() as jest.MockedFunction<(path: string, init?: RequestInit) => Promise<unknown>>
const mockUploadState = {
  mode: 'success' as 'success' | 'error',
  dispatchBeforeUnload: false,
  lastBeforeUnloadEvent: undefined as BeforeUnloadEvent | undefined,
  pauseUpload: false,
  finishUpload: undefined as (() => void) | undefined,
}

const mockTusUpload = class MockTusUpload {
  opts: any
  file: File

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

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

jest.mock('./api', () => ({
  api: mockApi,
}))

jest.mock('tus-js-client', () => ({
  Upload: mockTusUpload,
}))

import useUploaderController from './uploader-controller'

const originalAlertDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'alert')
const originalConfirmDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'confirm')
const originalPromptDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'prompt')
const originalLocationDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'location')
const originalNavigatorDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'navigator')

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

function installGlobals() {
  Object.defineProperty(globalThis, 'alert', { configurable: true, value: jest.fn() })
  Object.defineProperty(globalThis, 'confirm', { configurable: true, value: jest.fn(() => true) })
  Object.defineProperty(globalThis, 'prompt', { configurable: true, value: jest.fn(() => 'renamed') })
}

function restoreGlobal(name: string, descriptor: PropertyDescriptor | undefined) {
  if (descriptor) {
    Object.defineProperty(globalThis, name, descriptor)
    return
  }
  Reflect.deleteProperty(globalThis as Record<string, unknown>, name)
}

function restoreGlobals() {
  restoreGlobal('alert', originalAlertDescriptor)
  restoreGlobal('confirm', originalConfirmDescriptor)
  restoreGlobal('prompt', originalPromptDescriptor)
  restoreGlobal('location', originalLocationDescriptor)
  restoreGlobal('navigator', originalNavigatorDescriptor)
}

function installApi() {
  mockApi.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(() => {
  mockApi.mockReset()
  mockUploadState.mode = 'success'
  mockUploadState.dispatchBeforeUnload = false
  mockUploadState.lastBeforeUnloadEvent = undefined
  mockUploadState.pauseUpload = false
  mockUploadState.finishUpload = undefined
  installGlobals()
  installApi()
})

afterEach(() => {
  jest.restoreAllMocks()
  restoreGlobals()
})

describe('useUploaderController', () => {
  it('syncs folder input attributes for desktop and mobile UAs', async () => {
    Object.defineProperty(globalThis, 'navigator', { configurable: true, value: { userAgent: 'iPhone' } })

    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(mockApi).toHaveBeenCalledWith('/api/mkdir', expect.any(Object))
    expect(mockApi).toHaveBeenCalledWith('/api/rename', expect.any(Object))
    expect(mockApi).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 () => {
    mockUploadState.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))

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

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

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

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

    mockApi.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 () => {
    mockApi.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(mockApi).toHaveBeenCalledWith('/api/logout', { method: 'POST' })
  })

  it('blocks unload while an upload is in flight', async () => {
    mockUploadState.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 = jest.spyOn(event, 'preventDefault')
    await act(async () => {
      window.dispatchEvent(event)
    })

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

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

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