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 fails */
  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

export type CoverageOptions<T extends CoverageProviderName = CoverageProviderName>
  = T extends 'istanbul'
    ? { provider: T } & CoverageIstanbulOptions
    : T extends 'v8' ? {
      /**
       * Provider to use for coverage collection.
       *
       * @default 'v8'
       */
      provider: T
    } & CoverageV8Options
      : T extends 'custom'
        ? { provider: T } & CustomProviderOptions
        : { provider?: T } & CoverageV8Options

/** Fields that have default values. Internally these will always be defined. */
type FieldsWithDefaultValues
  = | 'enabled'
    | 'clean'
    | 'cleanOnRerun'
    | 'reportsDirectory'
    | 'exclude'
    | 'reportOnFailure'
    | 'allowExternal'
    | 'processingConcurrency'

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

export interface BaseCoverageOptions {
  /**
   * 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'
      >
    } & 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

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

export interface CoverageIstanbulOptions extends BaseCoverageOptions {}

export interface CoverageV8Options extends BaseCoverageOptions {}

export interface CustomProviderOptions
  extends Pick<BaseCoverageOptions, FieldsWithDefaultValues> {
  /** 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. */
  perFile?: boolean

  /**
   * 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) => number)

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

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

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

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