import { Matcher, within } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { act } from 'react';

import { Status } from '../common';
import { Field } from '../field/Field';
import { mockMatchMedia, render, screen, waitFor } from '../test-utils';

import UploadInput, { UploadInputProps } from './UploadInput';
import { TEST_IDS as UPLOAD_BUTTON_TEST_IDS } from './uploadButton/UploadButton';

const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTimeAsync });

const deleteFileAndWaitForFocus = async (fileToDeleteTestId: Matcher, nextFocusTestId: Matcher) => {
  const fileToDelete = screen.getByTestId(fileToDeleteTestId);

  await user.click(within(fileToDelete).getByLabelText('Remove file', { exact: false }));

  const removeButton = screen.queryByText('Remove');
  if (removeButton) {
    await user.click(removeButton);
  }

  await waitFor(() => {
    expect(screen.getByTestId(nextFocusTestId)).toHaveFocus();
  });
};

mockMatchMedia();

describe('UploadInput', () => {
  const pngFile = new File(['foo'], 'foo.png', { type: 'image/png' });
  const jpgFile = new File(['foo'], 'foo.jpg', { type: 'image/jpeg' });

  const files = [
    {
      id: 1,
      filename: 'purchase-receipt.pdf',
      status: Status.SUCCEEDED,
    },
    {
      id: 2,
      filename: 'CoWork-0317-invoice.pdf',
      status: Status.PROCESSING,
    },
  ];

  const props: UploadInputProps = {
    onUploadFile: jest
      .fn()
      .mockResolvedValue({ id: 1 })
      .mockResolvedValueOnce({ id: 2 })
      .mockResolvedValueOnce({ id: 3 }),
    onDeleteFile: jest.fn().mockResolvedValue({}),
  };

  const renderComponent = (customProps: UploadInputProps = props) =>
    render(<UploadInput {...customProps} />);

  beforeEach(() => {
    jest.useFakeTimers();
  });

  afterEach(async () => {
    await jest.runOnlyPendingTimersAsync();
    jest.useRealTimers();
  });

  describe('onFilesChange mount behavior', () => {
    it('should not call onFilesChange on initial mount', () => {
      const onFilesChange = jest.fn();
      renderComponent({
        ...props,
        files: [
          {
            id: 1,
            filename: 'existing-file.pdf',
            status: Status.SUCCEEDED,
          },
        ],
        onFilesChange,
      });

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

  describe('single file upload', () => {
    it('should trigger onUploadFiles & onFilesChange with a single FormData entry containing `file` field', async () => {
      const date = Date.now();
      jest.setSystemTime(date);

      const onFilesChange = jest.fn();
      renderComponent({ ...props, onFilesChange });

      const input = screen.getByTestId(UPLOAD_BUTTON_TEST_IDS.uploadInput);
      await user.upload(input, [pngFile, jpgFile]);

      expect(props.onUploadFile).toHaveBeenCalledTimes(1);
      expect(onFilesChange).toHaveBeenCalledTimes(2);
      expect(onFilesChange).toHaveBeenNthCalledWith(1, [
        {
          filename: 'foo.png',
          id: `foo.png_3_${date}`,
          status: 'pending',
        },
      ]);

      expect(onFilesChange).toHaveBeenNthCalledWith(2, [
        {
          filename: 'foo.png',
          id: 2,
          status: 'succeeded',
          url: undefined,
        },
      ]);
    });

    it('should render only one file even if multiple ones were supplied', () => {
      renderComponent({ ...props, files });

      expect(screen.getByText(files[0].filename)).toBeInTheDocument();
      files.slice(1, files.length).forEach((file) => {
        expect(screen.queryByText(file.filename)).not.toBeInTheDocument();
      });
    });
  });

  describe('multiple file upload', () => {
    it('should render all files', () => {
      renderComponent({ ...props, files, multiple: true });

      files.forEach((file) => {
        expect(screen.getByText(file.filename)).toBeInTheDocument();
      });
    });

    it('should render the UploadButton also', () => {
      renderComponent({ ...props, files, multiple: true });

      expect(screen.getByText('Upload files')).toBeInTheDocument();
    });

    it('should trigger onUploadFile with multiple FormData entries containing `file` fields', async () => {
      renderComponent({ ...props, multiple: true });

      (props.onUploadFile as jest.Mock).mockClear();

      expect(screen.getByTestId(UPLOAD_BUTTON_TEST_IDS.uploadInput)).toHaveAttribute('multiple');

      const input = screen.getByTestId(UPLOAD_BUTTON_TEST_IDS.uploadInput);
      await user.upload(input, [pngFile, jpgFile]);

      await waitFor(() => {
        expect(props.onUploadFile).toHaveBeenCalledTimes(2);
      });
    });
  });

  describe('file deletion', () => {
    const onFilesChange = jest.fn();

    beforeEach(() => {
      onFilesChange.mockClear();
    });

    it('should delete file with modal confirmation', async () => {
      renderComponent({
        ...props,
        files,
        multiple: true,
        onFilesChange,
      });

      const fileToDelete = screen.getByTestId('1-uploadItem');
      await act(async () => {
        within(fileToDelete).getByLabelText('Remove file', { exact: false }).click();
        await jest.runOnlyPendingTimersAsync();
      });

      await act(async () => {
        screen.getByText('Remove').click();
        await jest.runOnlyPendingTimersAsync();
      });

      await waitFor(() => {
        expect(screen.queryByTestId('1-uploadItem')).not.toBeInTheDocument();
      });

      expect(props.onDeleteFile).toHaveBeenCalledWith(files[0].id);

      expect(onFilesChange).toHaveBeenCalledTimes(2);
      expect(onFilesChange).toHaveBeenNthCalledWith(1, [
        {
          error: undefined,
          filename: 'purchase-receipt.pdf',
          id: 1,
          status: 'processing',
        },
        {
          filename: 'CoWork-0317-invoice.pdf',
          id: 2,
          status: 'processing',
        },
      ]);

      expect(onFilesChange).toHaveBeenLastCalledWith([
        {
          filename: 'CoWork-0317-invoice.pdf',
          id: 2,
          status: 'processing',
        },
      ]);
    });

    it('should delete file with failed state without modal confirmation', async () => {
      const files = [
        {
          id: 1,
          filename: 'purchase-receipt.pdf',
          status: Status.FAILED,
        },
      ];

      renderComponent({
        ...props,
        files,
        multiple: true,
        onFilesChange,
      });

      const fileToDelete = screen.getByTestId('1-uploadItem');
      await act(async () => {
        within(fileToDelete).getByLabelText('Remove file', { exact: false }).click();
        await jest.runOnlyPendingTimersAsync();
      });

      expect(fileToDelete).not.toBeInTheDocument();

      expect(onFilesChange).toHaveBeenCalledTimes(1);
      expect(onFilesChange).toHaveBeenCalledWith([]);
    });

    it('should not render delete button when no delete callback is provided', () => {
      renderComponent({
        ...props,
        files,
        multiple: true,
        onDeleteFile: undefined,
      });

      expect(screen.queryByLabelText('Remove file ', { exact: false })).not.toBeInTheDocument();
    });

    it('should focus the next item after a file is deleted', async () => {
      const files = [
        { id: 1, filename: 'Sales-2024-invoice.pdf', status: Status.DONE },
        { id: 2, filename: 'CoWork-0317-invoice.pdf', status: Status.DONE },
        { id: 3, filename: 'purchase-receipt.pdf', status: Status.DONE },
      ];

      renderComponent({ ...props, files, multiple: true, onFilesChange });

      // Delete the first file and expect focus to move to the next one
      await deleteFileAndWaitForFocus('1-uploadItem', '2-action');
    });

    it('should focus the previous item after the last file is deleted', async () => {
      const files = [
        {
          id: 1,
          filename: 'Sales-2024-invoice.pdf',
          status: Status.DONE,
        },
        {
          id: 2,
          filename: 'CoWork-0317-invoice.pdf',
          status: Status.DONE,
        },
        {
          id: 3,
          filename: 'purchase-receipt.pdf',
          status: Status.DONE,
        },
      ];

      renderComponent({
        ...props,
        files,
        multiple: true,
        onFilesChange,
      });

      await deleteFileAndWaitForFocus('3-uploadItem', '2-action');
    });

    it('should focus the upload input after the only file is deleted', async () => {
      const singleFile = [
        {
          id: 3,
          filename: 'purchase-receipt.pdf',
          status: Status.DONE,
        },
      ];

      renderComponent({
        ...props,
        files: singleFile,
        multiple: true,
        onFilesChange,
      });

      await deleteFileAndWaitForFocus('3-uploadItem', 'uploadInput');
    });

    it('should focus on the next item or upload input after each file is deleted in sequence', async () => {
      const filesWithFailed = [
        {
          id: 1,
          filename: 'Sales-2024-invoice.pdf',
          status: Status.DONE,
        },
        {
          id: 2,
          filename: 'CoWork-0317-invoice.pdf',
          status: Status.FAILED,
        },
        {
          id: 3,
          filename: 'purchase-receipt.pdf',
          status: Status.DONE,
        },
      ];

      renderComponent({
        ...props,
        files: filesWithFailed,
        multiple: true,
        onFilesChange,
      });

      await deleteFileAndWaitForFocus('3-uploadItem', '2-action');
      await deleteFileAndWaitForFocus('1-uploadItem', '2-action');
      await deleteFileAndWaitForFocus('2-uploadItem', 'uploadInput');
    });
  });

  describe('Max File Upload limit', () => {
    it('should show max file number in the description next to the upload button', async () => {
      renderComponent({
        ...props,
        multiple: true,
        maxFiles: 2,
      });

      await waitFor(() => {
        expect(screen.getByText(/Maximum 2 files\./)).toBeInTheDocument();
      });
    });

    it('should show given error when maxFiles limit is applied and more files are uploaded', async () => {
      const maxFilesReachedMessage = 'Maximum files message from prop';
      const mockOnUploadFileFn = jest.fn();
      renderComponent({
        ...props,
        multiple: true,
        maxFiles: 3,
        maxFilesErrorMessage: maxFilesReachedMessage,
        onUploadFile: mockOnUploadFileFn,
      });

      mockOnUploadFileFn.mockImplementation(async (formData: FormData) => {
        const file = formData.get('file');
        return Promise.resolve({ file, id: Math.random() });
      });

      const input = screen.getByTestId(UPLOAD_BUTTON_TEST_IDS.uploadInput);
      await user.upload(input, [pngFile, jpgFile]);

      const pngFile2 = new File(['foo2'], 'foo2.png', { type: 'image/png' });
      const jpgFile2 = new File(['foo2'], 'foo2.jpg', { type: 'image/jpeg' });
      await user.upload(input, [pngFile2, jpgFile2]);

      await waitFor(() => {
        expect(screen.getByText(maxFilesReachedMessage)).toBeInTheDocument();
      });
    });

    it('should show default error when maxFiles limit exceeds and no error message provided', async () => {
      const defaultMaxFilesReachedMessage =
        'Sorry, this upload failed because we can only accept 2 files at once.';
      const mockOnUploadFileFn = jest.fn();
      renderComponent({
        ...props,
        multiple: true,
        maxFiles: 2,
        onUploadFile: mockOnUploadFileFn,
      });

      mockOnUploadFileFn.mockImplementation(async (formData: FormData) => {
        const file = formData.get('file');
        return Promise.resolve({ file, id: Math.random() });
      });

      const input = screen.getByTestId(UPLOAD_BUTTON_TEST_IDS.uploadInput);
      const pngFile2 = new File(['foo2'], 'foo2.png', { type: 'image/png' });

      await user.upload(input, [pngFile, jpgFile, pngFile2]);

      await waitFor(() => {
        expect(screen.getByText(defaultMaxFilesReachedMessage)).toBeInTheDocument();
      });
    });
  });

  describe('Misc', () => {
    it('should attach the input id accepted from props to the input components', () => {
      const inputId = 'testInputId';
      const labelText = 'LabelText';
      render(
        <>
          <label htmlFor={inputId}>{labelText}</label>
          <UploadInput {...props} id={inputId} />
        </>,
      );
      expect(screen.getByLabelText(labelText)).toHaveAttribute('id', inputId);
    });

    it('should show the file size error when provided in props', async () => {
      const bytesInaKb = 1024;
      const twoKbInBytes = 2 * bytesInaKb;

      const sizeLimitErrorMessage = 'file oversized';
      renderComponent({ ...props, sizeLimit: 1, sizeLimitErrorMessage });
      const input = screen.getByTestId(UPLOAD_BUTTON_TEST_IDS.uploadInput);
      const overSizedFile = new File([''], 'testFile.png', { type: 'image/png' });
      Object.defineProperty(overSizedFile, 'size', { value: twoKbInBytes });

      await user.upload(input, overSizedFile);
      await waitFor(() => {
        expect(screen.getByText(sizeLimitErrorMessage)).toBeInTheDocument();
      });
    });
  });

  it('supports `Field` for error messages', () => {
    render(
      <Field message="Something went wrong" sentiment="negative">
        <UploadInput {...props} />
      </Field>,
    );

    const container = screen.getAllByRole('group')[0];
    expect(container).toBeInvalid();
    expect(container).toHaveAccessibleDescription('Something went wrong');
  });
});
