import type { Test } from '../../vitest/src/runtime/runner/types'
import type { Assertion } from './types'
import { processError } from '@vitest/utils/error'
import { noop } from '@vitest/utils/helpers'

export function createAssertionMessage(
  util: Chai.ChaiUtils,
  assertion: Chai.Assertion,
  hasArgs: boolean,
) {
  const soft = util.flag(assertion, 'soft') ? '.soft' : ''
  const not = util.flag(assertion, 'negate') ? 'not.' : ''
  const name = `${util.flag(assertion, '_name')}(${hasArgs ? 'expected' : ''})`
  const promiseName = util.flag(assertion, 'promise')
  const promise = promiseName ? `.${promiseName}` : ''
  return `expect${soft}(actual)${promise}.${not}${name}`
}

export function recordAsyncExpect(
  _test: any,
  promise: Promise<any>,
  assertion: string,
  error: Error,
  isSoft?: boolean,
): Promise<any> {
  const test = _test as Test | undefined
  // record promise for test, that resolves before test ends
  if (test && promise instanceof Promise) {
    // if promise is explicitly awaited, remove it from the list
    promise = promise.finally(() => {
      if (!test.promises) {
        return
      }
      const index = test.promises.indexOf(promise)
      if (index !== -1) {
        test.promises.splice(index, 1)
      }
    })

    // record promise
    if (!test.promises) {
      test.promises = []
    }
    // setup `expect.soft` handler here instead of `wrapAssertion`
    // to avoid double error tracking while keeping non-await promise detection.
    if (isSoft) {
      promise = promise.then(noop, (err) => {
        handleTestError(test, err)
      })
    }
    test.promises.push(promise)

    let resolved = false
    test.onFinished ??= []
    test.onFinished.push(() => {
      if (!resolved) {
        const processor = (globalThis as any).__vitest_worker__?.onFilterStackTrace || ((s: string) => s || '')
        const stack = processor(error.stack)
        console.warn([
          `Promise returned by \`${assertion}\` was not awaited. `,
          'Vitest currently auto-awaits hanging assertions at the end of the test, but this will cause the test to fail in the next Vitest major. ',
          'Please remember to await the assertion.\n',
          stack,
        ].join(''))
      }
    })

    return {
      then(onFulfilled, onRejected) {
        resolved = true
        return promise.then(onFulfilled, onRejected)
      },
      catch(onRejected) {
        resolved = true
        return promise.catch(onRejected)
      },
      finally(onFinally) {
        resolved = true
        return promise.finally(onFinally)
      },
      [Symbol.toStringTag]: 'Promise',
    } satisfies Promise<any>
  }

  return promise
}

function handleTestError(test: Test, err: unknown) {
  test.result ||= { state: 'fail' }
  test.result.state = 'fail'
  test.result.errors ||= []
  test.result.errors.push(processError(err))
}

/** wrap assertion function to support `expect.soft` and provide assertion name as `_name` */
export function wrapAssertion(
  utils: Chai.ChaiUtils,
  name: string,
  fn: (this: Chai.AssertionStatic & Assertion, ...args: any[]) => void | PromiseLike<void>,
) {
  return function (this: Chai.AssertionStatic & Assertion, ...args: any[]): void | PromiseLike<void> {
    // private
    if (name !== 'withTest') {
      utils.flag(this, '_name', name)
    }

    if (!utils.flag(this, 'soft')) {
      // avoid WebKit's proper tail call to preserve stacktrace offset for inline snapshot
      // https://webkit.org/blog/6240/ecmascript-6-proper-tail-calls-in-webkit
      try {
        return fn.apply(this, args)
      }
      finally {
        // no lint
      }
    }

    const test: Test = utils.flag(this, 'vitest-test')

    if (!test) {
      throw new Error('expect.soft() can only be used inside a test')
    }

    try {
      const result = fn.apply(this, args)

      if (result && typeof result === 'object' && typeof result.then === 'function') {
        return result.then(noop, (err) => {
          handleTestError(test, err)
        })
      }

      return result
    }
    catch (err) {
      handleTestError(test, err)
    }
  }
}
