import type { Ivya } from 'ivya'
import type { SerializedConfig, VitestTestRunner, WorkerGlobalState } from 'vitest'
import type { OTELCarrier, Traces } from 'vitest/internal/traces'
import type { IframeOrchestrator } from './orchestrator'
import type { CommandsManager } from './tester/tester-utils'
import type { BrowserTraceAttempt } from './tester/trace'

export async function importId(id: string): Promise<any> {
  const name = `/@id/${id}`.replace(/\\/g, '/')
  return getBrowserState().wrapModule(() => import(/* @vite-ignore */ name))
}

export async function importFs(id: string): Promise<any> {
  const name = `/@fs/${id}`.replace(/\\/g, '/')
  return getBrowserState().wrapModule(() => import(/* @vite-ignore */ name))
}

export const moduleRunner = {
  isBrowser: true,

  import: (id: string): Promise<any> => {
    if (id[0] === '/' || id[1] === ':') {
      return importFs(id)
    }
    return importId(id)
  },
}

export const now: () => number = globalThis.performance
  ? globalThis.performance.now.bind(globalThis.performance)
  : Date.now

export function getConfig(): SerializedConfig {
  return getBrowserState().config
}

export function ensureAwaited<T>(promise: (error?: Error) => Promise<T>): Promise<T> {
  const test = getWorkerState().current
  if (!test || test.type !== 'test') {
    return promise()
  }
  let awaited = false
  const sourceError = new Error('STACK_TRACE_ERROR')
  test.onFinished ??= []
  test.onFinished.push(() => {
    if (!awaited) {
      const error = new Error(
        `The call was not awaited. This method is asynchronous and must be awaited; otherwise, the call will not start to avoid unhandled rejections.`,
      )
      error.stack = sourceError.stack?.replace(sourceError.message, error.message)
      throw error
    }
  })
  // don't even start the promise if it's not awaited to not cause any unhanded promise rejections
  let promiseResult: Promise<T> | undefined
  return {
    then(onFulfilled, onRejected) {
      awaited = true
      return (promiseResult ||= promise(sourceError)).then(onFulfilled, onRejected)
    },
    catch(onRejected) {
      awaited = true
      return (promiseResult ||= promise(sourceError)).catch(onRejected)
    },
    finally(onFinally) {
      awaited = true
      return (promiseResult ||= promise(sourceError)).finally(onFinally)
    },
    [Symbol.toStringTag]: 'Promise',
  } satisfies Promise<T>
}

// This object is the shared runtime bus for browser artifacts loaded into the
// same page. Some browser code is built and served as independent bundles, so
// getBrowserState() is the API they use to share runtime state instead of
// relying on imports resolving to the same module instance.
export interface BrowserRunnerState {
  files: string[]
  runningFiles: string[]
  config: SerializedConfig
  provider: string
  runner: VitestTestRunner
  viteConfig: {
    root: string
  }
  providedContext: string
  type: 'tester' | 'orchestrator'
  disposeExceptionTracker: () => void
  wrapModule: <T>(module: () => T) => T
  iframeId?: string
  sessionId: string
  testerId: string
  otelCarrier?: OTELCarrier
  method: 'run' | 'collect'
  orchestrator?: IframeOrchestrator
  commands: CommandsManager
  activeTraceTaskIds: Set<string>
  browserTraceAttempts: Map<string, BrowserTraceAttempt>
  // lazily loaded only when traceView is enabled
  browserTraceDomSnapshot?: typeof import('rrweb-snapshot')
  selectorEngine: Ivya
  traces: Traces
  cleanups: Array<() => unknown>
  cdp?: {
    on: (event: string, listener: (payload: any) => void) => void
    once: (event: string, listener: (payload: any) => void) => void
    off: (event: string, listener: (payload: any) => void) => void
    send: (method: string, params?: Record<string, unknown>) => Promise<unknown>
    emit: (event: string, payload: unknown) => void
  }
  aria: typeof import('ivya/aria')
}

/* @__NO_SIDE_EFFECTS__ */
export function getBrowserState(): BrowserRunnerState {
  // @ts-expect-error not typed global
  return window.__vitest_browser_runner__
}

/* @__NO_SIDE_EFFECTS__ */
export function getWorkerState(): WorkerGlobalState {
  // @ts-expect-error not typed global
  const state = window.__vitest_worker__
  if (!state) {
    throw new Error('Worker state is not found. This is an issue with Vitest. Please, open an issue.')
  }
  return state
}
