import type { Abi } from "../../../types/artifacts.js";
import type { ChainType } from "../../../types/network.js";
import type { SolidityTestProfileConfig } from "../../../types/test.js";
import type {
  SolidityTestRunnerConfigArgs,
  PathPermission,
  Artifact,
  ObservabilityConfig,
  TestFunctionOverride,
} from "@nomicfoundation/edr";

import { styleText } from "node:util";

import {
  opGenesisState,
  l1GenesisState,
  FsAccessPermission,
  CollectStackTraces,
  opHardforkFromString,
  l1HardforkFromString,
} from "@nomicfoundation/edr";
import { toBigInt } from "@nomicfoundation/hardhat-utils/bigint";
import { hexStringToBytes } from "@nomicfoundation/hardhat-utils/hex";

import { DEFAULT_VERBOSITY, OPTIMISM_CHAIN_TYPE } from "../../constants.js";
import { resolveHardfork } from "../network-manager/config-resolution.js";
import { hardhatHardforkToEdrSpecId } from "../network-manager/edr/utils/convert-to-edr.js";
import { verbosityToIncludeTraces } from "../network-manager/edr/utils/trace-formatters.js";

import { formatArtifactId } from "./formatters.js";

interface SolidityTestConfigParams {
  chainType: ChainType;
  projectRoot: string;
  hardfork?: string;
  config: Omit<SolidityTestProfileConfig, "eip712Types">;
  verbosity: number;
  observability?: ObservabilityConfig;
  testPattern?: string;
  generateGasReport: boolean;
  testFunctionOverrides?: TestFunctionOverride[];
  eip712CanonicalTypes?: string[];
}

export async function solidityTestConfigToSolidityTestRunnerConfigArgs({
  chainType,
  projectRoot,
  hardfork,
  config,
  verbosity,
  observability,
  testPattern,
  generateGasReport,
  testFunctionOverrides,
  eip712CanonicalTypes,
}: SolidityTestConfigParams): Promise<SolidityTestRunnerConfigArgs> {
  const fsPermissions: PathPermission[] | undefined = [
    config.fsPermissions?.readWriteFile?.map((p) => ({
      access: FsAccessPermission.ReadWriteFile,
      path: p,
    })) ?? [],
    config.fsPermissions?.readFile?.map((p) => ({
      access: FsAccessPermission.ReadFile,
      path: p,
    })) ?? [],
    config.fsPermissions?.writeFile?.map((p) => ({
      access: FsAccessPermission.WriteFile,
      path: p,
    })) ?? [],
    config.fsPermissions?.dangerouslyReadWriteDirectory?.map((p) => ({
      access: FsAccessPermission.DangerouslyReadWriteDirectory,
      path: p,
    })) ?? [],
    config.fsPermissions?.readDirectory?.map((p) => ({
      access: FsAccessPermission.ReadDirectory,
      path: p,
    })) ?? [],
    config.fsPermissions?.dangerouslyWriteDirectory?.map((p) => ({
      access: FsAccessPermission.DangerouslyWriteDirectory,
      path: p,
    })) ?? [],
  ].flat(1);

  const hexToBytes = (hex: string | undefined) =>
    hex !== undefined ? hexStringToBytes(hex) : undefined;

  const sender = hexToBytes(config.from);
  const txOrigin = hexToBytes(config.txOrigin);
  const blockCoinbase = hexToBytes(config.coinbase);

  const resolvedHardfork = hardhatHardforkToEdrSpecId(
    resolveHardfork(hardfork, chainType),
    chainType,
  );

  const localPredeploys =
    chainType === OPTIMISM_CHAIN_TYPE
      ? opGenesisState(opHardforkFromString(resolvedHardfork))
      : l1GenesisState(l1HardforkFromString(resolvedHardfork));

  const includeTraces = verbosityToIncludeTraces(verbosity);

  const blockGasLimit =
    typeof config.blockGasLimit === "number" ||
    typeof config.blockGasLimit === "bigint"
      ? toBigInt(config.blockGasLimit)
      : undefined;
  const disableBlockGasLimit = blockGasLimit === undefined;

  const transactionGasCap =
    typeof config.transactionGasCap === "number" ||
    typeof config.transactionGasCap === "bigint"
      ? toBigInt(config.transactionGasCap)
      : undefined;
  const disableTransactionGasCap = transactionGasCap === undefined;

  const blockDifficulty = config.prevRandao;

  let ethRpcUrl: string | undefined;
  if (config.forking?.url !== undefined) {
    ethRpcUrl = await config.forking.url.get();
  }

  const forkBlockNumber = config.forking?.blockNumber;

  let rpcEndpoints: Record<string, string> | undefined;
  if (config.forking?.rpcEndpoints !== undefined) {
    rpcEndpoints = {};
    for (const [name, configValue] of Object.entries(
      config.forking.rpcEndpoints,
    )) {
      rpcEndpoints[name] = await configValue.get();
    }
  }

  const shouldAlwaysCollectStackTraces = verbosity > DEFAULT_VERBOSITY;

  return {
    projectRoot,
    hardfork: resolvedHardfork,
    ...config,
    fsPermissions,
    localPredeploys,
    sender,
    txOrigin,
    blockCoinbase,
    observability,
    testPattern,
    includeTraces,
    blockGasLimit,
    disableBlockGasLimit,
    transactionGasCap,
    disableTransactionGasCap,
    blockDifficulty,
    ethRpcUrl,
    forkBlockNumber,
    rpcEndpoints,
    generateGasReport,
    collectStackTraces: shouldAlwaysCollectStackTraces
      ? CollectStackTraces.Always
      : CollectStackTraces.OnFailure,
    testFunctionOverrides,
    eip712CanonicalTypes,
  };
}

export function isTestSuiteArtifact(artifact: Artifact): boolean {
  const bytecode = artifact.contract.bytecode;

  // Skip abstract contracts and interfaces i.e. those with no bytecode
  if (bytecode === "" || bytecode === "0x" || bytecode === undefined) {
    return false;
  }

  const abi: Abi = JSON.parse(artifact.contract.abi);
  return abi.some(({ type, name }) => {
    if (type === "function" && typeof name === "string") {
      return name.startsWith("test") || name.startsWith("invariant");
    }

    return false;
  });
}

export function warnDeprecatedTestFail(
  artifact: Artifact,
  sourceNameToUserSourceName: Map<string, string>,
): void {
  const abi: Abi = JSON.parse(artifact.contract.abi);

  abi.forEach(({ type, name }) => {
    if (
      type === "function" &&
      typeof name === "string" &&
      name.startsWith("testFail")
    ) {
      const formattedLocation = formatArtifactId(
        artifact.id,
        sourceNameToUserSourceName,
      );
      const warningMessage = `${styleText("yellow", "Warning")}: ${name} The support for the prefix \`testFail*\` has been removed. Consider using \`vm.expectRevert()\` for testing reverts in ${formattedLocation}\n`;

      console.warn(warningMessage);
    }
  });
}
