import { fireEvent, render } from '@testing-library/react';
import React from 'react';
import { act } from 'react-dom/test-utils';
import { describe, expect, it, vi } from 'vitest';
import { noop } from '../../../helpers/utils';
import { FileUploadControl } from './FileUploadControl';

describe('FileUploadControl', () => {
  const mockFile = new File([new ArrayBuffer(1)], 'file.json');
  const mockFileList = {
    0: mockFile,
    length: 1,
    item: () => mockFile,
  };

  const mockEventProps = {
    preventDefault: noop,
    stopPropagation: noop,
  };

  it('renders the component without crashing', () => {
    const { container } = render(
      <FileUploadControl name={'test-name'} onFileSelected={noop} />,
    );

    expect(container).toBeTruthy();
    expect(container.querySelector('input[type="file"]')).toBeInTheDocument();
  });

  it('displays a label', () => {
    const mockLabel = 'mockLabel';
    const { container } = render(
      <FileUploadControl
        name={'test-name'}
        label={mockLabel}
        onFileSelected={noop}
      />,
    );

    const label = container.querySelector('label');

    expect(label?.textContent).toBe(mockLabel);
  });

  it('should not display the progress bar by default', () => {
    const { container } = render(
      <FileUploadControl name={'test-name'} onFileSelected={noop} />,
    );

    const progressBar = container.querySelector('.progressbar') as HTMLElement;

    expect(progressBar.style.display).toBe('none');
  });

  it('fires on file selected when a file is selected', () => {
    const spy = vi.fn();

    const { container } = render(
      <FileUploadControl name={'test-name'} onFileSelected={spy} />,
    );

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

    fireEvent.change(fileInput, { target: { files: mockFileList } });

    expect(spy).toHaveBeenCalledTimes(1);
    expect(spy).toHaveBeenCalledWith({
      file: mockFile,
      uploadCompleted: expect.any(Function),
      uploadProgress: expect.any(Function),
      uploadStarted: expect.any(Function),
    });
  });

  it('shows the file name', () => {
    const { container } = render(
      <FileUploadControl
        name={'test-name'}
        value={{
          file: mockFile,
          uploadCompleted: noop,
          uploadProgress: noop,
          uploadStarted: noop,
        }}
        onFileSelected={noop}
      />,
    );

    const filename = container.querySelector('.filename') as HTMLInputElement;

    expect(filename.value).toBe(mockFile.name);
  });

  it('shows the progress bar when onUploadStarted is called', () => {
    const { container } = render(
      <FileUploadControl
        name={'test-name'}
        onFileSelected={({ uploadStarted }) => {
          uploadStarted();
        }}
      />,
    );

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

    fireEvent.change(fileInput, { target: { files: mockFileList } });

    const progressBar = container.querySelector('.progressbar') as HTMLElement;

    expect(progressBar.style.display).toBe('block');
  });

  it('hides the progess bar when onUploadCompleted is called', () => {
    const { container } = render(
      <FileUploadControl
        name={'test-name'}
        onFileSelected={({ uploadStarted, uploadCompleted }) => {
          uploadStarted();
          uploadCompleted();
        }}
      />,
    );

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

    fireEvent.change(fileInput, { target: { files: mockFileList } });

    const progressBar = container.querySelector('.progressbar') as HTMLElement;

    expect(progressBar.style.display).toBe('none');
  });

  it('displays the drop target when an item is dragged into the window', () => {
    const map: { [key: string]: any } = {};

    document.addEventListener = vi.fn((event, cb) => {
      map[event] = cb;
    });

    const { container } = render(
      <FileUploadControl name={'test-name'} onFileSelected={noop} />,
    );

    expect(map).toEqual(
      expect.objectContaining({
        dragover: expect.any(Function),
      }),
    );

    expect(container.querySelector('.dropTarget')).not.toBeInTheDocument();

    act(() => {
      map.dragover({
        ...mockEventProps,
        dataTransfer: { items: mockFileList },
      });
    });

    expect(container.querySelector('.dropTarget')).toBeInTheDocument();
  });

  it('accepts files dropped on to the drop target', () => {
    const spy = vi.fn();

    const map: { [key: string]: any } = {};

    document.addEventListener = vi.fn((event, cb) => {
      map[event] = cb;
    });

    const { container } = render(
      <FileUploadControl name={'test-name'} onFileSelected={spy} />,
    );

    expect(map).toEqual(
      expect.objectContaining({
        dragover: expect.any(Function),
      }),
    );

    expect(container.querySelector('.dropTarget')).not.toBeInTheDocument();

    act(() => {
      map.dragover({
        ...mockEventProps,
        dataTransfer: { items: mockFileList },
      });
    });

    const dropTarget = container.querySelector('.dropTarget') as HTMLElement;

    fireEvent.drop(dropTarget, {
      ...mockEventProps,
      dataTransfer: { files: mockFileList },
    });

    expect(spy).toHaveBeenCalledTimes(1);
    expect(spy).toHaveBeenCalledWith(
      expect.objectContaining({ file: mockFile }),
    );
  });

  it('allows files with the same name to be uploaded multiple times', () => {
    const spy = vi.fn();

    const { container } = render(
      <FileUploadControl name={'test-name'} onFileSelected={spy} />,
    );

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

    // Simulate first file upload
    fireEvent.change(fileInput, { target: { files: mockFileList } });

    expect(spy).toHaveBeenCalledTimes(1);
    expect(spy).toHaveBeenCalledWith({
      file: mockFile,
      uploadCompleted: expect.any(Function),
      uploadProgress: expect.any(Function),
      uploadStarted: expect.any(Function),
    });

    // Reset spy
    spy.mockReset();

    // Simulate second file upload with the same file
    fireEvent.change(fileInput, { target: { files: mockFileList } });

    expect(spy).toHaveBeenCalledTimes(1);
    expect(spy).toHaveBeenCalledWith({
      file: mockFile,
      uploadCompleted: expect.any(Function),
      uploadProgress: expect.any(Function),
      uploadStarted: expect.any(Function),
    });
  });

  describe('MIME Types', () => {
    const mockTypes = 'image/jpeg,image/png';
    const mockFile = new File([new ArrayBuffer(1)], 'image.png', {
      type: 'image/png',
    });
    const mockFileList = {
      0: mockFile,
      length: 1,
      item: () => mockFile,
    };

    it('receives MIME types', () => {
      const { container } = render(
        <FileUploadControl
          name={'test-name'}
          onFileSelected={noop}
          accept={mockTypes}
        />,
      );

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

      expect(container).toBeTruthy();
      expect(input.accept).toBe(mockTypes);
    });

    it(`accepts file in drag&drop if MIME Type is set and matches`, () => {
      const spy = vi.fn();
      const map: { [key: string]: any } = {};
      document.addEventListener = vi.fn((event, cb) => {
        map[event] = cb;
      });
      const { container } = render(
        <FileUploadControl
          name={'test-name'}
          onFileSelected={spy}
          accept={mockTypes}
        />,
      );

      act(() => {
        map.dragover({
          ...mockEventProps,
          dataTransfer: { items: mockFileList },
        });
      });

      const dropTarget = container.querySelector('.dropTarget') as HTMLElement;

      fireEvent.drop(dropTarget, {
        ...mockEventProps,
        dataTransfer: { files: mockFileList },
      });

      expect(spy).toHaveBeenCalledTimes(1);
      expect(spy).toHaveBeenCalledWith(
        expect.objectContaining({ file: mockFile }),
      );
    });

    it(`if MIME Type is set and file type does not match, file is not set on drop`, () => {
      const spy = vi.fn();
      const map: { [key: string]: any } = {};
      document.addEventListener = vi.fn((event, cb) => {
        map[event] = cb;
      });
      const { container } = render(
        <FileUploadControl
          name={'test-name'}
          onFileSelected={spy}
          accept="image/tiff"
        />,
      );

      act(() => {
        map.dragover({
          ...mockEventProps,
          dataTransfer: { items: mockFileList },
        });
      });

      const dropTarget = container.querySelector('.dropTarget') as HTMLElement;

      fireEvent.drop(dropTarget, {
        ...mockEventProps,
        dataTransfer: { files: mockFileList },
      });

      expect(spy).not.toHaveBeenCalled();
    });
  });
});
