import type { InlineConfig as ViteInlineConfig, UserConfig as ViteUserConfig } from 'vite'
import type { environments } from '../../integrations/env'
import type { Vitest, VitestOptions } from '../core'
import type { TestModule, TestSuite } from '../reporters/reported-tasks'
import type { TestSpecification } from '../test-specification'
import type { UserConfig, VitestEnvironment, VitestRunMode } from '../types/config'
import { mkdirSync, writeFileSync } from 'node:fs'
import { dirname, isAbsolute, relative, resolve } from 'pathe'
import { CoverageProviderMap } from '../../utils/coverage'
import { createVitest } from '../create'
import { FilesNotFoundError, GitNotFoundError, IncludeTaskLocationDisabledError, LocationFilterFileNotFoundError, RangeLocationFilterProvidedError } from '../errors'
import { registerConsoleShortcuts } from '../stdin'

export interface CliOptions extends UserConfig {
  /**
   * Override the watch mode
   */
  run?: boolean
  /**
   * Removes colors from the console output
   */
  color?: boolean
  /**
   * Output collected tests as JSON or to a file
   */
  json?: string | boolean
  /**
   * Output collected test files only
   */
  filesOnly?: boolean
  /**
   * Parse files statically instead of running them to collect tests
   * @experimental
   */
  staticParse?: boolean
  /**
   * How many tests to process at the same time
   * @experimental
   */
  staticParseConcurrency?: number

  /**
   * Override vite config's configLoader from CLI.
   * Use `bundle` to bundle the config with esbuild or `runner` (experimental) to process it on the fly (default: `bundle`).
   * This is only available with **vite version 6.1.0** and above.
   * @experimental
   */
  configLoader?: ViteInlineConfig extends { configLoader?: infer T } ? T : never

  /**
   * Only run benchmark projects, filtering out all other projects.
   * Set automatically by `vitest bench`.
   * @internal
   */
  benchmarkOnly?: boolean
}

/**
 * Start Vitest programmatically
 *
 * Returns a Vitest instance if initialized successfully.
 */
export async function startVitest(
  cliFilters?: string[],
  options?: CliOptions,
  viteOverrides?: ViteUserConfig,
  vitestOptions?: VitestOptions,
): Promise<Vitest>
/**
 * @deprecated The `mode` argument is no longer used. Use `startVitest(cliFilters?, options?, viteOverrides?, vitestOptions?)` instead.
 */
export async function startVitest(
  mode: VitestRunMode,
  cliFilters?: string[],
  options?: CliOptions,
  viteOverrides?: ViteUserConfig,
  vitestOptions?: VitestOptions,
): Promise<Vitest>
export async function startVitest(
  modeOrCliFilters?: VitestRunMode | string[],
  cliFiltersOrOptions?: string[] | CliOptions,
  optionsOrViteOverrides?: CliOptions | ViteUserConfig,
  viteOverridesOrVitestOptions?: ViteUserConfig | VitestOptions,
  maybeVitestOptions?: VitestOptions,
): Promise<Vitest> {
  let cliFilters: string[]
  let options: CliOptions
  let viteOverrides: ViteUserConfig | undefined
  let vitestOptions: VitestOptions | undefined
  if (typeof modeOrCliFilters === 'string') {
    cliFilters = (cliFiltersOrOptions as string[] | undefined) ?? []
    options = (optionsOrViteOverrides as CliOptions | undefined) ?? {}
    viteOverrides = viteOverridesOrVitestOptions as ViteUserConfig | undefined
    vitestOptions = maybeVitestOptions
  }
  else {
    cliFilters = modeOrCliFilters ?? []
    options = (cliFiltersOrOptions as CliOptions | undefined) ?? {}
    viteOverrides = optionsOrViteOverrides as ViteUserConfig | undefined
    vitestOptions = viteOverridesOrVitestOptions as VitestOptions | undefined
  }
  const root = resolve(options.root || process.cwd())

  const ctx = await prepareVitest(
    options,
    viteOverrides,
    vitestOptions,
    cliFilters,
  )

  if (ctx._coverageOptions.enabled) {
    const provider = ctx._coverageOptions.provider || 'v8'
    const requiredPackages = CoverageProviderMap[provider]

    if (requiredPackages) {
      if (
        !(await ctx.packageInstaller.ensureInstalled(requiredPackages, root, ctx.version))
      ) {
        process.exitCode = 1
        return ctx
      }
    }
  }

  const stdin = vitestOptions?.stdin || process.stdin
  const stdout = vitestOptions?.stdout || process.stdout
  let stdinCleanup
  if (stdin.isTTY && ctx.config.watch) {
    stdinCleanup = registerConsoleShortcuts(ctx, stdin, stdout)
  }

  ctx.onAfterSetServer(() => {
    if (ctx.config.standalone) {
      ctx.standalone()
    }
    else {
      ctx.start(cliFilters)
    }
  })

  try {
    if (ctx.config.listTags) {
      await ctx.listTags()
    }
    else if (ctx.config.clearCache) {
      await ctx.experimental_clearCache()
    }
    else if (ctx.config.mergeReports) {
      await ctx.mergeReports()
    }
    else if (ctx.config.standalone) {
      await ctx.standalone()
    }
    else {
      await ctx.start(cliFilters)
    }
    return ctx
  }
  catch (e) {
    if (e instanceof FilesNotFoundError) {
      return ctx
    }

    if (e instanceof GitNotFoundError) {
      ctx.logger.error(e.message)
      return ctx
    }

    if (
      e instanceof IncludeTaskLocationDisabledError
      || e instanceof RangeLocationFilterProvidedError
      || e instanceof LocationFilterFileNotFoundError
    ) {
      ctx.logger.printError(e, { verbose: false })
      return ctx
    }

    process.exitCode = 1
    ctx.logger.printError(e, { fullStack: true, type: 'Unhandled Error' })
    ctx.logger.error('\n\n')
    return ctx
  }
  finally {
    if (!ctx?.shouldKeepServer()) {
      stdinCleanup?.()
      await ctx.close()
    }
  }
}

export async function prepareVitest(
  options?: CliOptions,
  viteOverrides?: ViteUserConfig,
  vitestOptions?: VitestOptions,
  cliFilters?: string[],
): Promise<Vitest>
/**
 * @deprecated The `mode` argument is no longer used. Use `prepareVitest(options?, viteOverrides?, vitestOptions?, cliFilters?)` instead.
 */
export async function prepareVitest(
  mode: VitestRunMode,
  options?: CliOptions,
  viteOverrides?: ViteUserConfig,
  vitestOptions?: VitestOptions,
  cliFilters?: string[],
): Promise<Vitest>
export async function prepareVitest(
  modeOrOptions?: VitestRunMode | CliOptions,
  optionsOrViteOverrides?: CliOptions | ViteUserConfig,
  viteOverridesOrVitestOptions?: ViteUserConfig | VitestOptions,
  vitestOptionsOrCliFilters?: VitestOptions | string[],
  maybeCliFilters?: string[],
): Promise<Vitest> {
  let options: CliOptions
  let viteOverrides: ViteUserConfig | undefined
  let vitestOptions: VitestOptions | undefined
  let cliFilters: string[] | undefined
  if (typeof modeOrOptions === 'string') {
    options = (optionsOrViteOverrides as CliOptions | undefined) ?? {}
    viteOverrides = viteOverridesOrVitestOptions as ViteUserConfig | undefined
    vitestOptions = vitestOptionsOrCliFilters as VitestOptions | undefined
    cliFilters = maybeCliFilters
  }
  else {
    options = modeOrOptions ?? {}
    viteOverrides = optionsOrViteOverrides as ViteUserConfig | undefined
    vitestOptions = viteOverridesOrVitestOptions as VitestOptions | undefined
    cliFilters = vitestOptionsOrCliFilters as string[] | undefined
  }
  process.env.TEST = 'true'
  process.env.VITEST = 'true'
  process.env.NODE_ENV ??= 'test'

  if (options.run) {
    options.watch = false
  }

  if (options.standalone && (cliFilters?.length || 0) > 0) {
    options.standalone = false
  }

  // this shouldn't affect _application root_ that can be changed inside config
  const root = resolve(options.root || process.cwd())

  const ctx = await createVitest(options, viteOverrides, vitestOptions)

  const environmentPackage = getEnvPackageName(ctx.config.environment)

  if (
    environmentPackage
    && !(await ctx.packageInstaller.ensureInstalled(environmentPackage, root))
  ) {
    process.exitCode = 1
    return ctx
  }

  return ctx
}

export function processCollected(ctx: Vitest, files: TestModule[], options: CliOptions): void {
  let errorsPrinted = false

  forEachSuite(files, (suite) => {
    suite.errors().forEach((error) => {
      errorsPrinted = true
      ctx.logger.printError(error, {
        project: suite.project,
      })
    })
  })

  if (errorsPrinted) {
    return
  }

  if (typeof options.json !== 'undefined') {
    return processJsonOutput(files, options)
  }

  return formatCollectedAsString(files).forEach(test => console.log(test))
}

export function outputFileList(files: TestSpecification[], options: CliOptions): void {
  if (typeof options.json !== 'undefined') {
    return outputJsonFileList(files, options)
  }

  formatFilesAsString(files, options).map(file => console.log(file))
}

function outputJsonFileList(files: TestSpecification[], options: CliOptions) {
  if (typeof options.json === 'boolean') {
    return console.log(JSON.stringify(formatFilesAsJSON(files), null, 2))
  }
  if (typeof options.json === 'string') {
    const jsonPath = resolve(options.root || process.cwd(), options.json)
    mkdirSync(dirname(jsonPath), { recursive: true })
    writeFileSync(jsonPath, JSON.stringify(formatFilesAsJSON(files), null, 2))
  }
}

function formatFilesAsJSON(files: TestSpecification[]) {
  return files.map((file) => {
    const result: any = {
      file: file.moduleId,
    }

    if (file.project.name) {
      result.projectName = file.project.name
    }
    return result
  })
}

function formatFilesAsString(files: TestSpecification[], options: CliOptions) {
  return files.map((file) => {
    let name = relative(options.root || process.cwd(), file.moduleId)
    if (file.project.name) {
      name = `[${file.project.name}] ${name}`
    }
    return name
  })
}

function processJsonOutput(files: TestModule[], options: CliOptions) {
  if (typeof options.json === 'boolean') {
    return console.log(JSON.stringify(formatCollectedAsJSON(files), null, 2))
  }

  if (typeof options.json === 'string') {
    const jsonPath = resolve(options.root || process.cwd(), options.json)
    mkdirSync(dirname(jsonPath), { recursive: true })
    writeFileSync(jsonPath, JSON.stringify(formatCollectedAsJSON(files), null, 2))
  }
}

function forEachSuite(modules: TestModule[], callback: (suite: TestSuite | TestModule) => void) {
  modules.forEach((testModule) => {
    callback(testModule)
    for (const suite of testModule.children.allSuites()) {
      callback(suite)
    }
  })
}

export interface TestCollectJSONResult {
  name: string
  file: string
  projectName?: string
  location?: { line: number; column: number }
}

export function formatCollectedAsJSON(files: TestModule[]): TestCollectJSONResult[] {
  const results: TestCollectJSONResult[] = []

  files.forEach((file) => {
    for (const test of file.children.allTests()) {
      if (test.result().state === 'skipped') {
        continue
      }
      const result: TestCollectJSONResult = {
        name: test.fullName,
        file: test.module.moduleId,
      }
      if (test.project.name) {
        result.projectName = test.project.name
      }
      if (test.location) {
        result.location = test.location
      }
      results.push(result)
    }
  })
  return results
}

export function formatCollectedAsString(testModules: TestModule[]): string[] {
  const results: string[] = []

  testModules.forEach((testModule) => {
    for (const test of testModule.children.allTests()) {
      if (test.result().state === 'skipped') {
        continue
      }
      const fullName = `${test.module.task.name} > ${test.fullName}`
      results.push(
        (test.project.name ? `[${test.project.name}] ` : '') + fullName,
      )
    }
  })

  return results
}

const envPackageNames: Record<
  Exclude<keyof typeof environments, 'node'>,
  string
> = {
  'jsdom': 'jsdom',
  'happy-dom': 'happy-dom',
  'edge-runtime': '@edge-runtime/vm',
}

function getEnvPackageName(env: VitestEnvironment) {
  if (env === 'node') {
    return null
  }
  if (env in envPackageNames) {
    return (envPackageNames as any)[env]
  }
  if (env[0] === '.' || isAbsolute(env)) {
    return null
  }
  return `vitest-environment-${env}`
}
