import { uploadComponentFolder } from '../index';
import configObj, {
  managementService,
  renderService,
  getTestComponents,
  createFile,
  deleteComponents,
  removeFile,
  addComponentSet,
  deleteComponentSet,
  addContentItem,
  deleteContentItem,
  managementServiceRoot,
} from './helper';

import path from 'path';
import supertest from 'supertest';
import { logger } from '../upload-component-folder';
import { ComponentSetWebModelForCreate } from '@squiz/component-lib';
import fsp from 'fs/promises';
import { randomUUID } from 'crypto';
import { buildDevelopmentJwt, DxpServiceId } from '@squiz/dxp-auth-lib';

const authToken =
  'Bearer ' +
  buildDevelopmentJwt({
    service: {
      [DxpServiceId.contentStore]: {
        privileges: [
          'CONTENT_SCHEMA_READ',
          'CONTENT_SCHEMA_WRITE',
          'CONTENT_ITEM_READ',
          'CONTENT_ITEM_WRITE',
          'SETTINGS_READ',
          'SETTINGS_WRITE',
          'PAGE_CONTENTS_WRITE',
          'PAGE_CONTENTS_READ',
        ],
      },
      [DxpServiceId.components]: {
        privileges: [
          'COMPONENT_READ',
          'COMPONENT_WRITE',
          'COMPONENT_DEPLOY',
          'COMPONENT_SET_WRITE',
          'COMPONENT_DELETE',
          'COMPONENT_SET_WRITE',
          'COMPONENT_SET_READ',
          'COMPONENT_SET_ENVIRONMENT_READ',
          'COMPONENT_SET_ENVIRONMENT_WRITE',
        ],
      },
      [DxpServiceId.segmentation]: {
        privileges: ['SEGMENT_READ'],
      },
    },
  });

const webPath = 'set-' + randomUUID();
const contentItemId = randomUUID();
const testFilesDir = path.resolve(__dirname, 'test-files');

describe('uploading a component', () => {
  beforeAll(async () => {
    await fsp.rm(testFilesDir, { force: true, recursive: true });
    await fsp.mkdir(testFilesDir);
    logger.silent = true;
  });

  afterAll(async () => {
    // clean up the component added by the test
    await deleteComponentSet(webPath);
    await deleteContentItem(contentItemId);
    await deleteComponents(await getTestComponents());
    // clean up the test component files
    await fsp.rm(testFilesDir, { force: true, recursive: true });
    logger.silent = false;
  });

  describe('Test isolated test cases', () => {
    beforeEach(() => {
      // jest.spyOn(logger, 'error').mockImplementation(mockConsoleError);
      // jest.spyOn(logger, 'info').mockImplementation(mockConsoleLog);
    });

    it('Should fail uploading a component without manifest.json', async () => {
      const componentPath = path.join(__dirname, '/__components__/invalid-upload');
      await expect(
        uploadComponentFolder(managementServiceRoot, configObj.managementServiceUrl, componentPath, testFilesDir),
      ).rejects.toThrowError(new Error('manifest could not be found'));
    });

    it('Should fail uploading a component that has invalid manifest.json', async () => {
      const componentPath = path.join(__dirname, '/__components__/invalid-manifest');
      await expect(
        uploadComponentFolder(
          managementServiceRoot,
          configObj.managementServiceUrl,
          componentPath,

          testFilesDir,
        ),
      ).rejects.toThrowError(
        new Error(
          'failed validation: Value in `#/name` should match `^[a-z][a-z0-9_\\-]+$`, but received `invalid-manifes@t`',
        ),
      );
    });

    it('Should fail uploading the component that has size more than 100MB', async () => {
      const componentPath = path.join(__dirname, '/__components__/big-package');
      const filePath = `${componentPath}/105mb-file`;
      await createFile(filePath, 105); // Higher limit has been used because compression reduces the size if you use closer to 100MB
      await expect(
        uploadComponentFolder(
          managementServiceRoot,
          configObj.managementServiceUrl,
          componentPath,

          testFilesDir,
        ),
      ).rejects.toThrowError(new Error('File size exceeds the maximum limit of 100MB'));

      removeFile(filePath);
    });
  });

  describe('Deploy a basic component having a input with multiline format', () => {
    beforeAll(async () => {
      await deleteComponentSet(webPath);
      await deleteContentItem(contentItemId);
      await deleteComponents(await getTestComponents());
    });

    it('Should upload the component and return a valid url to preview', async () => {
      const componentPath = path.join(__dirname, '/__components__/cmp-format-string');
      await uploadComponentFolder(managementServiceRoot, configObj.managementServiceUrl, componentPath, testFilesDir);
      const uploadedComponent =
        '<a href="/r/smoke-test-components/cmp-format-string/1.0.0?_previewKey=test-preview">1.0.0</a>';
      const get = await supertest(configObj.renderServiceUrl).get('/');
      expect(get.status).toEqual(200);
      expect((get as any)?.res?.text).toContain(uploadedComponent);
    });

    it('Should render a component with multi-line format string input', async () => {
      const componentSet: ComponentSetWebModelForCreate = {
        webPath,
        displayName: 'some-display-name',
        description: 'Set description',
        headers: {},
        environmentVariables: {},
        components: {},
        componentVersionRules: {
          'smoke-test-components/cmp-format-string': {
            renderableVersionPattern: '1.0.0',
            editableVersions: [],
            excludedVersions: [],
          },
        },
      };
      await addComponentSet(componentSet);
      await addContentItem({
        id: contentItemId,
        schemaName: 'smoke-test-components/cmp-format-string/1.0.0/main',
        content: {
          text: 'from-content-item-service',
        },
      });
      const response = await renderService.get(
        `/r/smoke-test-components/cmp-format-string/1.0.0/?_contentItemId=${contentItemId}&_componentSet=${webPath}`,
      );
      expect(response.status).toEqual(200);
      expect(response.data).toEqual(`<div>Input: from-content-item-service</div>`);
    });

    it('Should return relevant error if upload fails at the server', async () => {
      // delete the previously uploaded component
      await managementService.delete(`/component/smoke-test-components/cmp-format-string`);

      // trying uploading the component again should fail at the server
      // due to content item schema already exist matching the component name/version
      const componentPath = path.join(__dirname, '/__components__/cmp-format-string');
      await expect(
        uploadComponentFolder(managementServiceRoot, configObj.managementServiceUrl, componentPath, testFilesDir),
      ).rejects.toThrowErrorMatchingInlineSnapshot(
        `"Unexpected response code 500. Error occurred when creating content schema: Content schema smoke-test-components/cmp-format-string/1.0.0/main already exists"`,
      );
    });
  });

  describe('Deploy a basic component with input format matrix-asset-uri', () => {
    beforeAll(async () => {
      await deleteComponentSet(webPath);
      await deleteContentItem(contentItemId);
      await deleteComponents(await getTestComponents());
    });
    it('Should upload the component and return a valid url to preview', async () => {
      const componentPath = path.join(__dirname, '/__components__/matrix-asset-uri');
      await uploadComponentFolder(managementServiceRoot, configObj.managementServiceUrl, componentPath, testFilesDir);
      const uploadedComponent =
        '<a href="/r/smoke-test-components/matrix-asset-uri/1.0.0?_previewKey=test-preview">1.0.0</a>';
      const get = await supertest(configObj.renderServiceUrl).get('/');
      expect(get.status).toEqual(200);
      expect((get as any)?.res?.text).toContain(uploadedComponent);
    });

    it('Should render a component with matrix-asset-uri format string input with set and content item', async () => {
      const componentSet: ComponentSetWebModelForCreate = {
        webPath,
        displayName: 'some-display-name',
        description: 'Set description',
        headers: {},
        environmentVariables: {},
        components: {
          'smoke-test-components/matrix-asset-uri': [{ environmentVariables: {}, version: '1.0.0' }],
        },
        componentVersionRules: {
          'smoke-test-components/matrix-asset-uri': {
            renderableVersionPattern: '1.0.0',
            editableVersions: [],
            excludedVersions: [],
          },
        },
      };
      await addComponentSet(componentSet);
      await addContentItem({
        id: contentItemId,
        schemaName: 'smoke-test-components/matrix-asset-uri/1.0.0/main',
        content: {
          text: 'matrix-asset://canary.uat.matrix.squiz.cloud/123',
        },
      });

      const response = await renderService.get(
        `/r/smoke-test-components/matrix-asset-uri/1.0.0?_componentSet=${webPath}&_contentItemId=${contentItemId}`,
      );
      expect(response.status).toEqual(200);
      expect(response.data).toEqual(`<div>Input: matrix-asset://canary.uat.matrix.squiz.cloud/123</div>`);
    });
  });

  describe('Deploy basic component having a static file', () => {
    // component to deploy for this test
    const componentPath = path.join(__dirname, '/__components__/cmp-static-file-test');

    beforeAll(async () => {
      await deleteComponentSet(webPath);
      await deleteContentItem(contentItemId);
      await deleteComponents(await getTestComponents());
    });

    it('Should upload the component and return a valid url to preview', async () => {
      await uploadComponentFolder(
        managementServiceRoot,
        configObj.managementServiceUrl,
        componentPath,

        testFilesDir,
      );

      const uploadedComponent =
        '<a href="/r/smoke-test-components/cmp-static-file-test/1.0.0?_componentSet=local-development-only">1.0.0</a>';
      const get = await supertest(configObj.renderServiceUrl).get('/');
      expect(get.status).toEqual(200);
      expect((get as any)?.res?.text).toContain(uploadedComponent);
    });

    it('Should fail upload the component with same version', async () => {
      await expect(
        uploadComponentFolder(
          managementServiceRoot,
          configObj.managementServiceUrl,
          componentPath,

          testFilesDir,
        ),
      ).rejects.toThrowError(
        new Error('Cannot upload component version, smoke-test-components/cmp-static-file-test 1.0.0 already exists'),
      );
    });

    it('Should render component', async () => {
      const componentSet: ComponentSetWebModelForCreate = {
        webPath,
        displayName: 'some-display-name',
        description: 'Set description',
        headers: {},
        environmentVariables: {},
        components: {},
        componentVersionRules: {
          'smoke-test-components/cmp-static-file-test': {
            renderableVersionPattern: '1.0.0',
            editableVersions: [],
            excludedVersions: [],
          },
        },
      };

      await addComponentSet(componentSet);

      const response = await renderService.get(
        `/r/smoke-test-components/cmp-static-file-test/1.0.0/?_componentSet=${webPath}&something=hello`,
      );
      expect(response.status).toEqual(200);
      expect(response.data).toEqual(
        [
          '<div>Input: undefined</div>',
          '<div>smoke-test-components/cmp-static-file-test 1.0.0 ',
          `${configObj.renderServiceUrl}/s/smoke-test-components/cmp-static-file-test/1.0.0/birthday-cake.png?_componentSet=${webPath}</div>`,
        ].join(''),
      );
    });

    it('Should render component with content item ID input', async () => {
      const componentSet: ComponentSetWebModelForCreate = {
        webPath,
        displayName: 'some-display-name',
        description: 'Set description',
        headers: {},
        environmentVariables: {},
        components: {},
        componentVersionRules: {
          'smoke-test-components/cmp-static-file-test': {
            renderableVersionPattern: '1.0.0',
            editableVersions: [],
            excludedVersions: [],
          },
        },
      };
      await addComponentSet(componentSet);

      // add a component item for the test
      await addContentItem({
        id: contentItemId,
        schemaName: 'smoke-test-components/cmp-static-file-test/1.0.0/main',
        content: {
          something: 'from-content-item-service',
        },
      });

      const response = await renderService.get(
        `/r/smoke-test-components/cmp-static-file-test/1.0.0/?_componentSet=${webPath}&_contentItemId=${contentItemId}`,
      );
      expect(response.status).toEqual(200);
      expect(response.data).toEqual(
        [
          '<div>Input: from-content-item-service</div>',
          '<div>smoke-test-components/cmp-static-file-test 1.0.0 ',
          `${configObj.renderServiceUrl}/s/smoke-test-components/cmp-static-file-test/1.0.0/birthday-cake.png?_componentSet=${webPath}</div>`,
        ].join(''),
      );
    });
  });

  describe('Deploy a basic component using and not expose the x-api-key', () => {
    // component to deploy for this test
    const componentPath = path.join(__dirname, '/__components__/cmp-no-api-key');

    beforeAll(async () => {
      await deleteComponentSet(webPath);
      await deleteContentItem(contentItemId);
      await deleteComponents(await getTestComponents());
    });

    it('Should upload the component and return a valid url', async () => {
      await uploadComponentFolder(
        managementServiceRoot,
        configObj.managementServiceUrl,
        componentPath,

        testFilesDir,
      );

      const uploadedComponent =
        '<a href="/r/smoke-test-components/cmp-no-api-key/1.0.0?_previewKey=test-preview">1.0.0</a>';
      const get = await supertest(configObj.renderServiceUrl).get('/');
      expect(get.status).toEqual(200);
      expect((get as any)?.res?.text).toContain(uploadedComponent);
    });

    it('Should render component', async () => {
      const componentSet: ComponentSetWebModelForCreate = {
        webPath,
        displayName: 'some-display-name',
        description: 'Set description',
        headers: {},
        environmentVariables: {},
        components: {},
        componentVersionRules: {
          'smoke-test-components/cmp-no-api-key': {
            renderableVersionPattern: '1.0.0',
            editableVersions: [],
            excludedVersions: [],
          },
        },
      };

      await addComponentSet(componentSet);

      const response = await renderService.get(
        `/r/smoke-test-components/cmp-no-api-key/1.0.0/?_componentSet=${webPath}&text=hello`,
        { headers: { 'x-api-key': 'this is key' } },
      );
      expect(response.status).toEqual(200);
      expect(response.data).toEqual(false);
    });
  });

  describe('Deploy component with properties defined in a specific order', () => {
    // component to deploy for this test
    const componentPath = path.join(__dirname, '/__components__/cmp-property-order');

    beforeAll(async () => {
      await deleteComponentSet(webPath);
      await deleteContentItem(contentItemId);
      await deleteComponents(await getTestComponents());

      await uploadComponentFolder(managementServiceRoot, configObj.managementServiceUrl, componentPath, testFilesDir);
    });

    it('Should preserve input field order when uploading schema to content store', async () => {
      const response = await supertest(configObj.contentServiceUrl)
        .get('/content-schema/smoke-test-components/cmp-property-order/1.0.0/main')
        .set('Authorization', authToken)
        .send();
      // Relates to DEVX-891, property order is preserved so that we can reliably draw a UI from it.
      //
      // The sequence here is important and should follow the rules defined here:
      //  * https://tc39.es/ecma262/#sec-ordinaryownpropertykeys
      //  * https://stackoverflow.com/a/38218582
      //
      // Per EMCA-262 numeric keys appear first in ascending order but all other keys should appear
      // in the order they were originally defined, they should not be affected by them being stored in
      // a JSONB DB column or anything else.
      //
      // Asserting on the encoded contents of the response to avoid any decoding in the test having
      // a potential influence on the outcome.
      const expectedProperties =
        '"111":{"type":"string"},' +
        '"222":{"type":"string"},' +
        '"333":{"type":"string"},' +
        '"input1":{"type":"string"},' +
        '"input333":{"type":"string"},' +
        '"\\"":{"type":"string"},' +
        '"~":{"type":"string"},' +
        '"":{"type":"string"},' +
        '"$":{"type":"string"},' +
        '"input22":{"type":"string"},' +
        '"*":{"type":"string"},' +
        '"null":{"type":"null","default":null}';

      expect(response.status).toEqual(200);
      expect(response.text).toContain(
        `{"name":"smoke-test-components/cmp-property-order/1.0.0/main",` +
          `"schema":{"type":"object","properties":{${expectedProperties}},"required":[]},"contentSchemaType":"component"}`,
      );
    });
  });
});
