import { stripIndents } from '@angular-devkit/core/src/utils/literals';
import { exec, execSync } from 'child_process';
import * as http from 'http';
import * as treeKill from 'tree-kill';
import {
  checkFilesDoNotExist,
  checkFilesExist,
  createFile,
  killPorts,
  newProject,
  readFile,
  readJson,
  runCLI,
  runCLIAsync,
  runCommandUntil,
  tmpProjPath,
  uniq,
  updateFile,
  updateWorkspaceConfig,
} from '@nrwl/e2e/utils';
import { accessSync, constants } from 'fs-extra';

function getData(): Promise<any> {
  return new Promise((resolve) => {
    http.get('http://localhost:3333/api', (res) => {
      expect(res.statusCode).toEqual(200);
      let data = '';
      res.on('data', (chunk) => {
        data += chunk;
      });
      res.once('end', () => {
        resolve(JSON.parse(data));
      });
    });
  });
}

describe('Node Applications', () => {
  beforeEach(() => newProject());
  afterEach(() => killPorts());

  it('should be able to generate an empty application', async () => {
    const nodeapp = uniq('nodeapp');

    runCLI(`generate @nrwl/node:app ${nodeapp} --linter=eslint`);

    const lintResults = runCLI(`lint ${nodeapp}`);
    expect(lintResults).toContain('All files pass linting.');

    updateFile(`apps/${nodeapp}/src/main.ts`, `console.log('Hello World!');`);
    await runCLIAsync(`build ${nodeapp}`);

    checkFilesExist(`dist/apps/${nodeapp}/main.js`);
    const result = execSync(`node dist/apps/${nodeapp}/main.js`, {
      cwd: tmpProjPath(),
    }).toString();
    expect(result).toContain('Hello World!');
  }, 60000);

  xit('should be able to generate an express application', async () => {
    const nodeapp = uniq('nodeapp');

    runCLI(`generate @nrwl/express:app ${nodeapp} --linter=eslint`);
    const lintResults = runCLI(`lint ${nodeapp}`);
    expect(lintResults).toContain('All files pass linting.');

    updateFile(
      `apps/${nodeapp}/src/app/test.spec.ts`,
      `
          describe('test', () => {
            it('should work', () => {
              expect(true).toEqual(true);
            })
          })
        `
    );

    updateFile(`apps/${nodeapp}/src/assets/file.txt`, ``);
    const jestResult = await runCLIAsync(`test ${nodeapp}`);
    expect(jestResult.combinedOutput).toContain(
      'Test Suites: 1 passed, 1 total'
    );
    await runCLIAsync(`build ${nodeapp}`);

    checkFilesExist(
      `dist/apps/${nodeapp}/main.js`,
      `dist/apps/${nodeapp}/assets/file.txt`,
      `dist/apps/${nodeapp}/main.js.map`
    );

    // checking build
    const server = exec(`node ./dist/apps/${nodeapp}/main.js`, {
      cwd: tmpProjPath(),
    });

    await new Promise((resolve) => {
      server.stdout.on('data', async (data) => {
        expect(data.toString()).toContain('Listening at http://localhost:3333');
        const result = await getData();

        expect(result.message).toEqual(`Welcome to ${nodeapp}!`);

        console.log('kill server');
        server.kill();
        resolve(null);
      });
    });
    // checking serve
    const p = await runCommandUntil(`serve ${nodeapp}`, (output) =>
      output.includes('Listening at http://localhost:3333')
    );
    const result = await getData();
    expect(result.message).toEqual(`Welcome to ${nodeapp}!`);
    treeKill(p.pid, 'SIGTERM', (err) => {
      expect(err).toBeFalsy();
    });
  }, 120000);

  xit('should be able to generate a nest application', async () => {
    const nestapp = uniq('nestapp');
    runCLI(`generate @nrwl/nest:app ${nestapp} --linter=eslint`);

    const lintResults = runCLI(`lint ${nestapp}`);
    expect(lintResults).toContain('All files pass linting.');

    updateFile(`apps/${nestapp}/src/assets/file.txt`, ``);
    const jestResult = await runCLIAsync(`test ${nestapp}`);
    expect(jestResult.combinedOutput).toContain(
      'Test Suites: 2 passed, 2 total'
    );

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

    checkFilesExist(
      `dist/apps/${nestapp}/main.js`,
      `dist/apps/${nestapp}/assets/file.txt`,
      `dist/apps/${nestapp}/main.js.map`
    );

    const server = exec(`node ./dist/apps/${nestapp}/main.js`, {
      cwd: tmpProjPath(),
    });
    expect(server).toBeTruthy();

    // checking build
    await new Promise((resolve) => {
      server.stdout.on('data', async (data) => {
        const message = data.toString();
        if (message.includes('Listening at http://localhost:3333')) {
          const result = await getData();

          expect(result.message).toEqual(`Welcome to ${nestapp}!`);
          server.kill();
          resolve(null);
        }
      });
    });

    // checking serve
    const p = await runCommandUntil(`serve ${nestapp}`, (output) =>
      output.includes('Listening at http://localhost:3333')
    );
    const result = await getData();
    expect(result.message).toEqual(`Welcome to ${nestapp}!`);
    treeKill(p.pid, 'SIGTERM', (err) => {
      expect(err).toBeFalsy();
    });
  }, 120000);
});

describe('Build Node apps', () => {
  beforeEach(() => newProject());

  it('should generate a package.json with the `--generatePackageJson` flag', async () => {
    newProject();
    const nestapp = uniq('nestapp');
    runCLI(`generate @nrwl/nest:app ${nestapp} --linter=eslint`);

    await runCLIAsync(`build ${nestapp} --generatePackageJson`);

    checkFilesExist(`dist/apps/${nestapp}/package.json`);
    const packageJson = JSON.parse(
      readFile(`dist/apps/${nestapp}/package.json`)
    );
    expect(packageJson).toEqual(
      expect.objectContaining({
        dependencies: {
          '@nestjs/common': '^7.0.0',
          '@nestjs/core': '^7.0.0',
          '@nestjs/platform-express': '^7.0.0',
          'reflect-metadata': '^0.1.13',
          rxjs: '~6.6.3',
        },
        main: 'main.js',
        name: expect.any(String),
        version: '0.0.1',
      })
    );
  }, 300000);
});

describe('Node Libraries', () => {
  it('should be able to generate a node library', async () => {
    newProject();
    const nodelib = uniq('nodelib');

    runCLI(`generate @nrwl/node:lib ${nodelib}`);

    const lintResults = runCLI(`lint ${nodelib}`);
    expect(lintResults).toContain('All files pass linting.');

    const jestResult = await runCLIAsync(`test ${nodelib}`);
    expect(jestResult.combinedOutput).toContain(
      'Test Suites: 1 passed, 1 total'
    );

    checkFilesDoNotExist(`libs/${nodelib}/package.json`);
  }, 300000);

  it('should be able to generate a publishable node library', async () => {
    const proj = newProject();

    const nodeLib = uniq('nodelib');
    runCLI(
      `generate @nrwl/node:lib ${nodeLib} --publishable --importPath=@${proj}/${nodeLib}`
    );
    checkFilesExist(`libs/${nodeLib}/package.json`);
    const tslibConfig = readJson(`libs/${nodeLib}/tsconfig.lib.json`);
    expect(tslibConfig).toEqual({
      extends: './tsconfig.json',
      compilerOptions: {
        module: 'commonjs',
        outDir: '../../dist/out-tsc',
        declaration: true,
        types: ['node'],
      },
      exclude: ['**/*.spec.ts'],
      include: ['**/*.ts'],
    });
    await runCLIAsync(`build ${nodeLib}`);
    checkFilesExist(
      `dist/libs/${nodeLib}/src/index.js`,
      `dist/libs/${nodeLib}/src/index.d.ts`,
      `dist/libs/${nodeLib}/package.json`
    );

    expect(readJson(`dist/libs/${nodeLib}/package.json`)).toEqual({
      name: `@${proj}/${nodeLib}`,
      version: '0.0.1',
      main: './src/index.js',
      typings: './src/index.d.ts',
    });

    // Copying package.json from assets
    updateWorkspaceConfig((workspace) => {
      workspace.projects[nodeLib].targets.build.options.assets.push(
        `libs/${nodeLib}/package.json`
      );
      return workspace;
    });
    createFile(`dist/libs/${nodeLib}/_should_remove.txt`); // Output directory should be removed
    await runCLIAsync(`build ${nodeLib}`);
    expect(readJson(`dist/libs/${nodeLib}/package.json`)).toEqual({
      name: `@${proj}/${nodeLib}`,
      version: '0.0.1',
      main: './src/index.js',
      typings: './src/index.d.ts',
    });
  }, 300000);

  it('should be able to generate a publishable node library with CLI wrapper', async () => {
    const proj = newProject();

    const nodeLib = uniq('nodelib');
    runCLI(
      `generate @nrwl/node:lib ${nodeLib} --publishable --importPath=@${proj}/${nodeLib}`
    );

    updateWorkspaceConfig((workspace) => {
      workspace.projects[nodeLib].targets.build.options.cli = true;
      return workspace;
    });

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

    const binFile = `dist/libs/${nodeLib}/index.bin.js`;
    checkFilesExist(binFile);
    expect(() =>
      accessSync(tmpProjPath(binFile), constants.X_OK)
    ).not.toThrow();

    expect(readJson(`dist/libs/${nodeLib}/package.json`).bin).toEqual({
      [nodeLib]: './index.bin.js',
    });
    checkFilesDoNotExist(`dist/libs/${nodeLib}/_should_remove.txt`);

    // Support not deleting output path before build
    createFile(`dist/libs/${nodeLib}/_should_keep.txt`);
    await runCLIAsync(`build ${nodeLib} --delete-output-path=false`);
    checkFilesExist(`dist/libs/${nodeLib}/_should_keep.txt`);
  }, 300000);

  it('should support --js flag', async () => {
    const proj = newProject();

    const nodeLib = uniq('nodelib');
    runCLI(
      `generate @nrwl/node:lib ${nodeLib} --publishable --importPath=@${proj}/${nodeLib} --js`
    );
    checkFilesExist(
      `libs/${nodeLib}/package.json`,
      `libs/${nodeLib}/src/index.js`,
      `libs/${nodeLib}/src/lib/${nodeLib}.js`,
      `libs/${nodeLib}/src/lib/${nodeLib}.spec.js`
    );
    checkFilesDoNotExist(
      `libs/${nodeLib}/src/index.ts`,
      `libs/${nodeLib}/src/lib/${nodeLib}.ts`,
      `libs/${nodeLib}/src/lib/${nodeLib}.spec.ts`
    );
    await runCLIAsync(`build ${nodeLib}`);
    checkFilesExist(
      `dist/libs/${nodeLib}/src/index.js`,
      `dist/libs/${nodeLib}/package.json`
    );
  }, 300000);

  it('should be able to copy assets', () => {
    const proj = newProject();
    const nodelib = uniq('nodelib');
    const nglib = uniq('nglib');

    // Generating two libraries just to have a lot of files to copy
    runCLI(
      `generate @nrwl/node:lib ${nodelib} --publishable --importPath=@${proj}/${nodelib}`
    );
    /**
     * The angular lib contains a lot sub directories that would fail without
     * `nodir: true` in the package.impl.ts
     */
    runCLI(
      `generate @nrwl/angular:lib ${nglib} --publishable --importPath=@${proj}/${nglib}`
    );

    updateWorkspaceConfig((workspace) => {
      workspace.projects[nodelib].targets.build.options.assets.push({
        input: `./dist/libs/${nglib}`,
        glob: '**/*',
        output: '.',
      });
      return workspace;
    });

    runCLI(`build ${nglib}`);
    runCLI(`build ${nodelib}`);
    checkFilesExist(`./dist/libs/${nodelib}/esm2015/index.js`);
  }, 300000);

  it('should fail when trying to compile typescript files that are invalid', () => {
    const proj = newProject();
    const nodeLib = uniq('nodelib');
    runCLI(
      `generate @nrwl/node:lib ${nodeLib} --publishable --importPath=@${proj}/${nodeLib}`
    );
    updateFile(
      `libs/${nodeLib}/src/index.ts`,
      stripIndents`
        const temp: number = 'should fail'
        `
    );
    expect(() => runCLI(`build ${nodeLib}`)).toThrow();
  });
});

describe('nest libraries', function () {
  beforeEach(() => newProject());

  it('should be able to generate a nest library', async () => {
    const nestlib = uniq('nestlib');
    runCLI(`generate @nrwl/nest:lib ${nestlib}`);

    const jestConfigContent = readFile(`libs/${nestlib}/jest.config.js`);

    expect(stripIndents`${jestConfigContent}`).toEqual(
      stripIndents`module.exports = {
                displayName: '${nestlib}',
                preset: '../../jest.preset.js',
                globals: {
                  'ts-jest': {
                  tsconfig: '<rootDir>/tsconfig.spec.json',
                  },
                },
                testEnvironment: 'node',
                 transform: {
                '^.+\\.[tj]sx?$': 'ts-jest',
                },
                moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
                coverageDirectory: '../../coverage/libs/${nestlib}',
            };
            `
    );

    const lintResults = runCLI(`lint ${nestlib}`);
    expect(lintResults).toContain('All files pass linting.');
  }, 60000);

  it('should be able to generate a nest library w/ service', async () => {
    const nestlib = uniq('nestlib');

    runCLI(`generate @nrwl/nest:lib ${nestlib} --service`);

    const lintResults = runCLI(`lint ${nestlib}`);
    expect(lintResults).toContain('All files pass linting.');

    const jestResult = await runCLIAsync(`test ${nestlib}`);
    expect(jestResult.combinedOutput).toContain(
      'Test Suites: 1 passed, 1 total'
    );
  }, 200000);

  it('should be able to generate a nest library w/ controller', async () => {
    const nestlib = uniq('nestlib');

    runCLI(`generate @nrwl/nest:lib ${nestlib} --controller`);

    const lintResults = runCLI(`lint ${nestlib}`);
    expect(lintResults).toContain('All files pass linting.');

    const jestResult = await runCLIAsync(`test ${nestlib}`);
    expect(jestResult.combinedOutput).toContain(
      'Test Suites: 1 passed, 1 total'
    );
  }, 200000);

  it('should be able to generate a nest library w/ controller and service', async () => {
    const nestlib = uniq('nestlib');

    runCLI(`generate @nrwl/nest:lib ${nestlib} --controller --service`);

    const lintResults = runCLI(`lint ${nestlib}`);
    expect(lintResults).toContain('All files pass linting.');

    const jestResult = await runCLIAsync(`test ${nestlib}`);
    expect(jestResult.combinedOutput).toContain(
      'Test Suites: 2 passed, 2 total'
    );
  }, 200000);
});

describe('with dependencies', () => {
  /**
   * 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();

    runCLI(`generate @nrwl/express:app ${app}`);
    runCLI(`generate @nrwl/node:lib ${parentLib} --buildable=true`);
    runCLI(`generate @nrwl/node:lib ${childLib} --buildable=true`);
    runCLI(`generate @nrwl/node:lib ${childLib2} --buildable=true`);

    // create dependencies by importing
    const createDep = (parent, children: string[]) => {
      updateFile(
        `libs/${parent}/src/lib/${parent}.ts`,
        `
                ${children
                  .map(
                    (entry) => `import { ${entry} } from '@${proj}/${entry}';`
                  )
                  .join('\n')}

                export function ${parent}(): string {
                  return '${parent}' + ' ' + ${children
          .map((entry) => `${entry}()`)
          .join('+')}
                }
                `
      );
    };

    createDep(parentLib, [childLib, childLib2]);

    updateFile(
      `apps/${app}/src/main.ts`,
      `
        import "@${proj}/${parentLib}";
        `
    );

    // we are setting paths to {} to make sure built libs are read from dist
    updateFile('tsconfig.base.json', (c) => {
      const json = JSON.parse(c);
      json.compilerOptions.paths = {};
      return JSON.stringify(json, null, 2);
    });
  });

  it('should throw an error if the dependent library has not been built before building the parent lib', () => {
    expect.assertions(2);

    try {
      runCLI(`build ${parentLib}`);
    } catch (e) {
      expect(e.stderr.toString()).toContain(
        `Some of the project ${parentLib}'s dependencies have not been built yet. Please build these libraries before:`
      );
      expect(e.stderr.toString()).toContain(`${childLib}`);
    }
  });

  it('should build a library without dependencies', () => {
    const childLibOutput = runCLI(`build ${childLib}`);

    expect(childLibOutput).toContain(
      `Done compiling TypeScript files for project "${childLib}"`
    );
  });

  it('should build a parent library if the dependent libraries have been built before', () => {
    const childLibOutput = runCLI(`build ${childLib}`);
    expect(childLibOutput).toContain(
      `Done compiling TypeScript files for project "${childLib}"`
    );

    const childLib2Output = runCLI(`build ${childLib2}`);
    expect(childLib2Output).toContain(
      `Done compiling TypeScript files for project "${childLib2}"`
    );

    const parentLibOutput = runCLI(`build ${parentLib}`);
    expect(parentLibOutput).toContain(
      `Done compiling TypeScript files for project "${parentLib}"`
    );

    //   assert package.json deps have been set
    const assertPackageJson = (
      parent: string,
      lib: string,
      version: string
    ) => {
      const jsonFile = readJson(`dist/libs/${parent}/package.json`);
      const childDependencyVersion = jsonFile.dependencies[`@${proj}/${lib}`];
      expect(childDependencyVersion).toBe(version);
    };

    assertPackageJson(parentLib, childLib, '0.0.1');
    assertPackageJson(parentLib, childLib2, '0.0.1');
  });

  it('should build an app composed out of buildable libs', () => {
    const buildWithDeps = runCLI(
      `build ${app} --with-deps --buildLibsFromSource=false`
    );
    expect(buildWithDeps).toContain(`Running target "build" succeeded`);
    checkFilesDoNotExist(`apps/${app}/tsconfig/tsconfig.nx-tmp`);

    // we remove all path mappings from the root tsconfig, so when trying to build
    // libs from source, the builder will throw
    const failedBuild = runCLI(
      `build ${app} --with-deps --buildLibsFromSource`,
      { silenceError: true }
    );
    expect(failedBuild).toContain(`Can't resolve`);
  }, 1000000);
});
