import { createContext } from '@bamboocss/fixture'
import { esc } from '@bamboocss/shared'
import type { Config } from '@bamboocss/types'
import { foldSource, type FoldResult, type ForeignRecipes } from '../src/fold'
import { createRuntimeCss } from '../src/runtime-css'
import { createStaticStyleSetCompiler } from '../src/style-set'

export const FILE_PATH = 'app/src/test.tsx'

export const createFoldFixture = (userConfig?: Parameters<typeof createContext>[0]) => {
  const ctx = createContext(userConfig)
  const runtimeCss = createRuntimeCss(ctx)
  const styleCompiler = createStaticStyleSetCompiler(ctx, runtimeCss)

  const fold = (code: string, filePath = FILE_PATH, reportSurvivors = false): FoldResult => {
    const sourceFile = ctx.project.addSourceFile(filePath, code)
    const parserResult = ctx.project.parseSourceFile(filePath)
    if (!parserResult) return { code, map: null, folded: [], skipped: [], dependencies: [] }
    return foldSource({
      ctx,
      code,
      parserResult,
      filePath,
      runtimeCss,
      styleCompiler,
      parseModule: (path) => ctx.project.parseSourceFile(path),
      reportSurvivors,
      sourceFile,
    })
  }

  /** What `strict` would see: the fold with its output-based survivor check on. */
  const foldStrict = (code: string, filePath = FILE_PATH): FoldResult => fold(code, filePath, true)

  /** Fold recipes to globally shared utility atoms, as the strict production compiler does. */
  const foldStyleSets = (code: string, filePath = FILE_PATH, maxRecipeStates?: number): FoldResult => {
    const sourceFile = ctx.project.addSourceFile(filePath, code)
    const parserResult = ctx.project.parseSourceFile(filePath)
    if (!parserResult) return { code, map: null, folded: [], skipped: [], dependencies: [] }
    return foldSource({
      ctx,
      code,
      parserResult,
      filePath,
      runtimeCss,
      styleCompiler,
      maxRecipeStates,
      parseModule: (path) => ctx.project.parseSourceFile(path),
      sourceFile,
    })
  }

  /** Add the modules an entry imports before folding it. */
  const addFiles = (files: Record<string, string>) => {
    for (const [path, source] of Object.entries(files)) {
      ctx.project.addSourceFile(path, source)
      ctx.project.parseSourceFile(path)
    }
  }

  /** CSS for everything parsed through this fixture so far. */
  const getCss = () => {
    const sheet = ctx.createSheet()
    ctx.appendParserCss(sheet)
    return ctx.getCss(sheet)
  }

  /** CSS backing the classes emitted by `foldStyleSets`. */
  const getStyleSetCss = () => {
    ctx.encoder.atomizeObservedRecipes()
    const sheet = ctx.createSheet()
    ctx.appendParserCss(sheet)
    return ctx.getCss(sheet)
  }

  /**
   * A fold sharing one config cache across calls, as the vite plugin does for a build.
   *
   * Separate from `fold` because the shared cache has its own failure mode: it outlives the
   * modules it read, and `addSourceFile` forgets a file's nodes.
   */
  const recipeConfigCache = new Map<string, ForeignRecipes>()
  const foldWithCache = (code: string, filePath = FILE_PATH): FoldResult => {
    ctx.project.addSourceFile(filePath, code)
    const parserResult = ctx.project.parseSourceFile(filePath)
    if (!parserResult) return { code, map: null, folded: [], skipped: [], dependencies: [] }
    return foldSource({
      ctx,
      code,
      parserResult,
      filePath,
      runtimeCss,
      styleCompiler,
      parseModule: (path) => ctx.project.parseSourceFile(path),
      recipeConfigCache,
    })
  }

  return { ctx, fold, foldStrict, foldStyleSets, foldWithCache, addFiles, getCss, getStyleSetCss, runtimeCss }
}

/**
 * The class names a folded call resolved to.
 *
 * A fold emits the attribute form (`c_red.300`); the stylesheet emits the escaped
 * selector form (`.c_red\.300`). `esc` is the forward transform the decoder applies,
 * so applying it here compares the two without needing a fragile inverse.
 */
export const selectorsFor = (className: string) =>
  className
    .split(' ')
    .filter(Boolean)
    .map((name) => `.${esc(name)}`)

export const foldCode = (code: string, userConfig?: Config) => createFoldFixture(userConfig).fold(code).code
