import React, { PropsWithChildren } from 'react';
import { Alert } from 'react-native';

import { cleanup, renderHook, waitFor } from '@testing-library/react-native';
import type { Channel, StreamChat } from 'stream-chat';

import { Chat } from '../../../components';
import { initiateClientWithChannels } from '../../../mock-builders/api/initiateClientWithChannels';
import { generateFileReference } from '../../../mock-builders/attachments';

import { NativeHandlers } from '../../../native';

import { AttachmentPickerProvider } from '../../attachmentPickerContext/AttachmentPickerContext';
import { ChannelContextValue, ChannelProvider } from '../../channelContext/ChannelContext';
import { MessageComposerProvider } from '../../messageComposerContext/MessageComposerContext';
import {
  InputMessageInputContextValue,
  MessageInputProvider,
  useMessageInputContext,
} from '../MessageInputContext';

jest.spyOn(Alert, 'alert');

const Wrapper = ({
  channel,
  client,
  props,
}: {
  channel: Channel;
  client: StreamChat;
  props: PropsWithChildren<Partial<InputMessageInputContextValue>>;
}) => {
  return (
    <Chat client={client}>
      <ChannelProvider
        value={
          {
            uploadAbortControllerRef: {
              current: new Map(),
            },
          } as ChannelContextValue
        }
      >
        <AttachmentPickerProvider
          value={
            { disableAttachmentPicker: true } as unknown as React.ComponentProps<
              typeof AttachmentPickerProvider
            >['value']
          }
        >
          <MessageComposerProvider
            value={
              { channel } as unknown as React.ComponentProps<
                typeof MessageComposerProvider
              >['value']
            }
          >
            <MessageInputProvider
              value={
                {
                  ...props,
                } as InputMessageInputContextValue
              }
            >
              {props.children}
            </MessageInputProvider>
          </MessageComposerProvider>
        </AttachmentPickerProvider>
      </ChannelProvider>
    </Chat>
  );
};

describe("MessageInputContext's pickFile", () => {
  let channel: Channel;
  let chatClient: StreamChat;

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

  afterEach(() => {
    jest.clearAllMocks();
    cleanup();
  });

  const initialProps = {};

  it('run pickFile when availableUploadSlots is 0 and alert is triggered 1 number of times', async () => {
    jest
      .spyOn(channel.messageComposer.attachmentManager, 'availableUploadSlots', 'get')
      .mockReturnValue(0);
    const { result } = renderHook(() => useMessageInputContext(), {
      initialProps,
      wrapper: (props) => <Wrapper channel={channel} client={chatClient} props={props} />,
    });

    await waitFor(() => {
      result.current.pickFile();
    });

    expect(Alert.alert).toHaveBeenCalledTimes(1);
    expect(Alert.alert).toHaveBeenCalledWith('Maximum number of files reached');
  });

  it.each([
    [false, undefined, true],
    [false, undefined, false],
    [false, [generateFileReference(), generateFileReference()], true],
    [false, [], false],
    [true, [generateFileReference(), generateFileReference()], false],
  ])(
    'allow uploads %p when pickDocument returns assets %p and cancelled %p',
    async (allowed, assets, cancelled) => {
      const { attachmentManager } = channel.messageComposer;
      jest.spyOn(NativeHandlers, 'pickDocument').mockImplementation(
        jest.fn().mockResolvedValue({
          assets,
          cancelled,
        }),
      );

      jest.spyOn(attachmentManager, 'availableUploadSlots', 'get').mockReturnValue(2);

      const { result } = renderHook(() => useMessageInputContext(), {
        initialProps,
        wrapper: (props) => <Wrapper channel={channel} client={chatClient} props={props} />,
      });

      const uploadFilesSpy = jest.spyOn(attachmentManager, 'uploadFiles');

      await waitFor(() => {
        result.current.pickFile();
      });

      await waitFor(() => {
        expect(NativeHandlers.pickDocument).toHaveBeenCalledTimes(1);
        expect(NativeHandlers.pickDocument).toHaveBeenCalledWith({ maxNumberOfFiles: 2 });
        if (allowed) {
          expect(uploadFilesSpy).toHaveBeenCalledTimes(2);
        } else {
          expect(uploadFilesSpy).not.toHaveBeenCalled();
        }
      });
    },
  );
});

describe("MessageInputContext's pickAndUploadImageFromNativePicker", () => {
  let channel: Channel;
  let chatClient: StreamChat;

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

  afterEach(() => {
    jest.clearAllMocks();
    cleanup();
  });

  const initialProps = {};

  it('run pickAndUploadImageFromNativePicker when availableUploadSlots is 0 and alert is triggered 1 number of times', async () => {
    jest
      .spyOn(channel.messageComposer.attachmentManager, 'availableUploadSlots', 'get')
      .mockReturnValue(0);
    const { result } = renderHook(() => useMessageInputContext(), {
      initialProps,
      wrapper: (props) => <Wrapper channel={channel} client={chatClient} props={props} />,
    });

    await waitFor(() => {
      result.current.pickAndUploadImageFromNativePicker();
    });

    expect(Alert.alert).toHaveBeenCalledTimes(1);
    expect(Alert.alert).toHaveBeenCalledWith('Maximum number of files reached');
  });

  it('should show permissions alert when askToOpenSettings is true', async () => {
    jest
      .spyOn(channel.messageComposer.attachmentManager, 'availableUploadSlots', 'get')
      .mockReturnValue(2);

    jest.spyOn(NativeHandlers, 'pickImage').mockImplementation(
      jest.fn().mockResolvedValue({
        askToOpenSettings: true,
      }),
    );

    const { result } = renderHook(() => useMessageInputContext(), {
      initialProps,
      wrapper: (props) => <Wrapper channel={channel} client={chatClient} props={props} />,
    });

    await waitFor(() => {
      result.current.pickAndUploadImageFromNativePicker();
    });

    expect(Alert.alert).toHaveBeenCalledTimes(1);
  });

  it.each([
    [false, undefined, true],
    [false, undefined, false],
    [false, [generateFileReference(), generateFileReference()], true],
    [false, [], false],
    [true, [generateFileReference(), generateFileReference()], false],
  ])(
    'allow uploads %p when pickImage returns assets %p and cancelled %p',
    async (allowed, assets, cancelled) => {
      const { attachmentManager } = channel.messageComposer;
      jest.spyOn(NativeHandlers, 'pickImage').mockImplementation(
        jest.fn().mockResolvedValue({
          assets,
          cancelled,
        }),
      );

      jest.spyOn(attachmentManager, 'availableUploadSlots', 'get').mockReturnValue(2);

      const { result } = renderHook(() => useMessageInputContext(), {
        initialProps,
        wrapper: (props) => <Wrapper channel={channel} client={chatClient} props={props} />,
      });

      const uploadFilesSpy = jest.spyOn(attachmentManager, 'uploadFiles');

      await waitFor(() => {
        result.current.pickAndUploadImageFromNativePicker();
      });

      await waitFor(() => {
        expect(NativeHandlers.pickImage).toHaveBeenCalledTimes(1);
        if (allowed) {
          expect(uploadFilesSpy).toHaveBeenCalledTimes(2);
        } else {
          expect(uploadFilesSpy).not.toHaveBeenCalled();
        }
      });
    },
  );

  it('does not crash when pickImage returns an asset with a null mime type', async () => {
    const { attachmentManager } = channel.messageComposer;
    jest.spyOn(NativeHandlers, 'pickImage').mockImplementation(
      jest.fn().mockResolvedValue({
        assets: [
          {
            duration: 0,
            height: 100,
            name: 'IMG_0001',
            size: 123,
            type: null,
            uri: 'file:///tmp/IMG_0001',
            width: 200,
          },
        ],
        cancelled: false,
      }),
    );

    jest.spyOn(attachmentManager, 'availableUploadSlots', 'get').mockReturnValue(2);

    const { result } = renderHook(() => useMessageInputContext(), {
      initialProps,
      wrapper: (props) => <Wrapper channel={channel} client={chatClient} props={props} />,
    });

    const uploadFilesSpy = jest.spyOn(attachmentManager, 'uploadFiles');

    await waitFor(() => {
      result.current.pickAndUploadImageFromNativePicker();
    });

    await waitFor(() => {
      expect(uploadFilesSpy).toHaveBeenCalledWith([
        expect.objectContaining({
          type: 'image/*',
        }),
      ]);
    });
  });
});

describe("MessageInputContext's takeAndUploadImage", () => {
  let channel: Channel;
  let chatClient: StreamChat;

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

  afterEach(() => {
    jest.clearAllMocks();
    cleanup();
  });

  const initialProps = {};

  it('run takeAndUploadImage when availableUploadSlots is 0 and alert is triggered 1 number of times', async () => {
    jest
      .spyOn(channel.messageComposer.attachmentManager, 'availableUploadSlots', 'get')
      .mockReturnValue(0);
    const { result } = renderHook(() => useMessageInputContext(), {
      initialProps,
      wrapper: (props) => <Wrapper channel={channel} client={chatClient} props={props} />,
    });

    await waitFor(() => {
      result.current.takeAndUploadImage();
    });

    expect(Alert.alert).toHaveBeenCalledTimes(1);
    expect(Alert.alert).toHaveBeenCalledWith('Maximum number of files reached');
  });

  it('should show permissions alert when askToOpenSettings is true', async () => {
    jest
      .spyOn(channel.messageComposer.attachmentManager, 'availableUploadSlots', 'get')
      .mockReturnValue(2);

    jest.spyOn(NativeHandlers, 'takePhoto').mockImplementation(
      jest.fn().mockResolvedValue({
        askToOpenSettings: true,
      }),
    );

    const { result } = renderHook(() => useMessageInputContext(), {
      initialProps,
      wrapper: (props) => <Wrapper channel={channel} client={chatClient} props={props} />,
    });

    await waitFor(() => {
      result.current.takeAndUploadImage();
    });

    expect(Alert.alert).toHaveBeenCalledTimes(1);
  });

  it.each([
    [false, undefined, true],
    [false, undefined, false],
    [false, generateFileReference(), true],
    [false, [], false],
    [true, generateFileReference(), false],
  ])(
    'allow uploads %p when pickImage returns assets %p and cancelled %p',
    async (allowed, asset, cancelled) => {
      const { attachmentManager } = channel.messageComposer;
      jest.spyOn(NativeHandlers, 'takePhoto').mockImplementation(
        jest.fn().mockResolvedValue({
          ...asset,
          cancelled,
        }),
      );

      jest.spyOn(attachmentManager, 'availableUploadSlots', 'get').mockReturnValue(2);

      const { result } = renderHook(() => useMessageInputContext(), {
        initialProps,
        wrapper: (props) => <Wrapper channel={channel} client={chatClient} props={props} />,
      });

      const uploadFilesSpy = jest.spyOn(attachmentManager, 'uploadFiles');

      await waitFor(() => {
        result.current.takeAndUploadImage();
      });

      await waitFor(() => {
        expect(NativeHandlers.takePhoto).toHaveBeenCalledTimes(1);
        expect(NativeHandlers.takePhoto).toHaveBeenCalledWith({
          compressImageQuality: undefined,
          mediaType: undefined,
        });
        if (allowed) {
          expect(uploadFilesSpy).toHaveBeenCalledTimes(1);
        } else {
          expect(uploadFilesSpy).not.toHaveBeenCalled();
        }
      });
    },
  );
});
