import type { File, FileSpecification, SuiteHooks, VitestRunner } from './types'
import { processError } from '@vitest/utils/error' // TODO: load dynamically
import { toArray } from '@vitest/utils/helpers'
import {
  calculateSuiteHash,
  createFileTask,
  interpretTaskModes,
  someTasksAreOnly,
} from '../../utils/tasks'
import { collectorContext } from './context'
import { getHooks, setHooks } from './map'
import { runSetupFiles } from './setup'
import {
  clearCollectorContext,
  createSuiteHooks,
  getDefaultSuite,
} from './suite'
import { createTagsFilter, validateTags } from './utils/tags'

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

export async function collectTests(
  specs: string[] | FileSpecification[],
  runner: VitestRunner,
): Promise<File[]> {
  const files: File[] = []

  const config = runner.config
  const $ = runner.trace!
  let defaultTagsFilter: ((testTags: string[]) => boolean) | undefined

  for (const spec of specs) {
    const filepath = typeof spec === 'string' ? spec : spec.filepath
    await $(
      'collect_spec',
      { 'code.file.path': filepath },
      async () => {
        runner._currentSpecification = typeof spec === 'string' ? { filepath: spec } : spec

        const testLocations = typeof spec === 'string' ? undefined : spec.testLocations
        const testNamePattern = typeof spec === 'string' ? undefined : spec.testNamePattern
        const testIds = typeof spec === 'string' ? undefined : spec.testIds
        const testTagsFilter = typeof spec === 'object' && spec.testTagsFilter
          ? createTagsFilter(spec.testTagsFilter, config.tags)
          : undefined

        const fileTags: string[] = typeof spec === 'string' ? [] : (spec.fileTags || [])

        const file = createFileTask(
          filepath,
          config.root,
          config.name,
          runner.pool,
          runner.viteEnvironment,
          { __vitest_label__: config.mergeReportsLabel },
        )
        file.tags = fileTags
        file.shuffle = config.sequence.shuffle

        try {
          validateTags(runner.config, fileTags)

          runner.onCollectStart?.(file)

          clearCollectorContext(file, runner)

          const setupFiles = toArray(config.setupFiles)
          if (setupFiles.length) {
            const setupStart = now()
            await runSetupFiles(config, setupFiles, runner)
            const setupEnd = now()
            file.setupDuration = setupEnd - setupStart
          }
          else {
            file.setupDuration = 0
          }

          const collectStart = now()

          await runner.importFile(filepath, 'collect')

          const durations = runner.getImportDurations?.()
          if (durations) {
            file.importDurations = durations
          }

          const defaultTasks = await getDefaultSuite().collect(file)

          const fileHooks = createSuiteHooks()
          mergeHooks(fileHooks, getHooks(defaultTasks))

          for (const c of [...defaultTasks.tasks, ...collectorContext.tasks]) {
            if (c.type === 'test' || c.type === 'suite') {
              file.tasks.push(c)
            }
            else if (c.type === 'collector') {
              const suite = await c.collect(file)
              if (suite.name || suite.tasks.length) {
                mergeHooks(fileHooks, getHooks(suite))
                file.tasks.push(suite)
              }
            }
            else {
              // check that types are exhausted
              c satisfies never
            }
          }

          setHooks(file, fileHooks)
          file.collectDuration = now() - collectStart
        }
        catch (e) {
          const errors = e instanceof AggregateError
            ? e.errors.map(e => processError(e, runner.config._diffOptions))
            : [processError(e, runner.config._diffOptions)]
          file.result = {
            state: 'fail',
            errors,
          }

          const durations = runner.getImportDurations?.()
          if (durations) {
            file.importDurations = durations
          }
        }

        calculateSuiteHash(file)

        const hasOnlyTasks = someTasksAreOnly(file)
        if (!testTagsFilter && !defaultTagsFilter && config.tagsFilter) {
          defaultTagsFilter = createTagsFilter(config.tagsFilter, config.tags)
        }
        interpretTaskModes(
          file,
          testNamePattern ?? config.testNamePattern,
          testLocations,
          testIds,
          testTagsFilter ?? defaultTagsFilter,
          hasOnlyTasks,
          false,
          config.allowOnly,
        )

        if (file.mode === 'queued') {
          file.mode = 'run'
        }

        files.push(file)
      },
    )
  }

  return files
}

function mergeHooks(baseHooks: SuiteHooks, hooks: SuiteHooks): SuiteHooks {
  for (const _key in hooks) {
    const key = _key as keyof SuiteHooks
    baseHooks[key].push(...(hooks[key] as any))
  }

  return baseHooks
}
