/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { mergeConfig } from '@vendure/core';
import { AssetFragment } from '@vendure/core/e2e/graphql/generated-e2e-admin-types';
import { createTestEnvironment } from '@vendure/testing';
import fs from 'fs-extra';
import gql from 'graphql-tag';
import fetch from 'node-fetch';
import path from 'path';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';

import { initialData } from '../../../e2e-common/e2e-initial-data';
import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
import { AssetServerPlugin } from '../src/plugin';

import {
    CreateAssetsMutation,
    DeleteAssetMutation,
    DeleteAssetMutationVariables,
    DeletionResult,
} from './graphql/generated-e2e-asset-server-plugin-types';

const TEST_ASSET_DIR = 'test-assets';
const IMAGE_BASENAME = 'derick-david-409858-unsplash';

describe('AssetServerPlugin', () => {
    let asset: AssetFragment;
    const sourceFilePath = path.join(__dirname, TEST_ASSET_DIR, `source/b6/${IMAGE_BASENAME}.jpg`);
    const previewFilePath = path.join(__dirname, TEST_ASSET_DIR, `preview/71/${IMAGE_BASENAME}__preview.jpg`);

    const { server, adminClient, shopClient } = createTestEnvironment(
        mergeConfig(testConfig(), {
            // logger: new DefaultLogger({ level: LogLevel.Info }),
            plugins: [
                AssetServerPlugin.init({
                    assetUploadDir: path.join(__dirname, TEST_ASSET_DIR),
                    route: 'assets',
                }),
            ],
        }),
    );

    beforeAll(async () => {
        await fs.emptyDir(path.join(__dirname, TEST_ASSET_DIR, 'source'));
        await fs.emptyDir(path.join(__dirname, TEST_ASSET_DIR, 'preview'));
        await fs.emptyDir(path.join(__dirname, TEST_ASSET_DIR, 'cache'));

        await server.init({
            initialData,
            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-empty.csv'),
            customerCount: 1,
        });
        await adminClient.asSuperAdmin();
    }, TEST_SETUP_TIMEOUT_MS);

    afterAll(async () => {
        await server.destroy();
    });

    it('names the Asset correctly', async () => {
        const filesToUpload = [path.join(__dirname, `fixtures/assets/${IMAGE_BASENAME}.jpg`)];
        const { createAssets }: CreateAssetsMutation = await adminClient.fileUploadMutation({
            mutation: CREATE_ASSETS,
            filePaths: filesToUpload,
            mapVariables: filePaths => ({
                input: filePaths.map(p => ({ file: null })),
            }),
        });

        asset = createAssets[0] as AssetFragment;
        expect(asset.name).toBe(`${IMAGE_BASENAME}.jpg`);
    });

    it('creates the expected asset files', async () => {
        expect(fs.existsSync(sourceFilePath)).toBe(true);
        expect(fs.existsSync(previewFilePath)).toBe(true);
    });

    it('serves the source file', async () => {
        const res = await fetch(`${asset.source}`);
        const responseBuffer = await res.buffer();
        const sourceFile = await fs.readFile(sourceFilePath);

        expect(Buffer.compare(responseBuffer, sourceFile)).toBe(0);
    });

    it('serves the untransformed preview file', async () => {
        const res = await fetch(`${asset.preview}`);
        const responseBuffer = await res.buffer();
        const previewFile = await fs.readFile(previewFilePath);

        expect(Buffer.compare(responseBuffer, previewFile)).toBe(0);
    });

    it('can handle non-latin filenames', async () => {
        const FILE_NAME_ZH = '白飯';
        const filesToUpload = [path.join(__dirname, `fixtures/assets/${FILE_NAME_ZH}.jpg`)];
        const { createAssets }: { createAssets: AssetFragment[] } = await adminClient.fileUploadMutation({
            mutation: CREATE_ASSETS,
            filePaths: filesToUpload,
            mapVariables: filePaths => ({
                input: filePaths.map(p => ({ file: null })),
            }),
        });
        expect(createAssets[0].name).toBe(`${FILE_NAME_ZH}.jpg`);
        expect(createAssets[0].source).toContain(`${FILE_NAME_ZH}.jpg`);

        const previewUrl = encodeURI(`${createAssets[0].preview}`);
        const res = await fetch(previewUrl);

        expect(res.status).toBe(200);

        const previewFilePathZH = path.join(
            __dirname,
            TEST_ASSET_DIR,
            `preview/3f/${FILE_NAME_ZH}__preview.jpg`,
        );

        const responseBuffer = await res.buffer();
        const previewFile = await fs.readFile(previewFilePathZH);

        expect(Buffer.compare(responseBuffer, previewFile)).toBe(0);
    });

    describe('caching', () => {
        const cacheDir = path.join(__dirname, TEST_ASSET_DIR, 'cache');
        const cacheFileDir = path.join(__dirname, TEST_ASSET_DIR, 'cache', 'preview', '71');

        it('cache initially empty', async () => {
            const files = await fs.readdir(cacheDir);
            expect(files.length).toBe(0);
        });

        it('creates cached image on first request', async () => {
            const res = await fetch(`${asset.preview}?preset=thumb`);
            const responseBuffer = await res.buffer();
            expect(fs.existsSync(cacheFileDir)).toBe(true);

            const files = await fs.readdir(cacheFileDir);
            expect(files.length).toBe(1);
            expect(files[0]).toContain(`${IMAGE_BASENAME}__preview`);

            const cachedFile = await fs.readFile(path.join(cacheFileDir, files[0]));

            // was the file returned the exact same file as is stored in the cache dir?
            expect(Buffer.compare(responseBuffer, cachedFile)).toBe(0);
        });

        it('does not create a new cached image on a second request', async () => {
            const res = await fetch(`${asset.preview}?preset=thumb`);
            const files = await fs.readdir(cacheFileDir);

            expect(files.length).toBe(1);
        });

        it('does not create a new cached image for an untransformed image', async () => {
            const res = await fetch(`${asset.preview}`);
            const files = await fs.readdir(cacheFileDir);

            expect(files.length).toBe(1);
        });

        it('does not create a new cached image for an invalid preset', async () => {
            const res = await fetch(`${asset.preview}?preset=invalid`);
            const files = await fs.readdir(cacheFileDir);

            expect(files.length).toBe(1);

            const previewFile = await fs.readFile(previewFilePath);
            const responseBuffer = await res.buffer();
            expect(Buffer.compare(responseBuffer, previewFile)).toBe(0);
        });

        it('does not create a new cached image if cache=false', async () => {
            const res = await fetch(`${asset.preview}?preset=tiny&cache=false`);
            const files = await fs.readdir(cacheFileDir);

            expect(files.length).toBe(1);
        });

        it('creates a new cached image if cache=true', async () => {
            const res = await fetch(`${asset.preview}?preset=tiny&cache=true`);
            const files = await fs.readdir(cacheFileDir);

            expect(files.length).toBe(2);
        });
    });

    describe('unexpected input', () => {
        it('does not error on non-integer width', async () => {
            return fetch(`${asset.preview}?w=10.5`);
        });

        it('does not error on non-integer height', async () => {
            return fetch(`${asset.preview}?h=10.5`);
        });
    });

    describe('deletion', () => {
        it('deleting Asset deletes binary file', async () => {
            const { deleteAsset } = await adminClient.query<
                DeleteAssetMutation,
                DeleteAssetMutationVariables
            >(DELETE_ASSET, {
                input: {
                    assetId: asset.id,
                    force: true,
                },
            });

            expect(deleteAsset.result).toBe(DeletionResult.DELETED);

            expect(fs.existsSync(sourceFilePath)).toBe(false);
            expect(fs.existsSync(previewFilePath)).toBe(false);
        });
    });

    describe('MIME type detection', () => {
        let testImages: AssetFragment[] = [];

        async function testMimeTypeOfAssetWithExt(ext: string, expectedMimeType: string) {
            const testImage = testImages.find(i => i.source.endsWith(ext))!;
            const result = await fetch(testImage.source);
            const contentType = result.headers.get('Content-Type');

            expect(contentType).toBe(expectedMimeType);
        }

        beforeAll(async () => {
            const formats = ['gif', 'jpg', 'png', 'svg', 'tiff', 'webp'];

            const filesToUpload = formats.map(ext => path.join(__dirname, `fixtures/assets/test.${ext}`));
            const { createAssets }: CreateAssetsMutation = await adminClient.fileUploadMutation({
                mutation: CREATE_ASSETS,
                filePaths: filesToUpload,
                mapVariables: filePaths => ({
                    input: filePaths.map(p => ({ file: null })),
                }),
            });

            testImages = createAssets as AssetFragment[];
        });

        it('gif', async () => {
            await testMimeTypeOfAssetWithExt('gif', 'image/gif');
        });

        it('jpg', async () => {
            await testMimeTypeOfAssetWithExt('jpg', 'image/jpeg');
        });

        it('png', async () => {
            await testMimeTypeOfAssetWithExt('png', 'image/png');
        });

        it('svg', async () => {
            await testMimeTypeOfAssetWithExt('svg', 'image/svg+xml');
        });

        it('tiff', async () => {
            await testMimeTypeOfAssetWithExt('tiff', 'image/tiff');
        });

        it('webp', async () => {
            await testMimeTypeOfAssetWithExt('webp', 'image/webp');
        });
    });

    // https://github.com/vendure-ecommerce/vendure/issues/1563
    it('falls back to binary preview if image file cannot be processed', async () => {
        const filesToUpload = [path.join(__dirname, 'fixtures/assets/bad-image.jpg')];
        const { createAssets }: CreateAssets.Mutation = await adminClient.fileUploadMutation({
            mutation: CREATE_ASSETS,
            filePaths: filesToUpload,
            mapVariables: filePaths => ({
                input: filePaths.map(p => ({ file: null })),
            }),
        });

        expect(createAssets.length).toBe(1);
        expect(createAssets[0].name).toBe('bad-image.jpg');
    });
});

export const CREATE_ASSETS = gql`
    mutation CreateAssets($input: [CreateAssetInput!]!) {
        createAssets(input: $input) {
            ... on Asset {
                id
                name
                source
                preview
                focalPoint {
                    x
                    y
                }
            }
        }
    }
`;

export const DELETE_ASSET = gql`
    mutation DeleteAsset($input: DeleteAssetInput!) {
        deleteAsset(input: $input) {
            result
        }
    }
`;
