#!/usr/bin/env node
/* eslint-disable no-console */
const fs = require('fs')
const os = require('os')
const path = require('path')

const { runCodemodOnFile } = require('..')
const { applyDepsFileUpdates } = require('../../../commands/template-from-jsx-shared')
const { preprocessBilditTemplate } = require('../../preprocessBilditTemplate')
const webTranspile = require('../../transpile')

let failed = 0

function assert(cond, msg) {
  if (cond) {
    console.log(`  ok   ${msg}`)
  } else {
    failed += 1
    console.error(`  FAIL ${msg}`)
  }
}

function makeTmpDepsFile() {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'bild-codemod-'))
  return path.join(dir, 'cmsDependencies.ts')
}

/**
 * Minimal Next-style tree so `extractDependencies` can use the Components barrel
 * (local imports in fixtures must never be inlined into cmsDependencies).
 */
function makeTmpNextSite() {
  const siteRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'bild-codemod-site-'))
  fs.writeFileSync(path.join(siteRoot, 'package.json'), '{"name":"codemod-site"}\n')
  fs.mkdirSync(path.join(siteRoot, 'app', 'components'), { recursive: true })
  const barrelPath = path.join(siteRoot, 'app', 'components', 'Components.tsx')
  fs.writeFileSync(barrelPath, 'export {}\n')
  fs.mkdirSync(path.join(siteRoot, 'utils'), { recursive: true })
  const depsFilePath = path.join(siteRoot, 'utils', 'cmsDependencies.ts')
  fs.writeFileSync(
    depsFilePath,
    `import * as React from 'react'
import * as Components from '@/app/components/Components'
const cmsDependencies = {
  react: { module: React },
  '@components': { module: Components },
}
export default cmsDependencies
`,
  )
  return { siteRoot, depsFilePath, barrelPath }
}

async function testSimpleModule() {
  console.log('\n[simple-module]')
  const fixture = path.resolve(__dirname, 'fixtures/simple-module/hero.jsx')
  const { siteRoot, depsFilePath, barrelPath } = makeTmpNextSite()

  const { output, cssReport, depsReport, fieldReport } = runCodemodOnFile(fixture, {
    cssMode: 'hybrid',
    namingStrategy: 'camelComponent',
    depsFilePath,
    fieldMapPath: path.join(path.dirname(depsFilePath), 'field-map.json'),
    groupingStyle: 'lineComments',
    sitePath: siteRoot,
  })

  assert(!depsReport.appendedSource || !String(depsReport.appendedSource).trim(), 'no inlined source appended to cmsDependencies')
  assert(depsReport.useBarrelReexports, 'uses Components barrel re-exports')
  await applyDepsFileUpdates(depsFilePath, depsReport, 'test')

  assert(!/from ['"]\.\/hero\.module\.css['"]/.test(output), 'removes CSS module import')
  assert(!/from ['"]\.\/lib\/s7['"]/.test(output), 'removes relative util import')
  assert(
    /from ['"]@\/app\/components\/Components['"]/.test(output),
    'rewrites utility import through Components barrel',
  )
  assert(/\$\([^)]*:RichText/.test(output), 'emits RichText template fields')
  assert(/\$\([^)]*:Color/.test(output), 'emits Color template fields')
  assert(/\$\([^)]*:String/.test(output), 'emits String template fields (src/href)')
  assert(/\/\/ group \{/.test(output), 'emits line-comment groups (legacy style)')
  assert(!/\/\/ @template v2/.test(output), 'legacy output has no // @template v2 marker')
  assert(/\/\/ endgroup/.test(output), 'emits endgroup comments')
  assert(/const \w+ = \$\(/.test(output), 'hoists fields to const declarations')
  assert(cssReport.inlinedRules >= 2, `inlined >=2 CSS rules (got ${cssReport.inlinedRules})`)
  assert(depsReport.movedSymbols.some((s) => s.name === 'buildS7Image'), 'moved buildS7Image')
  assert(fieldReport.richTextCount >= 2, `tagged >=2 RichText (got ${fieldReport.richTextCount})`)
  assert(fieldReport.colorCount >= 1, `tagged >=1 Color (got ${fieldReport.colorCount})`)
  assert(typeof fieldReport.booleanCount === 'number', 'fieldReport has booleanCount')
  assert(typeof fieldReport.numberCount === 'number', 'fieldReport has numberCount')

  const barrelAfter = fs.readFileSync(barrelPath, 'utf8')
  assert(
    /buildS7Image/.test(barrelAfter) && /export\s+\{/.test(barrelAfter),
    'barrel file gains buildS7Image re-export',
  )

  const preprocessed = preprocessBilditTemplate(output, 'jsx')
  try {
    webTranspile(preprocessed, 'jsx', 'hero')
    assert(true, 'output transpiles')
  } catch (e) {
    assert(false, `output transpiles (${e.message})`)
  }
}

function testJsdocGrouping() {
  console.log('\n[jsdoc-grouping]')
  const fixture = path.resolve(__dirname, 'fixtures/simple-module/hero.jsx')
  const { siteRoot, depsFilePath } = makeTmpNextSite()
  const { output } = runCodemodOnFile(fixture, {
    cssMode: 'hybrid',
    namingStrategy: 'camelComponent',
    depsFilePath,
    fieldMapPath: path.join(path.dirname(depsFilePath), 'field-map.json'),
    groupingStyle: 'jsdoc',
    sitePath: siteRoot,
  })
  assert(/^\/\/ @template v2/m.test(output), 'prepends // @template v2 marker')
  assert(/@group /.test(output), 'emits @group on v2 const JSDoc')
  assert(/@type /.test(output), 'emits @type on v2 const JSDoc')
  assert(!/\/\/ group \{/.test(output), 'does not emit // group when using jsdoc style')
  assert(
    !/\$\([^)]*:(?:RichText|Color|String|Number|Boolean)/.test(output),
    'v2 output has no legacy $() template calls (plain const literals)',
  )
}

function testLowercaseButtonRichText() {
  console.log('\n[button-richtext]')
  const fixture = path.resolve(__dirname, 'fixtures/button-richtext/Button.jsx')
  const depsFilePath = makeTmpDepsFile()
  const { output, fieldReport } = runCodemodOnFile(fixture, {
    cssMode: 'hybrid',
    namingStrategy: 'camelComponent',
    depsFilePath,
    fieldMapPath: path.join(path.dirname(depsFilePath), 'field-map.json'),
    groupingStyle: 'lineComments',
  })
  assert(fieldReport.richTextCount >= 1, `RichText from lowercase <button> (got ${fieldReport.richTextCount})`)
  assert(/\$\([^)]*Shop Look/.test(output) || /Shop Look/.test(output), 'button label becomes template field')
}

function testStringLiteralRichText() {
  console.log('\n[string-literal-child]')
  const fixture = path.resolve(__dirname, 'fixtures/string-literal-child/string.jsx')
  const depsFilePath = makeTmpDepsFile()
  const { output, fieldReport } = runCodemodOnFile(fixture, {
    cssMode: 'hybrid',
    namingStrategy: 'camelComponent',
    depsFilePath,
    fieldMapPath: path.join(path.dirname(depsFilePath), 'field-map.json'),
    groupingStyle: 'lineComments',
  })
  assert(fieldReport.richTextCount >= 1, `RichText from string in braces (got ${fieldReport.richTextCount})`)
  assert(/Hello from braces/.test(output) || /\$\([^)]*RichText/.test(output), 'string literal converted to field')
}

function testUnicodeRichTextPreserved() {
  console.log('\n[unicode-richtext]')
  const fixture = path.resolve(__dirname, 'fixtures/unicode-richtext/unicode.jsx')
  const depsFilePath = makeTmpDepsFile()
  const { output } = runCodemodOnFile(fixture, {
    cssMode: 'hybrid',
    namingStrategy: 'camelComponent',
    depsFilePath,
    fieldMapPath: path.join(path.dirname(depsFilePath), 'field-map.json'),
    groupingStyle: 'lineComments',
  })
  assert(/7×/.test(output), 'preserves special characters in RichText defaults (×)')
}

function testNamedExportHoist() {
  console.log('\n[named-export-hoist]')
  const fixture = path.resolve(__dirname, 'fixtures/named-export/Named.tsx')
  const depsFilePath = makeTmpDepsFile()
  const { output } = runCodemodOnFile(fixture, {
    cssMode: 'hybrid',
    namingStrategy: 'camelComponent',
    depsFilePath,
    fieldMapPath: path.join(path.dirname(depsFilePath), 'field-map.json'),
    groupingStyle: 'jsdoc',
  })
  assert(/export function NamedExport/.test(output), 'keeps named export function')
  assert(/export default NamedExport/.test(output), 'adds default export for named-only component')
  assert(/@type RichText/.test(output), 'JSDoc @type RichText for named-export fields')
  assert(/const \w+ = /.test(output), 'v2 hoists RichText as plain string const')
  assert(/@group /.test(output), 'hoists with JSDoc for named export')
}

function testModuleArrayConfigLegacyScalars() {
  console.log('\n[module-array-config-legacy-scalars]')
  const fixture = path.resolve(__dirname, 'fixtures/array-config-legacy/Stats.jsx')
  const depsFilePath = makeTmpDepsFile()
  const { output, fieldReport } = runCodemodOnFile(fixture, {
    cssMode: 'hybrid',
    namingStrategy: 'camelComponent',
    depsFilePath,
    fieldMapPath: path.join(path.dirname(depsFilePath), 'field-map.json'),
    groupingStyle: 'lineComments',
  })
  assert((fieldReport.moduleArrayFieldCount || 0) >= 4, 'lifts STATS literals')
  assert(/firstTitle\.text/.test(output), 'RichText array field uses .text in legacy mode')
  assert(/firstValue[^.]/m.test(output), 'String array field stays bare identifier')
  assert(/firstPanelSource\.url/.test(output), 'Image array field uses .url in legacy mode')
  assert(/firstCtaHref\.href/.test(output), 'Link array field uses .href in legacy mode')
  assert(/\$\([^)]*firstTitle:RichText/.test(output), 'RichText template var emitted')
}

function testModuleArrayScalarStringsLegacy() {
  console.log('\n[module-array-scalar-strings-legacy]')
  const fixture = path.resolve(__dirname, 'fixtures/array-config-scalar-strings/NegationLines.jsx')
  const depsFilePath = makeTmpDepsFile()
  const { output, fieldReport } = runCodemodOnFile(fixture, {
    cssMode: 'hybrid',
    namingStrategy: 'camelComponent',
    depsFilePath,
    fieldMapPath: path.join(path.dirname(depsFilePath), 'field-map.json'),
    groupingStyle: 'lineComments',
  })
  assert((fieldReport.moduleArrayFieldCount || 0) >= 2, 'lifts each string row as RichText')
  assert(/firstText\.text/.test(output), 'scalar string uses .text in legacy mode')
  assert(/secondText\.text/.test(output), 'second scalar row uses .text')
  assert(/\$\([^)]*firstText:RichText/.test(output), 'scalar RichText template var')
  assert(/NEGATION_LINES\s*=\s*\[\s*firstText\.text/.test(output), 'array rewritten to field refs')
}

function testModuleArrayScalarStringsV2() {
  console.log('\n[module-array-scalar-strings-v2]')
  const fixture = path.resolve(__dirname, 'fixtures/array-config-scalar-strings/NegationLines.jsx')
  const depsFilePath = makeTmpDepsFile()
  const { output, fieldReport } = runCodemodOnFile(fixture, {
    cssMode: 'hybrid',
    namingStrategy: 'camelComponent',
    depsFilePath,
    fieldMapPath: path.join(path.dirname(depsFilePath), 'field-map.json'),
    groupingStyle: 'jsdoc',
  })
  assert((fieldReport.moduleArrayFieldCount || 0) >= 2, 'lifts scalar strings in v2 mode')
  assert(/const NEGATION_LINES = \[\s*firstText,\s*secondText/.test(output), 'v2 array uses bare identifiers')
  assert(!/\]\s*as\s+const\b/.test(output), 'drops as const on scalar array output')
}

function testModuleArrayConfig() {
  console.log('\n[module-array-config]')
  const fixture = path.resolve(__dirname, 'fixtures/array-config/Steps.tsx')
  const depsFilePath = makeTmpDepsFile()
  const { output, fieldReport } = runCodemodOnFile(fixture, {
    cssMode: 'hybrid',
    namingStrategy: 'camelComponent',
    depsFilePath,
    fieldMapPath: path.join(path.dirname(depsFilePath), 'field-map.json'),
    groupingStyle: 'jsdoc',
  })
  assert(
    (fieldReport.moduleArrayFieldCount || 0) >= 14,
    `lifts literals from STEPS (got moduleArrayFieldCount=${fieldReport.moduleArrayFieldCount})`,
  )
  assert(/@group STEPS \(build\)/.test(output), 'JSDoc group includes array name and step id')
  assert(/const STEPS = /.test(output), 'preserves STEPS const')
  assert(/Icon: PenLine/.test(output), 'preserves non-literal Icon reference')
  assert(/@type RichText/.test(output), 'JSDoc marks RichText for step titles/descriptions')
  assert(/['"]Build visually['"]/.test(output), 'RichText defaults as plain string literals')
  assert(!/\]\s*as\s+const\b/.test(output), 'drops `as const` so CMS JSX parsers accept the file')
  assert(
    !/\}\s*:\s*\{\s*className\?/.test(output),
    'strips TS prop type annotations from the component signature',
  )
}

function testImageElementLegacy() {
  console.log('\n[image-element-legacy]')
  const fixture = path.resolve(__dirname, 'fixtures/image-element/Hero.jsx')
  const depsFilePath = makeTmpDepsFile()
  const { output, fieldReport } = runCodemodOnFile(fixture, {
    cssMode: 'hybrid',
    namingStrategy: 'camelComponent',
    depsFilePath,
    fieldMapPath: path.join(path.dirname(depsFilePath), 'field-map.json'),
    groupingStyle: 'lineComments',
  })
  const preprocessed = preprocessBilditTemplate(output, 'jsx')
  assert(fieldReport.imageCount === 2, `tags both <img> elements as Image (got ${fieldReport.imageCount})`)
  assert(/Image=\{\s*url:\s*"https:\/\/example\.com\/hero-banner\.png"/.test(output), 'emits Image with url from src')
  assert(/alt:\s*"Hero banner"\s*\}/.test(output), 'emits Image with alt from existing alt attribute')
  assert(/alt:\s*""\s*\}/.test(output), 'second image (no alt) defaults alt to empty string')
  assert(/<img src=\{heroBannerImage\.url\} alt=\{heroBannerImage\.alt\}/.test(output), 'rewrites src/alt to field.url/field.alt')
  assert(/<img src=\{iconImage\.url\} alt=\{iconImage\.alt\}/.test(output), 'adds alt attr when source had none')
  try {
    webTranspile(preprocessed, 'jsx', 'image-element')
    assert(true, 'legacy Image output transpiles')
  } catch (e) {
    assert(false, `legacy Image output transpiles (${e.message})`)
  }
}

function testImageElementV2() {
  console.log('\n[image-element-v2]')
  const fixture = path.resolve(__dirname, 'fixtures/image-element/Hero.jsx')
  const depsFilePath = makeTmpDepsFile()
  const { output, fieldReport } = runCodemodOnFile(fixture, {
    cssMode: 'hybrid',
    namingStrategy: 'camelComponent',
    depsFilePath,
    fieldMapPath: path.join(path.dirname(depsFilePath), 'field-map.json'),
    groupingStyle: 'jsdoc',
  })
  assert(fieldReport.imageCount === 2, `tags both <img> elements as Image in v2 (got ${fieldReport.imageCount})`)
  assert(/@type Image/.test(output), 'emits @type Image JSDoc tag')
  assert(
    /const heroBannerImage = \{\s*url:\s*"https:\/\/example\.com\/hero-banner\.png",\s*alt:\s*"Hero banner"\s*\}/.test(output),
    'v2 hoists Image as plain `{ url, alt }` object literal',
  )
  assert(
    /const iconImage = \{\s*url:\s*"https:\/\/example\.com\/icon\.svg",\s*alt:\s*""\s*\}/.test(output),
    'v2 hoists missing alt as empty string',
  )
}

function testEnsureReactDefaultImport() {
  console.log('\n[ensure-react-import]')
  const depsFilePath = makeTmpDepsFile()
  const fieldMapPath = path.join(path.dirname(depsFilePath), 'field-map.json')
  const baseOpts = {
    cssMode: 'hybrid',
    namingStrategy: 'camelComponent',
    depsFilePath,
    fieldMapPath,
    groupingStyle: 'lineComments',
  }
  const noDefault = path.resolve(__dirname, 'fixtures/missing-react/NoDefault.jsx')
  const { output: o1 } = runCodemodOnFile(noDefault, baseOpts)
  assert(
    /import React, \{ useState \} from ['"]react['"]/.test(o1),
    'merges default React with existing named imports from react',
  )

  const noReact = path.resolve(__dirname, 'fixtures/missing-react/NoReactAtAll.jsx')
  const { output: o2 } = runCodemodOnFile(noReact, baseOpts)
  assert(/^import React from ['"]react['"]/m.test(o2), 'inserts import React from react when JSX has no React import')
}

async function main() {
  await testSimpleModule()
  testJsdocGrouping()
  testStringLiteralRichText()
  testLowercaseButtonRichText()
  testUnicodeRichTextPreserved()
  testNamedExportHoist()
  testModuleArrayConfigLegacyScalars()
  testModuleArrayScalarStringsLegacy()
  testModuleArrayScalarStringsV2()
  testModuleArrayConfig()
  testImageElementLegacy()
  testImageElementV2()
  testEnsureReactDefaultImport()
  if (failed) {
    console.error(`\n${failed} assertion(s) failed`)
    process.exit(1)
  }
  console.log('\nAll codemod tests passed.')
}

main().catch((e) => {
  console.error(e)
  process.exit(1)
})
