import type { SerializedDiffOptions } from '@vitest/utils/diff'
import type { SerializedConfig } from '../../runtime/config'
import type { TestProject } from '../project'
import { resolve } from 'node:path'
import { configDefaults } from '../../defaults'
import { getCoverageFilesDirectory } from '../../utils/coverage'
import { isAgent, isForceColor } from '../../utils/env'

export function serializeConfig(project: TestProject): SerializedConfig {
  const { config, globalConfig, viteConfig } = project
  const optimizer = config.deps?.optimizer || {}

  return {
    // TODO: remove functions from environmentOptions
    environmentOptions: config.environmentOptions,
    isolate: config.isolate,
    maxWorkers: config.maxWorkers,
    base: config.base,
    logHeapUsage: config.logHeapUsage,
    runner: config.runner,
    bail: config.bail,
    defines: config.defines,
    chaiConfig: config.chaiConfig,
    taskTitleValueFormatTruncate: config.taskTitleValueFormatTruncate,
    setupFiles: config.setupFiles,
    allowOnly: config.allowOnly,
    testTimeout: config.testTimeout,
    testNamePattern: config.testNamePattern,
    hookTimeout: config.hookTimeout,
    clearMocks: config.clearMocks,
    mockReset: config.mockReset,
    restoreMocks: config.restoreMocks,
    unstubEnvs: config.unstubEnvs,
    unstubGlobals: config.unstubGlobals,
    maxConcurrency: config.maxConcurrency,
    pool: config.pool,
    expect: config.expect,
    snapshotSerializers: config.snapshotSerializers,
    api: {
      allowExec: config.api.allowExec,
      allowWrite: config.api.allowWrite,
    },
    diff: serializeDiffOptions(config.diff),
    retry: config.retry,
    repeats: config.repeats,
    disableConsoleIntercept: config.disableConsoleIntercept,
    root: config.root,
    name: config.name,
    color: config.color,
    globals: config.globals,
    injectCjsGlobals: config.injectCjsGlobals,
    snapshotEnvironment: config.snapshotEnvironment,
    passWithNoTests: config.passWithNoTests,
    coverage: ((coverage) => {
      const reportsDirectory = resolve(globalConfig.root, coverage.reportsDirectory)
      return {
        reportsDirectory,
        coverageFilesDirectory: getCoverageFilesDirectory(reportsDirectory, globalConfig.shard),
        provider: coverage.provider,
        enabled: coverage.enabled,
        customProviderModule: 'customProviderModule' in coverage
          ? coverage.customProviderModule
          : undefined,
        htmlDir: coverage.htmlDir,
        autoAttachSubprocess: coverage.autoAttachSubprocess ?? false,
      }
    })(config.coverage),
    fakeTimers: config.fakeTimers,
    deps: {
      web: config.deps.web || {},
      optimizer: Object.entries(optimizer).reduce((acc, [name, option]) => {
        acc[name] = { enabled: option?.enabled ?? false }
        return acc
      }, {} as Record<string, { enabled: boolean }>),
      interopDefault: config.deps.interopDefault,
      moduleDirectories: config.deps.moduleDirectories,
    },
    snapshotOptions: {
      // TODO: store it differently, not on the config
      snapshotEnvironment: undefined!,
      updateSnapshot: globalConfig.snapshotOptions.updateSnapshot,
      snapshotFormat: {
        ...globalConfig.snapshotOptions.snapshotFormat,
      },
      expand:
        config.snapshotOptions.expand
        ?? globalConfig.snapshotOptions.expand,
    },
    sequence: {
      shuffle: config.sequence.shuffle,
      concurrent: config.sequence.concurrent,
      // `seed` and `sequencer` drive cross-project file ordering, so they are
      // resolved from the root config and shared across all projects.
      seed: globalConfig.sequence.seed,
      hooks: config.sequence.hooks,
      setupFiles: config.sequence.setupFiles,
    },
    inspect: globalConfig.inspect,
    inspectBrk: globalConfig.inspectBrk,
    inspector: globalConfig.inspector,
    detectAsyncLeaks: globalConfig.detectAsyncLeaks,
    watch: config.watch,
    includeTaskLocation:
      config.includeTaskLocation
      ?? globalConfig.includeTaskLocation,
    env: {
      ...viteConfig?.env,
      ...config.env,
    },
    browser: ((browser) => {
      const provider = project.browser?.provider
      return {
        name: browser.name,
        headless: browser.headless,
        ui: browser.ui,
        detailsPanelPosition: browser.detailsPanelPosition ?? 'right',
        viewport: browser.viewport,
        screenshotFailures: browser.screenshotFailures,
        locators: {
          testIdAttribute: browser.locators.testIdAttribute,
          exact: browser.locators.exact,
          errorFormat: browser.locators.errorFormat,
        },
        providerOptions: provider?.name === 'playwright'
          ? {
              actionTimeout: (provider as any)?.options?.actionTimeout,
            }
          : {},
        trackUnhandledErrors: browser.trackUnhandledErrors ?? true,
        trace: browser.trace.mode,
        traceView: browser.traceView,
      }
    })(config.browser),
    standalone: config.standalone,
    printConsoleTrace:
      config.printConsoleTrace ?? globalConfig.printConsoleTrace,
    benchmark: {
      enabled: config.benchmark.enabled,
      retainSamples: config.benchmark.retainSamples,
      provider: config.benchmark.provider,
      suppressExportGetterWarnings: config.benchmark.suppressExportGetterWarnings,
      projectName: config.benchmark.projectName,
    },
    // the browser initialized them via `@vite/env` import
    serializedDefines: config.browser.enabled
      ? ''
      : project._serializedDefines || '',
    fsModuleCache: config.fsModuleCache ?? false,
    experimental: {
      importDurations: config.experimental.importDurations,
      viteModuleRunner: config.experimental.viteModuleRunner ?? true,
      nodeLoader: config.experimental.nodeLoader ?? true,
      openTelemetry: config.experimental.openTelemetry,
    },
    tags: config.tags || [],
    tagsFilter: config.tagsFilter,
    strictTags: config.strictTags ?? true,
    mergeReportsLabel: config.mergeReportsLabel,
    slowTestThreshold:
      config.slowTestThreshold
      ?? globalConfig.slowTestThreshold
      ?? configDefaults.slowTestThreshold,
    disableColors: isAgent && !isForceColor(),
  }
}

const serializableDiffKeys = [
  'aAnnotation',
  'aIndicator',
  'bAnnotation',
  'bIndicator',
  'commonIndicator',
  'contextLines',
  'emptyFirstOrLastLinePlaceholder',
  'expand',
  'includeChangeCounts',
  'omitAnnotationLines',
  'printBasicPrototype',
  'maxDepth',
  'truncateThreshold',
  'truncateAnnotation',
] satisfies (keyof SerializedDiffOptions)[]

// `diff` can be an inline object containing color/compareKeys functions
// (`DiffOptions`). Those functions are not structured-cloneable (threads pool)
// and are silently dropped over `child_process` IPC (forks pool), so passing
// the raw object to workers throws `DataCloneError`. Forward only the
// serializable fields declared by `SerializedDiffOptions`, and only the ones
// actually set — explicit `undefined` values would override the diff defaults
// when the worker merges the options. The function-based options still
// require the file-path form, which workers import locally.
function serializeDiffOptions(
  diff: string | SerializedDiffOptions | undefined,
): string | SerializedDiffOptions | undefined {
  if (diff == null || typeof diff === 'string') {
    return diff
  }
  const result: SerializedDiffOptions = {}
  for (const key of serializableDiffKeys) {
    if (diff[key] !== undefined) {
      (result as Record<string, unknown>)[key] = diff[key]
    }
  }
  return result
}
