import {
  getPackageManagerCommand,
  joinPathFragments,
  names,
} from '@nrwl/devkit';
import {
  checkFilesExist,
  ensureNxProject,
  listFiles,
  readFile,
  runCommand,
  runNxCommand,
  runPackageManagerInstall,
  tmpProjPath,
  uniq,
  updateFile,
} from '@nrwl/nx-plugin/testing';
import { runCommandUntil } from '../../utils';

import { readFileSync, unlinkSync, writeFileSync } from 'fs';
import { join } from 'path';
import { XmlDocument } from 'xmldoc';

import { findProjectFileInPathSync } from '@nx-dotnet/utils';
import { readDependenciesFromNxDepGraph } from '@nx-dotnet/utils/e2e';
import { exec, execSync } from 'child_process';
import { ensureDirSync } from 'fs-extra';
import { Workspaces } from '@nrwl/tao/src/shared/workspace';
import { PackageJson } from 'nx/src/utils/package-json';

const e2eDir = tmpProjPath();

describe('nx-dotnet e2e', () => {
  beforeAll(() => {
    ensureNxProject('@nx-dotnet/core', 'dist/packages/core');
    initializeGitRepo(e2eDir);
  }, 1500000);

  it('should create apps, libs, and project references', async () => {
    const testApp = uniq('app');
    const testLib = uniq('lib');

    await runNxCommandAsync(
      `generate @nx-dotnet/core:app ${testApp} --language="C#" --template="webapi" --skipSwaggerLib`,
    );

    await runNxCommandAsync(
      `generate @nx-dotnet/core:lib ${testLib} --language="C#" --template="classlib"`,
    );

    const output = await runNxCommandAsync(
      `generate @nx-dotnet/core:project-reference ${testApp} ${testLib}`,
    );

    expect(output.stdout).toMatch(/Reference .* added to the project/);
  });

  it('should work with affected', async () => {
    const testApp = uniq('app');
    const testLib = uniq('lib');

    runCommand('git checkout -b "affected-tests"');
    updateFile('package.json', (f) => {
      const json = JSON.parse(f);
      json.dependencies['@nrwl/angular'] = json.devDependencies['nx'];
      return JSON.stringify(json);
    });
    runPackageManagerInstall();

    await runNxCommandAsync(
      `generate @nrwl/angular:app ng-app --style css --routing false --no-interactive`,
      // { cwd: e2eDir, stdio: 'inherit' },
    );

    await runNxCommandAsync(
      `generate @nx-dotnet/core:app ${testApp} --language="C#" --template="webapi" --skipSwaggerLib`,
    );

    await runNxCommandAsync(
      `generate @nx-dotnet/core:lib ${testLib} --language="C#" --template="classlib"`,
    );

    await runNxCommandAsync(
      `generate @nx-dotnet/core:project-reference ${testApp} ${testLib}`,
    );

    const deps = await readDependenciesFromNxDepGraph(join(e2eDir), testApp);
    expect(deps).toContain(testLib);
    runCommand('git checkout main');
  }, 300000);

  describe('nx g app', () => {
    it('should obey dry-run', async () => {
      const app = uniq('app');
      await runNxCommandAsync(
        `generate @nx-dotnet/core:app ${app} --language="C#" --template="webapi" --skipSwaggerLib --dry-run`,
      );

      expect(() => checkFilesExist(`apps/${app}`)).toThrow();
    });

    it('should generate an app without swagger library', async () => {
      const app = uniq('app');
      await runNxCommandAsync(
        `generate @nx-dotnet/core:app ${app} --language="C#" --template="webapi" --skip-swagger-lib`,
      );

      expect(() => checkFilesExist(`apps/${app}`)).not.toThrow();
      expect(() =>
        checkFilesExist(`libs/generated/${app}-swaggger/project.json`),
      ).toThrow();
    });

    it('should build and test an app', async () => {
      const app = uniq('app');
      const testProj = `${app}-test`;
      await runNxCommandAsync(
        `generate @nx-dotnet/core:app ${app} --language="C#" --template="webapi" --skip-swagger-lib`,
      );

      await runNxCommandAsync(`build ${app}`);
      await runNxCommandAsync(`test ${testProj}`);

      expect(() => checkFilesExist(`apps/${app}`)).not.toThrow();
      expect(() => checkFilesExist(`dist/apps/${app}`)).not.toThrow();
    });

    it('should build an app which depends on a lib', async () => {
      const app = uniq('app');
      const lib = uniq('lib');
      await runNxCommandAsync(
        `generate @nx-dotnet/core:app ${app} --language="C#" --template="webapi" --skip-swagger-lib`,
      );
      await runNxCommandAsync(
        `generate @nx-dotnet/core:lib ${lib} --language="C#" --template="classlib"`,
      );
      await runNxCommandAsync(
        `generate @nx-dotnet/core:project-reference --project ${app} --reference ${lib}`,
      );

      await runNxCommandAsync(`build ${app}`);

      expect(() => checkFilesExist(`apps/${app}`)).not.toThrow();
      expect(() => checkFilesExist(`dist/apps/${app}`)).not.toThrow();
      expect(() => checkFilesExist(`dist/libs/${lib}`)).not.toThrow();
    });

    it('should update output paths', async () => {
      const app = uniq('app');
      await runNxCommandAsync(
        `generate @nx-dotnet/core:app ${app} --language="C#" --template="webapi" --skip-swagger-lib`,
      );
      const configFilePath = findProjectFileInPathSync(
        join(e2eDir, 'apps', app),
      );
      const config = readFileSync(configFilePath).toString();
      const projectXml = new XmlDocument(config);
      const outputPath = projectXml
        .childNamed('PropertyGroup')
        ?.childNamed('OutputPath')?.val as string;
      expect(outputPath).toBeTruthy();
    });

    it('should lint', async () => {
      const app = uniq('app');
      await runNxCommandAsync(
        `generate @nx-dotnet/core:app ${app} --template webapi --language="C#"  --skip-swagger-lib`,
      );
      console.log('LINT TEST PROJECT GENERATED', app);
      const promise = runNxCommandAsync(`lint ${app}`);
      await expect(promise).rejects.toThrow(
        expect.objectContaining({
          message: expect.stringContaining('WHITESPACE'),
        }),
      );
    });
  });

  describe('nx g test', () => {
    it('should add a reference to the target project', async () => {
      const app = uniq('app');
      await runNxCommandAsync(
        `generate @nx-dotnet/core:app ${app} --language="C#" --template="webapi" --skip-swagger-lib --test-template="none"`,
      );
      await runNxCommandAsync(
        `generate @nx-dotnet/core:test ${app} --language="C#" --template="nunit"`,
      );

      const config = readFile(
        joinPathFragments(
          'apps',
          `${app}-test`,
          `Proj.${names(app).className}.Test.csproj`,
        ),
      );
      const projectXml = new XmlDocument(config);
      const projectReference = projectXml
        .childrenNamed('ItemGroup')[1]
        ?.childNamed('ProjectReference');

      expect(projectReference).toBeDefined();
    });

    it('should create test project using suffix', async () => {
      const app = uniq('app');
      await runNxCommandAsync(
        `generate @nx-dotnet/core:app ${app} --language="C#" --template="webapi" --skip-swagger-lib --test-template="none"`,
      );
      await runNxCommandAsync(
        `generate @nx-dotnet/core:test ${app} --language="C#" --template="nunit" --suffix="integration-tests"`,
      );

      const config = readFile(
        joinPathFragments(
          'apps',
          `${app}-integration-tests`,
          `Proj.${names(app).className}.IntegrationTests.csproj`,
        ),
      );

      expect(config).toBeDefined();
    });
  });

  describe('nx g lib', () => {
    it('should obey dry-run', async () => {
      const lib = uniq('lib');
      await runNxCommandAsync(
        `generate @nx-dotnet/core:lib ${lib} --language="C#" --template="webapi" --skip-swagger-lib --dry-run`,
      );

      expect(() => checkFilesExist(`libs/${lib}`)).toThrow();
    });

    it('should generate an lib', async () => {
      const lib = uniq('lib');
      await runNxCommandAsync(
        `generate @nx-dotnet/core:lib ${lib} --language="C#" --template="classlib"`,
      );

      expect(() => checkFilesExist(`libs/${lib}`)).not.toThrow();
    });
  });

  describe('nx g import-projects', () => {
    it('should import apps, libs, and test', async () => {
      const testApp = uniq('app');
      const testLib = uniq('lib');
      const testAppTest = `${testApp}-test`;
      const appDir = `${e2eDir}/apps/${testApp}`;
      const testAppDir = `${e2eDir}/apps/${testAppTest}`;
      const libDir = `${e2eDir}/libs/${testLib}`;
      ensureDirSync(appDir);
      ensureDirSync(libDir);
      ensureDirSync(testAppDir);
      execSync('dotnet new webapi', { cwd: appDir });
      execSync('dotnet new classlib', { cwd: libDir });
      execSync('dotnet new nunit', { cwd: testAppDir });

      await runNxCommandAsync(`generate @nx-dotnet/core:import-projects`);

      const workspace = new Workspaces(e2eDir).readWorkspaceConfiguration();

      expect(workspace.projects[testApp].targets?.serve).toBeDefined();
      expect(workspace.projects[testApp].targets?.build).toBeDefined();
      expect(workspace.projects[testApp].targets?.lint).toBeDefined();
      expect(workspace.projects[testLib].targets?.serve).not.toBeDefined();
      expect(workspace.projects[testLib].targets?.build).toBeDefined();
      expect(workspace.projects[testLib].targets?.lint).toBeDefined();
      expect(workspace.projects[testAppTest].targets?.build).toBeDefined();
      expect(workspace.projects[testAppTest].targets?.lint).toBeDefined();
      expect(workspace.projects[testAppTest].targets?.test).toBeDefined();

      await runNxCommandAsync(`build ${testApp}`);
      checkFilesExist(`dist/apps/${testApp}`);
    });
  });

  describe('solution handling', () => {
    // For solution handling, defaults fall back to if a file exists.
    // This ensures that the tests are ran in a clean state, without previous
    // test projects interfering with the test.
    beforeAll(() => {
      ensureNxProject('@nx-dotnet/core', 'dist/packages/core');
      initializeGitRepo(e2eDir);
    }, 1500000);

    it("shouldn't create a solution by default if not specified", async () => {
      const app = uniq('app');
      await runNxCommandAsync(
        `generate @nx-dotnet/core:app ${app} --language="C#" --skip-swagger-lib --template="webapi"`,
      );

      expect(() => checkFilesExist(`apps/${app}`)).not.toThrow();
      expect(listFiles('.').filter((x) => x.endsWith('.sln'))).toHaveLength(0);
    });

    it('should create a default solution file if specified as true', async () => {
      const app = uniq('app');
      await runNxCommandAsync(
        `generate @nx-dotnet/core:app ${app} --language="C#" --template="webapi" --skip-swagger-lib --solutionFile`,
      );

      expect(() => checkFilesExist(`apps/${app}`)).not.toThrow();
      expect(listFiles('.').filter((x) => x.endsWith('.sln'))).toHaveLength(1);
    });

    it('should create specified solution file if specified as string', async () => {
      const app = uniq('app');
      await runNxCommandAsync(
        `generate @nx-dotnet/core:app ${app} --language="C#" --template="webapi" --solutionFile="MyCompany.sln"`,
      );

      expect(() =>
        checkFilesExist(`apps/${app}`, `MyCompany.sln`),
      ).not.toThrow();
    });

    it('should add successive projects to default solution file', async () => {
      const app1 = uniq('app');
      await runNxCommandAsync(
        `generate @nx-dotnet/core:app ${app1} --language="C#" --skip-swagger-lib --template="webapi" --solutionFile`,
      );

      const app2 = uniq('app2');
      await runNxCommandAsync(
        `generate @nx-dotnet/core:app ${app2} --language="C#" --skip-swagger-lib --template="webapi" --solutionFile`,
      );

      const slnFile = readFile('proj.nx-dotnet.sln');

      expect(() => checkFilesExist(`apps/${app1}`)).not.toThrow();
      expect(slnFile).toContain(app1);
      expect(slnFile).toContain(app2);
    });

    it('should add test project to same solution as app project', async () => {
      const app = uniq('app');
      await runNxCommandAsync(
        `generate @nx-dotnet/core:app ${app} --language="C#" --template="webapi" --skip-swagger-lib --test-template="xunit" --solutionFile`,
      );

      const slnFile = readFile('proj.nx-dotnet.sln');
      expect(() => checkFilesExist(`apps/${app}`)).not.toThrow();
      expect(slnFile).toContain(app);
      expect(slnFile).toContain(app + '-test');
    });
  });

  describe('inferred targets', () => {
    let api: string;
    let projectFolder: string;

    beforeAll(() => {
      api = uniq('api');
      projectFolder = join(e2eDir, 'apps', api);
      ensureDirSync(projectFolder);
      execSync(`dotnet new webapi --language C#`, {
        cwd: projectFolder,
      });
      updateFile('nx.json', (contents) => {
        const json = JSON.parse(contents);
        json.plugins = ['@nx-dotnet/core'];
        return JSON.stringify(json, null, 2);
      });
    });

    it('should work with workspace.json + project.json', () => {
      const relativeProjectPath = joinPathFragments('apps', api);
      writeFileSync(
        join(projectFolder, 'project.json'),
        JSON.stringify({
          root: relativeProjectPath,
        }),
      );
      updateFile('workspace.json', (c) => {
        const json = JSON.parse(c);
        json.projects[api] = relativeProjectPath;
        return JSON.stringify(json, null, 2);
      });
      expect(() => runNxCommand(`build ${api}`)).not.toThrow();
    });

    it('should work without workspace.json or project.json', () => {
      const workspaceJsonContents = readFile('workspace.json');
      unlinkSync(join(e2eDir, 'workspace.json'));

      const projectJsonContents = readFile(
        joinPathFragments('apps', api, 'project.json'),
      );
      unlinkSync(join(projectFolder, 'project.json'));

      expect(() => runNxCommand(`build ${api}`)).not.toThrow();

      writeFileSync(join(e2eDir, 'workspace.json'), workspaceJsonContents);

      writeFileSync(join(projectFolder, 'project.json'), projectJsonContents);
    });
  });

  describe('@nx-dotnet/core:test', () => {
    it('should test with xunit', () => {
      const appProject = uniq('app');
      const testProject = `${appProject}-test`;
      runNxCommand(
        `generate @nx-dotnet/core:app ${appProject} --language="C#" --template="webapi" --skip-swagger-lib --test-runner xunit`,
      );

      expect(() => runNxCommand(`test ${testProject}`)).not.toThrow();

      updateFile(
        `apps/${testProject}/UnitTest1.cs`,
        `using Xunit;

namespace Proj.${names(appProject).className}.Test;

public class UnitTest1
{
    // This test should fail, as the e2e test is checking for test failures.
    [Fact]
    public void Test1()
    {
      Assert.Equal(1, 2)
    }
}`,
      );

      expect(() => runNxCommand(`test ${testProject}`)).toThrow();
    });

    it('should work with watch', async () => {
      const appProject = uniq('app');
      const testProject = `${appProject}-test`;
      runNxCommand(
        `generate @nx-dotnet/core:app ${appProject} --language="C#" --template="webapi" --skip-swagger-lib --test-runner xunit`,
      );
      const p = runCommandUntil(
        `test ${testProject} --watch`,
        (output) =>
          output.includes(
            'Waiting for a file to change before restarting dotnet...',
          ),
        { kill: true },
      );
      await expect(p).resolves.not.toThrow();
    });
  });

  describe('swagger integration', () => {
    it('should generate swagger project for webapi', async () => {
      const api = uniq('api');
      await runNxCommandAsync(
        `generate @nx-dotnet/core:app ${api} --language="C#" --template="webapi"`,
      );

      expect(() => checkFilesExist(`apps/${api}`)).not.toThrow();
      expect(() =>
        checkFilesExist(`libs/generated/${api}-swagger`),
      ).not.toThrow();
      expect(() => runNxCommand(`swagger ${api}`)).not.toThrow();
      expect(() =>
        checkFilesExist(`libs/generated/${api}-swagger/swagger.json`),
      ).not.toThrow();
    });
  });
});

function initializeGitRepo(cwd: string) {
  runCommand('git init');
  runCommand('git branch -m main');
  runCommand('git config user.email [email protected]');
  runCommand('git config user.name CI-Bot');
  runCommand('git add .');
  runCommand('git commit -m "initial commit"');
}

function runCommandAsync(
  command: string,
  opts = {
    silenceError: false,
    nxVerboseLogging: true,
  },
) {
  return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
    exec(
      command,
      {
        cwd: e2eDir,
        env: opts.nxVerboseLogging
          ? { ...process.env, NX_VERBOSE_LOGGING: 'true' }
          : process.env,
      },
      (err, stdout, stderr) => {
        if (!opts.silenceError && err) {
          reject(err);
        }
        resolve({ stdout, stderr });
      },
    );
  });
}
/**
 * Run a nx command asynchronously inside the e2e directory
 * @param command
 * @param opts
 */
function runNxCommandAsync(
  command: string,
  opts = {
    silenceError: false,
    nxVerboseLogging: true,
  },
) {
  const pmc = getPackageManagerCommand();
  return runCommandAsync(`${pmc.exec} nx ${command}`, opts);
}