text: // @flow
  import {
    addAssetToProject,
    getRequiredBehaviorsFromAsset,
    filterMissingBehaviors,
    downloadExtensions,
    installAsset,
    getRequiredExtensionsForEventsFromAsset,
    filterMissingExtensions,
    sanitizeObjectName,
  } from './InstallAsset';
  import { makeTestProject } from '../fixtures/TestProject';
  import { type EventsFunctionsExtensionsState } from '../EventsFunctionsExtensionsLoader/EventsFunctionsExtensionsContext';
  import {
    fakeAssetShortHeader1,
    fakeAsset1,
    fakeAssetWithBehaviorCustomizations1,
    fakeAssetWithUnknownBehaviorCustomizations1,
    fakeAssetWithFlashBehaviorCustomizations1,
    fakeAssetWithEventCustomizationsAndFlashExtension1,
    flashExtensionShortHeader,
    fireBulletExtensionShortHeader,
    fakeAssetWithEventCustomizationsAndUnknownExtension1,
  } from '../fixtures/GDevelopServicesTestData';
  import { makeTestExtensions } from '../fixtures/TestExtensions';
  import {
    getExtensionsRegistry,
    getExtension,
  } from '../Utils/GDevelopServices/Extension';
  import { jssPreset } from '@material-ui/core';
  import { getAsset } from '../Utils/GDevelopServices/Asset';
  const gd: libGDevelop = global.gd;
  
  jest.mock('../Utils/GDevelopServices/Extension');
  jest.mock('../Utils/GDevelopServices/Asset');
  
  const mockFn = (fn: Function): JestMockFn<any, any> => fn;
  
  describe('InstallAsset', () => {
    test('sanitizeObjectName', () => {
      expect(sanitizeObjectName('')).toBe('UnnamedObject');
      expect(sanitizeObjectName('HelloWorld')).toBe('HelloWorld');
      expect(sanitizeObjectName('Hello World')).toBe('HelloWorld');
      expect(sanitizeObjectName('hello world')).toBe('HelloWorld');
      expect(sanitizeObjectName('hello world12')).toBe('HelloWorld12');
      expect(sanitizeObjectName('12 hello world')).toBe('_12HelloWorld');
      expect(sanitizeObjectName('/-=hello/-=world/-=')).toBe('HelloWorld');
      expect(sanitizeObjectName('  hello/-=world/-=')).toBe('HelloWorld');
      expect(sanitizeObjectName('9hello/-=world/-=')).toBe('_9helloWorld');
      expect(sanitizeObjectName('  9hello/-=world/-=')).toBe('_9helloWorld');
    });
  
    describe('addAssetToProject', () => {
      it('installs an object asset in the project, without renaming it if not needed', async () => {
        const { project } = makeTestProject(gd);
        const layout = project.insertNewLayout('MyTestLayout', 0);
  
        const output = await addAssetToProject({
          project,
          objectsContainer: layout,
          events: layout.getEvents(),
          asset: fakeAsset1,
        });
  
        expect(output.createdObjects).toHaveLength(1);
        expect(layout.hasObjectNamed('PlayerSpaceship')).toBe(true);
        expect(output.createdObjects).toEqual([
          layout.getObject('PlayerSpaceship'),
        ]);
        expect(
          project.getResourcesManager().hasResource('player-ship1.png')
        ).toBe(true);
        expect(
          project.getResourcesManager().hasResource('player-ship2.png')
        ).toBe(true);
      });
  
      it('renames the object if name is already used', async () => {
        const { project } = makeTestProject(gd);
        const layout = project.insertNewLayout('MyTestLayout', 0);
        layout.insertNewObject(project, 'Sprite', 'PlayerSpaceship', 0);
  
        const output = await addAssetToProject({
          project,
          objectsContainer: layout,
          events: layout.getEvents(),
          asset: fakeAsset1,
        });
  
        expect(output.createdObjects).toHaveLength(1);
        expect(layout.hasObjectNamed('PlayerSpaceship')).toBe(true);
        expect(layout.hasObjectNamed('PlayerSpaceship2')).toBe(true);
        expect(
          project.getResourcesManager().hasResource('player-ship1.png')
        ).toBe(true);
        expect(
          project.getResourcesManager().hasResource('player-ship2.png')
        ).toBe(true);
      });
  
      it('does not add a resource if it is already existing', async () => {
        const { project } = makeTestProject(gd);
        const layout = project.insertNewLayout('MyTestLayout', 0);
  
        const originalResourceNames = project
          .getResourcesManager()
          .getAllResourceNames()
          .toJSArray();
  
        // Create a resource that is the same as the one added for the spaceship
        const resource = new gd.ImageResource();
        resource.setName('player-ship1.png');
        resource.setFile('https://example.com/player-ship1.png');
        resource.setOrigin(
          'gdevelop-asset-store',
          'https://example.com/player-ship1.png'
        );
        project.getResourcesManager().addResource(resource);
        resource.delete();
  
        // Install the spaceship
        await addAssetToProject({
          project,
          objectsContainer: layout,
          events: layout.getEvents(),
          asset: fakeAsset1,
        });
  
        // Verify there was not extra resource added.
        expect(
          project
            .getResourcesManager()
            .getAllResourceNames()
            .toJSArray()
        ).toEqual([
          ...originalResourceNames,
          'player-ship1.png',
          'player-ship2.png',
        ]);
      });
  
      it('does not add a resource if it is already existing, even if changed but origin is the same', async () => {
        const { project } = makeTestProject(gd);
        const layout = project.insertNewLayout('MyTestLayout', 0);
  
        const originalResourceNames = project
          .getResourcesManager()
          .getAllResourceNames()
          .toJSArray();
  
        // Create a resource that is originally the same as the one of the spaceship (same origin),
        // but which was modified (file is different, name is different)
        const resource = new gd.ImageResource();
        resource.setName('renamed-player-ship1.png');
        resource.setFile('https://example.com/modified-player-ship.png');
        resource.setOrigin(
          'gdevelop-asset-store',
          'https://example.com/player-ship1.png'
        );
        project.getResourcesManager().addResource(resource);
        resource.delete();
  
        // Install the spaceship
        await addAssetToProject({
          project,
          objectsContainer: layout,
          events: layout.getEvents(),
          asset: fakeAsset1,
        });
  
        // Verify there was not extra resource added.
        expect(
          project
            .getResourcesManager()
            .getAllResourceNames()
            .toJSArray()
        ).toEqual([
          ...originalResourceNames,
          'renamed-player-ship1.png',
          'player-ship2.png',
        ]);
      });
  
      it('add a resource with a new name, if this name is already taken by another', async () => {
        const { project } = makeTestProject(gd);
        const layout = project.insertNewLayout('MyTestLayout', 0);
  
        const originalResourceNames = project
          .getResourcesManager()
          .getAllResourceNames()
          .toJSArray();
  
        // Create a resource that is NOT the same as the one added for the spaceship
        // but has the same name.
        const resource = new gd.ImageResource();
        resource.setName('player-ship1.png');
        resource.setFile('https://example.com/some-unrelated-file.png');
        resource.setOrigin('some-origin', 'some-unrelated-identifier');
        project.getResourcesManager().addResource(resource);
        resource.delete();
  
        // Install the spaceship
        await addAssetToProject({
          project,
          objectsContainer: layout,
          events: layout.getEvents(),
          asset: fakeAsset1,
        });
  
        // Verify there was not extra resource added
        expect(
          project
            .getResourcesManager()
            .getAllResourceNames()
            .toJSArray()
        ).toEqual([
          ...originalResourceNames,
          'player-ship1.png',
          'player-ship1.png2',
          'player-ship2.png',
        ]);
        expect(
          project
            .getResourcesManager()
            .getResource('player-ship1.png2')
            .getFile()
        ).toBe('https://example.com/player-ship1.png');
  
        // Verify the resource names used by the object
        expect(layout.hasObjectNamed('PlayerSpaceship')).toBe(true);
        const object = layout.getObject('PlayerSpaceship');
  
        const resourcesInUse = new gd.ResourcesInUseHelper();
        object.exposeResources(resourcesInUse);
        const objectResourceNames = resourcesInUse
          .getAllImages()
          .toNewVectorString()
          .toJSArray();
        resourcesInUse.delete();
  
        expect(objectResourceNames).toEqual([
          'player-ship1.png2',
          'player-ship2.png',
        ]);
      });
  
      it('installs an object asset in the project, adding the required behaviors', async () => {
        makeTestExtensions(gd);
        const { project } = makeTestProject(gd);
        const layout = project.insertNewLayout('MyTestLayout', 0);
  
        await addAssetToProject({
          project,
          objectsContainer: layout,
          events: layout.getEvents(),
          asset: fakeAssetWithBehaviorCustomizations1,
        });
  
        expect(layout.hasObjectNamed('PlayerSpaceship')).toBe(true);
        expect(
          layout
            .getObject('PlayerSpaceship')
            .getAllBehaviorNames()
            .toJSArray()
        ).toEqual(['MyBehavior']);
        expect(
          layout
            .getObject('PlayerSpaceship')
            .getBehavior('MyBehavior')
            .getTypeName()
        ).toBe('FakeBehavior::FakeBehavior');
  
        // Check that the properties from customization were set.
        expect(
          gd.Serializer.toJSON(
            layout
              .getObject('PlayerSpaceship')
              .getBehavior('MyBehavior')
              .getContent()
          )
        ).toBe('{"property1":"Overriden value","property2":true}');
      });
  
      it('installs an object asset in the project, adding the required events', async () => {
        makeTestExtensions(gd);
        const { project } = makeTestProject(gd);
        const layout = project.insertNewLayout('MyTestLayout', 0);
        const eventsList = new gd.EventsList();
  
        await addAssetToProject({
          project,
          objectsContainer: layout,
          events: eventsList,
          asset: fakeAssetWithEventCustomizationsAndFlashExtension1,
        });
  
        expect(layout.hasObjectNamed('PlayerSpaceship')).toBe(true);
        expect(eventsList.getEventsCount()).toBe(1);
  
        // Check that the events had their customization parameters properly applied
        const serializedEvents = new gd.SerializerElement();
        eventsList.serializeTo(serializedEvents);
        const serializedEventsJson = gd.Serializer.toJSON(serializedEvents);
        expect(serializedEventsJson).toMatch(
          '3 + PlayerSpaceship.Variable(test)'
        );
        expect(serializedEventsJson).toMatch(
          '3 + PlayerSpaceship.Variable(test2)'
        );
      });
  
      it('renames the object if name is already used, including in events', async () => {
        makeTestExtensions(gd);
        const { project } = makeTestProject(gd);
        const layout = project.insertNewLayout('MyTestLayout', 0);
        const eventsList = new gd.EventsList();
  
        // Add an object with the same name as the object asset.
        layout.insertNewObject(project, 'Sprite', 'PlayerSpaceship', 0);
        expect(layout.hasObjectNamed('PlayerSpaceship')).toBe(true);
  
        await addAssetToProject({
          project,
          objectsContainer: layout,
          events: eventsList,
          asset: fakeAssetWithEventCustomizationsAndFlashExtension1,
        });
  
        expect(layout.hasObjectNamed('PlayerSpaceship2')).toBe(true);
        expect(eventsList.getEventsCount()).toBe(1);
  
        // Check that the events had their customization parameters properly applied
        // and the object renamed.
        const serializedEvents = new gd.SerializerElement();
        eventsList.serializeTo(serializedEvents);
        const serializedEventsJson = gd.Serializer.toJSON(serializedEvents);
        expect(serializedEventsJson).toMatch(
          '3 + PlayerSpaceship2.Variable(test)'
        );
        expect(serializedEventsJson).toMatch(
          '3 + PlayerSpaceship2.Variable(test2)'
        );
      });
    });
  
    describe('getRequiredBehaviorsFromAsset', () => {
      it('get the behaviors required for an asset', () => {
        expect(
          getRequiredBehaviorsFromAsset(
            fakeAssetWithEventCustomizationsAndFlashExtension1
          )
        ).toEqual([]);
        expect(
          getRequiredBehaviorsFromAsset(fakeAssetWithBehaviorCustomizations1)
        ).toEqual([
          {
            behaviorType: 'FakeBehavior::FakeBehavior',
            extensionName: 'FakeBehavior',
            extensionVersion: '1.0.0',
          },
        ]);
      });
    });
  
    describe('getRequiredExtensionsForEventsFromAsset', () => {
      it('get the extensions required for an asset', () => {
        expect(
          getRequiredExtensionsForEventsFromAsset(
            fakeAssetWithBehaviorCustomizations1
          )
        ).toEqual([]);
        expect(
          getRequiredExtensionsForEventsFromAsset(
            fakeAssetWithEventCustomizationsAndFlashExtension1
          )
        ).toEqual([
          {
            extensionName: 'Flash',
            extensionVersion: '1.0.0',
          },
        ]);
      });
    });
  
    describe('filterMissingBehaviors', () => {
      it('filters behaviors that are not loaded ', () => {
        makeTestExtensions(gd);
  
        expect(
          filterMissingBehaviors(gd, [
            // An unknown behavior not loaded:
            {
              extensionName: 'NotExistingExtension',
              extensionVersion: '1.0.0',
              behaviorType: 'NotExistingExtension::MissingBehavior',
            },
            // A fake behavior loaded in makeTestExtensions:
            {
              behaviorType: 'FakeBehavior::FakeBehavior',
              extensionName: 'FakeBehavior',
              extensionVersion: '1.0.0',
            },
          ])
        ).toEqual([
          {
            extensionName: 'NotExistingExtension',
            extensionVersion: '1.0.0',
            behaviorType: 'NotExistingExtension::MissingBehavior',
          },
        ]);
      });
    });
  
    describe('filterMissingExtensions', () => {
      it('filters extensions that are not loaded ', () => {
        makeTestExtensions(gd);
  
        expect(
          filterMissingExtensions(gd, [
            // An unknown behavior not loaded:
            {
              extensionName: 'NotExistingExtension',
              extensionVersion: '1.0.0',
            },
            // A fake extension loaded in makeTestExtensions:
            {
              extensionName: 'FakeBehavior',
              extensionVersion: '1.0.0',
            },
          ])
        ).toEqual([
          {
            extensionName: 'NotExistingExtension',
            extensionVersion: '1.0.0',
          },
        ]);
      });
    });
  
    describe('downloadExtensions', () => {
      it('loads the required extensions ', async () => {
        mockFn(getExtensionsRegistry).mockImplementationOnce(() => ({
          version: '1.0.0',
          allTags: [''],
          extensionShortHeaders: [
            flashExtensionShortHeader,
            fireBulletExtensionShortHeader,
          ],
        }));
  
        mockFn(getExtension).mockImplementationOnce(
          () => fireBulletExtensionShortHeader
        );
  
        await expect(downloadExtensions(['FireBullet'])).resolves.toEqual([
          fireBulletExtensionShortHeader,
        ]);
      });
  
      it('errors if an extension is not found ', async () => {
        mockFn(getExtensionsRegistry).mockImplementationOnce(() => ({
          version: '1.0.0',
          allTags: [''],
          extensionShortHeaders: [
            flashExtensionShortHeader,
            fireBulletExtensionShortHeader,
          ],
        }));
  
        await expect(
          downloadExtensions(['NotFoundExtension'])
        ).rejects.toMatchObject({
          message: 'Unable to find extension NotFoundExtension in the registry.',
        });
      });
  
      it("errors if the registry can't be loaded ", async () => {
        mockFn(getExtensionsRegistry).mockImplementationOnce(() => {
          throw new Error('Fake error');
        });
  
        await expect(downloadExtensions(['FakeExtension'])).rejects.toMatchObject(
          { message: 'Fake error' }
        );
      });
    });
  
    describe('installAsset', () => {
      beforeEach(() => {
        mockFn(getAsset).mockReset();
        mockFn(getExtensionsRegistry).mockReset();
        mockFn(getExtension).mockReset();
      });
  
      const mockEventsFunctionsExtensionsState: EventsFunctionsExtensionsState = {
        eventsFunctionsExtensionsError: null,
        loadProjectEventsFunctionsExtensions: () => Promise.resolve(),
        unloadProjectEventsFunctionsExtensions: () => {},
        reloadProjectEventsFunctionsExtensions: () => Promise.resolve(),
        unloadProjectEventsFunctionsExtension: () => {},
        getEventsFunctionsExtensionWriter: () => null,
        getEventsFunctionsExtensionOpener: () => null,
        ensureLoadFinished: () => Promise.resolve(),
        getIncludeFileHashs: () => ({}),
      };
  
      it("throws if asset can't be downloaded", async () => {
        makeTestExtensions(gd);
        const { project } = makeTestProject(gd);
        const layout = project.insertNewLayout('MyTestLayout', 0);
        const eventsList = new gd.EventsList();
        mockFn(getAsset).mockImplementationOnce(() => {
          throw new Error('Fake error - unable to download');
        });
  
        await expect(
          installAsset({
            assetShortHeader: fakeAssetShortHeader1,
            events: eventsList,
            project,
            objectsContainer: layout,
            eventsFunctionsExtensionsState: mockEventsFunctionsExtensionsState,
          })
        ).rejects.toMatchObject({
          message: 'Fake error - unable to download',
        });
  
        expect(getExtensionsRegistry).not.toHaveBeenCalled();
        expect(getExtension).not.toHaveBeenCalled();
      });
  
      it("throws if an extension for a behavior can't be found in the registry", async () => {
        makeTestExtensions(gd);
        const { project } = makeTestProject(gd);
        const layout = project.insertNewLayout('MyTestLayout', 0);
        const eventsList = new gd.EventsList();
  
        // Get an asset that uses a behavior...
        mockFn(getAsset).mockImplementationOnce(
          () => fakeAssetWithUnknownBehaviorCustomizations1
        );
  
        // ...but this behavior extension does not exist in the registry
        mockFn(getExtensionsRegistry).mockImplementationOnce(() => ({
          version: '1.0.0',
          allTags: [''],
          extensionShortHeaders: [
            flashExtensionShortHeader,
            fireBulletExtensionShortHeader,
          ],
        }));
  
        // Check that the extension is stated as not found in the registry
        await expect(
          installAsset({
            assetShortHeader: fakeAssetShortHeader1,
            events: eventsList,
            project,
            objectsContainer: layout,
            eventsFunctionsExtensionsState: mockEventsFunctionsExtensionsState,
          })
        ).rejects.toMatchObject({
          message: 'Unable to find extension UnknownBehavior in the registry.',
        });
  
        expect(getExtensionsRegistry).toHaveBeenCalledTimes(1);
        expect(getExtension).not.toHaveBeenCalled();
      });
  
      it("throws if an extension can't be found in the registry", async () => {
        makeTestExtensions(gd);
        const { project } = makeTestProject(gd);
        const layout = project.insertNewLayout('MyTestLayout', 0);
        const eventsList = new gd.EventsList();
  
        // Get an asset that uses an extension...
        mockFn(getAsset).mockImplementationOnce(
          () => fakeAssetWithEventCustomizationsAndUnknownExtension1
        );
  
        // ...but this extension does not exist in the registry
        mockFn(getExtensionsRegistry).mockImplementationOnce(() => ({
          version: '1.0.0',
          allTags: [''],
          extensionShortHeaders: [
            flashExtensionShortHeader,
            fireBulletExtensionShortHeader,
          ],
        }));
  
        // Check that the extension is stated as not found in the registry
        await expect(
          installAsset({
            assetShortHeader: fakeAssetShortHeader1,
            events: eventsList,
            project,
            objectsContainer: layout,
            eventsFunctionsExtensionsState: mockEventsFunctionsExtensionsState,
          })
        ).rejects.toMatchObject({
          message: 'Unable to find extension UnknownExtension in the registry.',
        });
  
        expect(getExtensionsRegistry).toHaveBeenCalledTimes(1);
        expect(getExtension).not.toHaveBeenCalled();
      });
  
      it("throws if a behavior can't be installed, even if its extension was properly found in the registry", async () => {
        makeTestExtensions(gd);
        const { project } = makeTestProject(gd);
        const layout = project.insertNewLayout('MyTestLayout', 0);
        const eventsList = new gd.EventsList();
  
        // Get an asset that uses a behavior...
        mockFn(getAsset).mockImplementationOnce(
          () => fakeAssetWithFlashBehaviorCustomizations1
        );
  
        // ...and this behavior extension is in the registry
        mockFn(getExtensionsRegistry).mockImplementationOnce(() => ({
          version: '1.0.0',
          allTags: [''],
          extensionShortHeaders: [
            flashExtensionShortHeader,
            fireBulletExtensionShortHeader,
          ],
        }));
  
        mockFn(getExtension).mockImplementationOnce(
          () => flashExtensionShortHeader
        );
  
        // Verify that, because we use `mockEventsFunctionsExtensionsState`, the
        // extension won't be loaded, so the behavior won't be installed.
        await expect(
          installAsset({
            assetShortHeader: fakeAssetShortHeader1,
            events: eventsList,
            project,
            objectsContainer: layout,
            eventsFunctionsExtensionsState: mockEventsFunctionsExtensionsState,
          })
        ).rejects.toMatchObject({
          message: 'These behaviors could not be installed: Flash::Flash (Flash)',
        });
  
        expect(getExtensionsRegistry).toHaveBeenCalledTimes(1);
        expect(getExtension).toHaveBeenCalledTimes(1);
      });
  
      it("throws if an extension can't be installed, even if it was properly found in the registry", async () => {
        makeTestExtensions(gd);
        const { project } = makeTestProject(gd);
        const layout = project.insertNewLayout('MyTestLayout', 0);
        const eventsList = new gd.EventsList();
  
        // Get an asset that uses an extension...
        mockFn(getAsset).mockImplementationOnce(
          () => fakeAssetWithEventCustomizationsAndFlashExtension1
        );
  
        // ...and this extension is in the registry
        mockFn(getExtensionsRegistry).mockImplementationOnce(() => ({
          version: '1.0.0',
          allTags: [''],
          extensionShortHeaders: [
            flashExtensionShortHeader,
            fireBulletExtensionShortHeader,
          ],
        }));
  
        mockFn(getExtension).mockImplementationOnce(
          () => flashExtensionShortHeader
        );
  
        // Verify that, because we use `mockEventsFunctionsExtensionsState`, the
        // extension won't be loaded, so the extension won't be installed.
        await expect(
          installAsset({
            assetShortHeader: fakeAssetShortHeader1,
            events: eventsList,
            project,
            objectsContainer: layout,
            eventsFunctionsExtensionsState: mockEventsFunctionsExtensionsState,
          })
        ).rejects.toMatchObject({
          message: 'These extensions could not be installed: Flash',
        });
  
        expect(getExtensionsRegistry).toHaveBeenCalledTimes(1);
        expect(getExtension).toHaveBeenCalledTimes(1);
      });
  
      it('install an asset, with a behavior that is already installed', async () => {
        makeTestExtensions(gd);
        const { project } = makeTestProject(gd);
        const layout = project.insertNewLayout('MyTestLayout', 0);
        const eventsList = new gd.EventsList();
  
        // Fake an asset with a behavior of type "FakeBehavior::FakeBehavior",
        // that is installed already.
        mockFn(getAsset).mockImplementationOnce(
          () => fakeAssetWithBehaviorCustomizations1
        );
  
        // Install the asset
        await installAsset({
          assetShortHeader: fakeAssetShortHeader1,
          events: eventsList,
          project,
          objectsContainer: layout,
          eventsFunctionsExtensionsState: mockEventsFunctionsExtensionsState,
        });
  
        // No extensions fetched because the behavior is already installed.
        expect(getExtension).not.toHaveBeenCalled();
        expect(getExtensionsRegistry).not.toHaveBeenCalled();
  
        // Check that the object was created, with the proper behavior:
        expect(layout.getObjectsCount()).toBe(1);
        expect(layout.getObjectAt(0).getName()).toBe('PlayerSpaceship');
        expect(
          layout
            .getObjectAt(0)
            .getAllBehaviorNames()
            .toJSArray()
        ).toEqual(['MyBehavior']);
      });
    });
  });
  