import type { CommandEntry } from './generate-command-tree.js'
import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import {
  buildTree,
  discoverCommands,
  formatModule,
  generate,
  pathToTokens,
  tokensToIdentifier,
} from './generate-command-tree.js'

describe('pathToTokens', () => {
  it('extracts tokens for nested command', () => {
    expect(pathToTokens('src/commands/auth/devices/list/index.ts', 'src/commands')).toEqual([
      'auth',
      'devices',
      'list',
    ])
  })

  it('extracts tokens for top-level command', () => {
    expect(pathToTokens('src/commands/version/index.ts', 'src/commands')).toEqual(['version'])
  })

  it('normalizes backslashes (windows-style paths)', () => {
    expect(pathToTokens('src\\commands\\auth\\login\\index.ts', 'src/commands')).toEqual([
      'auth',
      'login',
    ])
  })
})

describe('tokensToIdentifier', () => {
  it('pascal-cases joined tokens', () => {
    expect(tokensToIdentifier(['auth', 'devices', 'list'])).toBe('AuthDevicesList')
    expect(tokensToIdentifier(['version'])).toBe('Version')
    expect(tokensToIdentifier(['run', 'app', 'resume'])).toBe('RunAppResume')
  })

  it('splits hyphenated tokens', () => {
    expect(tokensToIdentifier(['agent-chat'])).toBe('AgentChat')
    expect(tokensToIdentifier(['my-cmd', 'sub-thing'])).toBe('MyCmdSubThing')
  })

  it('prefixes underscore on reserved words', () => {
    expect(tokensToIdentifier(['delete'])).toBe('_Delete')
  })
})

describe('buildTree', () => {
  it('assembles a nested tree from entries', () => {
    const entries = [
      {
        tokens: ['auth', 'login'],
        identifier: 'AuthLogin',
        importPath: '@/commands/auth/login/index',
      },
      {
        tokens: ['auth', 'devices', 'list'],
        identifier: 'AuthDevicesList',
        importPath: '@/commands/auth/devices/list/index',
      },
      { tokens: ['version'], identifier: 'Version', importPath: '@/commands/version/index' },
    ]
    const tree = buildTree(entries)
    expect(tree.subcommands.get('auth')?.command).toBeUndefined()
    expect(tree.subcommands.get('auth')?.subcommands.get('login')?.command).toBe('AuthLogin')
    expect(
      tree.subcommands.get('auth')?.subcommands.get('devices')?.subcommands.get('list')?.command,
    ).toBe('AuthDevicesList')
    expect(tree.subcommands.get('version')?.command).toBe('Version')
  })

  it('supports a parent command with its own children', () => {
    const entries = [
      { tokens: ['run', 'app'], identifier: 'RunApp', importPath: '@/commands/run/app/index' },
      {
        tokens: ['run', 'app', 'resume'],
        identifier: 'RunAppResume',
        importPath: '@/commands/run/app/resume/index',
      },
    ]
    const tree = buildTree(entries)
    const runApp = tree.subcommands.get('run')?.subcommands.get('app')
    expect(runApp?.command).toBe('RunApp')
    expect(runApp?.subcommands.get('resume')?.command).toBe('RunAppResume')
  })
})

describe('formatModule', () => {
  it('produces a deterministic ESM file with imports + tree literal', () => {
    const entries: CommandEntry[] = [
      {
        tokens: ['auth', 'login'],
        identifier: 'AuthLogin',
        importPath: '@/commands/auth/login/index',
      },
      { tokens: ['version'], identifier: 'Version', importPath: '@/commands/version/index' },
      {
        tokens: ['auth', 'devices', 'list'],
        identifier: 'AuthDevicesList',
        importPath: '@/commands/auth/devices/list/index',
      },
    ]
    const tree = buildTree(entries)
    const out = formatModule(entries, tree)
    expect(out).toBe(
      `// @generated by scripts/generate-command-tree.ts — DO NOT EDIT.
// Regenerate via \`pnpm tree:gen\`. Drift gated by \`pnpm tree:check\` in CI.

import type { CommandTree } from '@/framework/registry'
import AuthDevicesList from '@/commands/auth/devices/list/index'
import AuthLogin from '@/commands/auth/login/index'
import Version from '@/commands/version/index'

export const commandTree: CommandTree = {
  auth: {
    subcommands: {
      devices: {
        subcommands: {
          list: { command: AuthDevicesList, subcommands: {} },
        },
      },
      login: { command: AuthLogin, subcommands: {} },
    },
  },
  version: { command: Version, subcommands: {} },
}
`,
    )
  })

  it('emits parent-with-own-command shape', () => {
    const entries: CommandEntry[] = [
      { tokens: ['run', 'app'], identifier: 'RunApp', importPath: '@/commands/run/app/index' },
      {
        tokens: ['run', 'app', 'resume'],
        identifier: 'RunAppResume',
        importPath: '@/commands/run/app/resume/index',
      },
    ]
    const tree = buildTree(entries)
    const out = formatModule(entries, tree)
    expect(out).toContain(`run: {
    subcommands: {
      app: {
        command: RunApp,
        subcommands: {
          resume: { command: RunAppResume, subcommands: {} },
        },
      },
    },
  },`)
  })

  it('imports sorted alphabetically by import path', () => {
    const entries: CommandEntry[] = [
      { tokens: ['version'], identifier: 'Version', importPath: '@/commands/version/index' },
      {
        tokens: ['auth', 'login'],
        identifier: 'AuthLogin',
        importPath: '@/commands/auth/login/index',
      },
    ]
    const out = formatModule(entries, buildTree(entries))
    const authIdx = out.indexOf('AuthLogin')
    const verIdx = out.indexOf('Version')
    expect(authIdx).toBeLessThan(verIdx)
  })

  it('quotes hyphenated keys and leaves plain identifier keys unquoted', () => {
    const entries: CommandEntry[] = [
      {
        tokens: ['export', 'app'],
        identifier: 'ExportApp',
        importPath: '@/commands/export/app/index',
      },
      {
        tokens: ['export', 'studio-app'],
        identifier: 'ExportStudioApp',
        importPath: '@/commands/export/studio-app/index',
      },
    ]
    const out = formatModule(entries, buildTree(entries))
    expect(out).toContain(`'studio-app': { command: ExportStudioApp, subcommands: {} },`)
    expect(out).toContain(`app: { command: ExportApp, subcommands: {} },`)
    expect(out).not.toContain(`'app':`)
  })
})

function makeFixture(): string {
  const root = mkdtempSync(join(tmpdir(), 'difyctl-codegen-'))
  const commands = join(root, 'src', 'commands')
  mkdirSync(join(commands, 'auth', 'login'), { recursive: true })
  writeFileSync(join(commands, 'auth', 'login', 'index.ts'), 'export default class Login {}\n')
  mkdirSync(join(commands, 'auth', 'devices', 'list'), { recursive: true })
  writeFileSync(
    join(commands, 'auth', 'devices', 'list', 'index.ts'),
    'export default class DevicesList {}\n',
  )
  mkdirSync(join(commands, '_shared'), { recursive: true })
  writeFileSync(join(commands, '_shared', 'index.ts'), 'export default class Shared {}\n')
  mkdirSync(join(commands, 'version'), { recursive: true })
  writeFileSync(join(commands, 'version', 'index.ts'), 'export default class Version {}\n')
  return root
}

describe('discoverCommands', () => {
  it('returns sorted entries, skipping _-prefixed segments', async () => {
    const root = makeFixture()
    const entries = await discoverCommands(join(root, 'src', 'commands'))
    expect(entries.map((e) => e.tokens.join('/'))).toEqual([
      'auth/devices/list',
      'auth/login',
      'version',
    ])
    expect(entries.find((e) => e.tokens[0] === '_shared')).toBeUndefined()
  })

  it('errors on a loose .ts file under commands/', async () => {
    const root = mkdtempSync(join(tmpdir(), 'difyctl-codegen-loose-'))
    const commands = join(root, 'src', 'commands')
    mkdirSync(commands, { recursive: true })
    writeFileSync(join(commands, 'foo.ts'), 'export default class Foo {}\n')
    await expect(discoverCommands(commands)).rejects.toThrow(/must live under their own folder/)
  })
})

describe('generate', () => {
  it('writes tree.generated.ts on default mode', async () => {
    const root = makeFixture()
    const commandsDir = join(root, 'src', 'commands')
    const result = await generate({ commandsDir, mode: 'write' })
    expect(result.mode).toBe('write')
    const target = join(commandsDir, 'tree.generated.ts')
    expect(existsSync(target)).toBe(true)
    const content = readFileSync(target, 'utf8')
    expect(content).toContain('@generated')
    expect(content).toContain('import AuthLogin')
    expect(content).toContain('import AuthDevicesList')
    expect(content).toContain('import Version')
  })

  it('returns ok: true on --check when file matches', async () => {
    const root = makeFixture()
    const commandsDir = join(root, 'src', 'commands')
    await generate({ commandsDir, mode: 'write' })
    const result = await generate({ commandsDir, mode: 'check' })
    if (result.mode !== 'check') throw new Error('expected check mode')
    expect(result.ok).toBe(true)
  })

  it('returns ok: false on --check when file is stale', async () => {
    const root = makeFixture()
    const commandsDir = join(root, 'src', 'commands')
    await generate({ commandsDir, mode: 'write' })
    writeFileSync(join(commandsDir, 'tree.generated.ts'), '// stale\n')
    const result = await generate({ commandsDir, mode: 'check' })
    if (result.mode !== 'check') throw new Error('expected check mode')
    expect(result.ok).toBe(false)
    if (!result.ok) expect(result.diff).toBeDefined()
  })
})
