import { execSync } from 'node:child_process'
import fsSync from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, test } from 'vitest'
import { suggestPostcss } from '../src/interactive'
import fs from 'node:fs/promises'
import { afterAll } from 'vitest'
import { beforeAll } from 'vitest'

// Helper to run command and capture output even if it fails
function runCommand(cmd: string, options: any) {
  const opts = { ...options, encoding: 'utf8', env: { ...process.env, NO_COLOR: '1' } }
  try {
    return execSync(cmd, opts).toString()
  } catch (error: any) {
    // If command fails, still return the output (stdout + stderr)
    const stdout = error.stdout ? error.stdout.toString() : ''
    const stderr = error.stderr ? error.stderr.toString() : ''
    const output = stdout + stderr

    // Debug: log first 200 chars of output if test is failing
    if (process.env.DEBUG_CLI_TEST) {
      console.log('Command failed:', cmd)
      console.log('Output (first 200 chars):', output.slice(0, 200))
      console.log('stdout length:', stdout.length, 'stderr length:', stderr.length)
    }

    if (output) return output
    throw error
  }
}

describe('CLI', () => {
  const cwd = process.cwd()
  const _dirname = path.dirname(fileURLToPath(import.meta.url))
  const binPath = path.resolve(cwd, _dirname, '../bin.js')

  const testsCwd = path.resolve(cwd, _dirname, './samples')
  const paths = {
    config: path.resolve(testsCwd, 'bamboo.config.ts'),
    postcssConfig: path.resolve(testsCwd, 'postcss.config.cjs'),
    styledSystem: path.resolve(testsCwd, 'styled-system'),
    logFile: path.resolve(testsCwd, 'bamboo.log'),
    pkgJson: path.resolve(testsCwd, 'package.json'),
  }

  beforeAll(async () => {
    // Create the `samples` folder
    await fs.mkdir(testsCwd, { recursive: true })
  })

  afterAll(async () => {
    try {
      await Promise.allSettled([
        fs.unlink(paths.config),
        fs.unlink(paths.postcssConfig),
        fs.unlink(paths.logFile),
        fs.rm(paths.styledSystem, { recursive: true }),
      ])
    } catch {
      //
    }
  })

  test('init', async () => {
    // Clean up config file before test to ensure fresh state
    try {
      await fs.unlink(paths.config)
    } catch {
      // Ignore if file doesn't exist
    }

    const cmd = `node ${binPath} init --cwd="${testsCwd}"`

    // init
    const output = runCommand(cmd, { cwd: testsCwd })
    // Check for either "Thanks" (new config) or existing config message
    expect(output.includes('Thanks') || output.includes('It looks like you already have bamboo created')).toBe(true)

    // Check if the config file was created
    const configFileExists = await fs.access(paths.config)
    expect(configFileExists).toBeUndefined()

    // init on existing project
    const output2 = runCommand(cmd, { cwd: testsCwd })
    expect(output2.includes('It looks like you already have bamboo created')).toBe(true)

    // init with --force
    const output3 = runCommand(cmd + ' --force --postcss --logfile="./bamboo.log"', { cwd: testsCwd })
    expect(output3.includes('Bamboo initialized')).toBe(true)

    // Check if the postcss config file was created
    const postcssConfigFileExists = await fs.access(paths.postcssConfig)
    expect(postcssConfigFileExists).toBeUndefined()

    // Check if the log file was created
    const logFileExists = await fs.access(paths.logFile)
    expect(logFileExists).toBeUndefined()
  })

  /**
   * `--strict-values` used to be collected and dropped.
   *
   * cac parsed it, `interactive()` returned it, and the init action destructured everything
   * except it — so a project that asked for strict tokens got a config without the key and
   * nothing said so. The flag is documented in the CLI reference, which made it worse: the
   * only way to find out was to write a bad token and watch it not be reported.
   */
  test('init --strict-values', async () => {
    const cmd = `node ${binPath} init --cwd="${testsCwd}" --force --no-codegen`

    runCommand(`${cmd} --strict-values`, { cwd: testsCwd })
    expect(await fs.readFile(paths.config, 'utf8')).toContain('strictValues: true')

    // Absent unless asked for. This writes a *policy* — every raw css value has to become
    // `[14px]` — and a default cannot make that decision for a team. The mistake it used to
    // exist to catch, a misspelled token, is reported by the build with no setting at all.
    runCommand(cmd, { cwd: testsCwd })
    expect(await fs.readFile(paths.config, 'utf8')).not.toContain('strictValues')

    runCommand(`${cmd} --no-strict-values`, { cwd: testsCwd })
    expect(await fs.readFile(paths.config, 'utf8')).not.toContain('strictValues')
  }, 120_000)

  test('codegen', async () => {
    const cmd = `node ${binPath} codegen --cwd="${testsCwd}"`

    // codegen
    const output = runCommand(cmd + ' --cpu-prof', { cwd: testsCwd })
    expect(output.includes('the css function to author styles')).toBe(true)

    // Check that the `styled-system` folder was created
    const styledSystemExists = await fs.access(paths.styledSystem)
    expect(styledSystemExists).toBeUndefined()

    // Check that the `styled-system/jsx` was NOT created
    await expect(fs.access(path.resolve(paths.styledSystem, 'jsx'))).rejects.toThrow()

    // Check that the `.cpuprof` file was created
    const cpuProfPath = output.split('[cpu-prof]').pop()!.trim()!
    const cpuProfExists = await fs.access(cpuProfPath)
    expect(cpuProfExists).toBeUndefined()

    await fs.unlink(cpuProfPath)
  })

  test('cssgen', async () => {
    const cmd = `node ${binPath} cssgen --cwd="${testsCwd}"`

    // cssgen
    const output = runCommand(cmd, { cwd: testsCwd })
    expect(output.includes('Successfully extracted css')).toBe(true)

    // Check that the `styled-system/styles.css` was created
    const stylesCssExists = await fs.access(path.resolve(paths.styledSystem, 'styles.css'))
    expect(stylesCssExists).toBeUndefined()

    // Check that `--outfile` is fine
    const output3 = runCommand(cmd + ' --outfile="./styles.css"', { cwd: testsCwd })
    expect(output3.includes('Successfully extracted css')).toBe(true)

    await fs.unlink(path.resolve(testsCwd, 'styles.css'))

    // Check that `--silent` is fine
    const output4 = runCommand(cmd + ' --silent', { cwd: testsCwd })
    expect(output4.trim().length).toBe(0)
  })

  test('default', async () => {
    const cmd = `node ${binPath} --cwd="${testsCwd}"`

    // default
    const output = runCommand(cmd, { cwd: testsCwd })
    expect(output.includes('Successfully extracted css')).toBe(true)

    // Check that the `styled-system` folder was created
    const styledSystemExists = await fs.access(paths.styledSystem)
    expect(styledSystemExists).toBeUndefined()

    // Check that the `styled-system/styles.css` was created
    const stylesCssExists = await fs.access(path.resolve(paths.styledSystem, 'styles.css'))
    expect(stylesCssExists).toBeUndefined()
  })

  test('debug', async () => {
    const cmd = `node ${binPath} debug --cwd="${testsCwd}"`

    // debug
    const output = runCommand(cmd, { cwd: testsCwd })
    expect(output.includes('files using Bamboo')).toBe(true)

    // Check that the `styled-system/debug` folder was created
    const debugExists = await fs.access(path.resolve(paths.styledSystem, 'debug'))
    expect(debugExists).toBeUndefined()

    // Check that the `styled-system/debug/config.json` file was created
    const debugConfigExists = await fs.access(path.resolve(paths.styledSystem, 'debug/config.json'))
    expect(debugConfigExists).toBeUndefined()
  })

  test('ship', async () => {
    const cmd = `node ${binPath} ship --cwd="${testsCwd}"`

    // ship
    const output = runCommand(cmd, { cwd: testsCwd })
    expect(output.includes('files using Bamboo')).toBe(true)

    // Check that the `styled-system/bamboo.buildinfo.json` file was created
    const buildInfoExists = await fs.access(path.resolve(paths.styledSystem, 'bamboo.buildinfo.json'))
    expect(buildInfoExists).toBeUndefined()
  })

  test('emit-pkg', async () => {
    const cmd = `node ${binPath} emit-pkg --cwd="${testsCwd}"`

    // emit-pkg
    const output = runCommand(cmd, { cwd: testsCwd })
    expect(output.includes('Emit package.json')).toBe(true)

    // Check that the `package.json` file was created
    const pkgExists = await fs.access(paths.pkgJson)
    expect(pkgExists).toBeUndefined()

    // Clean up
    await fs.unlink(paths.pkgJson)
  })

  /**
   * Codegen leaves a `private` `package.json` in the outdir to carry the `sideEffects`
   * hint, so pointing `emit-pkg` at that directory finds a file already there. It still
   * has to produce a package that can be resolved and published: an entrypoint map alone,
   * on top of a `private` file with no version or license, is not one.
   */
  test('emit-pkg over the codegen output', async () => {
    const outdirPkg = path.resolve(paths.styledSystem, 'package.json')

    // The name is not decoration. Without one, a workspace glob that reaches this
    // directory takes down `pnpm install` and `changeset publish` with
    // `missing the "name" field` -- naming no directory in particular.
    expect(JSON.parse(await fs.readFile(outdirPkg, 'utf8'))).toMatchObject({
      name: 'styled-system',
      private: true,
      sideEffects: ['*.css', '**/*.css'],
    })

    runCommand(`node ${binPath} emit-pkg --outdir styled-system --cwd="${testsCwd}"`, { cwd: testsCwd })

    const pkg = JSON.parse(await fs.readFile(outdirPkg, 'utf8'))

    expect(pkg).toMatchObject({
      name: 'styled-system',
      version: '0.1.0',
      license: 'ISC',
      type: 'module',
      sideEffects: ['*.css', '**/*.css'],
      scripts: { prepare: 'bamboo codegen --clean' },
    })
    expect(pkg.exports['./css']).toBeDefined()
    expect('private' in pkg).toBe(false)
  })

  /** A file the consumer named is theirs; only the entrypoints are ours to write. */
  test('emit-pkg leaves a package the consumer named alone', async () => {
    const outdirPkg = path.resolve(paths.styledSystem, 'package.json')
    const owned = { name: '@acme/styled-system', version: '2.4.0', private: true, type: 'module' }
    await fs.writeFile(outdirPkg, JSON.stringify(owned, null, 2))

    runCommand(`node ${binPath} emit-pkg --outdir styled-system --cwd="${testsCwd}"`, { cwd: testsCwd })

    const pkg = JSON.parse(await fs.readFile(outdirPkg, 'utf8'))

    expect(pkg).toMatchObject(owned)
    expect(pkg.exports['./css']).toBeDefined()
    expect(pkg.license).toBeUndefined()
  })
})

/**
 * Which integration the interactive prompt suggests, before anybody is asked.
 *
 * "Would you like to use PostCSS?" defaulted to yes for every project, and that question decides
 * whether style calls are compiled away or the style engine ships to the client. Both render
 * identically, so pressing Enter picked the heavier one and nothing afterwards said so.
 *
 * The prompt itself needs a TTY; this is the decision behind it, which is the part with a wrong
 * answer.
 */
describe('the PostCSS suggestion', () => {
  const dirs: string[] = []

  const project = (files: Record<string, string>) => {
    const dir = fsSync.mkdtempSync(path.join(os.tmpdir(), 'bamboo-suggest-'))
    dirs.push(dir)
    for (const [name, body] of Object.entries(files)) fsSync.writeFileSync(path.join(dir, name), body)
    return dir
  }

  afterAll(() => {
    for (const dir of dirs) fsSync.rmSync(dir, { force: true, recursive: true })
  })

  const react = JSON.stringify({ dependencies: { react: '^19' } })

  test('is no for a Vite project whose components the compiler can transform', () => {
    expect(suggestPostcss(project({ 'vite.config.ts': '', 'package.json': react }))).toBe('no')
  })

  test('is yes without a Vite config, where the compiler is not an option', () => {
    expect(suggestPostcss(project({ 'package.json': react }))).toBe('yes')
    expect(suggestPostcss(project({ 'vitest.config.ts': '', 'package.json': react }))).toBe('yes')
  })

  test('is yes for Svelte, Vue and Astro, whose components it cannot transform', () => {
    for (const dependency of ['svelte', '@sveltejs/kit', 'vue', 'nuxt', 'astro']) {
      const dir = project({
        'vite.config.ts': '',
        'package.json': JSON.stringify({ devDependencies: { [dependency]: '*' } }),
      })
      expect(suggestPostcss(dir), dependency).toBe('yes')
    }
  })
})
