import type { Arrayable } from '@vitest/utils'
import type { ReportOptions } from 'istanbul-reports'
import type { TransformResult as ViteTransformResult } from 'vite'
import type { AfterSuiteRunMeta } from '../../types/general'
import type { RuntimeCoverageModuleLoader, RuntimeCoverageProviderModule } from '../../utils/coverage'
import type { Vitest } from '../core'

type TransformResult
  = | string
    | Partial<ViteTransformResult>
    | undefined
    | null
    | void
type CoverageResults = unknown

export interface CoverageProvider {
  name: string

  /** Called when provider is being initialized before tests run */
  initialize: (ctx: Vitest) => Promise<void> | void

  /** Called when setting coverage options for Vitest context (`ctx.config.coverage`) */
  resolveOptions: () => ResolvedCoverageOptions

  /** Callback to clean previous reports */
  clean: (clean?: boolean) => void | Promise<void>

  /** Called with coverage results after a single test file has been run */
  onAfterSuiteRun: (meta: AfterSuiteRunMeta) => void | Promise<void>

  /** Callback called when test run starts */
  onTestRunStart?: () => void | Promise<void>

  /** Callback called when test run fails due to test failures */
  onTestFailure?: () => void | Promise<void>

  /** Callback to generate final coverage results */
  generateCoverage: (
    reportContext: ReportContext,
  ) => CoverageResults | Promise<CoverageResults>

  /** Callback to convert coverage results to coverage reports. Called with results returned from `generateCoverage` */
  reportCoverage: (
    coverage: CoverageResults,
    reportContext: ReportContext,
  ) => void | Promise<void>

  /** Callback for `--merge-reports` options. Called with multiple coverage results generated by `generateCoverage`. */
  mergeReports?: (coverages: CoverageResults[]) => void | Promise<void>

  /** Callback called for instrumenting files with coverage counters. */
  onFileTransform?: (
    sourceCode: string,
    id: string,
    // TODO: when upgrading vite, import Rollup from vite
    pluginCtx: any,
  ) => TransformResult | Promise<TransformResult>

  /**
   * Return `true` if this file is transformed by the coverage provider.
   * This is used to generate the persistent file hash by `fsModuleCache`
   * @experimental
   */
  requiresTransform?: (id: string) => boolean

  /** Callback that's called when the coverage is enabled via a programmatic `enableCoverage` API. */
  onEnabled?: () => void | Promise<void>
}

export interface ReportContext {
  /** Indicates whether all tests were run. False when only specific tests were run. */
  allTestsRun?: boolean
}

export interface CoverageModuleLoader extends RuntimeCoverageModuleLoader {
  import: (id: string) => Promise<{ default: CoverageProviderModule }>
}

export interface CoverageProviderModule extends RuntimeCoverageProviderModule {
  /**
   * Factory for creating a new coverage provider
   */
  getProvider: () => CoverageProvider | Promise<CoverageProvider>
}

export type CoverageReporter = keyof ReportOptions | (string & {})

export type CoverageReporterWithOptions<
  ReporterName extends CoverageReporter = CoverageReporter,
> = ReporterName extends keyof ReportOptions
  ? ReportOptions[ReporterName] extends never
    ? [ReporterName, object] // E.g. the "none" reporter
    : [ReporterName, Partial<ReportOptions[ReporterName]>]
  : [ReporterName, Record<string, unknown>]

export type CoverageProviderName = 'v8' | 'istanbul' | 'custom' | undefined

/** Fields that have default values. Internally these will always be defined. */
export type FieldsWithDefaultValues
  = | 'provider'
    | 'enabled'
    | 'clean'
    | 'cleanOnRerun'
    | 'reportsDirectory'
    | 'exclude'
    | 'reportOnFailure'
    | 'allowExternal'
    | 'processingConcurrency'
    | 'reporter'
    | 'excludeAfterRemap'
    | 'ignoreClassMethods'
    | 'skipFull'
    | 'watermarks'
    | 'autoAttachSubprocess'

export type ResolvedCoverageOptions
  = CoverageOptions
    & Required<Pick<CoverageOptions, FieldsWithDefaultValues>>
    & {
    // Resolved fields which may have different typings as public configuration API has
      reporter: CoverageReporterWithOptions[]
      htmlDir?: string
    }

export interface CoverageOptions {
  /**
   * Coverage provider to use.
   *
   * @default 'v8'
   */
  provider?: 'v8' | 'istanbul' | 'custom'

  /**
   * Enables coverage collection. Can be overridden using `--coverage` CLI option.
   *
   * @default false
   */
  enabled?: boolean

  /**
   * List of files included in coverage as glob patterns.
   * By default only files covered by tests are included.
   *
   * See [Including and excluding files from coverage report](https://vitest.dev/guide/coverage.html#including-and-excluding-files-from-coverage-report) for examples.
   */
  include?: string[]

  /**
   * List of files excluded from coverage as glob patterns.
   * Files are first checked against `coverage.include`.
   *
   * See [Including and excluding files from coverage report](https://vitest.dev/guide/coverage.html#including-and-excluding-files-from-coverage-report) for examples.
   */
  exclude?: string[]

  /**
   * Clean coverage results before running tests
   *
   * @default true
   */
  clean?: boolean

  /**
   * Clean coverage report on watch rerun
   *
   * @default true
   */
  cleanOnRerun?: boolean

  /**
   * Directory to write coverage report to
   *
   * @default './coverage'
   */
  reportsDirectory?: string

  /**
   * Coverage reporters to use.
   * See [istanbul documentation](https://istanbul.js.org/docs/advanced/alternative-reporters/) for detailed list of all reporters.
   *
   * @default ['text', 'html', 'clover', 'json']
   */
  reporter?:
    | Arrayable<CoverageReporter>
    | (CoverageReporter | [CoverageReporter] | CoverageReporterWithOptions)[]

  /**
   * Do not show files with 100% statement, branch, and function coverage
   *
   * @default false
   */
  skipFull?: boolean

  /**
   * Configurations for thresholds
   *
   * @example
   *
   * ```ts
   * {
   *   // Thresholds for all files
   *   functions: 95,
   *   branches: 70,
   *   perFile: true,
   *   autoUpdate: true,
   *
   *   // Thresholds for utilities
   *   'src/utils/**.ts': {
   *     lines: 100,
   *     statements: 95,
   *   }
   * }
   * ```
   */
  thresholds?:
    | Thresholds
    | ({
      [glob: string]: Pick<
        Thresholds,
          100 | 'statements' | 'functions' | 'branches' | 'lines' | 'perFile'
      >
    } & Thresholds)

  /**
   * Watermarks for statements, lines, branches and functions.
   *
   * Default value is `[50,80]` for each property.
   */
  watermarks?: {
    statements?: [number, number]
    functions?: [number, number]
    branches?: [number, number]
    lines?: [number, number]
  }

  /**
   * Generate coverage report even when tests fail.
   *
   * @default false
   */
  reportOnFailure?: boolean

  /**
   * Collect coverage of files outside the project `root`.
   *
   * @default false
   */
  allowExternal?: boolean

  /**
   * Apply exclusions again after coverage has been remapped to original sources.
   * This is useful when your source files are transpiled and may contain source maps
   * of non-source files.
   *
   * Use this option when you are seeing files that show up in report even if they
   * match your `coverage.exclude` patterns.
   *
   * @default false
   */
  excludeAfterRemap?: boolean

  /**
   * Concurrency limit used when processing the coverage results.
   * Defaults to `Math.min(20, os.availableParallelism?.() ?? os.cpus().length)`
   */
  processingConcurrency?: number

  /**
   * Track coverage of the `node:child_process` and `node:worker_threads` spawned during test run.
   * Supported only by `v8` provider.
   *
   * @default false
   */
  autoAttachSubprocess?: boolean

  /**
   * Set to array of class method names to ignore for coverage
   *
   * @default []
   */
  ignoreClassMethods?: string[]

  /**
   * Custom instrumenter factory to use instead of the default `istanbul-lib-instrument`.
   *
   * The factory receives the same runtime coverage options Vitest passes to its
   * built-in Istanbul instrumenter and must return an object implementing the
   * `CoverageInstrumenter` interface.
   *
   * This allows using faster instrumenters (e.g., oxc-coverage-instrument, SWC) while
   * keeping the Istanbul coverage pipeline for collection, merging, and reporting.
   *
   * @example
   * ```ts
   * import { defineConfig } from 'vitest/config'
   * import { createOxcInstrumenter } from 'oxc-coverage-instrument/vitest'
   *
   * export default defineConfig({
   *   test: {
   *     coverage: {
   *       provider: 'istanbul',
   *       instrumenter: options => createOxcInstrumenter(options),
   *     }
   *   }
   * })
   *
   * @experimental
   */
  instrumenter?: (options: InstrumenterOptions) => CoverageInstrumenter

  /**
   * Directory of HTML coverage output to be served in UI mode and HTML reporter.
   * This is automatically configured for builtin reporter with html output (`html`, `html-spa`, and `lcov` reporters).
   * Use this option to override with custom coverage reporting location.
   */
  htmlDir?: string

  /**
   * Collect coverage only for files changed since a specified commit or branch.
   * Inherits the default value from `test.changed`.
   */
  changed?: boolean | string

  /**
   * Name of the module or path to a file to load the custom provider from
   */
  customProviderModule?: string
}

interface Thresholds {
  /** Set global thresholds to `100` */
  100?: boolean

  /**
   * Check thresholds per file. When set to an object, the top-level thresholds
   * still apply to the aggregate and every file must additionally meet these
   * per-file minimums.
   *
   * Can also be set per glob pattern via `thresholds['<glob>'].perFile`. Glob
   * patterns do not inherit this top-level `perFile`; set it on each glob
   * explicitly.
   *
   * @default false
   */
  perFile?: boolean | Pick<Thresholds, 100 | 'statements' | 'functions' | 'branches' | 'lines'>

  /**
   * Update threshold values automatically when current coverage is higher than earlier thresholds
   * Also can accept a function to format the new threshold values
   *
   * @default false
   */
  autoUpdate?: boolean | ((newThreshold: number, previousThreshold: number) => number)

  /** Thresholds for statements */
  statements?: number

  /** Thresholds for functions */
  functions?: number

  /** Thresholds for branches */
  branches?: number

  /** Thresholds for lines */
  lines?: number
}

/**
 * Options passed to the custom instrumenter factory.
 */
export interface InstrumenterOptions {
  /** Global variable name that Vitest uses to store coverage data at runtime. */
  coverageVariable: string
  /** Global scope where the coverage variable is attached at runtime. */
  coverageGlobalScope: string
  /** Whether the coverage global scope should be resolved through an evaluated function. */
  coverageGlobalScopeFunc: boolean
  /** Class method names to exclude from function coverage. */
  ignoreClassMethods: string[]
}

/**
 * Interface for custom coverage instrumenters.
 *
 * Matches the subset of istanbul-lib-instrument's `Instrumenter` that Vitest
 * actually uses. Implement this to plug in a faster instrumenter while keeping
 * the Istanbul coverage pipeline for collection, merging, and reporting.
 */
export interface CoverageInstrumenter {
  /** Instrument source code synchronously. Returns the instrumented code string. */
  instrumentSync: (code: string, filename: string, inputSourceMap?: any) => string
  /** Get the source map of the last instrumented file. */
  lastSourceMap: () => any
  /** Get the Istanbul-compatible file coverage object of the last instrumented file. */
  lastFileCoverage: () => any
}

/** @deprecated Use `CoverageOptions` instead */
export interface CoverageV8Options extends CoverageOptions {}

/** @deprecated Use `CoverageOptions` instead */
export interface CoverageIstanbulOptions extends CoverageOptions {}

/** @deprecated Use `CoverageOptions` instead */
export interface BaseCoverageOptions extends CoverageOptions {}

/** @deprecated Use `CoverageOptions` instead */
export interface CustomProviderOptions extends CoverageOptions {}
