import type { Awaitable, TestError } from '@vitest/utils'
import type { TestFixtures } from '../fixture'
import type { afterAll, afterEach, aroundAll, aroundEach, beforeAll, beforeEach } from '../hooks'
import type { kChainableContext, TypedChainableFunction } from '../utils/chain'

export type RunMode = 'run' | 'skip' | 'only' | 'todo' | 'queued'
export type TaskState = RunMode | 'pass' | 'fail'

export interface TaskBase {
  /**
   * Unique task identifier. Based on the file id and the position of the task.
   * The id of the file task is based on the file path relative to root and project name.
   * It will not change between runs.
   * @example `1201091390`, `1201091390_0`, `1201091390_0_1`
   */
  id: string
  /**
   * Task name provided by the user. If no name was provided, it will be an empty string.
   */
  name: string
  /**
   * Full name including the file path, any parent suites, and this task's name.
   *
   * Uses ` > ` as the separator between levels.
   *
   * @example
   * // file
   * 'test/task-names.test.ts'
   * @example
   * // suite
   * 'test/task-names.test.ts > meal planning'
   * 'test/task-names.test.ts > meal planning > grocery lists'
   * @example
   * // test
   * 'test/task-names.test.ts > meal planning > grocery lists > calculates ingredients'
   */
  fullName: string
  /**
   * Full name excluding the file path, including any parent suites and this task's name. `undefined` for file tasks.
   *
   * Uses ` > ` as the separator between levels.
   *
   * @example
   * // file
   * undefined
   * @example
   * // suite
   * 'meal planning'
   * 'meal planning > grocery lists'
   * @example
   * // test
   * 'meal planning > grocery lists > calculates ingredients'
   */
  fullTestName?: string
  /**
   * Task mode.
   * - **skip**: task is skipped
   * - **only**: only this task and other tasks with `only` mode will run
   * - **todo**: task is marked as a todo, alias for `skip`
   * - **run**: task will run or already ran
   * - **queued**: task will start running next. It can only exist on the File
   */
  mode: RunMode
  /**
   * Custom metadata for the task. JSON reporter will save this data.
   */
  meta: TaskMeta
  /**
   * Whether the task was produced with `.each()` method.
   */
  each?: boolean
  /**
   * Whether the task should run concurrently with other tasks.
   */
  concurrent?: boolean
  /**
   * Whether the tasks of the suite run in a random order.
   */
  shuffle?: boolean
  /**
   * Suite that this task is part of. File task or the global suite will have no parent.
   */
  suite?: Suite
  /**
   * Result of the task. Suite and file tasks will only have the result if there
   * was an error during collection or inside `afterAll`/`beforeAll`.
   */
  result?: TaskResult
  /**
   * Retry configuration for the task.
   * - If a number, specifies how many times to retry
   * - If an object, allows fine-grained retry control
   * @default 0
   */
  retry?: Retry
  /**
   * The amount of times the task should be repeated after the successful run.
   * If the task fails, it will not be retried unless `retry` is specified.
   * @default 0
   */
  repeats?: number
  /**
   * Location of the task in the file. This field is populated only if
   * `includeTaskLocation` option is set. It is generated by calling `new Error`
   * and parsing the stack trace, so the location might differ depending on the runtime.
   */
  location?: Location
  /**
   * If the test was collected by parsing the file AST, and the name
   * is not a static string, this property will be set to `true`.
   * @experimental
   */
  dynamic?: boolean
  /**
   * Custom tags of the task. Useful for filtering tasks.
   */
  tags?: string[]
}

export interface TaskPopulated extends TaskBase {
  /**
   * File task. It's the root task of the file.
   */
  file: File
  /**
   * Whether the task should succeed if it fails. If the task fails, it will be marked as passed.
   */
  fails?: boolean
  /**
   * Hooks that will run if the task fails. The order depends on the `sequence.hooks` option.
   * @internal
   */
  onFailed?: OnTestFailedHandler[]
  /**
   * Hooks that will run after the task finishes. The order depends on the `sequence.hooks` option.
   * @internal
   */
  onFinished?: OnTestFinishedHandler[]
  /**
   * Store promises (from async expects) to wait for them before finishing the test
   */
  promises?: Promise<any>[]
}

/**
 * Custom metadata that can be used in reporters.
 */
export interface TaskMeta {}

/**
 * The result of calling a task.
 */
export interface TaskResult {
  /**
   * State of the task. Inherits the `task.mode` during collection.
   * When the task has finished, it will be changed to `pass` or `fail`.
   * - **pass**: task ran successfully
   * - **fail**: task failed
   */
  state: TaskState
  /**
   * Errors that occurred during the task execution. It is possible to have several errors
   * if `expect.soft()` failed multiple times or `retry` was triggered.
   */
  errors?: TestError[]
  /**
   * How long in milliseconds the task took to run.
   */
  duration?: number
  /**
   * Time in milliseconds when the task started running.
   */
  startTime?: number
  /**
   * Heap size in bytes after the task finished.
   * Only available if `logHeapUsage` option is set and `process.memoryUsage` is defined.
   */
  heap?: number
  /**
   * State of related to this task hooks. Useful during reporting.
   */
  hooks?: Partial<Record<keyof SuiteHooks, TaskState>>
  /**
   * The amount of times the task was retried. The task is retried only if it
   * failed and `retry` option is set.
   */
  retryCount?: number
  /**
   * The amount of times the task was repeated. The task is repeated only if
   * `repeats` option is set. This number also contains `retryCount`.
   */
  repeatCount?: number
  /** @internal */
  note?: string
  /**
   * Whether the task was skipped by calling `context.skip()`.
   * @internal
   */
  pending?: boolean
}

/** The time spent importing & executing a non-externalized file. */
export interface ImportDuration {
  /** The time spent importing & executing the file itself, not counting all non-externalized imports that the file does. */
  selfTime: number

  /** The time spent importing & executing the file and all its imports. */
  totalTime: number

  /** Will be set to `true`, if the module was externalized. In this case totalTime and selfTime are identical. */
  external?: boolean

  /** Which module imported this module first. All subsequent imports are cached. */
  importer?: string
}

/**
 * The tuple representing a single task update.
 * Usually reported after the task finishes.
 */
export type TaskResultPack = [
  /**
   * Unique task identifier from `task.id`.
   */
  id: string,
  /**
   * The result of running the task from `task.result`.
   */
  result: TaskResult | undefined,
  /**
   * Custom metadata from `task.meta`.
   */
  meta: TaskMeta,
]

export interface TaskEventData {
  annotation?: TestAnnotation | undefined
  artifact?: TestArtifact | undefined
}

export type TaskEventPack = [
  /**
   * Unique task identifier from `task.id`.
   */
  id: string,
  /**
   * The name of the event that triggered the update.
   */
  event: TaskUpdateEvent,
  /**
   * Data associated with the event
   */
  data: TaskEventData | undefined,
]

export type TaskUpdateEvent
  = | 'test-failed-early'
    | 'suite-failed-early'
    | 'test-prepare'
    | 'test-finished'
    | 'test-retried'
    | 'test-cancel'
    | 'suite-prepare'
    | 'suite-finished'
    | 'before-hook-start'
    | 'before-hook-end'
    | 'after-hook-start'
    | 'after-hook-end'
    | 'test-annotation'
    | 'test-artifact'

export interface Suite extends TaskBase {
  type: 'suite'
  /**
   * File task. It's the root task of the file.
   */
  file: File
  /**
   * An array of tasks that are part of the suite.
   */
  tasks: Task[]
}

export interface File extends Suite {
  /**
   * The name of the pool that the file belongs to.
   * @default 'forks'
   */
  pool?: string
  /**
   * The environment that processes the file on the server.
   */
  viteEnvironment?: string
  /**
   * The path to the file in UNIX format.
   */
  filepath: string
  /**
   * The name of the workspace project the file belongs to.
   */
  projectName: string | undefined
  /**
   * The time it took to collect all tests in the file.
   * This time also includes importing all the file dependencies.
   */
  collectDuration?: number
  /**
   * The time it took to import the setup file.
   */
  setupDuration?: number
  /**
   * Whether the file is initiated without running any tests.
   * This is done to populate state on the server side by Vitest.
   * @internal
   */
  local?: boolean

  /** The time spent importing every non-externalized dependency that Vitest has processed. */
  importDurations?: Record<string, ImportDuration>
}

export interface Test<ExtraContext = object> extends TaskPopulated {
  type: 'test'
  /**
   * Test context that will be passed to the test function.
   */
  context: TestContext & ExtraContext
  /**
   * The test timeout in milliseconds.
   */
  timeout: number
  /**
   * An array of custom annotations.
   */
  annotations: TestAnnotation[]
  /**
   * An array of artifacts produced by the test.
   *
   * @experimental
   */
  artifacts: TestArtifact[]
  fullTestName: string
}

export type Task = Test | Suite | File

export type TestFunction<ExtraContext = object> = (
  context: TestContext & ExtraContext,
) => Awaitable<any> | void

// jest's ExtractEachCallbackArgs
type ExtractEachCallbackArgs<T extends ReadonlyArray<any>> = {
  1: [T[0]]
  2: [T[0], T[1]]
  3: [T[0], T[1], T[2]]
  4: [T[0], T[1], T[2], T[3]]
  5: [T[0], T[1], T[2], T[3], T[4]]
  6: [T[0], T[1], T[2], T[3], T[4], T[5]]
  7: [T[0], T[1], T[2], T[3], T[4], T[5], T[6]]
  8: [T[0], T[1], T[2], T[3], T[4], T[5], T[6], T[7]]
  9: [T[0], T[1], T[2], T[3], T[4], T[5], T[6], T[7], T[8]]
  10: [T[0], T[1], T[2], T[3], T[4], T[5], T[6], T[7], T[8], T[9]]
  fallback: Array<T extends ReadonlyArray<infer U> ? U : any>
}[T extends Readonly<[any]>
  ? 1
  : T extends Readonly<[any, any]>
    ? 2
    : T extends Readonly<[any, any, any]>
      ? 3
      : T extends Readonly<[any, any, any, any]>
        ? 4
        : T extends Readonly<[any, any, any, any, any]>
          ? 5
          : T extends Readonly<[any, any, any, any, any, any]>
            ? 6
            : T extends Readonly<[any, any, any, any, any, any, any]>
              ? 7
              : T extends Readonly<[any, any, any, any, any, any, any, any]>
                ? 8
                : T extends Readonly<[any, any, any, any, any, any, any, any, any]>
                  ? 9
                  : T extends Readonly<[any, any, any, any, any, any, any, any, any, any]>
                    ? 10
                    : 'fallback']

interface EachFunctionReturn<T extends any[]> {
  (
    name: string | Function,
    fn: (...args: T) => Awaitable<void>,
    options?: number
  ): void
  (
    name: string | Function,
    options: TestCollectorOptions,
    fn: (...args: T) => Awaitable<void>
  ): void
}

interface TestEachFunction {
  <const T extends any[] | [any]>(cases: ReadonlyArray<T>): EachFunctionReturn<T>
  <const T extends ReadonlyArray<any>>(cases: ReadonlyArray<T>): EachFunctionReturn<
    ExtractEachCallbackArgs<T>
  >
  <const T>(cases: ReadonlyArray<T>): EachFunctionReturn<T[]>
  (...args: [TemplateStringsArray, ...any]): EachFunctionReturn<any[]>
}

interface TestForFunctionReturn<Arg, Context> {
  (
    name: string | Function,
    fn: (arg: Arg, context: Context) => Awaitable<void>
  ): void
  (
    name: string | Function,
    options: TestCollectorOptions,
    fn: (args: Arg, context: Context) => Awaitable<void>
  ): void
}

interface TestForFunction<ExtraContext> {
  // test.for([1, 2, 3])
  // test.for([[1, 2], [3, 4, 5]])
  <const T>(cases: ReadonlyArray<T>): TestForFunctionReturn<
    T,
    TestContext & ExtraContext
  >

  // test.for`
  //    a  |  b
  //   {1} | {2}
  //   {3} | {4}
  // `
  (strings: TemplateStringsArray, ...values: any[]): TestForFunctionReturn<
    any,
    TestContext & ExtraContext
  >
}

interface SuiteForFunction {
  <const T>(cases: ReadonlyArray<T>): EachFunctionReturn<[T]>
  (...args: [TemplateStringsArray, ...any]): EachFunctionReturn<any[]>
}

interface TestCollectorCallable<C = object> {
  <ExtraContext extends C>(
    name: string | Function,
    fn?: TestFunction<ExtraContext>,
    options?: number
  ): void
  <ExtraContext extends C>(
    name: string | Function,
    options?: TestCollectorOptions,
    fn?: TestFunction<ExtraContext>
  ): void
}

export interface InternalChainableContext<API = TestAPI> {
  /** @internal */
  mergeContext: (ctx: Partial<InternalTestContext>) => void
  /** @internal */
  setContext: (key: keyof InternalTestContext, value: any) => void
  /** @internal */
  withContext: () => API
  /** @internal */
  getFixtures: () => TestFixtures
}

type ChainableTestContextMap = Pick<
  Required<TestOptions>,
  'concurrent' | 'only' | 'skip' | 'todo' | 'fails'
>

type ChainableTestAPI<ExtraContext = object> = TypedChainableFunction<
  ChainableTestContextMap,
  TestCollectorCallable<ExtraContext>,
  {
    each: TestEachFunction
    for: TestForFunction<ExtraContext>
  }
>

type TestCollectorOptions = Omit<TestOptions, 'shuffle'>

/**
 * Retry configuration for tests.
 * Can be a number for simple retry count, or an object for advanced retry control.
 */
export type Retry = number | {
  /**
   * The number of times to retry the test if it fails.
   * @default 0
   */
  count?: number
  /**
   * Delay in milliseconds between retry attempts.
   * @default 0
   */
  delay?: number
  /**
   * Condition to determine if a test should be retried based on the error.
   * - If a RegExp, it is tested against the error message
   * - If a function, called with the TestError object; return true to retry
   *
   * NOTE: Functions can only be used in test files, not in vitest.config.ts,
   * because the configuration is serialized when passed to worker threads.
   *
   * @default undefined (retry on all errors)
   */
  condition?: RegExp | ((error: TestError) => boolean)
}

/**
 * Serializable retry configuration (used in config files).
 * Functions cannot be serialized, so only string conditions are allowed.
 */
export type SerializableRetry = number | {
  /**
   * The number of times to retry the test if it fails.
   * @default 0
   */
  count?: number
  /**
   * Delay in milliseconds between retry attempts.
   * @default 0
   */
  delay?: number
  /**
   * Condition to determine if a test should be retried based on the error.
   * Must be a RegExp tested against the error message.
   *
   * @default undefined (retry on all errors)
   */
  condition?: RegExp
}

export interface TestOptions {
  /**
   * Test timeout.
   */
  timeout?: number
  /**
   * Retry configuration for the test.
   * - If a number, specifies how many times to retry
   * - If an object, allows fine-grained retry control
   * @default 0
   */
  retry?: Retry
  /**
   * How many times the test will run again.
   * Only inner tests will repeat if set on `describe()`, nested `describe()` will inherit parent's repeat by default.
   *
   * @default 0
   */
  repeats?: number
  /**
   * Whether suites and tests run concurrently.
   * Tests inherit `concurrent` from `describe()` and nested `describe()` will inherit from parent's `concurrent`.
   */
  concurrent?: boolean
  /**
   * Whether the test should be skipped.
   */
  skip?: boolean
  /**
   * Should this test be the only one running in a suite.
   */
  only?: boolean
  /**
   * Whether the test should be skipped and marked as a todo.
   */
  todo?: boolean
  /**
   * Whether the test is expected to fail. If it does, the test will pass, otherwise it will fail.
   */
  fails?: boolean
  /**
   * Custom tags of the test. Useful for filtering tests.
   */
  tags?: keyof TestTags extends never
    ? string[] | string
    : TestTags[keyof TestTags] | TestTags[keyof TestTags][]
  /**
   * Custom test metadata available to reporters.
   */
  meta?: Partial<TaskMeta>
}

export interface TestTags {}

export interface SuiteOptions extends TestOptions {
  /**
   * Whether the tasks of the suite run in a random order.
   */
  shuffle?: boolean
}

interface ExtendedAPI<ExtraContext> {
  skipIf: (condition: any) => ChainableTestAPI<ExtraContext>
  runIf: (condition: any) => ChainableTestAPI<ExtraContext>
}

interface Hooks<ExtraContext> {
  /**
   * Suite-level hooks only receive file/worker scoped fixtures.
   * Test-scoped fixtures are NOT available in beforeAll/afterAll/aroundAll.
   */
  beforeAll: typeof beforeAll<ExtractSuiteContext<ExtraContext>>
  afterAll: typeof afterAll<ExtractSuiteContext<ExtraContext>>
  aroundAll: typeof aroundAll<ExtractSuiteContext<ExtraContext>>
  /**
   * Test-level hooks receive all fixtures including test-scoped ones.
   */
  beforeEach: typeof beforeEach<ExtraContext>
  afterEach: typeof afterEach<ExtraContext>
  aroundEach: typeof aroundEach<ExtraContext>
}

export type TestAPI<ExtraContext = object> = ChainableTestAPI<ExtraContext>
  & ExtendedAPI<ExtraContext> & Hooks<ExtraContext> & {
    /** @internal */
    [kChainableContext]: InternalChainableContext<TestAPI>
    /**
     * Extend the test API with custom fixtures.
     *
     * @example
     * ```ts
     * // Simple test fixtures (backward compatible)
     * const myTest = test.extend<{ foo: string }>({
     *   foo: 'value',
     * })
     *
     * // With scoped fixtures - use $test/$file/$worker structure
     * const myTest = test.extend<{
     *   $test: { testData: string }
     *   $file: { fileDb: Database }
     *   $worker: { workerConfig: Config }
     * }>({
     *   testData: async ({ fileDb }, use) => {
     *     await use(await fileDb.getData())
     *   },
     *   fileDb: [async ({ workerConfig }, use) => {
     *     // File fixture can only access workerConfig, NOT testData
     *     const db = new Database(workerConfig)
     *     await use(db)
     *     await db.close()
     *   }, { scope: 'file' }],
     *   workerConfig: [async ({}, use) => {
     *     // Worker fixture can only access other worker fixtures
     *     await use(loadConfig())
     *   }, { scope: 'worker' }],
     * })
     *
     * // Builder pattern with automatic type inference
     * const myTest = test
     *   .extend('config', { scope: 'worker' }, async ({}) => {
     *     return { port: 3000 }  // Type inferred as { port: number }
     *   })
     *   .extend('db', { scope: 'file' }, async ({ config }, { onCleanup }) => {
     *     // TypeScript knows config is { port: number }
     *     const db = new Database(config.port)
     *     onCleanup(() => db.close())  // Register cleanup
     *     return db  // Type inferred as Database
     *   })
     *   .extend('data', async ({ db }) => {
     *     // TypeScript knows db is Database
     *     return await db.getData()  // Type inferred from return
     *   })
     * ```
     */
    extend: {
      // Builder pattern overloads with automatic type inference from return value
      // MUST come first for correct TypeScript overload resolution

      // Function overloads (with cleanup support via onCleanup)
      // When extending with same key, T must match existing type (last value wins at runtime)
      // Overload 1: Worker scope function - can only access worker fixtures
      <K extends string, T extends (K extends keyof ExtraContext ? ExtraContext[K] : unknown)>(
        name: K,
        options: WorkerScopeFixtureOptions,
        fn: BuilderFixtureFn<T, WorkerScopeContext<ExtraContext>>,
      ): TestAPI<AddBuilderWorker<ExtraContext, K, T>>
      // Overload 2: File scope function - can access worker + file fixtures
      <K extends string, T extends (K extends keyof ExtraContext ? ExtraContext[K] : unknown)>(
        name: K,
        options: FileScopeFixtureOptions,
        fn: BuilderFixtureFn<T, FileScopeContext<ExtraContext>>,
      ): TestAPI<AddBuilderFile<ExtraContext, K, T>>
      // Overload 3: Test scope function with options - can access all fixtures
      <K extends string, T extends (K extends keyof ExtraContext ? ExtraContext[K] : unknown)>(
        name: K,
        options: TestScopeFixtureOptions,
        fn: BuilderFixtureFn<T, TestScopeContext<ExtraContext>>,
      ): TestAPI<AddBuilderTest<ExtraContext, K, T>>
      // Overload 4: Test scope function default (no options) - can access all fixtures
      <K extends string, T extends (K extends keyof ExtraContext ? ExtraContext[K] : unknown)>(
        name: K,
        fn: BuilderFixtureFn<T, TestScopeContext<ExtraContext>>,
      ): TestAPI<AddBuilderTest<ExtraContext, K, T>>

      // Non-function value overloads (simple values without cleanup)
      // Overload 5: Static value with worker scope options
      <K extends string, T extends (K extends keyof ExtraContext ? ExtraContext[K] : unknown)>(
        name: K,
        options: WorkerScopeFixtureOptions,
        value: T extends (...args: any[]) => any ? never : T,
      ): TestAPI<AddBuilderWorker<ExtraContext, K, T>>
      // Overload 6: Static value with file scope options
      <K extends string, T extends (K extends keyof ExtraContext ? ExtraContext[K] : unknown)>(
        name: K,
        options: FileScopeFixtureOptions,
        value: T extends (...args: any[]) => any ? never : T,
      ): TestAPI<AddBuilderFile<ExtraContext, K, T>>
      // Overload 7: Static value with test scope options
      <K extends string, T extends (K extends keyof ExtraContext ? ExtraContext[K] : unknown)>(
        name: K,
        options: TestScopeFixtureOptions,
        value: T extends (...args: any[]) => any ? never : T,
      ): TestAPI<AddBuilderTest<ExtraContext, K, T>>
      // Overload 8: Static value default (no options) - must exclude functions
      <K extends string, T extends (K extends keyof ExtraContext ? ExtraContext[K] : unknown)>(
        name: K,
        value: T extends (...args: any[]) => any ? never : T,
      ): TestAPI<AddBuilderTest<ExtraContext, K, T>>

      // Object syntax overloads
      // Overload 9: Scoped fixtures with { $test?, $file?, $worker? } structure
      <T extends ScopedFixturesDef>(
        fixtures: ScopedFixturesObject<T, ExtraContext>,
      ): TestAPI<ExtractScopedFixtures<T> & ExtraContext>
      // Overload 10: Legacy flat fixtures (backward compatible)
      <T extends Record<string, any> = object>(
        fixtures: Fixtures<T, ExtraContext>,
      ): TestAPI<{
        [K in keyof T | keyof ExtraContext]: K extends keyof T
          ? T[K]
          : K extends keyof ExtraContext
            ? ExtraContext[K]
            : never
      }>
    }
    /**
     * Overwrite fixture values for the current suite scope.
     * Supports both object syntax and builder pattern.
     *
     * @example
     * ```ts
     * describe('with custom config', () => {
     *   // Object syntax
     *   test.override({ config: { port: 4000 } })
     *
     *   // Builder pattern - value
     *   test.override('config', { port: 4000 })
     *
     *   // Builder pattern - function
     *   test.override('config', () => ({ port: 4000 }))
     *
     *   // Builder pattern - function with cleanup
     *   test.override('db', async ({ config }, { onCleanup }) => {
     *     const db = await createDb(config)
     *     onCleanup(() => db.close())
     *     return db
     *   })
     * })
     * ```
     */
    override: {
      // Builder pattern overloads
      // Overload 1: Function with options
      <K extends keyof ExtraContext>(
        name: K,
        options: FixtureOptions,
        fn: BuilderFixtureFn<ExtraContext[K], ExtraContext & TestContext>,
      ): TestAPI<ExtraContext>
      // Overload 2: Function without options
      <K extends keyof ExtraContext>(
        name: K,
        fn: BuilderFixtureFn<ExtraContext[K], ExtraContext & TestContext>,
      ): TestAPI<ExtraContext>
      // Overload 3: Static value with options
      <K extends keyof ExtraContext>(
        name: K,
        options: FixtureOptions,
        value: ExtraContext[K] extends (...args: any[]) => any ? never : ExtraContext[K],
      ): TestAPI<ExtraContext>
      // Overload 4: Static value without options
      <K extends keyof ExtraContext>(
        name: K,
        value: ExtraContext[K] extends (...args: any[]) => any ? never : ExtraContext[K],
      ): TestAPI<ExtraContext>
      // Overload 5: Object syntax
      (fixtures: Partial<Fixtures<ExtraContext>>): TestAPI<ExtraContext>
    }
    /**
     * @deprecated Use `test.override()` instead
     */
    scoped: (
      fixtures: Partial<Fixtures<ExtraContext>>,
    ) => TestAPI<ExtraContext>
    describe: SuiteAPI<ExtraContext>
    suite: SuiteAPI<ExtraContext>
  }

// use mapped type to preserve TestOptions references
type InternalTestChainableContext = {
  [K in keyof ChainableTestContextMap]: boolean | undefined
}

export interface InternalTestContext extends InternalTestChainableContext {
  each: boolean | undefined
  fixtures: TestFixtures
}

export interface FixtureOptions {
  /**
   * Whether to automatically set up current fixture, even though it's not being used.
   * Test-scoped auto fixtures are not initialized in suite-level hooks (`beforeAll`/`afterAll`/`aroundAll`).
   * @default false
   */
  auto?: boolean
  /**
   * Indicated if the injected value from the config should be preferred over the fixture value
   */
  injected?: boolean
  /**
   * When should the fixture be set up.
   * - **test**: fixture will be set up before every test
   * - **worker**: fixture will be set up once per worker
   * - **file**: fixture will be set up once per file
   *
   * **Warning:** The `vmThreads` and `vmForks` pools initiate worker fixtures once per test file.
   * @default 'test'
   */
  scope?: 'test' | 'worker' | 'file'
}

/**
 * Options for test-scoped fixtures.
 * Test fixtures are set up before each test and have access to all fixtures.
 */
export interface TestScopeFixtureOptions extends Omit<FixtureOptions, 'scope'> {
  /**
   * @default 'test'
   */
  scope?: 'test'
}

/**
 * Options for file-scoped fixtures.
 * File fixtures are set up once per file and can only access other file fixtures and worker fixtures.
 */
export interface FileScopeFixtureOptions extends Omit<FixtureOptions, 'scope'> {
  /**
   * Must be 'file' for file-scoped fixtures.
   */
  scope: 'file'
}

/**
 * Options for worker-scoped fixtures.
 * Worker fixtures are set up once per worker and can only access other worker fixtures.
 */
export interface WorkerScopeFixtureOptions extends Omit<FixtureOptions, 'scope'> {
  /**
   * Must be 'worker' for worker-scoped fixtures.
   */
  scope: 'worker'
}

export type Use<T> = (value: T) => Promise<void>

/**
 * Cleanup registration function for builder pattern fixtures.
 * Call this to register a cleanup function that runs after the test/file/worker completes.
 *
 * **Note:** This function can only be called once per fixture. If you need multiple
 * cleanup operations, either combine them into a single cleanup function or split
 * your fixture into multiple smaller fixtures.
 */
export type OnCleanup = (cleanup: () => Awaitable<void>) => void

/**
 * Builder pattern fixture function with automatic type inference.
 * Returns the fixture value directly (type is inferred from return).
 * Use onCleanup to register teardown logic.
 *
 * Parameters can be omitted if not needed:
 * - `async () => value` - no dependencies, no cleanup
 * - `async ({ dep }) => value` - with dependencies, no cleanup
 * - `async ({ dep }, { onCleanup }) => value` - with dependencies and cleanup
 */
export type BuilderFixtureFn<T, Context> = (
  context: Context,
  fixture: { onCleanup: OnCleanup },
) => T | Promise<T>

export type ExtractSuiteContext<C>
  = C extends { $__worker?: any } | { $__file?: any } | { $__test?: any }
    ? ExtractBuilderWorker<C> & ExtractBuilderFile<C>
    : C

/**
 * Extracts worker-scoped fixtures from a context that includes scope info.
 */
export type ExtractBuilderWorker<C> = C extends { $__worker?: infer W }
  ? W extends Record<string, any> ? W : object
  : object

/**
 * Extracts file-scoped fixtures from a context that includes scope info.
 */
export type ExtractBuilderFile<C> = C extends { $__file?: infer F }
  ? F extends Record<string, any> ? F : object
  : object

/**
 * Extracts test-scoped fixtures from a context that includes scope info.
 */
export type ExtractBuilderTest<C> = C extends { $__test?: infer T }
  ? T extends Record<string, any> ? T : object
  : object

/**
 * Adds a worker fixture to the context with proper scope tracking.
 */
export type AddBuilderWorker<C, K extends string, V> = Omit<C, '$__worker'> & Record<K, V> & {
  readonly $__worker?: ExtractBuilderWorker<C> & Record<K, V>
  readonly $__file?: ExtractBuilderFile<C>
  readonly $__test?: ExtractBuilderTest<C>
}

/**
 * Adds a file fixture to the context with proper scope tracking.
 */
export type AddBuilderFile<C, K extends string, V> = Omit<C, '$__file'> & Record<K, V> & {
  readonly $__worker?: ExtractBuilderWorker<C>
  readonly $__file?: ExtractBuilderFile<C> & Record<K, V>
  readonly $__test?: ExtractBuilderTest<C>
}

/**
 * Adds a test fixture to the context with proper scope tracking.
 */
export type AddBuilderTest<C, K extends string, V> = Omit<C, '$__test'> & Record<K, V> & {
  readonly $__worker?: ExtractBuilderWorker<C>
  readonly $__file?: ExtractBuilderFile<C>
  readonly $__test?: ExtractBuilderTest<C> & Record<K, V>
}

/**
 * Context available to worker-scoped fixtures.
 * Worker fixtures can only access other worker fixtures.
 * They do NOT have access to test context (task, expect, onTestFailed, etc.)
 * since they run once per worker, outside of any specific test.
 */
export type WorkerScopeContext<C> = ExtractBuilderWorker<C>

/**
 * Context available to file-scoped fixtures.
 * File fixtures can access worker and other file fixtures.
 * They do NOT have access to test context (task, expect, onTestFailed, etc.)
 * since they run once per file, outside of any specific test.
 */
export type FileScopeContext<C> = ExtractBuilderWorker<C> & ExtractBuilderFile<C>

/**
 * Context available to test-scoped fixtures (all fixtures + test context).
 */
export type TestScopeContext<C> = C & TestContext
export type FixtureFn<T, K extends keyof T, ExtraContext> = (
  context: Omit<T, K> & ExtraContext,
  use: Use<T[K]>,
) => Promise<void>
export type Fixture<T, K extends keyof T, ExtraContext = object> = ((
  ...args: any
) => any) extends T[K]
  ? T[K] extends any
    ? FixtureFn<T, K, Omit<ExtraContext, Exclude<keyof T, K>>>
    : never
  : | T[K]
    | (T[K] extends any
      ? FixtureFn<T, K, Omit<ExtraContext, Exclude<keyof T, K>>>
      : never)

/**
 * Fixture function with explicit context type for scoped fixtures.
 */
export type ScopedFixtureFn<Value, Context> = (
  context: Context,
  use: Use<Value>,
) => Promise<void>

/**
 * Fixtures definition for backward compatibility.
 * All fixtures are in T and any scope is allowed.
 */
export type Fixtures<T, ExtraContext = object> = {
  [K in keyof T]:
    | Fixture<T, K, ExtraContext & TestContext>
    | [Fixture<T, K, ExtraContext & TestContext>, FixtureOptions?]
}

/**
 * Scoped fixtures definition using a single generic with optional scope keys.
 * This provides better ergonomics than multiple generics.
 * Uses $ prefix to avoid conflicts with fixture names.
 *
 * @example
 * ```ts
 * test.extend<{
 *   $worker?: { config: Config }
 *   $file?: { db: Database }
 *   $test?: { data: string }
 * }>({ ... })
 * ```
 */
export interface ScopedFixturesDef {
  $test?: Record<string, any>
  $file?: Record<string, any>
  $worker?: Record<string, any>
}

/**
 * Extracts fixture types from a ScopedFixturesDef.
 * Handles optional properties by using Exclude to remove undefined.
 */
export type ExtractScopedFixtures<T extends ScopedFixturesDef>
  = ([Exclude<T['$test'], undefined>] extends [never] ? object : Exclude<T['$test'], undefined>)
    & ([Exclude<T['$file'], undefined>] extends [never] ? object : Exclude<T['$file'], undefined>)
    & ([Exclude<T['$worker'], undefined>] extends [never] ? object : Exclude<T['$worker'], undefined>)

/**
 * Creates the fixtures object type for ScopedFixturesDef with proper scope validation.
 * - Test fixtures: can be defined as value, function, or tuple with optional scope
 * - File fixtures: MUST have { scope: 'file' }
 * - Worker fixtures: MUST have { scope: 'worker' }
 */
export type ScopedFixturesObject<T extends ScopedFixturesDef, ExtraContext = object> = {
  // Test fixtures - scope is optional, have access to all fixtures + TestContext
  [K in keyof NonNullable<T['$test']>]:
    | NonNullable<T['$test']>[K]
    | ScopedFixtureFn<NonNullable<T['$test']>[K], ExtractScopedFixtures<T> & ExtraContext & TestContext>
    | [ScopedFixtureFn<NonNullable<T['$test']>[K], ExtractScopedFixtures<T> & ExtraContext & TestContext>, TestScopeFixtureOptions?]
} & {
  // File fixtures - scope: 'file' is REQUIRED, NO TestContext access
  [K in keyof NonNullable<T['$file']>]:
  [ScopedFixtureFn<NonNullable<T['$file']>[K], (NonNullable<T['$file']> & NonNullable<T['$worker']>) & ExtraContext>, FileScopeFixtureOptions]
} & {
  // Worker fixtures - scope: 'worker' is REQUIRED, NO TestContext access
  [K in keyof NonNullable<T['$worker']>]:
  [ScopedFixtureFn<NonNullable<T['$worker']>[K], NonNullable<T['$worker']> & ExtraContext>, WorkerScopeFixtureOptions]
}

export type InferFixturesTypes<T> = T extends TestAPI<infer C> ? C : T

interface SuiteCollectorCallable<ExtraContext = object> {
  <OverrideExtraContext extends ExtraContext = ExtraContext>(
    name: string | Function,
    fn?: SuiteFactory<OverrideExtraContext>,
    options?: number
  ): SuiteCollector<OverrideExtraContext>
  <OverrideExtraContext extends ExtraContext = ExtraContext>(
    name: string | Function,
    options: SuiteOptions,
    fn?: SuiteFactory<OverrideExtraContext>
  ): SuiteCollector<OverrideExtraContext>
}

type ChainableSuiteContextMap = Pick<
  Required<SuiteOptions>,
  'concurrent' | 'only' | 'skip' | 'todo' | 'shuffle'
>

type ChainableSuiteAPI<ExtraContext = object> = TypedChainableFunction<
  ChainableSuiteContextMap,
  SuiteCollectorCallable<ExtraContext>,
  {
    each: TestEachFunction
    for: SuiteForFunction
  }
>

export type SuiteAPI<ExtraContext = object> = ChainableSuiteAPI<ExtraContext> & {
  /** @internal */
  [kChainableContext]: InternalChainableContext<TestAPI>
  skipIf: (condition: any) => ChainableSuiteAPI<ExtraContext>
  runIf: (condition: any) => ChainableSuiteAPI<ExtraContext>
}

export interface BeforeAllListener<ExtraContext = object> {
  (context: ExtraContext, suite: Readonly<Suite | File>): Awaitable<unknown>
}

export interface AfterAllListener<ExtraContext = object> {
  (context: ExtraContext, suite: Readonly<Suite | File>): Awaitable<unknown>
}

export interface BeforeEachListener<ExtraContext = object> {
  (
    context: TestContext & ExtraContext,
    suite: Readonly<Suite>
  ): Awaitable<unknown>
}

export interface AfterEachListener<ExtraContext = object> {
  (
    context: TestContext & ExtraContext,
    suite: Readonly<Suite>
  ): Awaitable<unknown>
}

export interface AroundEachListener<ExtraContext = object> {
  (
    runTest: () => Promise<void>,
    context: TestContext & ExtraContext,
    suite: Readonly<Suite>
  ): Awaitable<unknown>
}

export interface AroundAllListener<ExtraContext = object> {
  (
    runSuite: () => Promise<void>,
    context: ExtraContext,
    suite: Readonly<Suite | File>
  ): Awaitable<unknown>
}

// Contexts are provided when registered, not when invoked
export interface RegisteredAllListener {
  (suite: Readonly<Suite | File>): Awaitable<unknown>
}

export interface RegisteredAroundAllListener {
  (
    runSuite: () => Promise<void>,
    suite: Readonly<Suite | File>
  ): Awaitable<unknown>
}

export interface SuiteHooks<ExtraContext = object> {
  beforeAll: RegisteredAllListener[]
  afterAll: RegisteredAllListener[]
  aroundAll: RegisteredAroundAllListener[]

  beforeEach: BeforeEachListener<ExtraContext>[]
  afterEach: AfterEachListener<ExtraContext>[]
  aroundEach: AroundEachListener<ExtraContext>[]
}

export interface TaskCustomOptions extends TestOptions {
  /**
   * Whether the task was produced with `.each()` method.
   */
  each?: boolean
  /**
   * Task fixtures.
   */
  fixtures?: TestFixtures
  /**
   * Function that will be called when the task is executed.
   * If nothing is provided, the runner will try to get the function using `getFn(task)`.
   * If the runner cannot find the function, the task will be marked as failed.
   */
  handler?: (context: TestContext) => Awaitable<void>
}

export interface SuiteCollector<ExtraContext = object> {
  readonly name: string
  readonly mode: RunMode
  options?: SuiteOptions
  type: 'collector'
  test: TestAPI<ExtraContext>
  tasks: (
    | Suite
    | Test<ExtraContext>
    | SuiteCollector<ExtraContext>
  )[]
  file: File
  suite?: Suite
  task: (name: string, options?: TaskCustomOptions) => Test<ExtraContext>
  collect: (file: File) => Promise<Suite>
  clear: () => void
  on: <T extends keyof SuiteHooks<ExtraContext>>(
    name: T,
    ...fn: SuiteHooks<ExtraContext>[T]
  ) => void
}

export type SuiteFactory<ExtraContext = object> = (
  test: TestAPI<ExtraContext>,
) => Awaitable<void>

export interface RuntimeContext {
  tasks: (SuiteCollector | Test)[]
  currentSuite: SuiteCollector | null
}

/**
 * User's custom test context.
 */
export interface TestContext {
  /**
   * Metadata of the current test
   */
  readonly task: Readonly<Test>

  /**
   * An [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that will be aborted if the test times out or
   * the test run was cancelled.
   * @see {@link https://vitest.dev/guide/test-context#signal}
   */
  readonly signal: AbortSignal

  /**
   * Register a callback to run when this specific test fails.
   * Useful when tests run concurrently.
   * @see {@link https://vitest.dev/guide/test-context#ontestfailed}
   */
  readonly onTestFailed: (fn: OnTestFailedHandler, timeout?: number) => void

  /**
   * Register a callback to run when this specific test finishes.
   * Useful when tests run concurrently.
   * @see {@link https://vitest.dev/guide/test-context#ontestfinished}
   */
  readonly onTestFinished: (fn: OnTestFinishedHandler, timeout?: number) => void

  /**
   * Mark tests as skipped. All execution after this call will be skipped.
   * This function throws an error, so make sure you are not catching it accidentally.
   * @see {@link https://vitest.dev/guide/test-context#skip}
   */
  readonly skip: {
    (note?: string): never
    (condition: boolean, note?: string): void
  }

  /**
   * Add a test annotation that will be displayed by your reporter.
   * @see {@link https://vitest.dev/guide/test-context#annotate}
   */
  readonly annotate: {
    (message: string, type?: string, attachment?: TestAttachment): Promise<TestAnnotation>
    (message: string, attachment?: TestAttachment): Promise<TestAnnotation>
  }
}

export type OnTestFailedHandler = (context: TestContext) => Awaitable<void>
export type OnTestFinishedHandler = (context: TestContext) => Awaitable<void>

export interface TaskHook<HookListener> {
  (fn: HookListener, timeout?: number): void
}

export type SequenceHooks = 'stack' | 'list' | 'parallel'
export type SequenceSetupFiles = 'list' | 'parallel'

export type WriteableTestContext = {
  -readonly [P in keyof TestContext]: TestContext[P]
}

// test artifacts

/**
 * Represents a file or data attachment associated with a test artifact.
 *
 * Attachments can be either file-based (via `path`) or inline content (via `body`).
 * The `contentType` helps consumers understand how to interpret the attachment data.
 */
export interface TestAttachment {
  /** MIME type of the attachment (e.g., 'image/png', 'text/plain') */
  contentType?: string
  /** File system path to the attachment */
  path?: string
  /** Inline attachment content as a string or raw binary data */
  body?: string | Uint8Array | undefined
  // TODO: change default to utf-8 on next major
  /**
   * @experimental
   * How the string `body` is encoded.
   * - `'base64'` (default): body is already base64-encoded
   * - `'utf-8'`: body is a utf8 string
   *
   * `body: Uint8Array` is always auto-encoded to string with `bodyEncoding: 'base64'`
   * regardless of this option.
   */
  bodyEncoding?: 'base64' | 'utf-8'
}

export interface Location {
  /** Line number in the source file (1-indexed) */
  line: number
  /** Column number in the line (1-indexed) */
  column: number
}

export interface FileLocation extends Location {
  /** Line number in the source file (1-indexed) */
  line: number
  /** Column number in the line (1-indexed) */
  column: number
  /** Path to the source file */
  file: string
}

/**
 * Source code location information for a test artifact.
 *
 * Indicates where in the source code the artifact originated from.
 */
export interface TestArtifactLocation extends FileLocation {}

/**
 * @experimental
 *
 * Base interface for all test artifacts.
 *
 * Extend this interface when creating custom test artifacts. Vitest automatically manages the `attachments` array and injects the `location` property to indicate where the artifact was created in your test code.
 *
 * **Important**: when running with [`api.allowWrite`](https://vitest.dev/config/api#api-allowwrite) or [`browser.api.allowWrite`](https://vitest.dev/config/browser/api#api-allowwrite) disabled, Vitest empties the `attachments` array on every artifact before reporting it.
 */
export interface TestArtifactBase {
  /** File or data attachments associated with this artifact */
  attachments?: TestAttachment[]
  /** Source location where this artifact was created */
  location?: TestArtifactLocation
}

/**
 * @deprecated Use {@linkcode TestArtifactLocation} instead.
 *
 * Kept for backwards compatibility.
 */
export type TestAnnotationLocation = TestArtifactLocation

export interface TestAnnotation {
  message: string
  type: string
  location?: TestArtifactLocation
  attachment?: TestAttachment
}

/**
 * @experimental
 *
 * Artifact type for test annotations.
 */
export interface TestAnnotationArtifact extends TestArtifactBase {
  type: 'internal:annotation'
  annotation: TestAnnotation
}

interface VisualRegressionArtifactAttachment extends TestAttachment {
  name: 'reference' | 'actual' | 'diff'
  width: number
  height: number
}

/**
 * @experimental
 *
 * Artifact type for visual regressions.
 */
export interface VisualRegressionArtifact extends TestArtifactBase {
  type: 'internal:toMatchScreenshot'
  kind: 'visual-regression'
  message: string
  attachments: VisualRegressionArtifactAttachment[]
}

/**
 * @experimental
 */
export interface BrowserTraceArtifact extends TestArtifactBase {
  type: 'internal:browserTrace'
  // TODO: should save a dedicated file/binary as attachments?
  data: unknown
}

interface FailureScreenshotArtifactAttachment extends TestAttachment {
  path: string
  /** Original file system path to the screenshot, before attachment resolution */
  originalPath: string
  body?: undefined
}

/**
 * @experimental
 *
 * Artifact type for failure screenshots.
 */
export interface FailureScreenshotArtifact extends TestArtifactBase {
  type: 'internal:failureScreenshot'
  attachments: [FailureScreenshotArtifactAttachment] | []
}

/**
 * @experimental
 * @advanced
 *
 * Registry for custom test artifact types.
 *
 * Augment this interface to register custom artifact types that your tests can produce.
 *
 * Each custom artifact should extend {@linkcode TestArtifactBase} and include a unique `type` discriminator property.
 *
 * @remarks
 * - Use a `Symbol` as the **registry key** to guarantee uniqueness
 * - The `type` property should follow the pattern `'package-name:artifact-name'`, `'internal:'` is a reserved prefix
 * - Use `attachments` to include files or data; extend {@linkcode TestAttachment} for custom metadata
 * - `location` property is automatically injected to indicate where the artifact was created
 *
 * @example
 *  ```ts
 * // Define custom attachment type for generated PDF
 * interface PDFAttachment extends TestAttachment {
 *   contentType: 'application/pdf'
 *   body: Uint8Array
 *   pageCount: number
 *   fileSize: number
 * }
 *
 * interface PDFGenerationArtifact extends TestArtifactBase {
 *   type: 'my-plugin:pdf-generation'
 *   templateName: string
 *   isValid: boolean
 *   attachments: [PDFAttachment]
 * }
 *
 * // Use a symbol to guarantee key uniqueness
 * const pdfKey = Symbol('pdf-generation')
 *
 * declare module 'vitest' {
 *   interface TestArtifactRegistry {
 *     [pdfKey]: PDFGenerationArtifact
 *   }
 * }
 *
 * // Custom assertion for PDF generation
 * async function toGenerateValidPDF(
 *   this: MatcherState,
 *   actual: PDFTemplate,
 *   data: Record<string, unknown>
 * ): AsyncExpectationResult {
 *   const pdfBuffer = await actual.render(data)
 *   const validation = await validatePDF(pdfBuffer)
 *
 *   await recordArtifact(this.task, {
 *     type: 'my-plugin:pdf-generation',
 *     templateName: actual.name,
 *     isValid: validation.success,
 *     attachments: [{
 *       contentType: 'application/pdf',
 *       body: pdfBuffer,
 *       pageCount: validation.pageCount,
 *       fileSize: pdfBuffer.byteLength
 *     }]
 *   })
 *
 *   return {
 *     pass: validation.success,
 *     message: () => validation.success
 *       ? `Generated valid PDF with ${validation.pageCount} pages`
 *       : `Invalid PDF: ${validation.error}`
 *   }
 * }
 * ```
 */
export interface TestArtifactRegistry {}

/**
 * @experimental
 *
 * Union type of all test artifacts, including built-in and custom registered artifacts.
 *
 * This type automatically includes all artifacts registered via {@link TestArtifactRegistry}.
 */
export type TestArtifact
  = | BrowserTraceArtifact
    | FailureScreenshotArtifact
    | TestAnnotationArtifact
    | VisualRegressionArtifact
    | TestArtifactRegistry[keyof TestArtifactRegistry]
