import {
  addProjectConfiguration,
  joinPathFragments,
  ProjectConfiguration,
  readJson,
  readProjectConfiguration,
  Tree,
  writeJson,
} from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { exampleRootTslintJson } from '@nx/linter';
import { conversionGenerator } from './convert-tslint-to-eslint';

/**
 * Don't run actual child_process implementation of installPackagesTask()
 */
jest.mock('child_process', () => {
  return {
    ...jest.requireActual<any>('child_process'),
    execSync: jest.fn((command: string) => {
      if (command.includes('pnpm --version')) {
        return '8.2.0';
      }
      return;
    }),
  };
});

const appProjectName = 'angular-app-1';
const appProjectRoot = `apps/${appProjectName}`;
const appProjectTSLintJsonPath = joinPathFragments(
  appProjectRoot,
  'tslint.json'
);
const projectPrefix = 'angular-app';
const libProjectName = 'angular-lib-1';
const libProjectRoot = `libs/${libProjectName}`;
const libProjectTSLintJsonPath = joinPathFragments(
  libProjectRoot,
  'tslint.json'
);
// Used to configure the test Tree and stub the response from tslint-to-eslint-config util findReportedConfiguration()
const projectTslintJsonData = {
  raw: {
    extends: '../../tslint.json',
    rules: {
      // Standard Nx/Angular CLI generated rules
      'directive-selector': [true, 'attribute', projectPrefix, 'camelCase'],
      'component-selector': [true, 'element', projectPrefix, 'kebab-case'],
      // User custom TS rule
      'no-empty-interface': true,
      // User custom template/HTML rule
      'template-banana-in-box': true,
      // User custom rule with no known automated converter
      'some-super-custom-rule-with-no-converter': true,
    },
    linterOptions: {
      exclude: ['!**/*'],
    },
  },
  tslintPrintConfigResult: {
    rules: {
      'directive-selector': {
        ruleArguments: ['attribute', projectPrefix, 'camelCase'],
        ruleSeverity: 'error',
      },
      'component-selector': {
        ruleArguments: ['element', projectPrefix, 'kebab-case'],
        ruleSeverity: 'error',
      },
      'no-empty-interface': {
        ruleArguments: [],
        ruleSeverity: 'error',
      },
      'template-banana-in-box': {
        ruleArguments: [],
        ruleSeverity: 'error',
      },
      'some-super-custom-rule-with-no-converter': {
        ruleArguments: [],
        ruleSeverity: 'error',
      },
    },
  },
};

function mockFindReportedConfiguration(_, pathToTslintJson) {
  switch (pathToTslintJson) {
    case 'tslint.json':
      return exampleRootTslintJson.tslintPrintConfigResult;
    case appProjectTSLintJsonPath:
      return {
        /**
         * Add in an example of rule which requires type-checking so we can test
         * that parserOptions.project is appropriately preserved in the final
         * config in this case.
         */
        rules: {
          ...projectTslintJsonData.tslintPrintConfigResult.rules,
          'await-promise': {
            ruleArguments: [],
            ruleSeverity: 'error',
          },
        },
      };
    case libProjectTSLintJsonPath:
      return projectTslintJsonData.tslintPrintConfigResult;
    default:
      throw new Error(
        `mockFindReportedConfiguration - Did not recognize path ${pathToTslintJson}`
      );
  }
}

/**
 * See ./mock-tslint-to-eslint-config.ts for why this is needed
 */
jest.mock('tslint-to-eslint-config', () => {
  return {
    // Since upgrading to (ts-)jest 26 this usage of this mock has caused issues...
    // @ts-ignore
    ...jest.requireActual<any>('tslint-to-eslint-config'),
    findReportedConfiguration: jest.fn(mockFindReportedConfiguration),
  };
});

/**
 * Mock the the mutating fs utilities used within the conversion logic, they are not
 * needed because of our stubbed response for findReportedConfiguration() above, and
 * they would cause noise in the git data of the actual Nx repo when the tests run.
 */
jest.mock('fs', () => {
  return {
    // Since upgrading to (ts-)jest 26 this usage of this mock has caused issues...
    // @ts-ignore
    ...jest.requireActual<any>('fs'),
    writeFileSync: jest.fn(),
    mkdirSync: jest.fn(),
  };
});

describe('convert-tslint-to-eslint', () => {
  let host: Tree;

  beforeEach(async () => {
    host = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });

    writeJson(host, 'tslint.json', exampleRootTslintJson.raw);

    addProjectConfiguration(host, appProjectName, {
      root: appProjectRoot,
      prefix: projectPrefix,
      projectType: 'application',
      targets: {
        /**
         * LINT TARGET CONFIG - BEFORE CONVERSION
         *
         * TSLint executor configured for the project
         */
        lint: {
          executor: '@angular-devkit/build-angular:tslint',
          options: {
            exclude: ['**/node_modules/**', '!apps/angular-app-1/**/*'],
            tsConfig: ['apps/angular-app-1/tsconfig.app.json'],
          },
        },
      },
    } as ProjectConfiguration);

    addProjectConfiguration(host, libProjectName, {
      root: libProjectRoot,
      prefix: projectPrefix,
      projectType: 'library',
      targets: {
        /**
         * LINT TARGET CONFIG - BEFORE CONVERSION
         *
         * TSLint executor configured for the project
         */
        lint: {
          executor: '@angular-devkit/build-angular:tslint',
          options: {
            exclude: ['**/node_modules/**', '!libs/angular-lib-1/**/*'],
            tsConfig: ['libs/angular-lib-1/tsconfig.app.json'],
          },
        },
      },
    } as ProjectConfiguration);

    /**
     * Existing tslint.json file for the app project
     */
    writeJson(host, 'apps/angular-app-1/tslint.json', {
      ...projectTslintJsonData.raw,
      rules: {
        ...projectTslintJsonData.raw.rules,
        /**
         * Add in an example of rule which requires type-checking so we can test
         * that parserOptions.project is appropriately preserved in the final
         * config in this case.
         */
        'await-promise': true,
      },
    });
    /**
     * Existing tslint.json file for the lib project
     */
    writeJson(
      host,
      'libs/angular-lib-1/tslint.json',
      projectTslintJsonData.raw
    );
  });

  it('should work for Angular applications', async () => {
    await conversionGenerator(host, {
      project: appProjectName,
      ignoreExistingTslintConfig: false,
      removeTSLintIfNoMoreTSLintTargets: false,
    });

    /**
     * It should ensure the required Nx packages are installed and available
     *
     * NOTE: tslint-to-eslint-config should NOT be present
     */
    expect(readJson(host, 'package.json')).toMatchSnapshot();

    /**
     * LINT TARGET CONFIG - AFTER CONVERSION
     *
     * It should replace the TSLint executor with the ESLint one
     */
    expect(readProjectConfiguration(host, appProjectName)).toMatchSnapshot();

    /**
     * The root level .eslintrc.json should now have been generated
     */
    const eslintJson = readJson(host, '.eslintrc.json');
    expect(eslintJson.overrides[3].rules['no-console'][1].allow).toContain(
      'log'
    );
    // Remove no-console config because it is not deterministic across node versions
    delete eslintJson.overrides[3].rules['no-console'][1].allow;
    expect(eslintJson).toMatchSnapshot();

    /**
     * The project level .eslintrc.json should now have been generated
     * and extend from the root, as well as applying any customizations
     * which are specific to this projectType.
     */
    expect(
      readJson(host, joinPathFragments(appProjectRoot, '.eslintrc.json'))
    ).toMatchSnapshot();

    /**
     * The project's TSLint file should have been deleted
     */
    expect(host.exists(appProjectTSLintJsonPath)).toEqual(false);
  });

  it('should work for Angular libraries', async () => {
    await conversionGenerator(host, {
      project: libProjectName,
      ignoreExistingTslintConfig: false,
      removeTSLintIfNoMoreTSLintTargets: false,
    });

    /**
     * It should ensure the required Nx packages are installed and available
     *
     * NOTE: tslint-to-eslint-config should NOT be present
     */
    expect(readJson(host, 'package.json')).toMatchSnapshot();

    /**
     * LINT TARGET CONFIG - AFTER CONVERSION
     *
     * It should replace the TSLint executor with the ESLint one
     */
    expect(readProjectConfiguration(host, libProjectName)).toMatchSnapshot();

    /**
     * The root level .eslintrc.json should now have been generated
     */
    const eslintJson = readJson(host, '.eslintrc.json');
    expect(eslintJson.overrides[3].rules['no-console'][1].allow).toContain(
      'log'
    );
    // Remove no-console config because it is not deterministic across node versions
    delete eslintJson.overrides[3].rules['no-console'][1].allow;
    expect(eslintJson).toMatchSnapshot();

    /**
     * The project level .eslintrc.json should now have been generated
     * and extend from the root, as well as applying any customizations
     * which are specific to this projectType.
     */
    expect(
      readJson(host, joinPathFragments(libProjectRoot, '.eslintrc.json'))
    ).toMatchSnapshot();

    /**
     * The project's TSLint file should have been deleted
     */
    expect(host.exists(libProjectTSLintJsonPath)).toEqual(false);
  });

  it('should not override .eslint config if migration in progress', async () => {
    /**
     * First we convert app
     */
    await conversionGenerator(host, {
      project: appProjectName,
      ignoreExistingTslintConfig: false,
      removeTSLintIfNoMoreTSLintTargets: true,
    });

    /**
     * The root level .eslintrc.json should now have been generated
     */
    const eslintContent = readJson(host, '.eslintrc.json');
    expect(eslintContent.overrides[3].rules['no-console'][1].allow).toContain(
      'log'
    );
    // Remove no-console config because it is not deterministic across node versions
    delete eslintContent.overrides[3].rules['no-console'][1].allow;
    expect(eslintContent).toMatchSnapshot();

    /**
     * We will make a change to the eslint config before the next step
     */
    eslintContent.overrides[0].rules[
      '@nx/enforce-module-boundaries'
    ][1].enforceBuildableLibDependency = false;
    writeJson(host, '.eslintrc.json', eslintContent);

    /**
     * Convert the lib
     */
    await conversionGenerator(host, {
      project: libProjectName,
      ignoreExistingTslintConfig: false,
      removeTSLintIfNoMoreTSLintTargets: true,
    });

    /**
     * The project level .eslintrc.json should now have been generated
     * and extend from the root, as well as applying any customizations
     * which are specific to this projectType.
     */
    expect(
      readJson(host, joinPathFragments(libProjectRoot, '.eslintrc.json'))
    ).toMatchSnapshot();

    /**
     * The root level .eslintrc.json should not be re-created
     * if it already existed
     */
    const eslintJson2 = readJson(host, '.eslintrc.json');
    expect(eslintJson2).toMatchSnapshot();

    /**
     * The root TSLint file should have been deleted
     */
    expect(host.exists('tslint.json')).toEqual(false);
  });
});
