import path from 'node:path'
import { transformSync } from '@babel/core'
import { describe, expect, it } from 'vitest'
import oneBabelPreset, { buildOneBabelPlugins } from './index'

const projectRoot = path.resolve(__dirname, '../../')

/**
 * Integration test: end-to-end load oneBabelPreset through @babel/core and
 * run it against representative One sources. Catches breakage in:
 *   - plugin specifier resolution (each `'one/babel-plugin-*'` must resolve)
 *   - plugin option shape (each plugin gets a valid config)
 *   - chain ordering (plugins compose without conflicts)
 *
 * Per-plugin semantic tests live alongside each plugin file. This file only
 * verifies that the preset wires them up correctly.
 */
describe('one/babel-preset integration', () => {
  // wiring tests run the One chain directly so assertions see untransformed
  // modules; syntax tests below run the full preset with its RN base.
  const onePlugins = buildOneBabelPlugins({ projectRoot, relativeRouterRoot: 'app' })
  const presetWithBase = [oneBabelPreset, { projectRoot }] as const

  // caller the Metro babel transformer sets on the `one dev` path, where the
  // Vite plugin already injected the One chain via customTransformOptions.
  const metroViteCaller = {
    name: 'metro',
    bundler: 'metro',
    platform: 'ios',
    projectRoot,
    oneViteMetroBabelConfig: true,
  }

  it('runs against a route file without throwing', () => {
    const code = `
      import { useLoader } from 'one'
      export default function Page() {
        return null
      }
    `
    const result = transformSync(code, {
      filename: path.join(projectRoot, 'app/index.tsx'),
      cwd: projectRoot,
      plugins: onePlugins,
      parserOpts: { sourceType: 'module', plugins: ['jsx'] },
    })

    expect(result?.code).toBeTruthy()
    // module-resolver should turn 'one' into a relative path under this
    // package because tsconfig.base.json has `one` aliased to packages/one
    // No assertion on the exact path — but it must still mention 'useLoader'
    expect(result?.code).toContain('useLoader')
  })

  it('substitutes ONE_ROUTER_* process.env placeholders in metro-entry', () => {
    // metro-entry-ctx.js uses process.env.ONE_ROUTER_* as placeholders that
    // one-router-metro replaces with the configured values at bundle time.
    const code = `
      const ctx = require.context(
        process.env.ONE_ROUTER_APP_ROOT_RELATIVE_TO_ENTRY,
        true,
        process.env.ONE_ROUTER_REQUIRE_CONTEXT_REGEX_STRING
      )
      const folder = process.env.ONE_ROUTER_ROOT_FOLDER_NAME
      module.exports = { ctx, folder }
    `
    const result = transformSync(code, {
      filename: path.join(projectRoot, 'metro-entry-ctx.js'),
      cwd: projectRoot,
      plugins: onePlugins,
      parserOpts: { sourceType: 'module' },
    })

    expect(result?.code).toBeTruthy()
    // placeholders must be replaced
    expect(result?.code).not.toContain(
      'process.env.ONE_ROUTER_APP_ROOT_RELATIVE_TO_ENTRY'
    )
    expect(result?.code).not.toContain('process.env.ONE_ROUTER_ROOT_FOLDER_NAME')
    expect(result?.code).not.toContain(
      'process.env.ONE_ROUTER_REQUIRE_CONTEXT_REGEX_STRING'
    )
    // routerRoot should be inlined as 'app'
    expect(result?.code).toContain('"app"')
    // require-context regex should be inlined as a regex literal
    expect(result?.code).toMatch(/\/\^.*\.tsx\?\$\//)
  })

  it('keeps client code intact in non-route files', () => {
    const code = `
      const x = 1
      export default x
    `
    const result = transformSync(code, {
      filename: path.join(projectRoot, 'src/utils/x.ts'),
      cwd: projectRoot,
      plugins: onePlugins,
      parserOpts: { sourceType: 'module' },
    })

    expect(result?.code).toContain('const x = 1')
  })

  it('strips `import type` on the one dev Metro path', () => {
    // every route file is TS. with no base preset Metro dies with
    // `TransformError: Unexpected token, expected "from"` on this line.
    const code = `
      import { Tabs } from 'one'
      import type { BottomTabNavigationOptions } from '@react-navigation/bottom-tabs'
      export default function Layout() {
        return null
      }
    `
    const result = transformSync(code, {
      filename: path.join(projectRoot, 'app/(tabs)/_layout.tsx'),
      cwd: projectRoot,
      presets: [presetWithBase],
      caller: metroViteCaller,
    })

    expect(result?.code).toBeTruthy()
    expect(result?.code).not.toContain('import type')
    expect(result?.code).toContain('Layout')
  })

  it('strips flow `import typeof` on the one dev Metro path', () => {
    // react-native/index.js ships `import typeof ... from './index.js.flow'`.
    // unstripped, Metro tries to resolve the .flow file and the iOS bundle
    // 500s with UnableToResolveError.
    const code = `import typeof * as ReactNativePublicAPI from './index.js.flow'
const x = 1
module.exports = x
`
    const result = transformSync(code, {
      filename: path.join(projectRoot, 'node_modules/react-native/index.js'),
      cwd: projectRoot,
      presets: [presetWithBase],
      caller: metroViteCaller,
    })

    expect(result?.code).toBeTruthy()
    expect(result?.code).not.toContain('index.js.flow')
  })

  it('executes namespace re-exports on the Metro path', () => {
    const result = transformSync('export * as core from "./core.js"', {
      filename: path.join(projectRoot, 'node_modules/dependency/index.js'),
      cwd: projectRoot,
      presets: [presetWithBase],
      caller: metroViteCaller,
    })
    const exports: Record<string, unknown> = {}
    const core = { answer: 42 }
    new Function('exports', 'require', result?.code ?? '')(exports, () => core)
    expect(exports.core).toMatchObject(core)
  })

  it('removes namespace type exports before transforming runtime exports', () => {
    const result = transformSync(
      'export type * as Types from "./types"; export * as core from "./core.js"',
      {
        filename: path.join(projectRoot, 'src/namespace.ts'),
        cwd: projectRoot,
        presets: [presetWithBase],
        caller: metroViteCaller,
      }
    )
    const requested: string[] = []
    const exports: Record<string, unknown> = {}
    new Function('exports', 'require', result?.code ?? '')(exports, (id: string) => {
      requested.push(id)
      return { answer: 42 }
    })
    expect(requested).toEqual(['./core.js'])
    expect(exports.core).toMatchObject({ answer: 42 })
    expect(exports).not.toHaveProperty('Types')
  })
})
