/**
 * @fileoverview Rule to suggest using "Reflect" api over Function/Object methods
 * @author Keith Cirkel <http://keithcirkel.co.uk>
 * @deprecated in ESLint v3.9.0
 */
'use strict';

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

/** @type {import('../types').Rule.RuleModule} */
module.exports = {
  meta: {
    type: 'suggestion',

    docs: {
      description: 'Require `Reflect` methods where applicable',
      recommended: false,
      url: 'https://eslint.org/docs/latest/rules/prefer-reflect',
    },

    deprecated: {
      message: 'The original intention of this rule was misguided.',
      url: 'https://eslint.org/docs/latest/rules/prefer-reflect',
      deprecatedSince: '3.9.0',
      availableUntil: null,
      replacedBy: [],
    },

    schema: [
      {
        type: 'object',
        properties: {
          exceptions: {
            type: 'array',
            items: {
              enum: [
                'apply',
                'call',
                'delete',
                'defineProperty',
                'getOwnPropertyDescriptor',
                'getPrototypeOf',
                'setPrototypeOf',
                'isExtensible',
                'getOwnPropertyNames',
                'preventExtensions',
              ],
            },
            uniqueItems: true,
          },
        },
        additionalProperties: false,
      },
    ],

    messages: {
      preferReflect: 'Avoid using {{existing}}, instead use {{substitute}}.',
    },
  },

  create(context) {
    const existingNames = {
      apply: 'Function.prototype.apply',
      call: 'Function.prototype.call',
      defineProperty: 'Object.defineProperty',
      getOwnPropertyDescriptor: 'Object.getOwnPropertyDescriptor',
      getPrototypeOf: 'Object.getPrototypeOf',
      setPrototypeOf: 'Object.setPrototypeOf',
      isExtensible: 'Object.isExtensible',
      getOwnPropertyNames: 'Object.getOwnPropertyNames',
      preventExtensions: 'Object.preventExtensions',
    };

    const reflectSubstitutes = {
      apply: 'Reflect.apply',
      call: 'Reflect.apply',
      defineProperty: 'Reflect.defineProperty',
      getOwnPropertyDescriptor: 'Reflect.getOwnPropertyDescriptor',
      getPrototypeOf: 'Reflect.getPrototypeOf',
      setPrototypeOf: 'Reflect.setPrototypeOf',
      isExtensible: 'Reflect.isExtensible',
      getOwnPropertyNames: 'Reflect.getOwnPropertyNames',
      preventExtensions: 'Reflect.preventExtensions',
    };

    const exceptions = (context.options[0] || {}).exceptions || [];

    /**
     * Reports the Reflect violation based on the `existing` and `substitute`
     * @param {Object} node The node that violates the rule.
     * @param {string} existing The existing method name that has been used.
     * @param {string} substitute The Reflect substitute that should be used.
     * @returns {void}
     */
    function report(node, existing, substitute) {
      context.report({
        node,
        messageId: 'preferReflect',
        data: {
          existing,
          substitute,
        },
      });
    }

    return {
      CallExpression(node) {
        const methodName = (node.callee.property || {}).name;
        const isReflectCall = (node.callee.object || {}).name === 'Reflect';
        const hasReflectSubstitute = Object.hasOwn(
          reflectSubstitutes,
          methodName
        );
        const userConfiguredException = exceptions.includes(methodName);

        if (
          hasReflectSubstitute &&
          !isReflectCall &&
          !userConfiguredException
        ) {
          report(
            node,
            existingNames[methodName],
            reflectSubstitutes[methodName]
          );
        }
      },
      UnaryExpression(node) {
        const isDeleteOperator = node.operator === 'delete';
        const targetsIdentifier = node.argument.type === 'Identifier';
        const userConfiguredException = exceptions.includes('delete');

        if (
          isDeleteOperator &&
          !targetsIdentifier &&
          !userConfiguredException
        ) {
          report(node, 'the delete keyword', 'Reflect.deleteProperty');
        }
      },
    };
  },
};