import type { ParsedStack } from '@vitest/utils'
import type { File, Suite, TaskBase } from '../types/tasks'
import { processError } from '@vitest/utils/error'
import { parseSingleStack } from '@vitest/utils/source-map'
import { relative } from 'pathe'

/**
 * If any tasks been marked as `only`, mark all other tasks as `skip`.
 */
export function interpretTaskModes(
  file: Suite,
  namePattern?: string | RegExp,
  testLocations?: number[] | undefined,
  testIds?: string[] | undefined,
  testTagsFilter?: ((testTags: string[]) => boolean) | undefined,
  onlyMode?: boolean,
  parentIsOnly?: boolean,
  allowOnly?: boolean,
): void {
  const matchedLocations: number[] = []

  const traverseSuite = (suite: Suite, parentIsOnly?: boolean, parentMatchedWithLocation?: boolean) => {
    const suiteIsOnly = parentIsOnly || suite.mode === 'only'

    // Check if any tasks in this suite have `.only` - if so, only those should run
    const hasSomeTasksOnly = onlyMode && suite.tasks.some(
      t => t.mode === 'only' || (t.type === 'suite' && someTasksAreOnly(t)),
    )

    suite.tasks.forEach((t) => {
      // Check if either the parent suite or the task itself are marked as included
      // If there are tasks with `.only` in this suite, only include those (not all tasks from describe.only)
      const includeTask = hasSomeTasksOnly
        ? (t.mode === 'only' || (t.type === 'suite' && someTasksAreOnly(t)))
        : (suiteIsOnly || t.mode === 'only')
      if (onlyMode) {
        if (t.type === 'suite' && (includeTask || someTasksAreOnly(t))) {
          // Don't skip this suite
          if (t.mode === 'only') {
            checkAllowOnly(t, allowOnly)
            t.mode = 'run'
          }
        }
        else if (t.mode === 'run' && !includeTask) {
          t.mode = 'skip'
        }
        else if (t.mode === 'only') {
          checkAllowOnly(t, allowOnly)
          t.mode = 'run'
        }
      }

      let hasLocationMatch = parentMatchedWithLocation
      // Match test location against provided locations, only run if present
      // in `testLocations`. Note: if `includeTaskLocation` is not enabled,
      // all test will be skipped.
      if (testLocations !== undefined && testLocations.length !== 0) {
        if (t.location && testLocations?.includes(t.location.line)) {
          t.mode = 'run'
          matchedLocations.push(t.location.line)
          hasLocationMatch = true
        }
        else if (parentMatchedWithLocation) {
          t.mode = 'run'
        }
        else if (t.type === 'test') {
          t.mode = 'skip'
        }
      }

      if (t.type === 'test') {
        if (namePattern && !getTaskFullName(t).match(namePattern)) {
          t.mode = 'skip'
        }
        if (testIds && !testIds.includes(t.id)) {
          t.mode = 'skip'
        }
        if (testTagsFilter && !testTagsFilter(t.tags || [])) {
          t.mode = 'skip'
        }
      }
      else if (t.type === 'suite') {
        if (t.mode === 'skip') {
          skipAllTasks(t)
        }
        else if (t.mode === 'todo') {
          todoAllTasks(t)
        }
        else {
          traverseSuite(t, includeTask, hasLocationMatch)
        }
      }
    })

    // if all subtasks are skipped, mark as skip
    if (suite.mode === 'run' || suite.mode === 'queued') {
      if (suite.tasks.length && suite.tasks.every(i => i.mode !== 'run' && i.mode !== 'queued')) {
        suite.mode = 'skip'
      }
    }
  }

  traverseSuite(file, parentIsOnly, false)

  const nonMatching = testLocations?.filter(loc => !matchedLocations.includes(loc))
  if (nonMatching && nonMatching.length !== 0) {
    const message = nonMatching.length === 1
      ? `line ${nonMatching[0]}`
      : `lines ${nonMatching.join(', ')}`

    if (file.result === undefined) {
      file.result = {
        state: 'fail',
        errors: [],
      }
    }
    if (file.result.errors === undefined) {
      file.result.errors = []
    }

    file.result.errors.push(
      processError(new Error(`No test found in ${file.name} in ${message}`)),
    )
  }
}

function getTaskFullName(task: TaskBase): string {
  return `${task.suite ? `${getTaskFullName(task.suite)} ` : ''}${task.name}`
}

export function someTasksAreOnly(suite: Suite): boolean {
  return suite.tasks.some(
    t => t.mode === 'only' || (t.type === 'suite' && someTasksAreOnly(t)),
  )
}

function skipAllTasks(suite: Suite) {
  suite.tasks.forEach((t) => {
    if (t.mode === 'run' || t.mode === 'queued') {
      t.mode = 'skip'
      if (t.type === 'suite') {
        skipAllTasks(t)
      }
    }
  })
}
function todoAllTasks(suite: Suite) {
  suite.tasks.forEach((t) => {
    if (t.mode === 'run' || t.mode === 'queued') {
      t.mode = 'todo'
      if (t.type === 'suite') {
        todoAllTasks(t)
      }
    }
  })
}

function checkAllowOnly(task: TaskBase, allowOnly?: boolean) {
  if (allowOnly) {
    return
  }
  const error = processError(
    new Error(
      '[Vitest] Unexpected .only modifier. Remove it or pass --allowOnly argument to bypass this error',
    ),
  )
  task.result = {
    state: 'fail',
    errors: [error],
  }
}

/* @__NO_SIDE_EFFECTS__ */
export function generateHash(str: string): string {
  let hash = 0
  if (str.length === 0) {
    return `${hash}`
  }
  for (let i = 0; i < str.length; i++) {
    const char = str.charCodeAt(i)
    hash = (hash << 5) - hash + char
    hash = hash & hash // Convert to 32bit integer
  }
  return `${hash}`
}

export function calculateSuiteHash(parent: Suite): void {
  parent.tasks.forEach((t, idx) => {
    t.id = `${parent.id}_${idx}`
    if (t.type === 'suite') {
      calculateSuiteHash(t)
    }
  })
}

interface HashMeta {
  typecheck?: boolean
  __vitest_label__?: string
}

export function createFileTask(
  filepath: string,
  root: string,
  projectName: string | undefined,
  pool?: string,
  viteEnvironment?: string,
  meta?: HashMeta,
): File {
  const path = relative(root, filepath)
  const file: File = {
    id: generateFileHash(path, projectName, meta),
    name: path,
    fullName: path,
    type: 'suite',
    mode: 'queued',
    filepath,
    tasks: [],
    meta: Object.assign(Object.create(null), meta),
    projectName,
    file: undefined!,
    pool,
    viteEnvironment,
  }
  file.file = file
  return file
}

/**
 * Generate a unique ID for a file based on its path and project name
 * @param file File relative to the root of the project to keep ID the same between different machines
 * @param projectName The name of the test project
 */
/* @__NO_SIDE_EFFECTS__ */
export function generateFileHash(
  file: string,
  projectName: string | undefined,
  meta?: HashMeta,
): string {
  const seed = [
    file,
    projectName || '',
    meta?.typecheck ? '__typecheck__' : '',
    meta?.__vitest_label__ || '',
  ].join('\0')
  return generateHash(seed)
}

export function findTestFileStackTrace(testFilePath: string, error: string): ParsedStack | undefined {
  // first line is the error message
  const lines = error.split('\n').slice(1)
  for (const line of lines) {
    const stack = parseSingleStack(line)
    if (stack && stack.file === testFilePath) {
      return stack
    }
  }
}
