import { faker } from '@faker-js/faker';

// Modules that don't expose simple data-generation functions
const SKIPPED_MODULES = new Set(['helpers', 'definitions', 'rawDefinitions']);

// Deprecated falso-style aliases kept for backwards compatibility.
// Maps legacy `$rand*` names to their faker equivalents (flattened names).
// NOTE: configuration options are NOT translated - options must be provided
// using the faker option shape, under the faker function name.
const DEPRECATED_ALIASES = {
  randEmail: 'internetEmail',
  randFullName: 'personFullName',
  randFirstName: 'personFirstName',
  randLastName: 'personLastName',
  randUserName: 'internetUsername',
  randPassword: 'internetPassword',
  randUuid: 'stringUuid',
  randPhoneNumber: 'phoneNumber',
  randCity: 'locationCity',
  randCountry: 'locationCountry',
  randStreetAddress: 'locationStreetAddress',
  randZipCode: 'locationZipCode',
  randCompanyName: 'companyName',
  randNumber: 'numberInt',
  randBoolean: 'datatypeBoolean',
  randUrl: 'internetUrl',
  randIp: 'internetIpv4',
  randWord: 'loremWord',
  randSentence: 'loremSentence',
  randParagraph: 'loremParagraph',
  randText: 'loremText',
  randJobTitle: 'personJobTitle',
  randColor: 'colorHuman',
  randProductName: 'commerceProductName'
};

const flattenName = (moduleName, functionName) =>
  `${moduleName}${functionName[0].toUpperCase()}${functionName.slice(1)}`;

// Builds a map of flattened function names (e.g. `internetEmail`) to
// zero/one-argument faker functions (e.g. `faker.internet.email`)
const buildFakerFunctionMap = () => {
  const functions = {};

  for (const moduleName of Object.keys(faker)) {
    if (SKIPPED_MODULES.has(moduleName) || moduleName.startsWith('_')) {
      continue;
    }

    const mod = faker[moduleName];
    if (!mod || typeof mod !== 'object') {
      continue;
    }

    // Walk the prototype chain: some faker modules inherit methods
    // (e.g. DateModule extends SimpleDateModule)
    let proto = Object.getPrototypeOf(mod);
    while (proto && proto !== Object.prototype) {
      for (const functionName of Object.getOwnPropertyNames(proto)) {
        if (functionName === 'constructor' || functionName.startsWith('_')) {
          continue;
        }

        if (typeof mod[functionName] !== 'function') {
          continue;
        }

        // Only functions taking at most one argument are supported, as
        // configuration is passed as a single (object) argument
        if (mod[functionName].length > 1) {
          continue;
        }

        const flatName = flattenName(moduleName, functionName);
        if (!functions[flatName]) {
          functions[flatName] = mod[functionName].bind(mod);
        }
      }

      proto = Object.getPrototypeOf(proto);
    }
  }

  return functions;
};

const fakerFunctionMap = buildFakerFunctionMap();

const getFakerFunctions = () => Object.keys(fakerFunctionMap);

const warnedAliases = new Set();

function ArtilleryPluginFakeData(script, events) {
  this.script = script;
  this.events = events;

  const pluginConfig =
    script.config['fake-data'] || script.config.plugins['fake-data'];

  function fakeDataHandler(context, _ee, next) {
    for (const [funcName, fakerFunc] of Object.entries(
      fakerFunctionMap
    ) as [string, any][]) {
      context.funcs[`$${funcName}`] = () => {
        if (pluginConfig[funcName]) {
          return fakerFunc(pluginConfig[funcName]);
        }
        return fakerFunc();
      };
    }

    for (const [aliasName, funcName] of Object.entries(DEPRECATED_ALIASES)) {
      context.funcs[`$${aliasName}`] = () => {
        if (!warnedAliases.has(aliasName)) {
          warnedAliases.add(aliasName);
          console.warn(
            `[fake-data] $${aliasName} is deprecated and will be removed in a future release. Use $${funcName} instead.`
          );
        }

        return context.funcs[`$${funcName}`]();
      };
    }

    next();
  }

  script.scenarios = script.scenarios.map((scenario) => {
    scenario.beforeScenario = [].concat(scenario.beforeScenario || []);
    scenario.beforeScenario.push('fakeDataHandler');
    return scenario;
  });

  if (!script.config.processor) {
    script.config.processor = {};
  }
  // In the main thread config.processor may still be an unresolved
  // path (a string). Attaching functions to it was a silent no-op
  // under sloppy mode; ES modules are strict, so guard explicitly.
  // Workers load the processor into an object before plugins run.
  const canAttach = typeof script.config.processor === 'object';

  if (canAttach) {
    script.config.processor.fakeDataHandler = fakeDataHandler;
  }

  return this;
}

const getDeprecatedAliases = () => ({ ...DEPRECATED_ALIASES });

export {
  ArtilleryPluginFakeData as Plugin,
  getFakerFunctions,
  getDeprecatedAliases
};
