import React, { ComponentProps } from 'react';

import { ActivityIndicator, ScrollView } from 'react-native';

import type { ReactTestInstance } from 'react-test-renderer';

import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react-native';

import type { Attachment, Channel as ChannelType, LocalAttachment, StreamChat } from 'stream-chat';

import { OverlayProvider } from '../../../contexts';
import { initiateClientWithChannels } from '../../../mock-builders/api/initiateClientWithChannels';
import {
  generateAudioAttachment as generateAudioAttachmentBase,
  generateFileAttachment as generateFileAttachmentBase,
  generateImageAttachment as generateImageAttachmentBase,
  generateVideoAttachment as generateVideoAttachmentBase,
} from '../../../mock-builders/attachments';

import { FileState } from '../../../utils/utils';
import { Channel } from '../../Channel/Channel';
import { Chat } from '../../Chat/Chat';
import { AttachmentUploadPreviewList } from '../components/AttachmentPreview/AttachmentUploadPreviewList';

const generateAudioAttachment = (a?: unknown): LocalAttachment =>
  generateAudioAttachmentBase(a as Partial<Attachment>) as unknown as LocalAttachment;
const generateFileAttachment = (a?: unknown): LocalAttachment =>
  generateFileAttachmentBase(a as Partial<Attachment>) as unknown as LocalAttachment;
const generateImageAttachment = (a?: unknown): LocalAttachment =>
  generateImageAttachmentBase(a as Partial<Attachment>) as unknown as LocalAttachment;
const generateVideoAttachment = (a?: unknown): LocalAttachment =>
  generateVideoAttachmentBase(a as Partial<Attachment>) as unknown as LocalAttachment;

jest.mock('../../../native.ts', () => {
  const { View } = require('react-native');

  return {
    isAudioRecorderAvailable: jest.fn(() => true),
    isDocumentPickerAvailable: jest.fn(() => true),
    isImageMediaLibraryAvailable: jest.fn(() => true),
    isImagePickerAvailable: jest.fn(() => true),
    isNativeMultipartUploadAvailable: jest.fn(() => false),
    isSoundPackageAvailable: jest.fn(() => false),
    NativeHandlers: {
      Sound: {
        Player: View,
      },
    },
  };
});

const renderComponent = ({
  client,
  channel,
  props,
}: {
  client: StreamChat;
  channel: ChannelType;
  props: Partial<ComponentProps<typeof AttachmentUploadPreviewList>>;
}) => {
  return render(
    <OverlayProvider>
      <Chat client={client}>
        <Channel channel={channel}>
          <AttachmentUploadPreviewList {...props} />
        </Channel>
      </Chat>
    </OverlayProvider>,
  );
};

type PendingUploadRecord = {
  id: string;
  uploadProgress?: number;
};

const setPendingUploads = (client: StreamChat, uploads: PendingUploadRecord[]) => {
  act(() => {
    client.uploadManager.state.partialNext({
      uploads: Object.fromEntries(
        uploads.map(({ id, uploadProgress }) => [id, { id, uploadProgress }]),
      ),
    });
  });
};

const countActivityIndicators = (nodes: ReactTestInstance[]) =>
  nodes.reduce(
    (count: number, node: ReactTestInstance) =>
      count + node.findAllByType(ActivityIndicator).length,
    0,
  );

describe('AttachmentUploadPreviewList', () => {
  let client: StreamChat;
  let channel: ChannelType;

  beforeEach(async () => {
    const { client: chatClient, channels } = await initiateClientWithChannels();
    client = chatClient;
    channel = channels[0];
  });

  afterEach(() => {
    jest.clearAllMocks();
    cleanup();
    act(() => {
      client?.uploadManager?.reset();
      channel.messageComposer.attachmentManager.initState();
    });
  });

  it('should return null when no files are uploaded', async () => {
    const props = {};

    renderComponent({ channel, client, props });

    const { queryAllByTestId } = screen;

    await waitFor(() => {
      expect(queryAllByTestId('file-upload-preview')).toHaveLength(0);
    });
  });

  it('should return null when the file is an image', async () => {
    const attachments = [
      generateImageAttachment({
        localMetadata: {
          id: 'image-attachment',
          uploadState: FileState.FINISHED,
        },
      }),
    ];
    const props = {};

    act(() => {
      channel.messageComposer.attachmentManager.upsertAttachments(attachments ?? []);
    });

    renderComponent({ channel, client, props });

    const { queryAllByTestId } = screen;

    await waitFor(() => {
      expect(queryAllByTestId('file-attachment-upload-preview')).toHaveLength(0);
    });
  });

  it('should render FileAttachmentUploadPreview when the sound package is unavailable', async () => {
    const attachments = [
      generateAudioAttachment({
        asset_url: undefined,
        localMetadata: {
          file: {
            uri: 'file://audio-attachment.mp3',
          },
          id: 'audio-attachment',
          uploadState: FileState.UPLOADING,
        },
      }),
    ];

    const props = {};

    act(() => {
      channel.messageComposer.attachmentManager.upsertAttachments(attachments);
    });
    setPendingUploads(client, [{ id: 'audio-attachment' }]);

    renderComponent({ channel, client, props });

    const { getAllByTestId, queryAllByTestId } = screen;

    await waitFor(() => {
      expect(queryAllByTestId('file-attachment-upload-preview')).toHaveLength(1);
      expect(countActivityIndicators(getAllByTestId('file-attachment-upload-preview'))).toBe(1);
    });
  });

  describe('FileAttachmentUploadPreview', () => {
    it('anchors the preview list to the end when content shrinks near the end', () => {
      const scrollToSpy = jest
        .spyOn(ScrollView.prototype, 'scrollTo')
        .mockImplementation(() => undefined);
      try {
        const attachments = [
          generateFileAttachment({
            localMetadata: {
              id: 'file-attachment-1',
              uploadState: FileState.FINISHED,
            },
          }),
          generateFileAttachment({
            localMetadata: {
              id: 'file-attachment-2',
              uploadState: FileState.FINISHED,
            },
          }),
        ];
        const props = {};

        act(() => {
          channel.messageComposer.attachmentManager.upsertAttachments(attachments);
        });

        renderComponent({ channel, client, props });

        const list = screen.UNSAFE_getByType(ScrollView);

        act(() => {
          fireEvent(list, 'layout', { nativeEvent: { layout: { width: 100 } } });
          list.props.onContentSizeChange(300, 0);
          list.props.onScroll({ nativeEvent: { contentOffset: { x: 190 } } });
          list.props.onContentSizeChange(250, 0);
        });

        expect(scrollToSpy).toHaveBeenCalledWith({
          animated: false,
          x: 150,
        });
      } finally {
        scrollToSpy.mockRestore();
      }
    });

    it('should render FileAttachmentUploadPreview with all uploading files', async () => {
      const attachments = [
        generateFileAttachment({
          asset_url: undefined,
          localMetadata: {
            file: {
              uri: 'file://file-attachment.xls',
            },
            id: 'file-attachment',
            uploadState: FileState.UPLOADING,
          },
        }),
        generateVideoAttachment({
          localMetadata: {
            file: {
              uri: 'file://video-attachment.mp4',
            },
            id: 'video-attachment',
            uploadState: FileState.UPLOADING,
          },
        }),
      ];
      const props = {};

      act(() => {
        channel.messageComposer.attachmentManager.upsertAttachments(attachments);
      });
      setPendingUploads(client, [{ id: 'file-attachment' }, { id: 'video-attachment' }]);

      renderComponent({ channel, client, props });

      const { getAllByTestId, queryAllByTestId } = screen;

      await waitFor(() => {
        expect(queryAllByTestId('file-attachment-upload-preview')).toHaveLength(2);
        expect(countActivityIndicators(getAllByTestId('file-attachment-upload-preview'))).toBe(2);
      });

      act(() => {
        fireEvent.press(getAllByTestId('remove-upload-preview')[0]);
      });

      await waitFor(() => {
        expect(channel.messageComposer.attachmentManager.attachments).toHaveLength(1);
      });

      act(() => {
        fireEvent.press(getAllByTestId('remove-upload-preview')[0]);
      });

      await waitFor(() => {
        expect(channel.messageComposer.attachmentManager.attachments).toHaveLength(0);
      });
    });

    it('should render FileAttachmentUploadPreview with all uploaded files', async () => {
      const attachments = [
        generateFileAttachment({
          localMetadata: {
            id: 'image-attachment',
            uploadState: FileState.FINISHED,
          },
        }),
        generateVideoAttachment({
          localMetadata: {
            id: 'video-attachment',
            uploadState: FileState.FINISHED,
          },
        }),
      ];
      const props = {};

      act(() => {
        channel.messageComposer.attachmentManager.upsertAttachments(attachments ?? []);
      });

      renderComponent({ channel, client, props });

      const { queryAllByTestId } = screen;

      await waitFor(() => {
        expect(queryAllByTestId('file-attachment-upload-preview')).toHaveLength(2);
      });
    });

    it('should render FileAttachmentUploadPreview with all failed files', async () => {
      const uploadAttachmentSpy = jest.fn();
      channel.messageComposer.attachmentManager.uploadAttachment = uploadAttachmentSpy;
      const attachments = [
        generateFileAttachment({
          localMetadata: {
            id: 'file-attachment',
            uploadState: FileState.FAILED,
          },
        }),
        generateVideoAttachment({
          localMetadata: {
            id: 'video-attachment',
            uploadState: FileState.FAILED,
          },
        }),
      ];
      const props = {};

      act(() => {
        channel.messageComposer.attachmentManager.upsertAttachments(attachments ?? []);
      });

      renderComponent({ channel, client, props });

      const { getAllByTestId, queryAllByTestId } = screen;

      await waitFor(() => {
        expect(queryAllByTestId('file-attachment-upload-preview')).toHaveLength(2);
        expect(queryAllByTestId('retry-upload-progress-indicator')).toHaveLength(2);
      });

      act(() => {
        fireEvent.press(getAllByTestId('retry-upload-progress-indicator')[0]);
      });

      await waitFor(() => {
        expect(queryAllByTestId('file-attachment-upload-preview')).toHaveLength(2);
        expect(channel.messageComposer.attachmentManager.attachments).toHaveLength(2);
        expect(uploadAttachmentSpy).toHaveBeenCalled();
      });
    });

    it('should render FileAttachmentUploadPreview with all unsupported', async () => {
      const attachments = [
        generateFileAttachment({
          localMetadata: {
            id: 'file-attachment',
            uploadState: FileState.BLOCKED,
          },
        }),
        generateVideoAttachment({
          localMetadata: {
            id: 'video-attachment',
            uploadState: FileState.BLOCKED,
          },
        }),
      ];
      const props = {};

      act(() => {
        channel.messageComposer.attachmentManager.upsertAttachments(attachments ?? []);
      });

      renderComponent({ channel, client, props });

      const { queryAllByTestId } = screen;

      await waitFor(() => {
        expect(queryAllByTestId('file-attachment-upload-preview')).toHaveLength(2);
        expect(queryAllByTestId('inline-not-supported-indicator')).toHaveLength(2);
      });
    });
  });

  describe('ImageAttachmentUploadPreview', () => {
    it('should render ImageAttachmentUploadPreview with all uploading images', async () => {
      const attachments = [
        generateImageAttachment({
          localMetadata: {
            id: 'image-attachment',
            previewUri: 'file://image-attachment.png',
            uploadState: FileState.UPLOADING,
          },
        }),
      ];
      const props = {};

      await act(() => {
        channel.messageComposer.attachmentManager.upsertAttachments(attachments ?? []);
      });
      setPendingUploads(client, [{ id: 'image-attachment' }]);

      renderComponent({ channel, client, props });

      const { getAllByTestId, queryAllByTestId } = screen;

      await waitFor(() => {
        expect(queryAllByTestId('image-attachment-upload-preview')).toHaveLength(1);
        expect(countActivityIndicators(getAllByTestId('image-attachment-upload-preview'))).toBe(1);
      });

      await act(() => {
        fireEvent.press(getAllByTestId('remove-upload-preview')[0]);
      });

      await waitFor(() => {
        expect(channel.messageComposer.attachmentManager.attachments).toHaveLength(0);
      });
    });

    it('should return null when no images are uploaded', async () => {
      const props = {};

      renderComponent({ channel, client, props });

      const { queryAllByTestId } = screen;

      await waitFor(() => {
        expect(queryAllByTestId('file-upload-preview')).toHaveLength(0);
      });
    });

    it('should render ImageAttachmentUploadPreview with all uploaded images', async () => {
      const attachments = [
        generateImageAttachment({
          localMetadata: {
            id: 'image-attachment',
            uploadState: FileState.FINISHED,
          },
        }),
      ];
      const props = {};

      await act(() => {
        channel.messageComposer.attachmentManager.upsertAttachments(attachments ?? []);
      });

      renderComponent({ channel, client, props });

      const { queryAllByTestId } = screen;

      await waitFor(() => {
        const imageAttachments = queryAllByTestId('image-attachment-upload-preview-image');
        for (const image of imageAttachments) {
          fireEvent(image, 'loadEnd');
        }
      });

      await waitFor(() => {
        expect(queryAllByTestId('image-attachment-upload-preview')).toHaveLength(1);
      });
    });

    it('should render ImageAttachmentUploadPreview with all failed images', async () => {
      const uploadAttachmentSpy = jest.fn();
      channel.messageComposer.attachmentManager.uploadAttachment = uploadAttachmentSpy;
      const attachments = [
        generateImageAttachment({
          localMetadata: {
            id: 'image-attachment',
            uploadState: FileState.FAILED,
          },
        }),
      ];
      const props = {};

      await act(() => {
        channel.messageComposer.attachmentManager.upsertAttachments(attachments ?? []);
      });

      renderComponent({ channel, client, props });

      const { getAllByTestId, queryAllByTestId } = screen;

      await waitFor(() => {
        const imageAttachments = queryAllByTestId('image-attachment-upload-preview-image');
        for (const image of imageAttachments) {
          fireEvent(image, 'loadEnd');
        }
      });

      await waitFor(() => {
        expect(queryAllByTestId('image-attachment-upload-preview')).toHaveLength(1);
        expect(queryAllByTestId('retry-upload-progress-indicator')).toHaveLength(1);
      });

      await act(() => {
        fireEvent.press(getAllByTestId('retry-upload-progress-indicator')[0]);
      });

      await waitFor(() => {
        expect(queryAllByTestId('image-attachment-upload-preview')).toHaveLength(1);
        expect(channel.messageComposer.attachmentManager.attachments).toHaveLength(1);
        expect(uploadAttachmentSpy).toHaveBeenCalled();
      });
    });

    it('should render ImageAttachmentUploadPreview with all unsupported', async () => {
      const attachments = [
        generateImageAttachment({
          localMetadata: {
            id: 'image-attachment',
            uploadState: FileState.BLOCKED,
          },
        }),
      ];
      const props = {};

      await act(() => {
        channel.messageComposer.attachmentManager.upsertAttachments(attachments ?? []);
      });

      renderComponent({ channel, client, props });

      const { queryAllByTestId } = screen;

      await waitFor(() => {
        const imageAttachments = queryAllByTestId('image-attachment-upload-preview-image');
        for (const image of imageAttachments) {
          fireEvent(image, 'loadEnd');
        }
      });

      await waitFor(() => {
        expect(queryAllByTestId('image-attachment-upload-preview')).toHaveLength(1);
        expect(queryAllByTestId('inline-not-supported-indicator')).toHaveLength(1);
      });
    });

    it('should render ImageAttachmentUploadPreview with 1 uploading, 1 uploaded, and 1 failed image, and 1 unsupported', async () => {
      const attachments = [
        generateImageAttachment({
          localMetadata: {
            id: 'image-attachment-1',
            previewUri: 'file://image-attachment-1.png',
            uploadState: FileState.UPLOADING,
          },
        }),
        generateImageAttachment({
          localMetadata: {
            id: 'image-attachment-2',
            uploadState: FileState.FINISHED,
          },
        }),
        generateImageAttachment({
          localMetadata: {
            id: 'image-attachment-3',
            uploadState: FileState.FAILED,
          },
        }),
        generateImageAttachment({
          localMetadata: {
            id: 'image-attachment-4',
            uploadState: FileState.BLOCKED,
          },
        }),
      ];

      const props = {};
      await act(() => {
        channel.messageComposer.attachmentManager.upsertAttachments(attachments ?? []);
      });
      setPendingUploads(client, [{ id: 'image-attachment-1' }]);

      renderComponent({ channel, client, props });

      const { getAllByTestId, queryAllByTestId } = screen;

      await waitFor(() => {
        const imageAttachments = queryAllByTestId('image-attachment-upload-preview-image');
        for (const image of imageAttachments) {
          fireEvent(image, 'loadEnd');
        }
      });

      await waitFor(() => {
        expect(queryAllByTestId('image-attachment-upload-preview')).toHaveLength(4);
        expect(countActivityIndicators(getAllByTestId('image-attachment-upload-preview'))).toBe(1);
        expect(queryAllByTestId('retry-upload-progress-indicator')).toHaveLength(1);
        expect(queryAllByTestId('inline-not-supported-indicator')).toHaveLength(1);
      });
    });
  });
});
