import type { NxJsonConfiguration } from '@nx/devkit';
import {
  getPackageManagerCommand,
  isNotWindows,
  newProject,
  readFile,
  readJson,
  cleanupProject,
  runCLI,
  runCLIAsync,
  runCommand,
  uniq,
  updateFile,
  updateProjectConfig,
  checkFilesExist,
  isWindows,
  fileExists,
  removeFile,
  readResolvedConfiguration,
} from '@nx/e2e/utils';

describe('Nx Affected and Graph Tests', () => {
  let proj: string;

  beforeAll(() => (proj = newProject()));
  afterAll(() => cleanupProject());

  describe('affected:*', () => {
    it('should print, build, and test affected apps', async () => {
      process.env.CI = 'true';
      const myapp = uniq('myapp');
      const myapp2 = uniq('myapp2');
      const mylib = uniq('mylib');
      const mylib2 = uniq('mylib2');
      const mypublishablelib = uniq('mypublishablelib');
      runCLI(`generate @nx/web:app ${myapp}`);
      runCLI(`generate @nx/web:app ${myapp2}`);
      runCLI(`generate @nx/js:lib ${mylib}`);
      runCLI(`generate @nx/js:lib ${mylib2}`);
      runCLI(
        `generate @nx/js:lib ${mypublishablelib} --publishable --importPath=@${proj}/${mypublishablelib} --tags=ui`
      );

      updateFile(
        `apps/${myapp}/src/app/app.element.spec.ts`,
        `
              import * as x from '@${proj}/${mylib}';
              describe('sample test', () => {
                it('should test', () => {
                  expect(1).toEqual(1);
                });
              });
            `
      );
      updateFile(
        `libs/${mypublishablelib}/src/lib/${mypublishablelib}.spec.ts`,
        `
              import * as x from '@${proj}/${mylib}';
              describe('sample test', () => {
                it('should test', () => {
                  expect(1).toEqual(1);
                });
              });
            `
      );

      const affectedApps = runCLI(
        `show projects --affected --files="libs/${mylib}/src/index.ts"`
      );
      expect(affectedApps).toContain(myapp);
      expect(affectedApps).not.toContain(myapp2);

      const implicitlyAffectedApps = runCLI(
        'show projects --affected --files="tsconfig.base.json"'
      );
      expect(implicitlyAffectedApps).toContain(myapp);
      expect(implicitlyAffectedApps).toContain(myapp2);

      const noAffectedApps = runCLI(
        'show projects --affected projects --files="README.md"'
      );
      expect(noAffectedApps).not.toContain(myapp);
      expect(noAffectedApps).not.toContain(myapp2);

      const affectedLibs = runCLI(
        `show projects --affected --files="libs/${mylib}/src/index.ts"`
      );
      expect(affectedLibs).toContain(mypublishablelib);
      expect(affectedLibs).toContain(mylib);
      expect(affectedLibs).not.toContain(mylib2);

      const implicitlyAffectedLibs = runCLI(
        'show projects --affected --files="tsconfig.base.json"'
      );
      expect(implicitlyAffectedLibs).toContain(mypublishablelib);
      expect(implicitlyAffectedLibs).toContain(mylib);
      expect(implicitlyAffectedLibs).toContain(mylib2);

      const noAffectedLibsNonExistentFile = runCLI(
        'show projects --affected --files="tsconfig.json"'
      );
      expect(noAffectedLibsNonExistentFile).not.toContain(mypublishablelib);
      expect(noAffectedLibsNonExistentFile).not.toContain(mylib);
      expect(noAffectedLibsNonExistentFile).not.toContain(mylib2);

      const noAffectedLibs = runCLI(
        'show projects --affected --files="README.md"'
      );
      expect(noAffectedLibs).not.toContain(mypublishablelib);
      expect(noAffectedLibs).not.toContain(mylib);
      expect(noAffectedLibs).not.toContain(mylib2);

      // build
      const build = runCLI(
        `affected:build --files="libs/${mylib}/src/index.ts" --parallel`
      );
      expect(build).toContain(`Running target build for 3 projects:`);
      expect(build).toContain(`- ${myapp}`);
      expect(build).toContain(`- ${mypublishablelib}`);
      expect(build).not.toContain('is not registered with the build command');
      expect(build).toContain('Successfully ran target build');

      const buildExcluded = runCLI(
        `affected:build --files="libs/${mylib}/src/index.ts" --exclude=${myapp}`
      );
      expect(buildExcluded).toContain(`Running target build for 2 projects:`);
      expect(buildExcluded).toContain(`- ${mypublishablelib}`);

      const buildExcludedByTag = runCLI(
        `affected:build --files="libs/${mylib}/src/index.ts" --exclude=tag:ui`
      );
      expect(buildExcludedByTag).toContain(
        `Running target build for 2 projects:`
      );
      expect(buildExcludedByTag).not.toContain(`- ${mypublishablelib}`);

      // test
      updateFile(
        `apps/${myapp}/src/app/app.element.spec.ts`,
        readFile(`apps/${myapp}/src/app/app.element.spec.ts`).replace(
          '.toEqual(1)',
          '.toEqual(2)'
        )
      );

      const failedTests = runCLI(
        `affected:test --files="libs/${mylib}/src/index.ts"`,
        { silenceError: true }
      );
      expect(failedTests).toContain(mylib);
      expect(failedTests).toContain(myapp);
      expect(failedTests).toContain(mypublishablelib);
      expect(failedTests).toContain(`Failed tasks:`);

      // Fix failing Unit Test
      updateFile(
        `apps/${myapp}/src/app/app.element.spec.ts`,
        readFile(`apps/${myapp}/src/app/app.element.spec.ts`).replace(
          '.toEqual(2)',
          '.toEqual(1)'
        )
      );
    }, 1000000);
  });

  describe('affected (with git)', () => {
    let myapp;
    let myapp2;
    let mylib;

    beforeEach(() => {
      myapp = uniq('myapp');
      myapp2 = uniq('myapp');
      mylib = uniq('mylib');
      const nxJson: NxJsonConfiguration = readJson('nx.json');

      updateFile('nx.json', JSON.stringify(nxJson));
      runCommand(`git init`);
      runCommand(`git config user.email "test@test.com"`);
      runCommand(`git config user.name "Test"`);
      runCommand(`git config commit.gpgsign false`);
      try {
        runCommand(
          `git add . && git commit -am "initial commit" && git checkout -b main`
        );
      } catch (e) {}
    });

    function generateAll() {
      runCLI(`generate @nx/web:app ${myapp}`);
      runCLI(`generate @nx/web:app ${myapp2}`);
      runCLI(`generate @nx/js:lib ${mylib}`);
      runCommand(`git add . && git commit -am "add all"`);
    }

    it('should not affect other projects by generating a new project', () => {
      // TODO: investigate why affected gives different results on windows
      if (isNotWindows()) {
        runCLI(`generate @nx/web:app ${myapp}`);
        expect(runCLI('print-affected --select projects')).toContain(myapp);
        runCommand(`git add . && git commit -am "add ${myapp}"`);

        runCLI(`generate @nx/web:app ${myapp2}`);
        let output = runCLI('print-affected --select projects');
        expect(output).not.toContain(myapp);
        expect(output).toContain(myapp2);
        runCommand(`git add . && git commit -am "add ${myapp2}"`);

        runCLI(`generate @nx/js:lib ${mylib}`);
        output = runCLI('print-affected --select projects');
        expect(output).not.toContain(myapp);
        expect(output).not.toContain(myapp2);
        expect(output).toContain(mylib);
      }
    }, 1000000);

    it('should detect changes to projects based on tags changes', async () => {
      // TODO: investigate why affected gives different results on windows
      if (isNotWindows()) {
        generateAll();
        await updateProjectConfig(myapp, (config) => ({
          ...config,
          tags: ['tag'],
        }));
        const output = runCLI('print-affected --select projects');
        expect(output).toContain(myapp);
        expect(output).not.toContain(myapp2);
        expect(output).not.toContain(mylib);
      }
    });

    it('should affect all projects by removing projects', async () => {
      generateAll();
      const root = (await readResolvedConfiguration())[mylib].root;
      removeFile(root);
      const output = runCLI('print-affected --select projects');
      expect(output).toContain(myapp);
      expect(output).toContain(myapp2);
      expect(output).not.toContain(mylib);
    });

    it('should detect changes to implicitly dependant projects', async () => {
      generateAll();
      await updateProjectConfig(myapp, (config) => ({
        ...config,
        implicitDependencies: ['*', `!${myapp2}`],
      }));

      runCommand('git commit -m "setup test"');
      updateFile(`libs/${mylib}/index.html`, '<html></html>');

      const output = runCLI('print-affected --select projects');

      expect(output).toContain(myapp);
      expect(output).not.toContain(myapp2);
      expect(output).toContain(mylib);

      // Clear implicit deps to not interfere with other tests.
      await updateProjectConfig(myapp, (config) => ({
        ...config,
        implicitDependencies: [],
      }));
    });

    it('should handle file renames', () => {
      generateAll();

      // Move file
      updateFile(
        `apps/${myapp2}/src/index.html`,
        readFile(`apps/${myapp}/src/index.html`)
      );
      removeFile(`apps/${myapp}/src/index.html`);

      const affectedProjects = runCLI(
        'print-affected --uncommitted --select projects'
      )
        .replace(
          /.*nx print-affected --uncommitted --select projects( --verbose)?\n/,
          ''
        )
        .split(', ');

      expect(affectedProjects).toContain(myapp);
      expect(affectedProjects).toContain(myapp2);
    });
  });

  describe('graph', () => {
    let myapp: string;
    let myapp2: string;
    let myapp3: string;
    let myappE2e: string;
    let myapp2E2e: string;
    let myapp3E2e: string;
    let mylib: string;
    let mylib2: string;

    beforeAll(() => {
      myapp = uniq('myapp');
      myapp2 = uniq('myapp2');
      myapp3 = uniq('myapp3');
      myappE2e = `${myapp}-e2e`;
      myapp2E2e = `${myapp2}-e2e`;
      myapp3E2e = `${myapp3}-e2e`;
      mylib = uniq('mylib');
      mylib2 = uniq('mylib2');

      runCLI(`generate @nx/web:app ${myapp}`);
      runCLI(`generate @nx/web:app ${myapp2}`);
      runCLI(`generate @nx/web:app ${myapp3}`);
      runCLI(`generate @nx/js:lib ${mylib}`);
      runCLI(`generate @nx/js:lib ${mylib2}`);

      runCommand(`git init`);
      runCommand(`git config user.email "test@test.com"`);
      runCommand(`git config user.name "Test"`);
      runCommand(`git config commit.gpgsign false`);
      runCommand(
        `git add . && git commit -am "initial commit" && git checkout -b main`
      );

      updateFile(
        `apps/${myapp}/src/main.ts`,
        `
      import '@${proj}/${mylib}';

      const s = {loadChildren: '@${proj}/${mylib2}'};
    `
      );

      updateFile(
        `apps/${myapp2}/src/app/app.element.spec.ts`,
        `import '@${proj}/${mylib}';`
      );

      updateFile(
        `libs/${mylib}/src/${mylib}.spec.ts`,
        `import '@${proj}/${mylib2}';`
      );
    });

    it('graph should output json to file', () => {
      runCLI(`graph --file=project-graph.json`);

      expect(() => checkFilesExist('project-graph.json')).not.toThrow();

      const jsonFileContents = readJson('project-graph.json');

      expect(jsonFileContents.graph.dependencies).toEqual(
        expect.objectContaining({
          [myapp3E2e]: [
            {
              source: myapp3E2e,
              target: myapp3,
              type: 'implicit',
            },
          ],
          [myapp2]: [
            {
              source: myapp2,
              target: mylib,
              type: 'static',
            },
          ],
          [myapp2E2e]: [
            {
              source: myapp2E2e,
              target: myapp2,
              type: 'implicit',
            },
          ],
          [mylib]: [
            {
              source: mylib,
              target: mylib2,
              type: 'static',
            },
          ],
          [mylib2]: [],
          [myapp]: [
            {
              source: myapp,
              target: mylib,
              type: 'static',
            },
          ],
          [myappE2e]: [
            {
              source: myappE2e,
              target: myapp,
              type: 'implicit',
            },
          ],
          [myapp3]: [],
        })
      );

      runCLI(
        `affected:graph --files="libs/${mylib}/src/index.ts" --file="project-graph.json"`
      );

      expect(() => checkFilesExist('project-graph.json')).not.toThrow();

      const jsonFileContents2 = readJson('project-graph.json');

      expect(jsonFileContents2.criticalPath).toContain(myapp);
      expect(jsonFileContents2.criticalPath).toContain(myapp2);
      expect(jsonFileContents2.criticalPath).toContain(mylib);
      expect(jsonFileContents2.criticalPath).not.toContain(mylib2);
    }, 1000000);

    if (isNotWindows()) {
      it('graph should output json to file by absolute path', () => {
        runCLI(`graph --file=/tmp/project-graph.json`);

        expect(() => checkFilesExist('/tmp/project-graph.json')).not.toThrow();
      }, 1000000);
    }

    if (isWindows()) {
      it('graph should output json to file by absolute path in Windows', () => {
        runCLI(`graph --file=C:\\tmp\\project-graph.json`);

        expect(fileExists('C:\\tmp\\project-graph.json')).toBeTruthy();
      }, 1000000);
    }

    it('graph should focus requested project', () => {
      runCLI(`graph --focus=${myapp} --file=project-graph.json`);

      expect(() => checkFilesExist('project-graph.json')).not.toThrow();

      const jsonFileContents = readJson('project-graph.json');
      const projectNames = Object.keys(jsonFileContents.graph.nodes);

      expect(projectNames).toContain(myapp);
      expect(projectNames).toContain(mylib);
      expect(projectNames).toContain(mylib2);
      expect(projectNames).toContain(myappE2e);

      expect(projectNames).not.toContain(myapp2);
      expect(projectNames).not.toContain(myapp3);
      expect(projectNames).not.toContain(myapp2E2e);
      expect(projectNames).not.toContain(myapp3E2e);
    }, 1000000);

    it('graph should exclude requested projects', () => {
      runCLI(
        `graph --exclude=${myappE2e},${myapp2E2e},${myapp3E2e} --file=project-graph.json`
      );

      expect(() => checkFilesExist('project-graph.json')).not.toThrow();

      const jsonFileContents = readJson('project-graph.json');
      const projectNames = Object.keys(jsonFileContents.graph.nodes);

      expect(projectNames).toContain(myapp);
      expect(projectNames).toContain(mylib);
      expect(projectNames).toContain(mylib2);
      expect(projectNames).toContain(myapp2);
      expect(projectNames).toContain(myapp3);

      expect(projectNames).not.toContain(myappE2e);
      expect(projectNames).not.toContain(myapp2E2e);
      expect(projectNames).not.toContain(myapp3E2e);
    }, 1000000);

    it('graph should exclude requested projects that were included by a focus', () => {
      runCLI(
        `graph --focus=${myapp} --exclude=${myappE2e} --file=project-graph.json`
      );

      expect(() => checkFilesExist('project-graph.json')).not.toThrow();

      const jsonFileContents = readJson('project-graph.json');
      const projectNames = Object.keys(jsonFileContents.graph.nodes);

      expect(projectNames).toContain(myapp);
      expect(projectNames).toContain(mylib);
      expect(projectNames).toContain(mylib2);

      expect(projectNames).not.toContain(myappE2e);
      expect(projectNames).not.toContain(myapp2);
      expect(projectNames).not.toContain(myapp3);
      expect(projectNames).not.toContain(myapp2E2e);
      expect(projectNames).not.toContain(myapp3E2e);
    }, 1000000);

    it('graph should output a deployable static website in an html file accompanied by a folder with static assets', () => {
      runCLI(`graph --file=project-graph.html`);

      expect(() => checkFilesExist('project-graph.html')).not.toThrow();
      expect(() => checkFilesExist('static/styles.css')).not.toThrow();
      expect(() => checkFilesExist('static/runtime.js')).not.toThrow();
      expect(() => checkFilesExist('static/main.js')).not.toThrow();
      expect(() => checkFilesExist('static/environment.js')).not.toThrow();

      const environmentJs = readFile('static/environment.js');

      expect(environmentJs).toContain('window.projectGraphResponse');
      expect(environmentJs).toContain('"affected":[]');
    });

    it('graph should output valid json when stdout is specified', () => {
      const result = runCLI(`affected -t build --graph stdout`);
      let model;
      expect(() => (model = JSON.parse(result))).not.toThrow();
      expect(model).toHaveProperty('graph');
      expect(model).toHaveProperty('tasks');
    });

    it('should include affected projects in environment file', () => {
      runCLI(`graph --affected --file=project-graph.html`);

      const environmentJs = readFile('static/environment.js');
      const affectedProjects = environmentJs
        .match(/"affected":\[(.*?)\]/)[1]
        ?.split(',');

      expect(affectedProjects).toContain(`"${myapp}"`);
      expect(affectedProjects).toContain(`"${myappE2e}"`);
      expect(affectedProjects).toContain(`"${myapp2}"`);
      expect(affectedProjects).toContain(`"${myapp2E2e}"`);
      expect(affectedProjects).toContain(`"${mylib}"`);
    });
  });
});

describe('Print-affected', () => {
  let proj: string;

  beforeAll(() => (proj = newProject()));
  afterAll(() => cleanupProject());

  it('should print information about affected projects', async () => {
    const myapp = uniq('myapp-a');
    const myapp2 = uniq('myapp-b');
    const mylib = uniq('mylib');
    const mylib2 = uniq('mylib2');
    const mypublishablelib = uniq('mypublishablelib');

    runCLI(`generate @nx/web:app ${myapp}`);
    runCLI(`generate @nx/web:app ${myapp2}`);
    runCLI(`generate @nx/js:lib ${mylib}`);
    runCLI(`generate @nx/js:lib ${mylib2}`);
    runCLI(`generate @nx/js:lib ${mypublishablelib}`);

    const app1ElementSpec = readFile(
      `apps/${myapp}/src/app/app.element.spec.ts`
    );

    updateFile(
      `apps/${myapp}/src/app/app.element.spec.ts`,
      `
        import "@${proj}/${mylib}";
        import "@${proj}/${mypublishablelib}";
        ${app1ElementSpec}
        `
    );

    const app2ElementSpec = readFile(
      `apps/${myapp2}/src/app/app.element.spec.ts`
    );

    updateFile(
      `apps/${myapp2}/src/app/app.element.spec.ts`,
      `
        import "@${proj}/${mylib}";
        import "@${proj}/${mypublishablelib}";
        ${app2ElementSpec}
        `
    );

    const resWithoutTarget = JSON.parse(
      (
        await runCLIAsync(
          `print-affected --files=apps/${myapp}/src/app/app.element.spec.ts`,
          {
            silent: true,
          }
        )
      ).stdout
    );
    expect(resWithoutTarget.tasks).toEqual([]);
    compareTwoArrays(resWithoutTarget.projects, [`${myapp}-e2e`, myapp]);

    const resWithTarget = JSON.parse(
      (
        await runCLIAsync(
          `print-affected --files=apps/${myapp}/src/app/app.element.spec.ts --target=test`,
          { silent: true }
        )
      ).stdout.trim()
    );

    const { runNx } = getPackageManagerCommand();
    expect(resWithTarget.tasks[0]).toMatchObject({
      id: `${myapp}:test`,
      overrides: {},
      target: {
        project: myapp,
        target: 'test',
      },
      command: `${runNx} run ${myapp}:test`,
      outputs: [`coverage/apps/${myapp}`],
    });
    compareTwoArrays(resWithTarget.projects, [myapp]);

    const resWithTargetWithSelect1 = (
      await runCLIAsync(
        `print-affected --files=apps/${myapp}/src/app/app.element.spec.ts --target=test --select=projects`,
        { silent: true }
      )
    ).stdout.trim();
    compareTwoSerializedArrays(resWithTargetWithSelect1, myapp);

    const resWithTargetWithSelect2 = (
      await runCLIAsync(
        `print-affected --files=apps/${myapp}/src/app/app.element.spec.ts --target=test --select="tasks.target.project"`,
        { silent: true }
      )
    ).stdout.trim();
    compareTwoSerializedArrays(resWithTargetWithSelect2, myapp);
  }, 120000);

  function compareTwoSerializedArrays(a: string, b: string) {
    compareTwoArrays(
      a.split(',').map((_) => _.trim()),
      b.split(',').map((_) => _.trim())
    );
  }

  function compareTwoArrays(a: string[], b: string[]) {
    expect(a.sort((x, y) => x.localeCompare(y))).toEqual(
      b.sort((x, y) => x.localeCompare(y))
    );
  }
});
