/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import typescriptEslint from "@typescript-eslint/eslint-plugin";
import tsParser from "@typescript-eslint/parser";
import notice from "eslint-plugin-notice";
import path from "path";
import { fileURLToPath } from "url";
import stylistic from "@stylistic/eslint-plugin";
import importRules from "eslint-plugin-import";
import progressPlugin from "./utils/eslint-plugin-progress/index.js";
import { fixupConfigRules } from "@eslint/compat";
import { FlatCompat } from "@eslint/eslintrc";
import js from "@eslint/js";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const plugins = {
  "@stylistic": stylistic,
  "@typescript-eslint": typescriptEslint,
  notice,
  import: importRules,
};

const ignores = [
  ".github/",
  "*.js",
  "**/.cache/",
  "**/*.d.ts",
  "index.d.ts",
  "node_modules/",
  "output/",
  "**/playwright-report/",
  "examples",
  "packages/*/lib/",
  "packages/playwright-core/bundles/utils/src/third_party/",
  "packages/playwright-core/src/generated/*",
  "packages/playwright-core/src/third_party/",
  "packages/playwright-core/types/*",
  "packages/playwright-ct-core/src/generated/*",
  "packages/playwright/bundles/expect/third_party/",
  "packages/html-reporter/bundle.ts",
  "packages/html-reporter/playwright.config.ts",
  "packages/html-reporter/playwright/*",
  "packages/html-reporter/vite.config.ts",
  "test-results/",
  "tests/assets/",
  "tests/components/",
  "tests/installation/fixture-scripts/",
  "tests/third_party/",
  "utils/",
];

export const baseRules = {
  "@typescript-eslint/no-unused-vars": [
    2,
    { args: "none", caughtErrors: "none" },
  ],

  /**
   * Enforced rules
   */
  // syntax preferences
  "object-curly-spacing": ["error", "always"],
  quotes: [
    2,
    "single",
    {
      avoidEscape: true,
      allowTemplateLiterals: true,
    },
  ],
  "jsx-quotes": [2, "prefer-single"],
  "no-extra-semi": 2,
  "@stylistic/semi": [2],
  "comma-style": [2, "last"],
  "wrap-iife": [2, "inside"],
  "spaced-comment": [
    2,
    "always",
    {
      markers: ["*"],
    },
  ],
  eqeqeq: [2],
  "accessor-pairs": [
    2,
    {
      getWithoutSet: false,
      setWithoutGet: false,
    },
  ],
  "brace-style": [2, "1tbs", { allowSingleLine: true }],
  curly: [2, "multi-or-nest", "consistent"],
  "new-parens": 2,
  "arrow-parens": [2, "as-needed"],
  "prefer-const": 2,
  "one-var": [2, "never"],
  "quote-props": [2, "consistent"],
  "nonblock-statement-body-position": [2, "below"],

  // anti-patterns
  "no-var": 2,
  "no-with": 2,
  "no-multi-str": 2,
  "no-caller": 2,
  "no-implied-eval": 2,
  "no-labels": 2,
  "no-new-object": 2,
  "no-octal-escape": 2,
  "no-self-compare": 2,
  "no-shadow-restricted-names": 2,
  "no-cond-assign": 2,
  "no-debugger": 2,
  "no-dupe-keys": 2,
  "no-duplicate-case": 2,
  "no-empty-character-class": 2,
  "no-unreachable": 2,
  "no-unsafe-negation": 2,
  radix: 2,
  "valid-typeof": 2,
  "no-implicit-globals": [2],
  "no-unused-expressions": [
    2,
    { allowShortCircuit: true, allowTernary: true, allowTaggedTemplates: true },
  ],
  "no-proto": 2,

  // es2015 features
  "require-yield": 2,
  "template-curly-spacing": [2, "never"],

  // spacing details
  "space-infix-ops": 2,
  "space-in-parens": [2, "never"],
  "array-bracket-spacing": [2, "never"],
  "comma-spacing": [2, { before: false, after: true }],
  "space-before-function-paren": [
    2,
    {
      anonymous: "never",
      named: "never",
      asyncArrow: "always",
    },
  ],
  "no-whitespace-before-property": 2,
  "keyword-spacing": [
    2,
    {
      overrides: {
        if: { after: true },
        else: { after: true },
        for: { after: true },
        while: { after: true },
        do: { after: true },
        switch: { after: true },
        return: { after: true },
      },
    },
  ],
  "arrow-spacing": [
    2,
    {
      after: true,
      before: true,
    },
  ],
  "@stylistic/function-call-spacing": 2,
  "@stylistic/type-annotation-spacing": 2,

  // file whitespace
  "no-multiple-empty-lines": [2, { max: 2, maxEOF: 0 }],
  "no-mixed-spaces-and-tabs": 2,
  "no-trailing-spaces": 2,
  "linebreak-style": [process.platform === "win32" ? 0 : 2, "unix"],
  "@stylistic/indent": [
    2,
    2,
    { SwitchCase: 1, CallExpression: { arguments: 2 }, MemberExpression: 2 },
  ],
  "key-spacing": [
    2,
    {
      beforeColon: false,
    },
  ],
  "eol-last": 2,

  // copyright
  "notice/notice": [
    2,
    {
      mustMatch: "Copyright",
      templateFile: path.join(__dirname, "utils", "copyright.js"),
    },
  ],

  // react
  "react/react-in-jsx-scope": 0,
};

const noFloatingPromisesRules = {
  "@typescript-eslint/no-floating-promises": "error",
};

const noBooleanCompareRules = {
  "@typescript-eslint/no-unnecessary-boolean-literal-compare": 2,
};

const noWebGlobalsRuleList = [
  // Keep in sync with builtins from utilityScript.ts
  { name: "window", message: "Use InjectedScript.window instead" },
  { name: "document", message: "Use InjectedScript.document instead" },
  { name: "globalThis", message: "Use InjectedScript.window instead" },
  { name: "setTimeout", message: "Use InjectedScript.utils.builtins.setTimeout instead" },
  { name: "clearTimeout", message: "Use InjectedScript.utils.builtins.clearTimeout instead" },
  { name: "setInterval", message: "Use InjectedScript.utils.builtins.setInterval instead" },
  { name: "clearInterval", message: "Use InjectedScript.utils.builtins.clearInterval instead" },
  { name: "requestAnimationFrame", message: "Use InjectedScript.utils.builtins.requestAnimationFrame instead" },
  { name: "cancelAnimationFrame", message: "Use InjectedScript.utils.builtins.cancelAnimationFrame instead" },
  { name: "requestIdleCallback", message: "Use InjectedScript.utils.builtins.requestIdleCallback instead" },
  { name: "cancelIdleCallback", message: "Use InjectedScript.utils.builtins.cancelIdleCallback instead" },
  { name: "Date", message: "Use InjectedScript.utils.builtins.Date instead" },
  { name: "Intl", message: "Use InjectedScript.utils.builtins.Intl instead" },
  { name: "performance", message: "Use InjectedScript.utils.builtins.performance instead" },
];

const noNodeGlobalsRuleList = [{ name: "process" }];

const importOrderRules = {
  "import/order": [
    2,
    {
      groups: [
        ["builtin", "external"],
        "internal",
        ["parent", "sibling"],
        "index",
        "type",
      ],
    },
  ],
  "import/consistent-type-specifier-style": [2, "prefer-top-level"],
};

const languageOptions = {
  parser: tsParser,
  ecmaVersion: 9,
  sourceType: "module",
};

const languageOptionsWithTsConfig = {
  parser: tsParser,
  ecmaVersion: 9,
  sourceType: "module",
  parserOptions: {
    project: path.join(__dirname, "tsconfig.json"),
  },
};

const compat = new FlatCompat({
  baseDirectory: __dirname,
  recommendedConfig: js.configs.recommended,
  allConfig: js.configs.all,
});
const reactBaseConfig = fixupConfigRules(
  compat.extends("plugin:react/recommended", "plugin:react-hooks/recommended")
);
const reactFiles = [
  `packages/html-reporter/src/**/*.ts`,
  `packages/html-reporter/src/**/*.tsx`,
  `packages/recorder/src/**/*.ts`,
  `packages/recorder/src/**/*.tsx`,
  `packages/trace-viewer/src/**/*.ts`,
  `packages/trace-viewer/src/**/*.tsx`,
  `packages/dashboard/src/**/*.ts`,
  `packages/dashboard/src/**/*.tsx`,
  `packages/web/src/**/*.ts`,
  `packages/web/src/**/*.tsx`,
];

function reactPackageSection(packageName) {
  return {
    files: [
      `packages/${packageName}/src/**/*.ts`,
      `packages/${packageName}/src/**/*.tsx`,
      `packages/web/src/**/*.ts`,
      `packages/web/src/**/*.tsx`,
    ],
    languageOptions: languageOptionsWithTsConfig,
    rules: {
      ...baseRules,
      "no-console": 2,
      "no-restricted-syntax": [
        "error",
        {
          selector: "JSXElement > JSXText[value=/^;\n/]",
          message: 'Unexpected semicolon after JSX element',
        },
      ]
    },
  };
}

export default [
  {
    ignores,
  },
  {
    files: ["**/*.ts", "**/*.tsx"],
    plugins,
    languageOptions,
    rules: baseRules,
    settings: {
      react: { version: "detect" },
    },
  },
  {
    files: ["packages/**/*.ts"],
    languageOptions: languageOptionsWithTsConfig,
    rules: {
      "no-console": 2,
      "no-restricted-properties": [
        2,
        {
          object: "process",
          property: "exit",
          message:
            "Please use gracefullyProcessExitDoNotHang function to exit the process.",
        },
        { object: "process", property: "stdout" },
        { object: "process", property: "stderr" },
      ],
    },
  },
  {
    files: ["packages/**/*.ts"],
    rules: {
      ...importOrderRules,
    },
  },
  {
    files: ["packages/playwright/**/*.ts"],
    rules: {
      ...noFloatingPromisesRules,
    },
  },
  {
    files: ["packages/playwright-core/src/**/*.ts"],
    ignores: [
      "packages/playwright-core/src/entry/**",
    ],
    rules: {
      "no-restricted-imports": [
        "error",
        {
          patterns: [{
            group: ["**/coreBundle"],
            message: "coreBundle can only be imported from entry/ files. Use direct imports instead.",
          }],
        },
      ],
    },
  },
  {
    files: ["packages/playwright-core/src/**/*.ts"],
    ignores: [
      "packages/playwright-core/src/package.ts",
      "packages/playwright-core/src/cli/programWithTestStub.ts",
    ],
    rules: {
      "no-restricted-properties": [
        "error",
        {
          object: "process",
          property: "exit",
          message:
            "Please use gracefullyProcessExitDoNotHang function to exit the process.",
        },
        { object: "process", property: "stdout" },
        { object: "process", property: "stderr" },
        {
          object: "require",
          property: "resolve",
          message: "Use libPath() from package.ts instead of require.resolve.",
        },
      ],
    },
  },
  {
    files: ["packages/playwright-core/src/tools/**/*.ts"],
    rules: {
      "no-restricted-imports": [
        "error",
        {
          patterns: [
            {
              group: ["**/client", "**/client/**"],
              message: "tools/ must not import from client/",
            },
            {
              group: ["**/coreBundle"],
              message: "coreBundle can only be imported from entry/ files. Use direct imports instead.",
            },
          ],
        },
      ],
      "no-restricted-syntax": [
        "error",
        {
          selector: "TSAsExpression > TSAnyKeyword",
          message: "Avoid 'as any' — risk of accidentally casting to client interfaces. Use a precise type or add an eslint-disable with justification.",
        },
      ],
    },
  },
  {
    files: [
      "packages/isomorphic/**/*.ts",
    ],
    languageOptions: languageOptionsWithTsConfig,
    rules: {
      "no-restricted-globals": [
        "error",
        ...noNodeGlobalsRuleList,
        ...noWebGlobalsRuleList,
      ],
      ...noFloatingPromisesRules,
      ...noBooleanCompareRules,
    },
  },
  {
    files: [
      "packages/injected/src/**/*.ts",
    ],
    languageOptions: languageOptionsWithTsConfig,
    rules: {
      "no-restricted-globals": [
        "error",
        ...noWebGlobalsRuleList,
      ],
      ...noFloatingPromisesRules,
      ...noBooleanCompareRules,
    },
  },
  {
    files: [
      "packages/playwright-core/src/client/**/*.ts",
      "packages/playwright-core/src/protocol/**/*.ts",
    ],
    languageOptions: languageOptionsWithTsConfig,
    rules: {
      "no-restricted-globals": [
        "error",
        ...noNodeGlobalsRuleList,
      ],
      ...noFloatingPromisesRules,
      ...noBooleanCompareRules,
    },
  },
  {
    files: [
      "packages/playwright-core/src/server/**/*.ts",
      "packages/utils/**/*.ts",
    ],
    plugins: {
      "progress": progressPlugin,
    },
    languageOptions: languageOptionsWithTsConfig,
    rules: {
      "progress/await-must-use-progress": "error",
    },
  },
  {
    files: ["tests/**/*.spec.js", "tests/**/*.ts"],
    languageOptions: {
      parser: tsParser,
      ecmaVersion: 9,
      sourceType: "module",
      parserOptions: {
        project: path.join(__dirname, "tests", "tsconfig.json"),
      },
    },
    rules: {
      ...noFloatingPromisesRules,
    },
  },
  {
    files: ["packages/extension/src/**/*.ts", "packages/extension/src/**/*.tsx"],
    ignores: ["packages/extension/src/ui/**"],
    languageOptions: {
      parser: tsParser,
      ecmaVersion: 9,
      sourceType: "module",
      parserOptions: {
        project: path.join(__dirname, "packages", "extension", "tsconfig.json"),
      },
    },
  },
  {
    files: ["packages/extension/src/ui/**/*.ts", "packages/extension/src/ui/**/*.tsx"],
    languageOptions: {
      parser: tsParser,
      ecmaVersion: 9,
      sourceType: "module",
      parserOptions: {
        project: path.join(__dirname, "packages", "extension", "tsconfig.ui.json"),
      },
    },
  },
  ...reactBaseConfig.map((config) => ({
    ...config,
    files: reactFiles,
  })),
  reactPackageSection("html-reporter"),
  reactPackageSection("recorder"),
  reactPackageSection("trace-viewer"),
];
