import fs from 'fs'
import path from 'path'
import resolve from 'resolve'

import { MockFn } from './types'

const I = (x: any): any => x
const ORIGINAL = Symbol('Original')

const resolveModulePath = (modulePath: string, context: string) => {
  if (resolve.isCore(modulePath)) {
    return modulePath
  }
  const folder = path.dirname(context)
  const resolvedPath = resolve.sync(modulePath, {
    basedir: folder,
    extensions: ['.js', '.ts']
  })
  return fs.realpathSync(resolvedPath)
}

const replace = (modulePath: string, context: string, value: any) => {
  const fullPath = resolveModulePath(modulePath, context)

  // Overwrite the require cache
  require.cache[fullPath] = {
    exports: value,
  }

  return value
}

const mock = (modulePath: string, context: string, mock: MockFn = I) => {
  const fullPath = resolveModulePath(modulePath, context)
  const cache = require.cache[fullPath]

  // Check if we have already rewired this path
  let original
  if (
    typeof cache !== 'undefined' &&
    Object.hasOwnProperty.call(cache, ORIGINAL)
  ) {
    original = cache[ORIGINAL]
  } else {
    original = require(fullPath)
  }

  // Mock the module
  const mockedModule = mock(original)

  // Overwrite the require cache
  require.cache[fullPath] = {
    exports: mockedModule,
    [ORIGINAL]: original,
  }

  return mockedModule
}

// Remove a module from the require cache
const flush = (modulePath: string, context: string) => {
  const fullPath = resolveModulePath(modulePath, context)
  return delete require.cache[fullPath]
}

// Completely clear the require cache
const flushAllModules = () => {
  for (const key in require.cache) {
    if (Object.hasOwnProperty.call(require.cache, key)) {
      delete require.cache[key]
    }
  }
}

export { flush, flushAllModules, mock, replace, resolveModulePath }
