import '@testing-library/jest-dom'

import { fireEvent, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { axe, toHaveNoViolations } from 'jest-axe'
import { useState } from 'react'
import { vi } from 'vitest'

import { PktFileUpload } from './FileUpload'
import { FileItem, TFileItemList } from './types'

// Polyfill DataTransfer for jsdom
if (!global.DataTransfer) {
  class DataTransferPolyfill {
    items: DataTransferItemList
    // @ts-ignore
    files: FileList

    constructor() {
      const files: File[] = []
      const items: DataTransferItem[] = []

      this.items = {
        add: (file: File) => {
          files.push(file)
          items.push({
            kind: 'file',
            type: file.type,
            getAsFile: () => file,
          } as DataTransferItem)
        },
        length: items.length,
      } as DataTransferItemList

      Object.defineProperty(this, 'files', {
        get: () => {
          const fileList = {
            length: files.length,
            item: (index: number) => files[index],
            [Symbol.iterator]: function* () {
              yield* files
            },
          }
          files.forEach((file, index) => {
            Object.defineProperty(fileList, index, {
              value: file,
              enumerable: true,
            })
          })
          return fileList as FileList
        },
      })
    }
  }

  global.DataTransfer = DataTransferPolyfill as any
}

// Polyfill for ResizeObserver
if (!global.ResizeObserver) {
  class ResizeObserverPolyfill {
    observe() {}
    unobserve() {}
    disconnect() {}
  }

  global.ResizeObserver = ResizeObserverPolyfill as any
}

const NOOP = () => {}

const getVisibleFilenameNode = (filename: string) =>
  screen.queryByText(
    (content, element) => content === filename && element?.getAttribute('data-pkt-truncate-part') === 'first',
  )

const expectVisibleFilename = (filename: string) => {
  expect(getVisibleFilenameNode(filename)).toBeInTheDocument()
}

const makeFilesPropWritable = (fileInput: HTMLInputElement) => {
  // Make the files property settable in jsdom
  let filesValue: FileList | null = null
  Object.defineProperty(fileInput, 'files', {
    get: () => filesValue,
    set: (value: FileList | null) => {
      filesValue = value
    },
    configurable: true,
  })
}

expect.extend(toHaveNoViolations)

const createMockFile = (name: string, type = 'text/plain'): File => {
  return new File(['content'], name, { type })
}

function createFileItem(name: string, fileId?: string) {
  return new FileItem(createMockFile(name), fileId)
}

describe('PktFileUpload', () => {
  describe('Rendering', () => {
    it('should render the drop zone with default placeholder text for multiple files', () => {
      render(<PktFileUpload multiple name={'pktFileUpload'} />)

      expect(screen.getByText(/Dra filer hit for å laste dem opp eller/)).toBeInTheDocument()
      expect(screen.getByText('velg filer')).toBeInTheDocument()
      expect(screen.getByText(/Format: .PDF, .JPEG, .JPG, .PNG, .HEIC, .DOC, .DOCX, .ODT/)).toBeInTheDocument()
    })

    it('should render the drop zone with single file text when multiple is false', () => {
      render(<PktFileUpload name={'pktFileUpload'} />)

      expect(screen.getByText(/Dra en fil for å laste den opp eller/)).toBeInTheDocument()
      expect(screen.getByText('velg en fil')).toBeInTheDocument()
    })

    it('should render the attachment icon', () => {
      const { container } = render(<PktFileUpload name={'pktFileUpload'} />)

      expect(container.querySelector('.pkt-fileupload__drop-zone__placeholder__icon')).toBeInTheDocument()
    })

    it('should render with initial value', () => {
      const initialValue: TFileItemList = [createFileItem('test.pdf', '1')]

      render(<PktFileUpload value={initialValue} name={'pktFileUpload'} onFilesChanged={NOOP} />)

      expectVisibleFilename('test.pdf')
    })

    it('should render multiple files in queue display', () => {
      const initialValue: TFileItemList = [
        createFileItem('file1.pdf', '1'),
        createFileItem('file2.docx', '2'),
        createFileItem('file3.png', '3'),
      ]

      render(<PktFileUpload value={initialValue} multiple name={'pktFileUpload'} onFilesChanged={NOOP} />)

      expectVisibleFilename('file1.pdf')
      expectVisibleFilename('file2.docx')
      expectVisibleFilename('file3.png')
    })
  })

  describe('File selection via dialog', () => {
    it('should trigger file input when clicking "velg filer" button', async () => {
      const user = userEvent.setup()
      render(<PktFileUpload multiple name="pktFileUpload" />)

      const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement
      const clickSpy = vi.spyOn(fileInput, 'click')

      const button = screen.getByRole('button', { name: /velg filer/i })
      await user.click(button)

      expect(clickSpy).toHaveBeenCalled()
    })

    it('should trigger file input when clicking the dropzone', async () => {
      const user = userEvent.setup()
      const { container } = render(<PktFileUpload name="pktFileUpload" />)

      const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement
      const clickSpy = vi.spyOn(fileInput, 'click')

      const dropZone = container.querySelector('.pkt-fileupload__drop-zone')!
      await user.click(dropZone)

      expect(clickSpy).toHaveBeenCalled()
    })

    it('should call onFilesChanged when files are selected via dialog', () => {
      const onFilesChanged = vi.fn()
      render(<PktFileUpload onFilesChanged={onFilesChanged} multiple name={'pktFileUpload'} />)

      const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement
      const file1 = createMockFile('test1.pdf')
      const file2 = createMockFile('test2.pdf')

      fireEvent.change(fileInput, { target: { files: [file1, file2] } })

      expect(onFilesChanged).toHaveBeenCalledTimes(1)
      const calledWith = onFilesChanged.mock.calls[0][0]
      expect(calledWith).toHaveLength(2)
      expect(calledWith[0].file).toBe(file1)
      expect(calledWith[1].file).toBe(file2)
    })

    it('should assign unique IDs to added files', () => {
      const onFilesChanged = vi.fn()
      render(<PktFileUpload onFilesChanged={onFilesChanged} multiple name={'pktFileUpload'} />)

      const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement
      const file1 = createMockFile('test1.pdf')
      const file2 = createMockFile('test2.pdf')

      fireEvent.change(fileInput, { target: { files: [file1, file2] } })

      const calledWith = onFilesChanged.mock.calls[0][0]
      expect(calledWith[0].fileId).toBeTruthy()
      expect(calledWith[1].fileId).toBeTruthy()
      expect(calledWith[0].fileId).not.toBe(calledWith[1].fileId)
    })

    it('should only keep one file when multiple is false', () => {
      const onFilesChanged = vi.fn()
      render(<PktFileUpload onFilesChanged={onFilesChanged} name={'pktFileUpload'} />)

      const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement
      const file1 = createMockFile('test1.pdf')
      const file2 = createMockFile('test2.pdf')

      fireEvent.change(fileInput, { target: { files: [file1, file2] } })

      const calledWith = onFilesChanged.mock.calls[0][0]
      expect(calledWith).toHaveLength(1)
      expect(calledWith[0].file).toBe(file1)
    })

    it('should clear file input value after files are selected', () => {
      const onFilesChanged = vi.fn()
      render(<PktFileUpload onFilesChanged={onFilesChanged} multiple name={'pktFileUpload'} />)

      const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement
      const file = createMockFile('test.pdf')

      fireEvent.change(fileInput, { target: { files: [file] } })

      expect(fileInput.value).toBe('')
    })
  })

  describe('Drag and drop functionality', () => {
    it('should show active state when dragging over drop zone', () => {
      const { container } = render(<PktFileUpload multiple name={'pktFileUpload'} />)

      const dropZone = container.querySelector('.pkt-fileupload__drop-zone') as HTMLElement

      fireEvent.dragOver(dropZone)

      expect(dropZone).toHaveClass('pkt-fileupload__drop-zone--drag-active')
      expect(screen.getByText(/Slipp filene her/)).toBeInTheDocument()
    })

    it('should show active state text for single file mode when dragging', () => {
      const { container } = render(<PktFileUpload name={'pktFileUpload'} />)

      const dropZone = container.querySelector('.pkt-fileupload__drop-zone') as HTMLElement

      fireEvent.dragOver(dropZone)

      expect(screen.getByText(/Slipp filen her/)).toBeInTheDocument()
    })

    it('should remove active state when drag leaves', () => {
      const { container } = render(<PktFileUpload multiple name={'pktFileUpload'} />)

      const dropZone = container.querySelector('.pkt-fileupload__drop-zone') as HTMLElement

      fireEvent.dragOver(dropZone)
      expect(dropZone).toHaveClass('pkt-fileupload__drop-zone--drag-active')

      fireEvent.dragLeave(dropZone)
      expect(dropZone).not.toHaveClass('pkt-fileupload__drop-zone--drag-active')
    })

    it('should call onFilesChanged when files are dropped', () => {
      const onFilesChanged = vi.fn()
      const { container } = render(<PktFileUpload onFilesChanged={onFilesChanged} multiple name={'pktFileUpload'} />)

      const dropZone = container.querySelector('.pkt-fileupload__drop-zone') as HTMLElement
      const file1 = createMockFile('dropped1.pdf')
      const file2 = createMockFile('dropped2.pdf')

      fireEvent.drop(dropZone, {
        dataTransfer: {
          files: [file1, file2],
        },
      })

      expect(onFilesChanged).toHaveBeenCalledTimes(1)
      const calledWith = onFilesChanged.mock.calls[0][0]
      expect(calledWith).toHaveLength(2)
      expect(calledWith[0].file).toBe(file1)
      expect(calledWith[1].file).toBe(file2)
    })

    it('should remove active state after dropping files', () => {
      const { container } = render(<PktFileUpload multiple name={'pktFileUpload'} />)

      const dropZone = container.querySelector('.pkt-fileupload__drop-zone') as HTMLElement
      const file = createMockFile('dropped.pdf')

      fireEvent.dragOver(dropZone)
      expect(dropZone).toHaveClass('pkt-fileupload__drop-zone--drag-active')

      fireEvent.drop(dropZone, {
        dataTransfer: {
          files: [file],
        },
      })

      expect(dropZone).not.toHaveClass('pkt-fileupload__drop-zone--drag-active')
    })

    it('should only keep one file when dropping in single file mode', () => {
      const onFilesChanged = vi.fn()
      const { container } = render(<PktFileUpload onFilesChanged={onFilesChanged} name={'pktFileUpload'} />)

      const dropZone = container.querySelector('.pkt-fileupload__drop-zone') as HTMLElement
      const file1 = createMockFile('dropped1.pdf')
      const file2 = createMockFile('dropped2.pdf')

      fireEvent.drop(dropZone, {
        dataTransfer: {
          files: [file1, file2],
        },
      })

      const calledWith = onFilesChanged.mock.calls[0][0]
      expect(calledWith).toHaveLength(1)
      expect(calledWith[0].file).toBe(file1)
    })
  })

  describe('Multiple file mode', () => {
    it('should append new files to existing files when multiple is true', () => {
      const onFilesChanged = vi.fn()
      const initialValue: TFileItemList = [createFileItem('existing.pdf', '1')]

      render(<PktFileUpload value={initialValue} onFilesChanged={onFilesChanged} multiple name={'pktFileUpload'} />)

      const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement
      const newFile = createMockFile('new.pdf')

      fireEvent.change(fileInput, { target: { files: [newFile] } })

      const calledWith = onFilesChanged.mock.calls[0][0]
      expect(calledWith).toHaveLength(2)
      expect(calledWith[0].file.name).toBe('existing.pdf')
      expect(calledWith[1].file.name).toBe('new.pdf')
    })

    it('should set file input multiple attribute when multiple is true', () => {
      render(<PktFileUpload multiple name={'pktFileUpload'} />)

      const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement
      expect(fileInput).toHaveAttribute('multiple')
    })

    it('should not set file input multiple attribute when multiple is false', () => {
      render(<PktFileUpload name={'pktFileUpload'} />)

      const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement
      expect(fileInput).not.toHaveAttribute('multiple')
    })
  })

  describe('Controlled component behavior', () => {
    it('should update display when value prop changes', () => {
      const initialValue: TFileItemList = [createFileItem('file1.pdf', '1')]

      const { rerender } = render(<PktFileUpload value={initialValue} name={'pktFileUpload'} onFilesChanged={NOOP} />)

      expectVisibleFilename('file1.pdf')

      const updatedValue: TFileItemList = [createFileItem('file2.pdf', '2')]

      rerender(<PktFileUpload value={updatedValue} name={'pktFileUpload'} onFilesChanged={NOOP} />)

      expect(getVisibleFilenameNode('file1.pdf')).not.toBeInTheDocument()
      expectVisibleFilename('file2.pdf')
    })

    it('should handle empty value prop', () => {
      render(<PktFileUpload value={[]} name={'pktFileUpload'} onFilesChanged={NOOP} />)

      expect(screen.queryByText(/.pdf/)).not.toBeInTheDocument()
    })

    it('should work as uncontrolled component without value prop', () => {
      const onFilesChanged = vi.fn()
      render(<PktFileUpload onFilesChanged={onFilesChanged} multiple name={'pktFileUpload'} />)

      const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement
      const file = createMockFile('test.pdf')

      fireEvent.change(fileInput, { target: { files: [file] } })

      expect(onFilesChanged).toHaveBeenCalledTimes(1)
    })
  })

  describe('File removal', () => {
    it('should render remove buttons for each file', () => {
      const initialValue: TFileItemList = [createFileItem('file1.pdf', '1'), createFileItem('file2.pdf', '2')]

      render(<PktFileUpload value={initialValue} name={'pktFileUpload'} onFilesChanged={NOOP} />)

      const removeButtons = screen.getAllByRole('button', { name: /Slett/ })
      expect(removeButtons).toHaveLength(2)
    })

    it.skip('should show close-circle icon on remove buttons', () => {
      const initialValue: TFileItemList = [createFileItem('file1.pdf', '1')]

      const { container } = render(<PktFileUpload value={initialValue} name={'pktFileUpload'} />)

      const closeIcon = container.querySelector('pkt-icon[name="close-circle"]')
      expect(closeIcon).toBeInTheDocument()
    })
  })

  describe('Accessibility', () => {
    it('keeps label and input associations isolated across multiple instances without explicit ids', () => {
      render(
        <>
          <PktFileUpload name="pktFileUploadA" label="Første vedlegg" helptext="Hjelpetekst A" />
          <PktFileUpload name="pktFileUploadB" label="Andre vedlegg" helptext="Hjelpetekst B" />
        </>,
      )

      const firstInput = screen.getByLabelText('Første vedlegg') as HTMLInputElement
      const secondInput = screen.getByLabelText('Andre vedlegg') as HTMLInputElement

      expect(firstInput.id).toBeTruthy()
      expect(secondInput.id).toBeTruthy()
      expect(firstInput.id).not.toBe(secondInput.id)
      expect(firstInput).toHaveAttribute('aria-describedby', `${firstInput.id}-helptext`)
      expect(secondInput).toHaveAttribute('aria-describedby', `${secondInput.id}-helptext`)
      expect(document.getElementById(`${firstInput.id}-helptext`)).toHaveTextContent('Hjelpetekst A')
      expect(document.getElementById(`${secondInput.id}-helptext`)).toHaveTextContent('Hjelpetekst B')
    })

    it('associates the label with the native file input and wires help text + error message ids', () => {
      render(
        <PktFileUpload
          id="test-upload"
          name="pktFileUpload"
          label="Last opp vedlegg"
          helptext="Last opp minst én fil."
          onFileValidation={() => 'Ugyldig fil.'}
        />,
      )

      const fileInput = screen.getByLabelText('Last opp vedlegg') as HTMLInputElement
      const file = createMockFile('test.pdf')

      fireEvent.change(fileInput, { target: { files: [file] } })

      expect(fileInput).toHaveAttribute('id', 'test-upload')
      expect(fileInput).toHaveAttribute('aria-invalid', 'true')
      expect(fileInput).toHaveAttribute('aria-describedby', 'test-upload-helptext test-upload-error')
      expect(screen.getByText('Ugyldig fil.')).toBeInTheDocument()
    })

    it('applies native required only for form strategy', () => {
      const { rerender } = render(
        <PktFileUpload id="required-upload" name="pktFileUpload" label="Vedlegg" required uploadStrategy="form" />,
      )

      let fileInput = screen.getByLabelText('Vedlegg') as HTMLInputElement
      expect(fileInput).toHaveAttribute('required')
      expect(fileInput).toHaveAttribute('aria-required', 'true')

      rerender(
        <PktFileUpload
          id="required-upload"
          name="pktFileUpload"
          label="Vedlegg"
          required
          uploadStrategy="custom"
          onFileUploadRequested={vi.fn()}
          transfers={[]}
        />,
      )

      fileInput = screen.getByLabelText('Vedlegg') as HTMLInputElement
      expect(fileInput).not.toHaveAttribute('required')
      expect(fileInput).toHaveAttribute('aria-required', 'true')
    })

    it('should have no accessibility violations', async () => {
      const { container } = render(<PktFileUpload multiple name={'pktFileUpload'} />)
      // Note: The hidden file input doesn't have a label, but it's intentionally hidden
      // and the visible UI provides the accessible interaction
      const results = await axe(container, {
        rules: {
          label: { enabled: false },
        },
      })

      expect(results).toHaveNoViolations()
    })

    it('should have no accessibility violations with files in queue', async () => {
      const initialValue: TFileItemList = [createFileItem('file1.pdf', '1'), createFileItem('file2.pdf', '2')]

      const { container } = render(
        <PktFileUpload value={initialValue} multiple name={'pktFileUpload'} onFilesChanged={NOOP} />,
      )
      // Note: The hidden file input doesn't have a label, but it's intentionally hidden
      // and the visible UI provides the accessible interaction
      const results = await axe(container, {
        rules: {
          label: { enabled: false },
        },
      })

      expect(results).toHaveNoViolations()
    })
  })

  describe('Form submission values', () => {
    it('blocks custom strategy submit when required and no files are selected', () => {
      const { container } = render(
        <form>
          <PktFileUpload
            id="custom-required-upload"
            name="pktFileUpload"
            label="Last opp vedlegg"
            required
            uploadStrategy="custom"
            onFileUploadRequested={vi.fn()}
            transfers={[]}
          />
        </form>,
      )

      const form = container.querySelector('form') as HTMLFormElement
      let submitWasPrevented = false
      form.addEventListener('submit', (event) => {
        submitWasPrevented = event.defaultPrevented
      })

      fireEvent.submit(form)

      const fileInput = screen.getByLabelText('Last opp vedlegg') as HTMLInputElement
      expect(submitWasPrevented).toBe(true)
      expect(fileInput).toHaveAttribute('aria-invalid', 'true')
      expect(fileInput).toHaveAttribute('aria-describedby', 'custom-required-upload-error')
      expect(screen.getByText('Velg minst én fil før du sender inn skjemaet.')).toBeInTheDocument()
    })

    it('should populate file input with selected files for form submission', () => {
      const TestFormComponent = () => {
        const [files, setFiles] = useState<TFileItemList>([])
        return (
          <form>
            <PktFileUpload
              multiple
              name={'pktFileUpload'}
              uploadStrategy="form"
              value={files}
              onFilesChanged={setFiles}
            />
          </form>
        )
      }

      const { container } = render(<TestFormComponent />)

      const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement
      makeFilesPropWritable(fileInput)

      const file1 = createMockFile('file1.pdf')
      const file2 = createMockFile('file2.pdf')

      // Simulate file selection
      fireEvent.change(fileInput, { target: { files: [file1, file2] } })

      // Verify that the file input contains the correct files that will be submitted
      expect(fileInput.files).not.toBeNull()
      expect(fileInput.files!.length).toBe(2)
      expect(fileInput.files![0].name).toBe('file1.pdf')
      expect(fileInput.files![1].name).toBe('file2.pdf')
      expect(fileInput).toHaveAttribute('name', 'pktFileUpload')
    })

    it('should populate file input with single file for form submission', () => {
      const TestFormComponent = () => {
        const [files, setFiles] = useState<TFileItemList>([])
        return (
          <form>
            <PktFileUpload name={'pktFileUpload'} uploadStrategy="form" value={files} onFilesChanged={setFiles} />
          </form>
        )
      }

      const { container } = render(<TestFormComponent />)

      const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement
      makeFilesPropWritable(fileInput)

      const file = createMockFile('single-file.pdf')

      // Simulate file selection
      fireEvent.change(fileInput, { target: { files: [file] } })

      // Verify that the file input contains the correct file that will be submitted
      expect(fileInput.files).not.toBeNull()
      expect(fileInput.files!.length).toBe(1)
      expect(fileInput.files![0].name).toBe('single-file.pdf')
      expect(fileInput).toHaveAttribute('name', 'pktFileUpload')
    })

    it('should include file IDs in form FormData with custom upload strategy', () => {
      const initialValue: TFileItemList = [
        createFileItem('file1.pdf', 'custom-id-1'),
        createFileItem('file2.pdf', 'custom-id-2'),
      ]

      const { container } = render(
        <form>
          <PktFileUpload
            value={initialValue}
            name={'pktFileUpload'}
            uploadStrategy="custom"
            id="test-upload"
            onFileUploadRequested={vi.fn()}
            transfers={[]}
            onFilesChanged={NOOP}
          />
        </form>,
      )

      const form = container.querySelector('form') as HTMLFormElement
      const formData = new FormData(form)

      const fileIds = formData.getAll('pktFileUpload')
      expect(fileIds).toHaveLength(2)
      expect(fileIds[0]).toBe('custom-id-1')
      expect(fileIds[1]).toBe('custom-id-2')
    })

    it('should call onFileUploadRequested when files are selected with custom upload strategy', () => {
      const onFileUploadRequested = vi.fn()
      const TestFormComponent = () => {
        const [files, setFiles] = useState<TFileItemList>([])
        return (
          <form>
            <PktFileUpload
              id={'pktFileUploadId'}
              multiple
              name={'pktFileUpload'}
              uploadStrategy="custom"
              value={files}
              onFilesChanged={setFiles}
              onFileUploadRequested={onFileUploadRequested}
              transfers={[]}
            />
          </form>
        )
      }

      const { container } = render(<TestFormComponent />)

      const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement
      const file1 = createMockFile('upload-test.pdf')
      const file2 = createMockFile('upload-test2.pdf')

      // Simulate file selection
      fireEvent.change(fileInput, { target: { files: [file1, file2] } })

      // Verify that onFileUploadRequested was called for each file
      expect(onFileUploadRequested).toHaveBeenCalledTimes(2)

      // Check that it was called with FileItem objects containing the correct files
      const firstCall = onFileUploadRequested.mock.calls[0][0]
      const secondCall = onFileUploadRequested.mock.calls[1][0]

      expect(firstCall.file).toBe(file1)
      expect(firstCall.fileId).toBeTruthy()
      expect(secondCall.file).toBe(file2)
      expect(secondCall.fileId).toBeTruthy()
    })
  })
})
