import FilePicker from "../../../UI/Components/FilePicker/FilePicker";
import ModelAPI from "../../../UI/Utils/ModelAPI/ModelAPI";
import { describe, expect, beforeEach, jest } from "@jest/globals";
import "@testing-library/jest-dom/extend-expect";
import {
  fireEvent,
  queryAllByAttribute,
  queryByAttribute,
  queryByTestId,
  render,
  screen,
  waitFor,
} from "@testing-library/react";
import HTTPResponse from "../../../Types/API/HTTPResponse";
import MimeType from "../../../Types/File/MimeType";
import ObjectID from "../../../Types/ObjectID";
import FileModel from "../../../Models/DatabaseModels/File";
import React from "react";
import { act } from "react-test-renderer";
import getJestMockFunction, { MockFunction } from "../../../Tests/MockType";
import Faker from "../../../Utils/Faker";

const mockOnChange: MockFunction = getJestMockFunction();
const mockOnBlur: MockFunction = getJestMockFunction();

jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
  return {
    create: jest.fn(),
  };
});

interface DefaultProps {
  onBlur: () => void;
  onChange: (files: FileModel[]) => void;
  mimeTypes: MimeType[];
  isOpen: boolean;
  onClose: () => void;
  initialValue?: FileModel | FileModel[];
  value?: FileModel[] | undefined;
  isMultiFilePicker?: boolean;
  readOnly?: boolean;
}

interface DataTransfer {
  dataTransfer: {
    files: File[];
    types: string[];
  };
}

type MockCreateResponseFunction = (
  file: File,
) => Promise<HTTPResponse<FileModel>>;

const mockCreateResponse: MockCreateResponseFunction = async (
  file: File,
): Promise<HTTPResponse<FileModel>> => {
  return new HTTPResponse(
    200,
    {
      file: (await file.arrayBuffer()) as Buffer,
      name: file.name,
      type: file.type as MimeType,
      slug: file.name,
      isPublic: true,
    },
    {},
  );
};

type MockFileModelFunction = (file: File) => Promise<FileModel>;

const mockFileModel: MockFileModelFunction = async (
  file: File,
): Promise<FileModel> => {
  const fileModel: FileModel = new FileModel(new ObjectID("123"));
  fileModel.name = file.name;
  fileModel.fileType = file.type as MimeType;
  fileModel.slug = file.name;
  fileModel.isPublic = true;
  fileModel.file = (await file.arrayBuffer()) as Buffer;
  return fileModel;
};

type MockFileFunction = () => File;

const mockFile: MockFileFunction = (): File => {
  const mockArrayBuffer: MockFunction = getJestMockFunction();
  mockArrayBuffer.mockResolvedValue(new ArrayBuffer(10)); // Mocked array buffer of size 10

  const file: File = new File(
    [Faker.generateRandomString()],
    Faker.generateRandomString() + ".png",
    { type: MimeType.png },
  );
  file.arrayBuffer = mockArrayBuffer;
  return file;
};

const defaultProps: DefaultProps = {
  onBlur: mockOnBlur,
  onChange: mockOnChange,
  mimeTypes: [MimeType.png],
  isOpen: true,
  onClose: jest.fn(),
};

describe("FilePicker", () => {
  const MOCK_FILE_URL: string = "https://mock-file-url";

  beforeAll(() => {
    global.URL.createObjectURL = jest.fn(() => {
      return MOCK_FILE_URL;
    });
  });

  afterAll(() => {
    (
      global.URL.createObjectURL as jest.MockedFunction<
        typeof global.URL.createObjectURL
      >
    ).mockRestore();
  });

  beforeEach(() => {
    delete defaultProps.isMultiFilePicker;
    delete defaultProps.initialValue;
    delete defaultProps.value;
    delete defaultProps.readOnly;
  });

  it("should render without crashing", () => {
    render(<FilePicker {...defaultProps} />);
    expect(screen.getByText("Upload a file")).toBeInTheDocument();
    expect(screen.getByRole("complementary")).toBeInTheDocument(); // aside element
  });

  it("should render with initial value", async () => {
    defaultProps.initialValue = await mockFileModel(mockFile());
    const { container } = render(<FilePicker {...defaultProps} />);
    expect(
      queryByAttribute("src", container, MOCK_FILE_URL),
    ).toBeInTheDocument();
  });

  it("should not render if file is missing the `file` attribute", async () => {
    const file: FileModel = await mockFileModel(mockFile());
    delete file.file;
    defaultProps.initialValue = file;
    const { container } = render(<FilePicker {...defaultProps} />);
    expect(
      queryByAttribute("src", container, MOCK_FILE_URL),
    ).not.toBeInTheDocument();
  });

  it("should render with initial value as array", async () => {
    defaultProps.initialValue = [
      await mockFileModel(mockFile()),
      await mockFileModel(mockFile()),
    ];
    render(<FilePicker {...defaultProps} />);
    const { container } = render(<FilePicker {...defaultProps} />);
    expect(queryAllByAttribute("src", container, MOCK_FILE_URL)).toHaveLength(
      2,
    );
  });

  it("should render with value array with one element", async () => {
    defaultProps.value = [await mockFileModel(mockFile())];
    const { container } = render(<FilePicker {...defaultProps} />);
    expect(
      queryByAttribute("src", container, MOCK_FILE_URL),
    ).toBeInTheDocument();
  });

  it("should render with value array with more than one element", async () => {
    defaultProps.value = [
      await mockFileModel(mockFile()),
      await mockFileModel(mockFile()),
    ];
    render(<FilePicker {...defaultProps} />);
    const { container } = render(<FilePicker {...defaultProps} />);
    expect(queryAllByAttribute("src", container, MOCK_FILE_URL)).toHaveLength(
      2,
    );
  });

  it("should not upload file when dropped and readOnly is true", async () => {
    defaultProps.readOnly = true;

    const file: File = mockFile();
    const data: DataTransfer = {
      dataTransfer: {
        files: [file],
        types: ["Files"],
      },
    };

    const createResponse: HTTPResponse<FileModel> =
      await mockCreateResponse(file);
    (
      ModelAPI.create as jest.MockedFunction<typeof ModelAPI.create>
    ).mockResolvedValue(createResponse);

    const { container } = render(<FilePicker {...defaultProps} />);

    const dropzone: HTMLElement = screen.getByLabelText("Upload a file");
    fireEvent.drop(dropzone, data);

    await waitFor(() => {
      expect(
        queryByAttribute("src", container, MOCK_FILE_URL),
      ).not.toBeInTheDocument();
    });
  });

  it('should throw an "File too large" when uploading a file that fails on arrayBuffer()', async () => {
    const file: File = mockFile();
    file.arrayBuffer = getJestMockFunction().mockRejectedValue(
      new Error("File too large"),
    );
    const data: DataTransfer = {
      dataTransfer: {
        files: [file],
        types: ["Files"],
      },
    };

    const { container } = render(<FilePicker {...defaultProps} />);

    const dropzone: HTMLElement = screen.getByLabelText("Upload a file");
    fireEvent.drop(dropzone, data);

    await waitFor(() => {
      expect(
        queryByAttribute("src", container, MOCK_FILE_URL),
      ).not.toBeInTheDocument();
    });
  });

  it("should upload a file when dropped", async () => {
    const file: File = mockFile();
    const data: DataTransfer = {
      dataTransfer: {
        files: [file],
        types: ["Files"],
      },
    };

    const createResponse: HTTPResponse<FileModel> =
      await mockCreateResponse(file);
    (
      ModelAPI.create as jest.MockedFunction<typeof ModelAPI.create>
    ).mockResolvedValue(createResponse);

    const { container } = render(<FilePicker {...defaultProps} />);

    const dropzone: HTMLElement = screen.getByLabelText("Upload a file");
    await act(async () => {
      fireEvent.drop(dropzone, data);
    });

    await waitFor(() => {
      expect(mockOnChange).toHaveBeenCalledWith([createResponse.data]);
      expect(mockOnBlur).toHaveBeenCalled();
      expect(mockOnBlur).toHaveBeenCalled();
      expect(
        queryByAttribute("src", container, MOCK_FILE_URL),
      ).toBeInTheDocument();
    });
  });

  it("should upload a file when dropped", async () => {
    const file: File = mockFile();
    const data: DataTransfer = {
      dataTransfer: {
        files: [file],
        types: ["Files"],
      },
    };

    const createResponse: HTTPResponse<FileModel> =
      await mockCreateResponse(file);
    (
      ModelAPI.create as jest.MockedFunction<typeof ModelAPI.create>
    ).mockResolvedValue(createResponse);

    const { container } = render(<FilePicker {...defaultProps} />);

    const dropzone: HTMLElement = screen.getByLabelText("Upload a file");
    await act(async () => {
      fireEvent.drop(dropzone, data);
    });

    await waitFor(() => {
      expect(mockOnChange).toHaveBeenCalledWith([createResponse.data]);
      expect(mockOnBlur).toHaveBeenCalled();
      expect(mockOnBlur).toHaveBeenCalled();
      expect(
        queryByAttribute("src", container, MOCK_FILE_URL),
      ).toBeInTheDocument();
    });
  });

  it("should show loader a file when files are being uploaded", async () => {
    const uploadPromise: Promise<unknown> = new Promise((resolve: any) => {
      (global as any).mockUploadResolve = resolve; // Store resolve function globally or in a scope accessible outside the test
    });

    (
      ModelAPI.create as jest.MockedFunction<typeof ModelAPI.create>
    ).mockImplementation((): any => {
      return uploadPromise;
    });

    const file: File = mockFile();
    const data: DataTransfer = {
      dataTransfer: {
        files: [file],
        types: ["Files"],
      },
    };

    const createResponse: HTTPResponse<FileModel> =
      await mockCreateResponse(file);

    const { container } = render(<FilePicker {...defaultProps} />);

    const dropzone: HTMLElement = screen.getByLabelText("Upload a file");
    await act(async () => {
      fireEvent.drop(dropzone, data);
    });

    expect(queryByTestId(container, "loader")).toBeInTheDocument();

    (global as any).mockUploadResolve(createResponse);

    await waitFor(() => {
      expect(mockOnChange).toHaveBeenCalledWith([createResponse.data]);
      expect(mockOnBlur).toHaveBeenCalled();
      expect(mockOnBlur).toHaveBeenCalled();
      expect(
        queryByAttribute("src", container, MOCK_FILE_URL),
      ).toBeInTheDocument();
    });
  });

  it("should delete an uploaded file when clicking on it", async () => {
    const file: File = mockFile();
    const data: DataTransfer = {
      dataTransfer: {
        files: [file],
        types: ["Files"],
      },
    };

    const createResponse: HTTPResponse<FileModel> =
      await mockCreateResponse(file);
    (
      ModelAPI.create as jest.MockedFunction<typeof ModelAPI.create>
    ).mockResolvedValue(createResponse);

    const { container } = render(<FilePicker {...defaultProps} />);

    const dropzone: HTMLElement = screen.getByLabelText("Upload a file");
    await act(async () => {
      fireEvent.drop(dropzone, data);
    });

    await waitFor(() => {
      // file should be in the dropzone
      expect(
        queryByAttribute("src", container, MOCK_FILE_URL),
      ).toBeInTheDocument();
    });

    const deleteIcon: ChildNode = screen.getByRole("icon").childNodes.item(0); // svg item
    // remove file by clicking on it
    if (deleteIcon) {
      await act(async () => {
        fireEvent.click(deleteIcon.childNodes.item(0), data);
      });
    }

    await waitFor(() => {
      // file should have been removed
      expect(mockOnChange).toHaveBeenCalledWith([createResponse.data]);
      expect(
        queryByAttribute("src", container, MOCK_FILE_URL),
      ).not.toBeInTheDocument();
    });
  });
});
