import fs from 'fs'
import { createRequire } from 'module'
import path from 'path'
import { prependDirsToPath } from '@pnpm/env.path'
import { tempDir, prepare as prepareWithPkg } from '@pnpm/prepare'
import { jest } from '@jest/globals'
import spawn from 'cross-spawn'
import nock from 'nock'

const require = createRequire(import.meta.dirname)
const pnpmTarballPath = require.resolve('@pnpm/tgz-fixtures/tgz/pnpm-9.1.0.tgz')

const actualModule = await import('@pnpm/cli-meta')
jest.unstable_mockModule('@pnpm/cli-meta', () => {
  return {
    ...actualModule,
    packageManager: {
      name: 'pnpm',
      version: '9.0.0',
    },
  }
})
const { selfUpdate } = await import('@pnpm/tools.plugin-commands-self-updater')

afterEach(() => {
  nock.cleanAll()
  nock.disableNetConnect()
})

beforeEach(() => {
  nock.enableNetConnect()
})

function prepare () {
  const dir = tempDir(false)
  return prepareOptions(dir)
}

function prepareOptions (dir: string) {
  return {
    argv: {
      original: [],
    },
    cliOptions: {},
    excludeLinksFromLockfile: false,
    linkWorkspacePackages: true,
    bail: true,
    pnpmHomeDir: dir,
    preferWorkspacePackages: true,
    registries: {
      default: 'https://registry.npmjs.org/',
    },
    rawLocalConfig: {},
    sort: false,
    rootProjectManifestDir: dir,
    bin: dir,
    workspaceConcurrency: 1,
    extraEnv: {},
    pnpmfile: '',
    rawConfig: {},
    cacheDir: path.join(dir, '.cache'),
    virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
    dir,
    managePackageManagerVersions: false,
  }
}

function createMetadata (latest: string, registry: string, otherVersions: string[] = []) {
  const versions = [...otherVersions, latest]
  return {
    name: 'pnpm',
    'dist-tags': { latest },
    versions: Object.fromEntries(versions.map((version) => [
      version,
      {
        name: 'pnpm',
        version,
        dist: {
          shasum: '217063ce3fcbf44f3051666f38b810f1ddefee4a',
          tarball: `${registry}pnpm/-/pnpm-${version}.tgz`,
          fileCount: 880,
          integrity: 'sha512-Z/WHmRapKT5c8FnCOFPVcb6vT3U8cH9AyyK+1fsVeMaq07bEEHzLO6CzW+AD62IaFkcayDbIe+tT+dVLtGEnJA==',
        },
      },
    ])),
  }
}

test('self-update', async () => {
  const opts = prepare()
  nock(opts.registries.default)
    .get('/pnpm')
    .reply(200, createMetadata('9.1.0', opts.registries.default))
  nock(opts.registries.default)
    .get('/pnpm/-/pnpm-9.1.0.tgz')
    .replyWithFile(200, pnpmTarballPath)

  await selfUpdate.handler(opts, [])

  const pnpmPkgJson = JSON.parse(fs.readFileSync(path.join(opts.pnpmHomeDir, '.tools/pnpm/9.1.0/node_modules/pnpm/package.json'), 'utf8'))
  expect(pnpmPkgJson.version).toBe('9.1.0')

  const pnpmEnv = prependDirsToPath([opts.pnpmHomeDir])
  const { status, stdout } = spawn.sync('pnpm', ['-v'], {
    env: {
      ...process.env,
      [pnpmEnv.name]: pnpmEnv.value,
    },
  })
  expect(status).toBe(0)
  expect(stdout.toString().trim()).toBe('9.1.0')
})

test('self-update by exact version', async () => {
  const opts = prepare()
  nock(opts.registries.default)
    .get('/pnpm')
    .reply(200, createMetadata('9.2.0', opts.registries.default, ['9.1.0']))
  nock(opts.registries.default)
    .get('/pnpm/-/pnpm-9.1.0.tgz')
    .replyWithFile(200, pnpmTarballPath)

  await selfUpdate.handler(opts, ['9.1.0'])

  const pnpmPkgJson = JSON.parse(fs.readFileSync(path.join(opts.pnpmHomeDir, '.tools/pnpm/9.1.0/node_modules/pnpm/package.json'), 'utf8'))
  expect(pnpmPkgJson.version).toBe('9.1.0')

  const pnpmEnv = prependDirsToPath([opts.pnpmHomeDir])
  const { status, stdout } = spawn.sync('pnpm', ['-v'], {
    env: {
      ...process.env,
      [pnpmEnv.name]: pnpmEnv.value,
    },
  })
  expect(status).toBe(0)
  expect(stdout.toString().trim()).toBe('9.1.0')
})

test('self-update does nothing when pnpm is up to date', async () => {
  const opts = prepare()
  nock(opts.registries.default)
    .get('/pnpm')
    .reply(200, createMetadata('9.0.0', opts.registries.default))

  const output = await selfUpdate.handler(opts, [])

  expect(output).toBe('The currently active pnpm v9.0.0 is already "latest" and doesn\'t need an update')
})

test('should update packageManager field when a newer pnpm version is available', async () => {
  const opts = prepare()
  const pkgJsonPath = path.join(opts.dir, 'package.json')
  fs.writeFileSync(pkgJsonPath, JSON.stringify({
    packageManager: 'pnpm@8.0.0',
  }), 'utf8')
  nock(opts.registries.default)
    .get('/pnpm')
    .reply(200, createMetadata('9.0.0', opts.registries.default))

  const output = await selfUpdate.handler({
    ...opts,
    managePackageManagerVersions: true,
    wantedPackageManager: {
      name: 'pnpm',
      version: '8.0.0',
    },
  }, [])

  expect(output).toBe('The current project has been updated to use pnpm v9.0.0')
  expect(JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8')).packageManager).toBe('pnpm@9.0.0')
})

test('should not update packageManager field when current version matches latest', async () => {
  const opts = prepare()
  const pkgJsonPath = path.join(opts.dir, 'package.json')
  fs.writeFileSync(pkgJsonPath, JSON.stringify({
    packageManager: 'pnpm@9.0.0',
  }), 'utf8')
  nock(opts.registries.default)
    .get('/pnpm')
    .reply(200, createMetadata('9.0.0', opts.registries.default))

  const output = await selfUpdate.handler({
    ...opts,
    managePackageManagerVersions: true,
    wantedPackageManager: {
      name: 'pnpm',
      version: '9.0.0',
    },
  }, [])

  expect(output).toBe('The current project is already set to use pnpm v9.0.0')
  expect(JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8')).packageManager).toBe('pnpm@9.0.0')
})

test('self-update links pnpm that is already present on the disk', async () => {
  const opts = prepare()
  nock(opts.registries.default)
    .get('/pnpm')
    .reply(200, createMetadata('9.2.0', opts.registries.default))

  const baseDir = path.join(opts.pnpmHomeDir, '.tools/pnpm/9.2.0')
  fs.mkdirSync(path.join(baseDir, 'bin'), { recursive: true })
  const latestPnpmDir = path.join(baseDir, 'node_modules/pnpm')
  fs.mkdirSync(latestPnpmDir, { recursive: true })
  fs.writeFileSync(path.join(latestPnpmDir, 'package.json'), JSON.stringify({ name: 'pnpm', bin: 'bin.js' }), 'utf8')
  fs.writeFileSync(path.join(latestPnpmDir, 'bin.js'), `#!/usr/bin/env node
console.log('9.2.0')`, 'utf8')
  const output = await selfUpdate.handler(opts, [])

  expect(output).toBe(`The latest version, v9.2.0, is already present on the system. It was activated by linking it from ${path.join(latestPnpmDir, '../..')}.`)

  const pnpmEnv = prependDirsToPath([opts.pnpmHomeDir])
  const { status, stdout } = spawn.sync('pnpm', ['-v'], {
    env: {
      ...process.env,
      [pnpmEnv.name]: pnpmEnv.value,
    },
  })
  expect(status).toBe(0)
  expect(stdout.toString().trim()).toBe('9.2.0')
})

test('self-update updates the packageManager field in package.json', async () => {
  prepareWithPkg({
    packageManager: 'pnpm@9.0.0',
  })
  const opts = {
    ...prepareOptions(process.cwd()),
    managePackageManagerVersions: true,
    wantedPackageManager: {
      name: 'pnpm',
      version: '9.0.0',
    },
  }
  nock(opts.registries.default)
    .get('/pnpm')
    .reply(200, createMetadata('9.1.0', opts.registries.default))
  nock(opts.registries.default)
    .get('/pnpm/-/pnpm-9.1.0.tgz')
    .replyWithFile(200, pnpmTarballPath)

  const output = await selfUpdate.handler(opts, [])

  expect(output).toBe('The current project has been updated to use pnpm v9.1.0')

  const pkgJson = JSON.parse(fs.readFileSync(path.resolve('package.json'), 'utf8'))
  expect(pkgJson.packageManager).toBe('pnpm@9.1.0')
})
