import {
  checkFilesDoNotExist,
  checkFilesExist,
  getSize,
  killPorts,
  newProject,
  readFile,
  readJson,
  rmDist,
  runCLI,
  runCLIAsync,
  tmpProjPath,
  uniq,
  updateFile,
  updateJson,
  updateProjectConfig,
} from '@nrwl/e2e/utils';
import { names } from '@nrwl/devkit';

describe('Build React libraries and apps', () => {
  /**
   * Graph:
   *
   *                      childLib
   *                     /
   * app => parentLib =>
   *                    \
   *                     childLib2
   *
   */
  let app: string;
  let parentLib: string;
  let childLib: string;
  let childLib2: string;

  let proj: string;

  beforeEach(() => {
    app = uniq('app');
    parentLib = uniq('parentlib');
    childLib = uniq('childlib');
    childLib2 = uniq('childlib2');

    proj = newProject();

    // create dependencies by importing
    const createDep = (parent, children: string[]) => {
      updateFile(
        `libs/${parent}/src/index.ts`,
        `
        export * from './lib/${parent}';

              ${children
                .map(
                  (entry) =>
                    `import { ${
                      names(entry).className
                    } } from '@${proj}/${entry}'; console.log(${
                      names(entry).className
                    });`
                )
                .join('\n')}
            `
      );
    };

    runCLI(`generate @nrwl/react:app ${app} `);
    updateJson('nx.json', (json) => ({
      ...json,
      generators: {
        ...json.generators,
        '@nrwl/react': {
          library: {
            unitTestRunner: 'none',
          },
        },
      },
    }));
    // generate buildable libs
    runCLI(
      `generate @nrwl/react:library ${parentLib} --bundler=rollup --importPath=@${proj}/${parentLib} --no-interactive --unitTestRunner=jest`
    );
    runCLI(
      `generate @nrwl/react:library ${childLib} --bundler=rollup --importPath=@${proj}/${childLib} --no-interactive --unitTestRunner=jest`
    );
    runCLI(
      `generate @nrwl/react:library ${childLib2} --bundler=rollup --importPath=@${proj}/${childLib2} --no-interactive --unitTestRunner=jest`
    );

    createDep(parentLib, [childLib, childLib2]);

    updateFile(
      `apps/${app}/src/main.tsx`,
      `
        import {${names(parentLib).className}} from "@${proj}/${parentLib}";
        console.log(${names(parentLib).className});
        `
    );

    // Add assets to child lib
    updateProjectConfig(childLib, (json) => {
      json.targets.build.options.assets = [`libs/${childLib}/src/assets`];
      return json;
    });
    updateFile(`libs/${childLib}/src/assets/hello.txt`, 'Hello World!');
  });

  afterEach(() => {
    killPorts();
    // cleanupProject();
  });

  describe('Buildable libraries', () => {
    it('should build libraries with and without dependencies', () => {
      /*
       * 1. Without dependencies
       */
      runCLI(`build ${childLib}`);
      runCLI(`build ${childLib2}`);

      checkFilesExist(`dist/libs/${childLib}/index.js`);

      checkFilesExist(`dist/libs/${childLib2}/index.js`);

      checkFilesExist(`dist/libs/${childLib}/assets/hello.txt`);
      checkFilesExist(`dist/libs/${childLib2}/README.md`);

      /*
       * 2. With dependencies
       */
      runCLI(`build ${parentLib}`);

      checkFilesExist(`dist/libs/${parentLib}/index.js`);

      const jsonFile = readJson(`dist/libs/${parentLib}/package.json`);
      expect(jsonFile.peerDependencies).toEqual(
        expect.objectContaining({
          [`@${proj}/${childLib}`]: '0.0.1',
          [`@${proj}/${childLib2}`]: '0.0.1',
        })
      );

      /*
       * 3. With dependencies without existing dist
       */
      rmDist();

      runCLI(`build ${parentLib} --skip-nx-cache`);

      checkFilesExist(`dist/libs/${parentLib}/index.js`);
      checkFilesExist(`dist/libs/${childLib}/index.js`);
      checkFilesExist(`dist/libs/${childLib2}/index.js`);

      expect(readFile(`dist/libs/${childLib}/index.js`)).not.toContain(
        'react/jsx-dev-runtime'
      );
      expect(readFile(`dist/libs/${childLib}/index.js`)).toContain(
        'react/jsx-runtime'
      );
    });

    it('should support --format option', () => {
      updateFile(
        `libs/${childLib}/src/index.ts`,
        (s) => `${s}
export async function f() { return 'a'; }
export async function g() { return 'b'; }
export async function h() { return 'c'; }
`
      );

      runCLI(`build ${childLib} --format cjs,esm`);

      checkFilesExist(`dist/libs/${childLib}/index.cjs`);
      checkFilesExist(`dist/libs/${childLib}/index.js`);

      const cjsPackageSize = getSize(
        tmpProjPath(`dist/libs/${childLib}/index.cjs`)
      );
      const esmPackageSize = getSize(
        tmpProjPath(`dist/libs/${childLib}/index.js`)
      );

      // This is a loose requirement that ESM should be smaller than CJS output.
      expect(esmPackageSize).toBeLessThanOrEqual(cjsPackageSize);
    });

    it('should preserve the tsconfig target set by user', () => {
      // Setup
      const myLib = uniq('my-lib');
      runCLI(
        `generate @nrwl/react:library ${myLib} --bundler=rollup --publishable --importPath="@mproj/${myLib}" --no-interactive --unitTestRunner=jest`
      );

      /**
       *
       * Here I update my library file
       * I am just adding this in the end:
       *
       * export const TestFunction = async () => {
       *     return await Promise.resolve('Done!')
       * }
       *
       * So that I can see the change in the Promise.
       *
       */

      updateFile(`libs/${myLib}/src/lib/${myLib}.tsx`, (content) => {
        return `
        ${content} \n
          export const TestFunction = async () => {
               return await Promise.resolve('Done!')
          }
        `;
      });

      updateFile(`libs/${myLib}/tsconfig.json`, (content) => {
        const json = JSON.parse(content);

        /**
         * Set target as es3!!
         */

        json.compilerOptions.target = 'es3';
        return JSON.stringify(json, null, 2);
      });
      // What we're testing
      runCLI(`build ${myLib}`);
      // Assertion
      const content = readFile(`dist/libs/${myLib}/index.js`);

      /**
       * Then check if the result contains this "promise" polyfill?
       */

      expect(content).toContain('function __generator(thisArg, body) {');
    });

    it('should build an app composed out of buildable libs', () => {
      const buildFromSource = runCLI(
        `build ${app} --buildLibsFromSource=false`
      );
      expect(buildFromSource).toContain('Successfully ran target build');
      checkFilesDoNotExist(`apps/${app}/tsconfig/tsconfig.nx-tmp`);
    }, 1000000);

    it('should not create a dist folder if there is an error', async () => {
      const libName = uniq('lib');

      runCLI(
        `generate @nrwl/react:lib ${libName} --bundler=rollup --importPath=@${proj}/${libName} --no-interactive --unitTestRunner=jest`
      );

      const mainPath = `libs/${libName}/src/lib/${libName}.tsx`;
      updateFile(mainPath, `${readFile(mainPath)}\n console.log(a);`); // should error - "a" will be undefined

      await expect(runCLIAsync(`build ${libName}`)).rejects.toThrow(
        /Bundle failed/
      );
      expect(() => {
        checkFilesExist(`dist/libs/${libName}/package.json`);
      }).toThrow();
    }, 250000);
  });
});

describe('Build React applications and libraries with Vite', () => {
  let proj: string;

  beforeEach(() => {
    proj = newProject();
  });

  it('should test and lint app with bundler=vite', async () => {
    const viteApp = uniq('viteapp');

    runCLI(
      `generate @nrwl/react:app ${viteApp} --bundler=vite --unitTestRunner=vitest --no-interactive`
    );

    const appTestResults = await runCLIAsync(`test ${viteApp}`);
    expect(appTestResults.combinedOutput).toContain(
      'Successfully ran target test'
    );

    const appLintResults = await runCLIAsync(`lint ${viteApp}`);
    expect(appLintResults.combinedOutput).toContain(
      'Successfully ran target lint'
    );

    await runCLIAsync(`build ${viteApp}`);
    checkFilesExist(`dist/apps/${viteApp}/index.html`);
  }, 300_000);

  it('should test and lint app with bundler=vite and inSourceTests', async () => {
    const viteApp = uniq('viteapp');
    const viteLib = uniq('vitelib');

    runCLI(
      `generate @nrwl/react:app ${viteApp} --bundler=vite --unitTestRunner=vitest --inSourceTests --no-interactive`
    );
    expect(() => {
      checkFilesExist(`apps/${viteApp}/src/app/app.spec.tsx`);
    }).toThrow();

    const appTestResults = await runCLIAsync(`test ${viteApp}`);
    expect(appTestResults.combinedOutput).toContain(
      'Successfully ran target test'
    );

    const appLintResults = await runCLIAsync(`lint ${viteApp}`);
    expect(appLintResults.combinedOutput).toContain(
      'Successfully ran target lint'
    );

    await runCLIAsync(`build ${viteApp}`);
    checkFilesExist(`dist/apps/${viteApp}/index.html`);

    runCLI(
      `generate @nrwl/react:lib ${viteLib} --bundler=vite --inSourceTests --unitTestRunner=vitest --no-interactive`
    );
    expect(() => {
      checkFilesExist(`libs/${viteLib}/src/lib/${viteLib}.spec.tsx`);
    }).toThrow();

    runCLI(
      `generate @nrwl/react:component comp1 --inSourceTests --export --project=${viteLib} --no-interactive`
    );
    expect(() => {
      checkFilesExist(`libs/${viteLib}/src/lib/comp1/comp1.spec.tsx`);
    }).toThrow();

    runCLI(
      `generate @nrwl/react:component comp2 --export --project=${viteLib} --no-interactive`
    );
    checkFilesExist(`libs/${viteLib}/src/lib/comp2/comp2.spec.tsx`);

    const libTestResults = await runCLIAsync(`test ${viteLib}`);
    expect(libTestResults.combinedOutput).toContain(
      'Successfully ran target test'
    );

    const libLintResults = await runCLIAsync(`lint ${viteLib}`);
    expect(libLintResults.combinedOutput).toContain(
      'Successfully ran target lint'
    );

    await runCLIAsync(`build ${viteLib}`);
    checkFilesExist(
      `dist/libs/${viteLib}/index.d.ts`,
      `dist/libs/${viteLib}/index.js`,
      `dist/libs/${viteLib}/index.mjs`
    );
  }, 300_000);

  it('should support bundling with Vite', async () => {
    const viteLib = uniq('vitelib');

    runCLI(
      `generate @nrwl/react:lib ${viteLib} --bundler=vite --no-interactive --unit-test-runner=none`
    );

    const packageJson = readJson('package.json');
    // Vite does not need these libraries to work.
    expect(packageJson.dependencies['core-js']).toBeUndefined();
    expect(packageJson.dependencies['tslib']).toBeUndefined();

    await runCLIAsync(`build ${viteLib}`);

    checkFilesExist(
      `dist/libs/${viteLib}/package.json`,
      `dist/libs/${viteLib}/index.d.ts`,
      `dist/libs/${viteLib}/index.js`,
      `dist/libs/${viteLib}/index.mjs`
    );

    // Convert non-buildable lib to buildable one
    const nonBuildableLib = uniq('nonbuildablelib');
    runCLI(
      `generate @nrwl/react:lib ${nonBuildableLib} --no-interactive --unitTestRunner=jest`
    );
    runCLI(
      `generate @nrwl/vite:configuration ${nonBuildableLib} --uiFramework=react --no-interactive`
    );
    await runCLIAsync(`build ${nonBuildableLib}`);
    checkFilesExist(
      `dist/libs/${nonBuildableLib}/index.d.ts`,
      `dist/libs/${nonBuildableLib}/index.js`,
      `dist/libs/${nonBuildableLib}/index.mjs`
    );
  }, 300_000);
});
