/**
 * @license
 * Copyright 2024 Google Inc.
 * SPDX-License-Identifier: Apache-2.0
 */
import fs from 'node:fs';
import path from 'node:path';
import {fileURLToPath} from 'node:url';

import {fixupConfigRules, fixupPluginRules} from '@eslint/compat';
import {FlatCompat} from '@eslint/eslintrc';
import js from '@eslint/js';
import typescriptEslint from '@typescript-eslint/eslint-plugin';
import tsParser from '@typescript-eslint/parser';
import _import from 'eslint-plugin-import';
import mocha from 'eslint-plugin-mocha';
import rulesdir from 'eslint-plugin-rulesdir';
import tsdoc from 'eslint-plugin-tsdoc';
import globals from 'globals';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
  baseDirectory: __dirname,
  recommendedConfig: js.configs.recommended,
  allConfig: js.configs.all,
});

rulesdir.RULES_DIR = 'tools/eslint/lib';

function getThirdPartyPackages() {
  return fs
    .readdirSync(path.join(__dirname, 'packages/puppeteer-core/third_party'), {
      withFileTypes: true,
    })
    .filter(dirent => {
      return dirent.isDirectory();
    })
    .map(({name}) => {
      return {
        name,
        message: `Import \`${name}\` from the vendored location: third_party/${name}/index.js`,
      };
    });
}

/**
 * @type {import('eslint').Linter.Config[]}
 */
export default [
  {
    ignores: [
      '**/node_modules',
      '**/build/',
      '**/lib/',
      '**/bin/',
      '**/*.tsbuildinfo',
      '**/*.api.json',
      '**/*.tgz',
      '**/yarn.lock',
      '**/.docusaurus/',
      '**/.cache-loader',
      'test/output-*/',
      '**/.dev_profile*',
      '**/coverage/',
      '**/generated/',
      '**/.eslintcache',
      '**/.cache/',
      '**/.vscode',
      '!.vscode/extensions.json',
      '!.vscode/*.template.json',
      '**/.devcontainer',
      '**/.DS_Store',
      '**/.env.local',
      '**/.env.development.local',
      '**/.env.test.local',
      '**/.env.production.local',
      '**/npm-debug.log*',
      '**/yarn-debug.log*',
      '**/yarn-error.log*',
      '**/.wireit',
      '**/assets/',
      '**/third_party/',
      'packages/ng-schematics/sandbox/**/*',
      'packages/ng-schematics/multi/**/*',
      'packages/ng-schematics/src/**/files/',
      'examples/puppeteer-in-browser/out/**/*',
      'examples/puppeteer-in-browser/node_modules/**/*',
      'examples/puppeteer-in-extension/out/**/*',
      'examples/puppeteer-in-extension/node_modules/**/*',
    ],
  },
  ...fixupConfigRules(
    compat.extends('plugin:prettier/recommended', 'plugin:import/typescript'),
  ),
  {
    name: 'JavaScript rules',
    plugins: {
      mocha,
      '@typescript-eslint': typescriptEslint,
      import: fixupPluginRules(_import),
      rulesdir,
    },

    languageOptions: {
      globals: {
        ...globals.node,
      },

      parser: tsParser,
    },

    settings: {
      'import/resolver': {
        typescript: true,
      },
    },

    rules: {
      curly: ['error', 'all'],
      'arrow-body-style': ['error', 'always'],
      'prettier/prettier': 'error',

      'spaced-comment': [
        'error',
        'always',
        {
          markers: ['*', '/'],
        },
      ],

      eqeqeq: ['error'],

      'accessor-pairs': [
        'error',
        {
          getWithoutSet: false,
          setWithoutGet: false,
        },
      ],

      'new-parens': 'error',
      'func-call-spacing': 'error',
      'prefer-const': 'error',

      'max-len': [
        'error',
        {
          /* this setting doesn't impact things as we use Prettier to format
           * our code and hence dictate the line length.
           * Prettier aims for 80 but sometimes makes the decision to go just
           * over 80 chars as it decides that's better than wrapping. ESLint's
           * rule defaults to 80 but therefore conflicts with Prettier. So we
           * set it to something far higher than Prettier would allow to avoid
           * it causing issues and conflicting with Prettier.
           */
          code: 200,
          comments: 90,
          ignoreTemplateLiterals: true,
          ignoreUrls: true,
          ignoreStrings: true,
          ignoreRegExpLiterals: true,
        },
      ],

      'no-var': 'error',
      'no-with': 'error',
      'no-multi-str': 'error',
      'no-caller': 'error',
      'no-implied-eval': 'error',
      'no-labels': 'error',
      'no-new-object': 'error',
      'no-octal-escape': 'error',
      'no-self-compare': 'error',
      'no-shadow-restricted-names': 'error',
      'no-cond-assign': 'error',
      'no-debugger': 'error',
      'no-dupe-keys': 'error',
      'no-duplicate-case': 'error',
      'no-empty-character-class': 'error',
      'no-unreachable': 'error',
      'no-unsafe-negation': 'error',
      radix: 'error',
      'valid-typeof': 'error',

      'no-unused-vars': [
        'error',
        {
          args: 'none',
          vars: 'local',
          varsIgnorePattern:
            '([fx]?describe|[fx]?it|beforeAll|beforeEach|afterAll|afterEach)',
        },
      ],

      // Disabled as it now reports issues - https://eslint.org/docs/latest/rules/no-implicit-globals
      // 'no-implicit-globals': ['error'],
      'require-yield': 'error',
      'template-curly-spacing': ['error', 'never'],
      'mocha/no-exclusive-tests': 'error',

      'import/order': [
        'error',
        {
          'newlines-between': 'always',

          alphabetize: {
            order: 'asc',
            caseInsensitive: true,
          },
        },
      ],

      'import/no-cycle': [
        'error',
        {
          maxDepth: Infinity,
        },
      ],

      // Keeps comments formatted.
      'rulesdir/prettier-comments': 'error',
      // Enforces consistent file extension
      'rulesdir/extensions': 'error',
      // Enforces license headers on files
      'rulesdir/check-license': 'error',
    },
  },
  ...compat
    .extends(
      'plugin:@typescript-eslint/eslint-recommended',
      'plugin:@typescript-eslint/recommended',
      'plugin:@typescript-eslint/stylistic',
    )
    .map(config => {
      return {
        ...config,
        files: ['**/*.ts'],
      };
    }),
  {
    name: 'TypeScript rules',
    files: ['**/*.ts'],

    plugins: {
      tsdoc,
    },

    languageOptions: {
      ecmaVersion: 5,
      sourceType: 'script',

      parserOptions: {
        allowAutomaticSingleRunInference: true,
        project: './tsconfig.base.json',
      },
    },

    rules: {
      // Enforces clean up of used resources.
      'rulesdir/use-using': 'error',

      '@typescript-eslint/array-type': [
        'error',
        {
          default: 'array-simple',
        },
      ],

      'no-unused-vars': 'off',

      '@typescript-eslint/no-unused-vars': [
        'error',
        {
          argsIgnorePattern: '^_',
          varsIgnorePattern: '^_',
        },
      ],

      'func-call-spacing': 'off',

      // TODO: Look up https://eslint.style/t
      // '@typescript-eslint/func-call-spacing': 'error',
      // '@typescript-eslint/semi': 'error',
      semi: 'off',
      '@typescript-eslint/no-empty-function': 'off',
      '@typescript-eslint/no-use-before-define': 'off',
      // We have to use any on some types so the warning isn't valuable.
      '@typescript-eslint/no-explicit-any': 'off',
      // We don't require explicit return types on basic functions or
      // dummy functions in tests, for example
      '@typescript-eslint/explicit-function-return-type': 'off',
      // We allow non-null assertions if the value was asserted using `assert` API.
      '@typescript-eslint/no-non-null-assertion': 'off',
      '@typescript-eslint/no-unnecessary-template-expression': 'error',
      '@typescript-eslint/no-unsafe-function-type': 'error',
      '@typescript-eslint/no-wrapper-object-types': 'error',

      // By default this is a warning but we want it to error.
      '@typescript-eslint/explicit-module-boundary-types': 'error',

      'no-restricted-syntax': [
        'error',
        {
          selector: "CallExpression[callee.name='require']",
          message: '`require` statements are not allowed. Use `import`.',
        },
      ],

      '@typescript-eslint/no-floating-promises': [
        'error',
        {
          ignoreVoid: true,
          ignoreIIFE: true,
        },
      ],

      '@typescript-eslint/prefer-ts-expect-error': 'error',
      // This is more performant; see https://v8.dev/blog/fast-async.
      '@typescript-eslint/return-await': ['error', 'always'],
      // This optimizes the dependency tracking for type-only files.
      '@typescript-eslint/consistent-type-imports': 'error',
      // So type-only exports get elided.
      '@typescript-eslint/consistent-type-exports': 'error',
      // Don't want to trigger unintended side-effects.
      '@typescript-eslint/no-import-type-side-effects': 'error',
    },
  },
  {
    name: 'Puppeteer Core syntax',
    files: ['packages/puppeteer-core/src/**/*.ts'],
    rules: {
      'no-restricted-syntax': [
        'error',
        {
          message: 'Use method `Deferred.race()` instead.',
          selector:
            'MemberExpression[object.name="Promise"][property.name="race"]',
        },
        {
          message:
            'Deferred `valueOrThrow` should not be called in `Deferred.race()` pass deferred directly',
          selector:
            'CallExpression[callee.object.name="Deferred"][callee.property.name="race"] > ArrayExpression > CallExpression[callee.property.name="valueOrThrow"]',
        },
      ],
      'no-restricted-imports': [
        'error',
        {
          patterns: ['*Events', '*.test.js'],
          paths: [...getThirdPartyPackages()],
        },
      ],
    },
  },
  {
    name: 'Packages',
    files: [
      'packages/puppeteer-core/src/**/*.test.ts',
      'tools/mocha-runner/src/test.ts',
    ],
    rules: {
      // With the Node.js test runner, `describe` and `it` are technically
      // promises, but we don't need to await them.
      '@typescript-eslint/no-floating-promises': 'off',
    },
  },
  {
    files: ['packages/**/*.ts'],

    rules: {
      'tsdoc/syntax': 'error',
    },
  },
  {
    name: 'Mocha',
    files: ['test/**/*.ts'],
    rules: {
      'no-restricted-imports': [
        'error',
        {
          /**
           * The mocha tests run on the compiled output in the /lib directory
           * so we should avoid importing from src.
           */
          patterns: ['*src*'],
        },
      ],
    },
  },
  {
    name: 'Mocha Tests',
    files: ['test/**/*.spec.ts'],

    rules: {
      '@typescript-eslint/no-unused-vars': [
        'error',
        {
          argsIgnorePattern: '^_',
          varsIgnorePattern: '^_',
        },
      ],

      'no-restricted-syntax': [
        'error',
        {
          message:
            'Use helper command `launch` to make sure the browsers get cleaned',
          selector:
            'MemberExpression[object.name="puppeteer"][property.name="launch"]',
        },
        {
          message: 'Unexpected debugging mocha test.',
          selector:
            'CallExpression[callee.object.name="it"] > MemberExpression > Identifier[name="deflake"], CallExpression[callee.object.name="it"] > MemberExpression > Identifier[name="deflakeOnly"]',
        },
        {
          message: 'No `expect` in EventHandler. They will never throw errors',
          selector:
            'CallExpression[callee.property.name="on"] BlockStatement > :not(TryStatement) > ExpressionStatement > CallExpression[callee.object.callee.name="expect"]',
        },
      ],
    },
  },
];
