import path from 'path'
import { filterPackagesFromDir } from '@pnpm/workspace.filter-packages-from-dir'
import { type LockfileObject } from '@pnpm/lockfile.types'
import { prepare, preparePackages } from '@pnpm/prepare'
import { REGISTRY_MOCK_PORT, addDistTag } from '@pnpm/registry-mock'
import { jest } from '@jest/globals'
import { sync as readYamlFile } from 'read-yaml-file'
import chalk from 'chalk'

jest.unstable_mockModule('enquirer', () => ({ default: { prompt: jest.fn() } }))
const { default: enquirer } = await import('enquirer')
const { add, install, update } = await import('@pnpm/plugin-commands-installation')

const prompt = jest.mocked(enquirer.prompt)

const REGISTRY_URL = `http://localhost:${REGISTRY_MOCK_PORT}`

const DEFAULT_OPTIONS = {
  argv: {
    original: [],
  },
  bail: false,
  bin: 'node_modules/.bin',
  excludeLinksFromLockfile: false,
  extraEnv: {},
  cliOptions: {},
  deployAllFiles: false,
  include: {
    dependencies: true,
    devDependencies: true,
    optionalDependencies: true,
  },
  lock: true,
  pnpmfile: ['.pnpmfile.cjs'],
  pnpmHomeDir: '',
  preferWorkspacePackages: true,
  rawConfig: { registry: REGISTRY_URL },
  rawLocalConfig: { registry: REGISTRY_URL },
  registries: {
    default: REGISTRY_URL,
  },
  rootProjectManifestDir: '',
  sort: true,
  userConfig: {},
  workspaceConcurrency: 1,
  virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
}

test('interactively update', async () => {
  const project = prepare({
    dependencies: {
      // has 1.0.0 and 1.0.1 that satisfy this range
      'is-negative': '^1.0.0',
      // only 2.0.0 satisfies this range
      'is-positive': '^2.0.0',
      // has many versions that satisfy ^3.0.0
      micromatch: '^3.0.0',
    },
  })

  const storeDir = path.resolve('pnpm-store')

  const headerChoice = {
    name: 'Package                                                    Current   Target            URL ',
    disabled: true,
    hint: '',
    value: '',
  }

  await Promise.all([
    addDistTag({ package: 'is-negative', version: '2.1.0', distTag: 'latest' }),
    addDistTag({ package: 'micromatch', version: '4.0.0', distTag: 'latest' }),
  ])

  await add.handler(
    {
      ...DEFAULT_OPTIONS,
      cacheDir: path.resolve('cache'),
      dir: process.cwd(),
      linkWorkspacePackages: true,
      save: false,
      storeDir,
    },
    ['is-negative@1.0.0', 'is-positive@2.0.0', 'micromatch@3.0.0']
  )

  prompt.mockResolvedValue({
    updateDependencies: [
      {
        value: 'is-negative',
        name: `is-negative 1.0.0 ❯ 1.0.${chalk.greenBright.bold('1')} https://pnpm.io/ `,
      },
    ],
  })

  prompt.mockClear()
  // Update to compatible versions
  await update.handler({
    ...DEFAULT_OPTIONS,
    cacheDir: path.resolve('cache'),
    dir: process.cwd(),
    interactive: true,
    linkWorkspacePackages: true,
    storeDir,
  })

  // eslint-disable-next-line
  expect((prompt.mock.calls[0][0] as any).choices).toStrictEqual([
    {
      choices: [
        headerChoice,
        {
          message: `is-negative                                                  1.0.0 ❯ 1.0.${chalk.greenBright.bold('1')}                 `,
          value: 'is-negative',
          name: 'is-negative',
        },
        {
          message: `micromatch                                                   3.0.0 ❯ 3.${chalk.yellowBright.bold('1.10')}                `,
          value: 'micromatch',
          name: 'micromatch',
        },
      ],
      name: '[dependencies]',
      message: 'dependencies',
    },
  ])
  expect(prompt).toHaveBeenCalledWith(
    expect.objectContaining({
      footer: '\nEnter to start updating. Ctrl-c to cancel.',
      message:
        'Choose which packages to update ' +
        `(Press ${chalk.cyan('<space>')} to select, ` +
        `${chalk.cyan('<a>')} to toggle all, ` +
        `${chalk.cyan('<i>')} to invert selection)`,
      name: 'updateDependencies',
      type: 'multiselect',
    })
  )

  {
    const lockfile = project.readLockfile()

    expect(lockfile.packages['micromatch@3.0.0']).toBeTruthy()
    expect(lockfile.packages['is-negative@1.0.1']).toBeTruthy()
    expect(lockfile.packages['is-positive@2.0.0']).toBeTruthy()
  }

  // Update to latest versions
  prompt.mockClear()
  await update.handler({
    ...DEFAULT_OPTIONS,
    cacheDir: path.resolve('cache'),
    dir: process.cwd(),
    interactive: true,
    latest: true,
    linkWorkspacePackages: true,
    storeDir,
  })

  // eslint-disable-next-line
  expect((prompt.mock.calls[0][0] as any).choices).toStrictEqual([
    {
      choices: [
        headerChoice,
        {
          message: `is-negative                                                  1.0.1 ❯ ${chalk.redBright.bold('2.1.0')}                 `,
          value: 'is-negative',
          name: 'is-negative',
        },
        {
          message: `is-positive                                                  2.0.0 ❯ ${chalk.redBright.bold('3.1.0')}                 `,
          value: 'is-positive',
          name: 'is-positive',
        },
        {
          message: `micromatch                                                   3.0.0 ❯ ${chalk.redBright.bold('4.0.0')}                 `,
          value: 'micromatch',
          name: 'micromatch',
        },
      ],
      name: '[dependencies]',
      message: 'dependencies',
    },
  ])
  expect(prompt).toHaveBeenCalledWith(
    expect.objectContaining({
      footer: '\nEnter to start updating. Ctrl-c to cancel.',
      message:
        'Choose which packages to update ' +
        `(Press ${chalk.cyan('<space>')} to select, ` +
        `${chalk.cyan('<a>')} to toggle all, ` +
        `${chalk.cyan('<i>')} to invert selection)`,
      name: 'updateDependencies',
      type: 'multiselect',
    })
  )

  {
    const lockfile = project.readLockfile()

    expect(lockfile.packages['micromatch@3.0.0']).toBeTruthy()
    expect(lockfile.packages['is-negative@2.1.0']).toBeTruthy()
    expect(lockfile.packages['is-positive@2.0.0']).toBeTruthy()
  }
})

test('interactive update of dev dependencies only', async () => {
  preparePackages([
    {
      name: 'project1',

      dependencies: {
        'is-negative': '^1.0.1',
      },
    },
    {
      name: 'project2',

      devDependencies: {
        'is-negative': '^1.0.0',
      },
    },
  ])
  const storeDir = path.resolve('store')

  prompt.mockResolvedValue({
    updateDependencies: [
      {
        value: 'is-negative',
        name: `is-negative 1.0.0 ❯ 1.0.${chalk.greenBright.bold('1')} https://pnpm.io/ `,
      },
    ],
  })

  const { allProjects, selectedProjectsGraph } = await filterPackagesFromDir(
    process.cwd(),
    []
  )
  await install.handler({
    ...DEFAULT_OPTIONS,
    cacheDir: path.resolve('cache'),
    allProjects,
    dir: process.cwd(),
    linkWorkspacePackages: true,
    lockfileDir: process.cwd(),
    recursive: true,
    selectedProjectsGraph,
    storeDir,
    workspaceDir: process.cwd(),
  })
  await update.handler({
    ...DEFAULT_OPTIONS,
    cacheDir: path.resolve('cache'),
    allProjects,
    cliOptions: {
      dev: true,
      optional: false,
      production: false,
    },
    dir: process.cwd(),
    interactive: true,
    latest: true,
    linkWorkspacePackages: true,
    lockfileDir: process.cwd(),
    recursive: true,
    selectedProjectsGraph,
    storeDir,
    workspaceDir: process.cwd(),
  })

  const lockfile = readYamlFile<LockfileObject>('pnpm-lock.yaml')

  expect(Object.keys(lockfile.packages ?? {})).toStrictEqual([
    'is-negative@1.0.1',
    'is-negative@2.1.0',
  ])
})

test('interactively update should ignore dependencies from the ignoreDependencies field', async () => {
  const project = prepare({
    dependencies: {
      // has 1.0.0 and 1.0.1 that satisfy this range
      'is-negative': '^1.0.0',
      // only 2.0.0 satisfies this range
      'is-positive': '^2.0.0',
      // has many versions that satisfy ^3.0.0
      micromatch: '^3.0.0',
    },
  })

  const storeDir = path.resolve('pnpm-store')

  await add.handler(
    {
      ...DEFAULT_OPTIONS,
      cacheDir: path.resolve('cache'),
      dir: process.cwd(),
      linkWorkspacePackages: true,
      save: false,
      storeDir,
    },
    ['is-negative@1.0.0', 'is-positive@2.0.0', 'micromatch@3.0.0']
  )

  prompt.mockResolvedValue({
    updateDependencies: [{ value: 'micromatch', name: 'anything' }],
  })

  prompt.mockClear()
  await update.handler({
    ...DEFAULT_OPTIONS,
    cacheDir: path.resolve('cache'),
    dir: process.cwd(),
    interactive: true,
    linkWorkspacePackages: true,
    storeDir,
    updateConfig: {
      ignoreDependencies: ['is-negative'],
    },
  })

  // eslint-disable-next-line
  expect((prompt.mock.calls[0][0] as any).choices as any).toStrictEqual(
    [
      {
        choices: [
          {
            disabled: true,
            hint: '',
            name: 'Package                                                    Current   Target            URL ',
            value: '',
          },
          {
            message: `micromatch                                                   3.0.0 ❯ 3.${chalk.yellowBright.bold('1.10')}                `,
            value: 'micromatch',
            name: 'micromatch',
          },
        ],
        name: '[dependencies]',
        message: 'dependencies',
      },
    ]
  )

  expect(prompt).toHaveBeenCalledWith(
    expect.objectContaining({
      footer: '\nEnter to start updating. Ctrl-c to cancel.',
      message:
        'Choose which packages to update ' +
        `(Press ${chalk.cyan('<space>')} to select, ` +
        `${chalk.cyan('<a>')} to toggle all, ` +
        `${chalk.cyan('<i>')} to invert selection)`,
      name: 'updateDependencies',
      type: 'multiselect',
    })
  )

  {
    const lockfile = project.readLockfile()

    expect(lockfile.packages['micromatch@3.1.10']).toBeTruthy()
    expect(lockfile.packages['is-negative@1.0.0']).toBeTruthy()
    expect(lockfile.packages['is-positive@2.0.0']).toBeTruthy()
  }
})
