import type { Formatter } from 'tinyrainbow'
import type { MatcherHintOptions, Tester } from './types'
import { diff, printDiffOrStringify } from '@vitest/utils/diff'
import { stringify } from '@vitest/utils/display'
import { getType } from '@vitest/utils/helpers'
import c from 'tinyrainbow'
import { JEST_MATCHERS_OBJECT } from './constants'

export { diff } from '@vitest/utils/diff'
export { stringify }

const EXPECTED_COLOR = c.green
const RECEIVED_COLOR = c.red
const INVERTED_COLOR = c.inverse
const BOLD_WEIGHT = c.bold
const DIM_COLOR = c.dim

function matcherHint(
  matcherName: string,
  received = 'received',
  expected = 'expected',
  options: MatcherHintOptions = {},
): string {
  const {
    comment = '',
    isDirectExpectCall = false, // seems redundant with received === ''
    isNot = false,
    promise = '',
    secondArgument = '',
    expectedColor = EXPECTED_COLOR,
    receivedColor = RECEIVED_COLOR,
    secondArgumentColor = EXPECTED_COLOR,
  } = options
  let hint = ''
  let dimString = 'expect' // concatenate adjacent dim substrings

  if (!isDirectExpectCall && received !== '') {
    hint += DIM_COLOR(`${dimString}(`) + receivedColor(received)
    dimString = ')'
  }

  if (promise !== '') {
    hint += DIM_COLOR(`${dimString}.`) + promise
    dimString = ''
  }

  if (isNot) {
    hint += `${DIM_COLOR(`${dimString}.`)}not`
    dimString = ''
  }

  if (matcherName.includes('.')) {
    // Old format: for backward compatibility,
    // especially without promise or isNot options
    dimString += matcherName
  }
  else {
    // New format: omit period from matcherName arg
    hint += DIM_COLOR(`${dimString}.`) + matcherName
    dimString = ''
  }

  if (expected === '') {
    dimString += '()'
  }
  else {
    hint += DIM_COLOR(`${dimString}(`) + expectedColor(expected)
    if (secondArgument) {
      hint += DIM_COLOR(', ') + secondArgumentColor(secondArgument)
    }
    dimString = ')'
  }

  if (comment !== '') {
    dimString += ` // ${comment}`
  }

  if (dimString !== '') {
    hint += DIM_COLOR(dimString)
  }

  return hint
}

const SPACE_SYMBOL = '\u{00B7}' // middle dot

// Instead of inverse highlight which now implies a change,
// replace common spaces with middle dot at the end of any line.
function replaceTrailingSpaces(text: string): string {
  return text.replace(/\s+$/gm, spaces => SPACE_SYMBOL.repeat(spaces.length))
}

function printReceived(object: unknown): string {
  return RECEIVED_COLOR(replaceTrailingSpaces(stringify(object)))
}
function printExpected(value: unknown): string {
  return EXPECTED_COLOR(replaceTrailingSpaces(stringify(value)))
}

export function getMatcherUtils(): {
  EXPECTED_COLOR: Formatter
  RECEIVED_COLOR: Formatter
  INVERTED_COLOR: Formatter
  BOLD_WEIGHT: Formatter
  DIM_COLOR: Formatter
  diff: typeof diff
  matcherHint: typeof matcherHint
  printReceived: typeof printReceived
  printExpected: typeof printExpected
  printDiffOrStringify: typeof printDiffOrStringify
  printWithType: typeof printWithType
} {
  return {
    EXPECTED_COLOR,
    RECEIVED_COLOR,
    INVERTED_COLOR,
    BOLD_WEIGHT,
    DIM_COLOR,

    diff,
    matcherHint,
    printReceived,
    printExpected,
    printDiffOrStringify,
    printWithType,
  }
}

export function printWithType<T>(
  name: string,
  value: T,
  print: (value: T) => string,
): string {
  const type = getType(value)
  const hasType
    = type !== 'null' && type !== 'undefined'
      ? `${name} has type:  ${type}\n`
      : ''
  const hasValue = `${name} has value: ${print(value)}`
  return hasType + hasValue
}

export function addCustomEqualityTesters(newTesters: Array<Tester>): void {
  if (!Array.isArray(newTesters)) {
    throw new TypeError(
      `expect.customEqualityTesters: Must be set to an array of Testers. Was given "${getType(
        newTesters,
      )}"`,
    )
  }

  (globalThis as any)[JEST_MATCHERS_OBJECT].customEqualityTesters.push(
    ...newTesters,
  )
}

export function getCustomEqualityTesters(): Array<Tester> {
  return (globalThis as any)[JEST_MATCHERS_OBJECT].customEqualityTesters
}
