import { render, fireEvent, waitFor, screen } from '../test-utils';
import { act } from 'react';
import Upload from '.';
import { asyncFileRead } from './utils/asyncFileRead';
import { postData } from './utils/postData';

jest.mock('./utils/asyncFileRead');
jest.mock('./utils/postData');
jest.mock('commonmark', () => ({
  Parser: jest.fn().mockImplementation(() => ({
    parse: jest.fn(() => ({})),
  })),
  HtmlRenderer: jest.fn().mockImplementation(() => ({
    render: jest.fn(() => ''),
  })),
}));

const TEST_FILE = new File(['test content'], 'test.png', { type: 'image/png' });
const INVALID_FILE = new File(['invalid content'], 'invalid.txt', { type: 'text/plain' });

beforeAll(() => {
  class MockDataTransfer {
    items: { add: (file: File) => void };
    files: File[];

    constructor() {
      this.items = {
        add: (file: File) => {
          this.files.push(file);
        },
      };
      this.files = [];
    }
  }

  // @ts-expect-error: Mocking global DataTransfer for testing purposes
  global.DataTransfer = MockDataTransfer;
});

describe('Upload Component', () => {
  const props = {
    animationDelay: 100,
    csButtonText: 'csButtonText',
    csFailureText: 'csFailureText',
    csSuccessText: 'csSuccessText',
    csTooLargeMessage: 'csTooLargeMessage',
    maxSize: 5000000,
    onCancel: jest.fn(),
    onChange: jest.fn(),
    onFailure: jest.fn(),
    onStart: jest.fn(),
    onSuccess: jest.fn(),
    psButtonText: 'psButtonText',
    psProcessingText: 'psProcessingText',
    usAccept: 'image/*',
    usButtonText: 'Or Select File',
    usButtonRetryText: 'Try again',
    usDropMessage: 'Drop file to start upload',
    usPlaceholder: 'Drag and drop a file less than 5MB',
  };

  const waitForUpload = async () => {
    for (let i = 0; i < 4; i += 1) {
      await act(async () => {
        await jest.runOnlyPendingTimersAsync();
      });
    }
  };

  beforeEach(() => {
    jest.useFakeTimers();
    (asyncFileRead as jest.Mock).mockImplementation(async () => 'a value');
    (postData as jest.Mock).mockResolvedValue('ServerResponse');
    Object.defineProperty(window, 'matchMedia', {
      writable: true,
      value: jest.fn().mockImplementation((query: string) => ({
        matches: false,
        media: query,
        onchange: null,
        addListener: jest.fn(),
        removeListener: jest.fn(),
        addEventListener: jest.fn(),
        removeEventListener: jest.fn(),
        dispatchEvent: jest.fn(),
      })),
    });
    props.onStart.mockReset();
    props.onSuccess.mockReset();
    props.onFailure.mockReset();
    props.onCancel.mockReset();
    props.onChange.mockReset();
  });

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

  test('adds droppable-dropping class on dragenter', async () => {
    const { container } = render(<Upload {...props} />);
    const droppableElement = await waitFor(() => container.querySelector('.droppable-area'));
    expect(droppableElement).toBeTruthy();

    fireEvent.dragEnter(droppableElement!, { dataTransfer: new DataTransfer() });

    await waitFor(() => {
      expect(droppableElement).toHaveClass('droppable-dropping');
    });
  });

  test('removes droppable-dropping class on dragleave', async () => {
    const { container } = render(<Upload {...props} />);
    const droppableElement = await waitFor(() => container.querySelector('.droppable-area'));
    expect(droppableElement).toBeTruthy();

    fireEvent.dragEnter(droppableElement!, { dataTransfer: new DataTransfer() });

    await waitFor(() => {
      expect(droppableElement).toHaveClass('droppable-dropping');
    });

    fireEvent.dragLeave(droppableElement!);

    await waitFor(() => {
      expect(droppableElement).not.toHaveClass('droppable-dropping');
    });
  });

  test('adds droppable-processing when file is dropped', async () => {
    const { container } = render(<Upload {...props} />);
    const droppableElement = await waitFor(() => container.querySelector('.droppable-area'));

    const dataTransfer = new DataTransfer();
    dataTransfer.items.add(TEST_FILE);

    fireEvent.drop(droppableElement!, { dataTransfer });

    await waitFor(() => expect(droppableElement).toHaveClass('droppable-processing'));
  });

  test('should handle successful file upload', async () => {
    (asyncFileRead as jest.Mock).mockResolvedValue('mockBase64Image');
    (postData as jest.Mock).mockResolvedValue('mockSuccessResponse');

    const { container } = render(<Upload {...props} />);

    await act(async () => {
      const droppableElement = await waitFor(() => container.querySelector('.droppable-area'));

      const dataTransfer = new DataTransfer();
      dataTransfer.items.add(TEST_FILE);
      fireEvent.drop(droppableElement!, { dataTransfer });
    });

    await act(async () => {
      jest.advanceTimersByTime(10);
      jest.advanceTimersByTime(props.animationDelay);
    });

    expect(await screen.findByText(/csSuccessText/i)).toBeInTheDocument();
  });

  test('should handle file size exceeding the limit', async () => {
    const largeFile = new File(['large '.repeat(1000000)], 'large.jpg', { type: 'image/jpeg' });
    const { container } = render(<Upload {...props} maxSize={10} />);

    await act(async () => {
      const droppableElement = await waitFor(() => container.querySelector('.droppable-area'));
      const dataTransfer = new DataTransfer();
      dataTransfer.items.add(largeFile);
      fireEvent.drop(droppableElement!, { dataTransfer });
    });

    await act(async () => {
      jest.advanceTimersByTime(1000);
      jest.advanceTimersByTime(props.animationDelay);
    });

    await waitForUpload();

    expect(await screen.findByText(/csTooLargeMessage/i)).toBeInTheDocument();
  });

  test('should handle invalid file type', async () => {
    (postData as jest.Mock).mockRejectedValue(new Error('mockErrorResponse'));

    const { container } = render(<Upload {...props} />);

    await act(async () => {
      const droppableElement = await waitFor(() => container.querySelector('.droppable-area'));

      const dataTransfer = new DataTransfer();
      dataTransfer.items.add(INVALID_FILE);
      fireEvent.drop(droppableElement!, { dataTransfer });
    });

    await waitForUpload();

    expect(
      await screen.findByText(/File type not supported. Please try again with a different file/i),
    ).toBeInTheDocument();
  });

  test('should handle canceling the upload', async () => {
    const { container } = render(<Upload {...props} />);

    await act(async () => {
      const droppableElement = await waitFor(() => container.querySelector('.droppable-area'));

      const dataTransfer = new DataTransfer();
      dataTransfer.items.add(TEST_FILE);
      fireEvent.drop(droppableElement!, { dataTransfer });
    });

    fireEvent.click(await screen.findByText(/csButtonText/i));

    expect(props.onCancel).toHaveBeenCalled();
  });

  test('should handle retrying the upload', async () => {
    (asyncFileRead as jest.Mock).mockRejectedValueOnce('error');

    const { container } = render(<Upload {...props} />);

    await act(async () => {
      const droppableElement = await waitFor(() => container.querySelector('.droppable-area'));

      const dataTransfer = new DataTransfer();
      dataTransfer.items.add(TEST_FILE);
      fireEvent.drop(droppableElement!, { dataTransfer });
    });

    await waitForUpload();

    fireEvent.click(await screen.findByText(/Try again/i));

    expect(props.onStart).toHaveBeenCalled();
  });

  test('should call onSuccess with the server response when httpOptions are provided', async () => {
    (asyncFileRead as jest.Mock).mockResolvedValue('mockBase64Image');
    (postData as jest.Mock).mockResolvedValue('mockServerResponse');

    const { container } = render(<Upload {...props} httpOptions={{ url: 'test-url' }} />);
    const droppableElement = await waitFor(() => container.querySelector('.droppable-area'));

    const dataTransfer = new DataTransfer();
    dataTransfer.items.add(TEST_FILE);

    await act(async () => {
      fireEvent.drop(droppableElement!, { dataTransfer });
    });

    await waitForUpload();

    expect(props.onSuccess).toHaveBeenCalledWith('mockServerResponse', TEST_FILE.name);
  });

  test('should not allow new file to process while current one is in progress', async () => {
    (asyncFileRead as jest.Mock).mockResolvedValue('someResult');

    const { container } = render(<Upload {...props} httpOptions={{ url: 'test-url' }} />);
    const droppableElement = await waitFor(() => container.querySelector('.droppable-area'));

    const dataTransfer = new DataTransfer();
    dataTransfer.items.add(TEST_FILE);

    await act(async () => {
      fireEvent.drop(droppableElement!, { dataTransfer });
    });

    const secondFileDataTransfer = new DataTransfer();
    secondFileDataTransfer.items.add(TEST_FILE);

    let dropResult = true;
    await act(async () => {
      try {
        fireEvent.drop(droppableElement!, { dataTransfer: secondFileDataTransfer });
      } catch {
        dropResult = false;
      }
    });

    expect(dropResult).toBe(true);
  });

  test('should override the error icon label when errorIconLabel prop is provided', async () => {
    (asyncFileRead as jest.Mock).mockRejectedValueOnce('mockReadError');
    (postData as jest.Mock).mockRejectedValueOnce(new Error('mockPostError'));

    const customLabel = 'Custom error label';
    const { container } = render(<Upload {...props} errorIconLabel={customLabel} />);
    const droppableElement = await waitFor(() => container.querySelector('.droppable-area'));

    const dataTransfer = new DataTransfer();
    dataTransfer.items.add(TEST_FILE);

    await act(async () => {
      fireEvent.drop(droppableElement!, { dataTransfer });
    });

    await waitForUpload();

    const errorIcon = await screen.findByLabelText(/Custom error label/i);
    expect(errorIcon).toBeInTheDocument();
  });

  function runAnimationDelayTest(label: string, delay: number) {
    test(`should respect a ${label} animationDelay`, async () => {
      (asyncFileRead as jest.Mock).mockResolvedValue('mockBase64Image');
      (postData as jest.Mock).mockResolvedValue('mockSuccessResponse');

      const { container, unmount } = render(<Upload {...props} animationDelay={delay} />);

      await act(async () => {
        const droppableElement = await waitFor(() => container.querySelector('.droppable-area'));
        const dataTransfer = new DataTransfer();
        dataTransfer.items.add(TEST_FILE);
        fireEvent.drop(droppableElement!, { dataTransfer });
      });

      // Advance half the delay, should still be processing
      await act(async () => {
        jest.advanceTimersByTime(delay / 2);
      });
      expect(container.querySelector('.droppable-processing')).toBeInTheDocument();
      expect(container.querySelector('.droppable-complete')).not.toBeInTheDocument();

      // Advance the rest of the delay
      await act(async () => {
        jest.advanceTimersByTime(delay / 2 + 10);
      });
      expect(container.querySelector('.droppable-complete')).toBeInTheDocument();
      expect(container.querySelector('.droppable-processing')).not.toBeInTheDocument();

      unmount();
    });
  }

  runAnimationDelayTest('short', 50);
  runAnimationDelayTest('long', 500);

  test('should handle disabled state correctly', async () => {
    const { container } = render(<Upload {...props} usDisabled />);
    const droppableElement = await waitFor(() => container.querySelector('.droppable-area'));

    const dataTransfer = new DataTransfer();
    dataTransfer.items.add(TEST_FILE);

    await act(async () => {
      fireEvent.drop(droppableElement!, { dataTransfer });
    });

    expect(container.querySelector('.droppable-processing')).not.toBeInTheDocument();
    expect(props.onStart).not.toHaveBeenCalled();
  });

  test('should remove max size limit, when maxSize is the `null`', async () => {
    render(<Upload {...props} usPlaceholder={undefined} maxSize={null} />);
    expect(await screen.findByText(/Drag and drop a file/i)).toBeInTheDocument();
    expect(screen.queryByText(/less than/i)).not.toBeInTheDocument();
  });
});
