import { isObject } from 'lodash';
import path from 'path';
import test from 'playwright/test';
import url from 'url';

async function makeStep(name: string, call: () => Promise<any>) {
  const location = Utils.findCalledPlaceInTest();

  if (!location) return await test.step(name, call);

  const { path, line, column } = location;
  return await test.step(name, call, {
    location: { file: path, line, column },
    box: true,
  });
}

export function baseStep(
  target: Function,
  context: ClassMethodDecoratorContext
) {
  return async function replacementMethod(this: any, ...args: any[]) {
    return makeStep(
      this.constructor.name + '.' + String(context.name),
      async () => await target.call(this, ...args)
    );
  };
}

export function widgetStep(target: Function, context: any) {
  return async function provideStepInfo(this: any, ...args: any[]) {
    let name = `${this.constructor.name}`;
    if (
      isObject(this.findOptions) &&
      Object.keys(this.findOptions).length > 0
    ) {
      name += `(${Utils.renderInline(this.findOptions)})`;
    }
    name += `.${String(context.name)}`;

    if (args && args.length > 0) name += `(${Utils.renderInline(args)})`;
    else name += '()';

    return await makeStep(name, async () => await target.call(this, ...args));
  };
}

type DescribedStepOptions = {
  withArgs?: boolean;
  argsToOmit?: number[];
};
export function describedStep(
  description: string,
  { withArgs, argsToOmit = [] }: DescribedStepOptions = {}
) {
  return function (target: Function, context: any) {
    return async function provideStepInfo(this: any, ...args: any[]) {
      let name = description;
      if (withArgs) {
        const filteredArgs =
          argsToOmit.length > 0
            ? args.filter((_, i) => !argsToOmit.includes(i))
            : args;

        if (filteredArgs.length > 0)
          name += `(${Utils.renderInline(filteredArgs)})`;
        else name += '()';
      }

      return makeStep(name, async () => await target.call(this, ...args));
    };
  };
}

class Utils {
  static findTestFile(err?: Error) {
    const _err = err || new Error();
    const stack = _err.stack?.split('\n') ?? [];

    return (
      stack.find(
        (line) => /\/tests?\//.test(line) && !line.includes('node_modules')
      ) || null
    );
  }

  static findCalledPlaceInTest(err?: Error) {
    const testFile = this.findTestFile();
    let result: { path: string; line: number; column: number } | null = null;

    if (testFile) {
      const match =
        testFile.match(/\((.*):(\d+):(\d+)\)/) ||
        testFile.match(/at (.*):(\d+):(\d+)/);

      if (match) {
        const [_, filePath, line, column] = match;
        result = {
          path: filePath,
          line: parseInt(line),
          column: parseInt(column),
        };

        if (result.path.startsWith('file://')) {
          result.path = url.fileURLToPath(result.path);
        }
        result.path = path.resolve(result.path);
      }
    }
    return result;
  }

  static renderInline(value: any): string {
    if (value === null) return 'null';
    if (value === undefined) return 'undefined';
    if (typeof value === 'string') return `"${value}"`;
    if (typeof value === 'number' || typeof value === 'boolean')
      return String(value);
    if (typeof value === 'function')
      return `[Function ${value.name || 'anonymous'}]`;
    if (value instanceof Date) return `Date("${value.toISOString()}")`;

    if (Array.isArray(value)) {
      if (value.length === 1) return this.renderInline(value[0]);
      return `[${value.map((v) => this.renderInline(v)).join(', ')}]`;
    }

    if (typeof value === 'object') {
      const entries = Object.entries(value)
        .map(([k, v]) => `${k}: ${this.renderInline(v)}`)
        .join(', ');
      return entries ? `{${entries}}` : '';
    }

    return String(value);
  }
}
