import { codeBlock } from 'common-tags';
import { extractAllPackageFiles } from '..';
import { logger } from '../../../../logger';
import type { ExtractConfig } from '../../types';
import { postExtract } from './post';
import * as npmExtract from '.';
import { Fixtures } from '~test/fixtures';
import { fs } from '~test/util';

vi.mock('../../../../util/fs');

const defaultExtractConfig = {
  skipInstalls: null,
} satisfies ExtractConfig;

const input01Content = Fixtures.get('inputs/01.json', '..');
const input02Content = Fixtures.get('inputs/02.json', '..');
const input01GlobContent = Fixtures.get('inputs/01-glob.json', '..');
const workspacesContent = Fixtures.get('inputs/workspaces.json', '..');
const vendorisedContent = Fixtures.get('is-object.json', '..');
const invalidNameContent = Fixtures.get('invalid-name.json', '..');

describe('modules/manager/npm/extract/index', () => {
  describe('.extractPackageFile()', () => {
    beforeEach(async () => {
      const realFs = await vi.importActual<typeof fs>('../../../../util/fs');
      fs.readLocalFile.mockResolvedValue(null);
      fs.localPathExists.mockResolvedValue(false);
      fs.getSiblingFileName.mockImplementation(realFs.getSiblingFileName);
    });

    it('returns null if cannot parse', async () => {
      const res = await npmExtract.extractPackageFile(
        'not json',
        'package.json',
        defaultExtractConfig,
      );
      expect(res).toBeNull();
    });

    it('catches invalid names', async () => {
      const res = await npmExtract.extractPackageFile(
        invalidNameContent,
        'package.json',
        defaultExtractConfig,
      );
      expect(res).toMatchSnapshot({
        deps: [{ skipReason: 'invalid-name' }],
      });
    });

    it('ignores vendorised package.json', async () => {
      const res = await npmExtract.extractPackageFile(
        vendorisedContent,
        'package.json',
        defaultExtractConfig,
      );
      expect(res).toBeNull();
    });

    it('throws error if non-root renovate config', async () => {
      await expect(
        npmExtract.extractPackageFile(
          '{ "renovate": {} }',
          'backend/package.json',
          defaultExtractConfig,
        ),
      ).rejects.toThrow();
    });

    it('returns null if no deps', async () => {
      const res = await npmExtract.extractPackageFile(
        '{ "renovate": {} }',
        'package.json',
        defaultExtractConfig,
      );
      expect(res).toBeNull();
    });

    it('handles invalid', async () => {
      const res = await npmExtract.extractPackageFile(
        '{"dependencies": true, "devDependencies": []}',
        'package.json',
        defaultExtractConfig,
      );
      expect(res).toBeNull();
    });

    it('returns an array of dependencies', async () => {
      const res = await npmExtract.extractPackageFile(
        input01Content,
        'package.json',
        defaultExtractConfig,
      );
      expect(res).toMatchSnapshot({
        deps: [
          { depName: 'autoprefixer', currentValue: '6.5.0' },
          { depName: 'bower', currentValue: '~1.6.0' },
          { depName: 'browserify', currentValue: '13.1.0' },
          { depName: 'browserify-css', currentValue: '0.9.2' },
          { depName: 'cheerio', currentValue: '=0.22.0' },
          { depName: 'config', currentValue: '1.21.0' },
          { depName: 'enabled', skipReason: 'invalid-value' },
          { depName: 'angular', currentValue: '^1.5.8' },
          { depName: 'angular-touch', currentValue: '1.5.8' },
          { depName: 'angular-sanitize', currentValue: '1.5.8' },
          { depName: '@angular/core', currentValue: '4.0.0-beta.1' },
          { depName: 'config', currentValue: '1.21.0' },
          { depName: '@angular/cli', currentValue: '8.0.0' },
          { depName: 'angular', currentValue: '1.33.0' },
          { depName: 'glob', currentValue: '1.0.0' },
        ],
      });
    });

    it('returns an array of dependencies with resolution comments', async () => {
      const res = await npmExtract.extractPackageFile(
        input01GlobContent,
        'package.json',
        defaultExtractConfig,
      );
      expect(res?.deps).toHaveLength(13);
      expect(res).toMatchSnapshot({
        extractedConstraints: {},
        deps: [
          ...[{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}],
          {
            depName: undefined,
            depType: 'resolutions',
            managerData: { key: '//' },
            prettyDepType: 'resolutions',
            skipReason: 'invalid-name',
          },
          {
            depName: 'config',
            currentValue: '1.21.0',
            depType: 'resolutions',
            managerData: { key: '**/config' },
            prettyDepType: 'resolutions',
          },
        ],
      });
    });

    it('finds a lock file', async () => {
      fs.readLocalFile.mockImplementation((fileName): Promise<any> => {
        if (fileName === 'yarn.lock') {
          return Promise.resolve('# yarn.lock');
        }
        return Promise.resolve(null);
      });
      const res = await npmExtract.extractPackageFile(
        input01Content,
        'package.json',
        defaultExtractConfig,
      );
      expect(res).toMatchSnapshot({
        managerData: {
          yarnLock: 'yarn.lock',
        },
      });
    });

    it('warns when multiple lock files found', async () => {
      fs.readLocalFile.mockImplementation((fileName): Promise<any> => {
        if (fileName === 'yarn.lock') {
          return Promise.resolve('# yarn.lock');
        }
        if (fileName === 'package-lock.json') {
          return Promise.resolve('# package-lock.json');
        }
        return Promise.resolve(null);
      });
      const res = await npmExtract.extractPackageFile(
        input01Content,
        'package.json',
        defaultExtractConfig,
      );
      expect(logger.warn).toHaveBeenCalledWith(
        'Updating multiple npm lock files is deprecated and support will be removed in future versions.',
      );
      expect(res).toMatchObject({
        managerData: {
          npmLock: 'package-lock.json',
          yarnLock: 'yarn.lock',
        },
      });
    });

    it('finds and filters .npmrc', async () => {
      fs.findLocalSiblingOrParent.mockImplementation(
        (packageFile, configFile): Promise<string | null> => {
          if (packageFile === 'package.json' && configFile === '.npmrc') {
            return Promise.resolve('.npmrc');
          }
          return Promise.resolve(null);
        },
      );
      fs.readLocalFile.mockImplementation((fileName): Promise<any> => {
        if (fileName === '.npmrc') {
          return Promise.resolve('save-exact = true\npackage-lock = false\n');
        }
        return Promise.resolve(null);
      });
      const res = await npmExtract.extractPackageFile(
        input01Content,
        'package.json',
        {},
      );
      expect(res?.npmrc).toBe('save-exact = true\n');
    });

    it('uses config.npmrc if no .npmrc is returned from search', async () => {
      const res = await npmExtract.extractPackageFile(
        input01Content,
        'package.json',
        { ...defaultExtractConfig, npmrc: 'config-npmrc' },
      );
      expect(res?.npmrc).toBe('config-npmrc');
    });

    it('uses config.npmrc if no .npmrc exists', async () => {
      fs.readLocalFile.mockResolvedValueOnce(null);
      const res = await npmExtract.extractPackageFile(
        input01Content,
        'package.json',
        { ...defaultExtractConfig, npmrc: 'config-npmrc' },
      );
      expect(res?.npmrc).toBe('config-npmrc');
    });

    it('uses config.npmrc if .npmrc does exist but npmrcMerge=false', async () => {
      fs.findLocalSiblingOrParent.mockImplementation(
        (packageFile, configFile): Promise<string | null> => {
          if (packageFile === 'package.json' && configFile === '.npmrc') {
            return Promise.resolve('.npmrc');
          }
          return Promise.resolve(null);
        },
      );
      fs.readLocalFile.mockImplementation((fileName): Promise<any> => {
        if (fileName === '.npmrc') {
          return Promise.resolve('repo-npmrc\n');
        }
        return Promise.resolve(null);
      });
      const res = await npmExtract.extractPackageFile(
        input01Content,
        'package.json',
        { npmrc: 'config-npmrc' },
      );
      expect(res?.npmrc).toBe('config-npmrc');
    });

    it('merges config.npmrc and repo .npmrc when npmrcMerge=true', async () => {
      fs.findLocalSiblingOrParent.mockImplementation(
        (packageFile, configFile): Promise<string | null> => {
          if (packageFile === 'package.json' && configFile === '.npmrc') {
            return Promise.resolve('.npmrc');
          }
          return Promise.resolve(null);
        },
      );
      fs.readLocalFile.mockImplementation((fileName): Promise<any> => {
        if (fileName === '.npmrc') {
          return Promise.resolve('repo-npmrc\n');
        }
        return Promise.resolve(null);
      });
      const res = await npmExtract.extractPackageFile(
        input01Content,
        'package.json',
        { npmrc: 'config-npmrc', npmrcMerge: true },
      );
      expect(res?.npmrc).toBe(`config-npmrc\nrepo-npmrc\n`);
    });

    it('finds and filters .npmrc with variables', async () => {
      fs.findLocalSiblingOrParent.mockImplementation(
        (packageFile, configFile): Promise<string | null> => {
          if (packageFile === 'package.json' && configFile === '.npmrc') {
            return Promise.resolve('.npmrc');
          }
          return Promise.resolve(null);
        },
      );
      fs.readLocalFile.mockImplementation((fileName): Promise<any> => {
        if (fileName === '.npmrc') {
          return Promise.resolve(
            'registry=https://registry.npmjs.org\n//registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN}\n',
          );
        }
        return Promise.resolve(null);
      });
      const res = await npmExtract.extractPackageFile(
        input01Content,
        'package.json',
        {},
      );
      expect(res?.npmrc).toBe('registry=https://registry.npmjs.org\n');
    });

    it('reads registryUrls from .yarnrc.yml', async () => {
      fs.findLocalSiblingOrParent.mockImplementation(
        (packageFile, otherFile): Promise<string | null> => {
          if (packageFile === 'package.json' && otherFile === '.yarnrc.yml') {
            return Promise.resolve('.yarnrc.yml');
          }
          return Promise.resolve(null);
        },
      );

      fs.readLocalFile.mockImplementation((fileName): Promise<any> => {
        if (fileName === '.yarnrc.yml') {
          return Promise.resolve(
            'npmRegistryServer: https://registry.example.com',
          );
        }
        return Promise.resolve(null);
      });
      const res = await npmExtract.extractPackageFile(
        input02Content,
        'package.json',
        {},
      );
      expect(
        res?.deps.flatMap((dep) => dep.registryUrls),
      ).toBeArrayIncludingOnly(['https://registry.example.com']);
    });

    it('reads registryUrls from .yarnrc', async () => {
      fs.findLocalSiblingOrParent.mockImplementation(
        (packageFile, otherFile): Promise<string | null> => {
          if (packageFile === 'package.json' && otherFile === '.yarnrc') {
            return Promise.resolve('.yarnrc');
          }
          return Promise.resolve(null);
        },
      );

      fs.readLocalFile.mockImplementation((fileName): Promise<any> => {
        if (fileName === '.yarnrc') {
          return Promise.resolve('registry "https://registry.example.com"');
        }
        return Promise.resolve(null);
      });

      const res = await npmExtract.extractPackageFile(
        input02Content,
        'package.json',
        {},
      );
      expect(
        res?.deps.flatMap((dep) => dep.registryUrls),
      ).toBeArrayIncludingOnly(['https://registry.example.com']);
    });

    it('resolves registry URLs using the package name if set', async () => {
      fs.findLocalSiblingOrParent.mockImplementation(
        (packageFile, otherFile): Promise<string | null> => {
          if (packageFile === 'package.json' && otherFile === '.yarnrc.yml') {
            return Promise.resolve('.yarnrc.yml');
          }
          return Promise.resolve(null);
        },
      );

      fs.readLocalFile.mockImplementation((fileName): Promise<any> => {
        if (fileName === '.yarnrc.yml') {
          return Promise.resolve(codeBlock`
            npmScopes:
              yarnpkg:
                  npmRegistryServer: https://registry.example.com
          `);
        }
        return Promise.resolve(null);
      });
      const res = await npmExtract.extractPackageFile(
        '{"packageManager": "yarn@4.1.1"}',
        'package.json',
        {},
      );
      expect(res?.deps).toEqual([
        expect.objectContaining({
          depName: 'yarn',
          registryUrls: ['https://registry.example.com'],
        }),
      ]);
    });

    it('finds complex yarn workspaces', async () => {
      fs.readLocalFile.mockImplementation((fileName): Promise<any> => {
        return Promise.resolve(null);
      });
      const res = await npmExtract.extractPackageFile(
        workspacesContent,
        'package.json',
        defaultExtractConfig,
      );
      expect(res).toMatchSnapshot({
        managerData: { workspacesPackages: ['packages/*'] },
      });
    });

    it('extracts engines', async () => {
      const pJson = {
        dependencies: {
          angular: '1.6.0',
        },
        devDependencies: {
          '@angular/cli': '1.6.0',
          foo: '*',
          bar: 'file:../foo/bar',
          baz: '',
          other: 'latest',
        },
        engines: {
          atom: '>=1.7.0 <2.0.0',
          node: '>= 8.9.2',
          npm: '^8.0.0',
          pnpm: '^1.2.0',
          yarn: 'disabled',
          vscode: '>=1.49.3',
        },
        main: 'index.js',
      };
      const pJsonStr = JSON.stringify(pJson);
      const res = await npmExtract.extractPackageFile(
        pJsonStr,
        'package.json',
        defaultExtractConfig,
      );
      expect(res).toMatchSnapshot({
        extractedConstraints: {
          node: '>= 8.9.2',
          npm: '^8.0.0',
          pnpm: '^1.2.0',
          vscode: '>=1.49.3',
        },
        deps: [
          { depName: 'angular', currentValue: '1.6.0' },
          { depName: '@angular/cli', currentValue: '1.6.0' },
          { depName: 'foo', currentValue: '*' },
          {
            depName: 'bar',
            currentValue: 'file:../foo/bar',
            skipReason: 'file',
          },
          { depName: 'baz', currentValue: '', skipReason: 'empty' },
          {
            depName: 'other',
            currentValue: 'latest',
            skipReason: 'unspecified-version',
          },
          {
            depName: 'atom',
            currentValue: '>=1.7.0 <2.0.0',
            skipReason: 'unknown-engines',
            depType: 'engines',
          },
          {
            depName: 'node',
            currentValue: '>= 8.9.2',
            datasource: 'node-version',
            depType: 'engines',
          },
          {
            depName: 'npm',
            currentValue: '^8.0.0',
            datasource: 'npm',
            depType: 'engines',
          },
          {
            depName: 'pnpm',
            currentValue: '^1.2.0',
            datasource: 'npm',
            depType: 'engines',
          },
          {
            depName: 'yarn',
            currentValue: 'disabled',
            datasource: 'npm',
            depType: 'engines',
            skipReason: 'unspecified-version',
          },
          {
            depName: 'vscode',
            currentValue: '>=1.49.3',
            depType: 'engines',
            datasource: 'github-tags',
          },
        ],
      });
    });

    it('extracts volta', async () => {
      const pJson = {
        main: 'index.js',
        engines: {
          node: '8.9.2',
        },
        volta: {
          node: '8.9.2',
          yarn: '1.12.3',
          npm: '5.9.0',
          pnpm: '6.11.2',
          invalid: '1.0.0',
        },
      };
      const pJsonStr = JSON.stringify(pJson);
      const res = await npmExtract.extractPackageFile(
        pJsonStr,
        'package.json',
        defaultExtractConfig,
      );
      expect(res).toMatchSnapshot({
        deps: [
          ...[{}, {}, {}, {}],
          {
            depType: 'volta',
            currentValue: '6.11.2',
            depName: 'pnpm',
            prettyDepType: 'volta',
          },
          {
            depType: 'volta',
            currentValue: '1.0.0',
            depName: 'invalid',
            prettyDepType: 'volta',
            skipReason: 'unknown-volta',
          },
        ],
      });
    });

    it('extracts volta yarn unspecified-version', async () => {
      const pJson = {
        main: 'index.js',
        engines: {
          node: '8.9.2',
        },
        volta: {
          node: '8.9.2',
          yarn: 'unknown',
        },
      };
      const pJsonStr = JSON.stringify(pJson);
      const res = await npmExtract.extractPackageFile(
        pJsonStr,
        'package.json',
        defaultExtractConfig,
      );
      expect(res).toMatchSnapshot({
        deps: [
          {},
          {
            commitMessageTopic: 'Node.js',
            currentValue: '8.9.2',
            datasource: 'node-version',
            depName: 'node',
            depType: 'volta',
            prettyDepType: 'volta',
          },
          {
            commitMessageTopic: 'Yarn',
            currentValue: 'unknown',
            datasource: 'npm',
            depName: 'yarn',
            depType: 'volta',
            prettyDepType: 'volta',
            skipReason: 'unspecified-version',
          },
        ],
      });
    });

    it('extracts volta yarn higher than 1', async () => {
      const pJson = {
        main: 'index.js',
        engines: {
          node: '16.0.0',
        },
        volta: {
          node: '16.0.0',
          yarn: '3.2.4',
        },
      };
      const pJsonStr = JSON.stringify(pJson);
      const res = await npmExtract.extractPackageFile(
        pJsonStr,
        'package.json',
        defaultExtractConfig,
      );

      expect(res).toMatchObject({
        deps: [
          {},
          {
            commitMessageTopic: 'Node.js',
            currentValue: '16.0.0',
            datasource: 'node-version',
            depName: 'node',
            depType: 'volta',
            prettyDepType: 'volta',
          },
          {
            commitMessageTopic: 'Yarn',
            currentValue: '3.2.4',
            datasource: 'npm',
            depName: 'yarn',
            depType: 'volta',
            prettyDepType: 'volta',
            packageName: '@yarnpkg/cli',
          },
        ],
      });
    });

    it('extracts non-npmjs', async () => {
      const pJson = {
        dependencies: {
          a: 'github:owner/a',
          b: 'github:owner/b#master',
          c: 'github:owner/c#v1.1.0',
          d: 'github:owner/d#a7g3eaf',
          e: 'github:owner/e#49b5aca613b33c5b626ae68c03a385f25c142f55',
          f: 'owner/f#v2.0.0',
          g: 'gitlab:owner/g#v1.0.0',
          h: 'github:-hello/world#v1.0.0',
          i: '@foo/bar#v2.0.0',
          j: 'github:frank#v0.0.1',
          k: 'github:owner/k#49b5aca',
          l: 'github:owner/l.git#abcdef0',
          m: 'https://github.com/owner/m.git#v1.0.0',
          n: 'git+https://github.com/owner/n#v2.0.0',
          o: 'git@github.com:owner/o.git#v2.0.0',
          p: 'Owner/P.git#v2.0.0',
          q: 'github:owner/q#semver:1.1.0',
          r: 'github:owner/r#semver:^1.0.0',
        },
      };
      const pJsonStr = JSON.stringify(pJson);
      const res = await npmExtract.extractPackageFile(
        pJsonStr,
        'package.json',
        defaultExtractConfig,
      );
      expect(res).toMatchSnapshot({
        deps: [
          { depName: 'a', skipReason: 'unspecified-version' },
          { depName: 'b', skipReason: 'unversioned-reference' },
          {
            depName: 'c',
            currentValue: 'v1.1.0',
            datasource: 'github-tags',
            sourceUrl: 'https://github.com/owner/c',
          },
          {
            depName: 'd',
            currentValue: 'github:owner/d#a7g3eaf',
            skipReason: 'unversioned-reference',
          },
          {
            depName: 'e',
            currentValue: null,
            currentDigest: '49b5aca613b33c5b626ae68c03a385f25c142f55',
            datasource: 'github-tags',
            sourceUrl: 'https://github.com/owner/e',
          },
          {
            depName: 'f',
            currentValue: 'v2.0.0',
            datasource: 'github-tags',
            sourceUrl: 'https://github.com/owner/f',
          },
          {
            depName: 'g',
            currentValue: 'gitlab:owner/g#v1.0.0',
            skipReason: 'unspecified-version',
          },
          {
            depName: 'h',
            currentValue: 'github:-hello/world#v1.0.0',
            skipReason: 'unspecified-version',
          },
          {
            depName: 'i',
            currentValue: '@foo/bar#v2.0.0',
            skipReason: 'unspecified-version',
          },
          {
            depName: 'j',
            currentValue: 'github:frank#v0.0.1',
            skipReason: 'unspecified-version',
          },
          {
            depName: 'k',
            currentValue: null,
            currentDigest: '49b5aca',
            currentRawValue: 'github:owner/k#49b5aca',
            datasource: 'github-tags',
            sourceUrl: 'https://github.com/owner/k',
          },
          {
            depName: 'l',
            currentValue: null,
            currentDigest: 'abcdef0',
            currentRawValue: 'github:owner/l.git#abcdef0',
            datasource: 'github-tags',
            sourceUrl: 'https://github.com/owner/l',
          },
          {
            depName: 'm',
            currentValue: 'v1.0.0',
            currentRawValue: 'https://github.com/owner/m.git#v1.0.0',
            datasource: 'github-tags',
            sourceUrl: 'https://github.com/owner/m',
          },
          {
            depName: 'n',
            currentValue: 'v2.0.0',
            datasource: 'github-tags',
            sourceUrl: 'https://github.com/owner/n',
          },
          {
            depName: 'o',
            currentValue: 'v2.0.0',
            datasource: 'github-tags',
            sourceUrl: 'https://github.com/owner/o',
          },
          {
            depName: 'p',
            currentValue: 'v2.0.0',
            datasource: 'github-tags',
            sourceUrl: 'https://github.com/Owner/P',
          },
          {
            depName: 'q',
            currentValue: '1.1.0',
            datasource: 'github-tags',
            sourceUrl: 'https://github.com/owner/q',
          },
          {
            depName: 'r',
            currentValue: '^1.0.0',
            datasource: 'github-tags',
            sourceUrl: 'https://github.com/owner/r',
          },
        ],
      });
    });

    it('does not set registryUrls for non-npmjs', async () => {
      fs.readLocalFile.mockImplementation((fileName): Promise<any> => {
        if (fileName === '.yarnrc.yml') {
          return Promise.resolve(
            'npmRegistryServer: https://registry.example.com',
          );
        }
        return Promise.resolve(null);
      });
      const pJson = {
        dependencies: {
          a: 'github:owner/a#v1.1.0',
        },
        engines: {
          node: '8.9.2',
        },
        volta: {
          yarn: '3.2.4',
        },
      };
      const pJsonStr = JSON.stringify(pJson);
      const res = await npmExtract.extractPackageFile(
        pJsonStr,
        'package.json',
        defaultExtractConfig,
      );
      expect(res).toMatchObject({
        deps: [
          {
            depName: 'a',
            currentValue: 'v1.1.0',
            datasource: 'github-tags',
            sourceUrl: 'https://github.com/owner/a',
          },
          {
            commitMessageTopic: 'Node.js',
            currentValue: '8.9.2',
            datasource: 'node-version',
            depName: 'node',
            depType: 'engines',
            prettyDepType: 'engine',
          },
          {
            commitMessageTopic: 'Yarn',
            currentValue: '3.2.4',
            datasource: 'npm',
            depName: 'yarn',
            depType: 'volta',
            prettyDepType: 'volta',
            packageName: '@yarnpkg/cli',
          },
        ],
      });
    });

    it('extracts npm package alias', async () => {
      fs.readLocalFile.mockImplementation((fileName: string): Promise<any> => {
        if (fileName === 'package-lock.json') {
          return Promise.resolve('{}');
        }
        return Promise.resolve(null);
      });
      const pJson = {
        dependencies: {
          a: 'npm:foo@1',
          b: 'npm:@foo/bar@1.2.3',
          c: 'npm:^1.2.3',
          d: 'npm:1.2.3',
          e: 'npm:1.x.x',
          f: 'npm:foo',
          g: 'npm:@foo/@bar/@1.2.3',
        },
      };
      const pJsonStr = JSON.stringify(pJson);
      const res = await npmExtract.extractPackageFile(
        pJsonStr,
        'package.json',
        defaultExtractConfig,
      );
      expect(logger.debug).toHaveBeenCalledWith(
        'Invalid npm package alias for dependency: "g":"npm:@foo/@bar/@1.2.3"',
      );
      expect(res).toMatchSnapshot({
        deps: [
          { packageName: 'foo' },
          { packageName: '@foo/bar' },
          { packageName: 'c', currentValue: '^1.2.3' },
          { packageName: 'd', currentValue: '1.2.3' },
          { packageName: 'e', currentValue: '1.x.x' },
          {
            packageName: 'f',
            currentValue: 'foo',
            npmPackageAlias: true,
            skipReason: 'unspecified-version',
          },
          {
            depName: 'g',
            currentValue: 'npm:@foo/@bar/@1.2.3',
            npmPackageAlias: true,
            skipReason: 'unspecified-version',
          },
        ],
      });
    });

    it('sets skipInstalls false if Yarn zero-install is used', async () => {
      fs.findLocalSiblingOrParent.mockImplementation(
        (packageFile, otherFile): Promise<string | null> => {
          if (packageFile === 'package.json' && otherFile === '.yarnrc.yml') {
            return Promise.resolve('.yarnrc.yml');
          }
          return Promise.resolve(null);
        },
      );

      fs.readLocalFile.mockImplementation((fileName): Promise<any> => {
        if (fileName === 'yarn.lock') {
          return Promise.resolve('# yarn.lock');
        }
        if (fileName === '.yarnrc.yml') {
          return Promise.resolve('pnpEnableInlining: false');
        }
        return Promise.resolve(null);
      });
      fs.localPathExists.mockResolvedValueOnce(true);
      const res = await npmExtract.extractPackageFile(
        input01Content,
        'package.json',
        defaultExtractConfig,
      );
      expect(res).toMatchSnapshot();
    });

    it('extracts packageManager', async () => {
      const pJson = {
        packageManager: 'yarn@3.0.0',
      };
      const pJsonStr = JSON.stringify(pJson);
      const res = await npmExtract.extractPackageFile(
        pJsonStr,
        'package.json',
        defaultExtractConfig,
      );
      expect(res).toMatchSnapshot({
        extractedConstraints: { yarn: '3.0.0' },
        deps: [
          {
            commitMessageTopic: 'Yarn',
            currentValue: '3.0.0',
            datasource: 'npm',
            depName: 'yarn',
            depType: 'packageManager',
            packageName: '@yarnpkg/cli',
            prettyDepType: 'packageManager',
          },
        ],
        managerData: {
          hasPackageManager: true,
        },
      });
    });

    it('sets hasPackageManager to true when devEngines detected in package file', async () => {
      const pJson = {
        devEngines: {
          packageManager: {
            name: 'yarn',
            version: '3.0.0',
          },
        },
        dependencies: {
          express: '2.0.0',
        },
      };
      const pJsonStr = JSON.stringify(pJson);
      const res = await npmExtract.extractPackageFile(
        pJsonStr,
        'package.json',
        defaultExtractConfig,
      );
      expect(res).toMatchObject({
        deps: [
          {
            currentValue: '2.0.0',
            datasource: 'npm',
            depName: 'express',
            depType: 'dependencies',
            prettyDepType: 'dependency',
          },
        ],
        managerData: {
          hasPackageManager: true,
        },
      });
    });

    it('extracts dependencies from overrides', async () => {
      const content = `{
        "devDependencies": {
          "@types/react": "18.0.5"
        },
        "overrides": {
          "node": "8.9.2",
          "@types/react": "18.0.5",
          "baz": {
            "node": "8.9.2",
            "bar": {
              "foo": "1.0.0"
            }
          },
          "foo2": {
            ".": "1.0.0",
            "bar2": "1.0.0"
          },
          "emptyObject":{}
        }
      }`;
      const res = await npmExtract.extractPackageFile(
        content,
        'package.json',
        defaultExtractConfig,
      );
      expect(res).toMatchObject({
        deps: [
          {
            depType: 'devDependencies',
            depName: '@types/react',
            currentValue: '18.0.5',
            datasource: 'npm',
            prettyDepType: 'devDependency',
          },
          {
            depType: 'overrides',
            depName: 'node',
            currentValue: '8.9.2',
            datasource: 'npm',
            commitMessageTopic: 'Node.js',
            prettyDepType: 'overrides',
          },
          {
            depType: 'overrides',
            depName: '@types/react',
            currentValue: '18.0.5',
            datasource: 'npm',
            prettyDepType: 'overrides',
          },
          {
            depName: 'node',
            managerData: { parents: ['baz'] },
            commitMessageTopic: 'Node.js',
            currentValue: '8.9.2',
            datasource: 'npm',
          },
          {
            depName: 'foo',
            managerData: { parents: ['baz', 'bar'] },
            currentValue: '1.0.0',
            datasource: 'npm',
          },
          {
            depName: 'foo2',
            managerData: { parents: ['foo2'] },
            currentValue: '1.0.0',
            datasource: 'npm',
          },
          {
            depName: 'bar2',
            managerData: { parents: ['foo2'] },
            currentValue: '1.0.0',
            datasource: 'npm',
          },
        ],
      });
    });

    it('extracts dependencies from pnpm.overrides', async () => {
      const content = `{
        "devDependencies": {
          "@types/react": "18.0.5"
        },
        "pnpm": {
          "overrides": {
            "node": "8.9.2",
            "@types/react": "18.0.5",
            "baz": {
              "node": "8.9.2",
              "bar": {
                "foo": "1.0.0"
              }
            },
            "foo2": {
              ".": "1.0.0",
              "bar2": "1.0.0"
            },
            "emptyObject":{}
          }
        }
      }`;
      const res = await npmExtract.extractPackageFile(
        content,
        'package.json',
        defaultExtractConfig,
      );
      expect(res).toMatchObject({
        deps: [
          {
            depType: 'devDependencies',
            depName: '@types/react',
            currentValue: '18.0.5',
            datasource: 'npm',
            prettyDepType: 'devDependency',
          },
          {
            depType: 'pnpm.overrides',
            depName: 'node',
            currentValue: '8.9.2',
            datasource: 'npm',
            commitMessageTopic: 'Node.js',
            prettyDepType: 'overrides',
          },
          {
            depType: 'pnpm.overrides',
            depName: '@types/react',
            currentValue: '18.0.5',
            datasource: 'npm',
            prettyDepType: 'overrides',
          },
          {
            depName: 'node',
            managerData: { parents: ['baz'] },
            commitMessageTopic: 'Node.js',
            currentValue: '8.9.2',
            datasource: 'npm',
          },
          {
            depName: 'foo',
            managerData: { parents: ['baz', 'bar'] },
            currentValue: '1.0.0',
            datasource: 'npm',
          },
          {
            depName: 'foo2',
            managerData: { parents: ['foo2'] },
            currentValue: '1.0.0',
            datasource: 'npm',
          },
          {
            depName: 'bar2',
            managerData: { parents: ['foo2'] },
            currentValue: '1.0.0',
            datasource: 'npm',
          },
        ],
      });
    });

    it('extracts dependencies from pnpm.overrides, with version ranges in flat syntax', async () => {
      const content = codeBlock`{
        "pnpm": {
          "overrides": {
            "foo>bar": "2.0.0",
            "foo@1.0.0": "2.0.0",
            "foo@>1.0.0": "2.0.0",
            "foo@>=1.0.0": "2.0.0",
            "foo@1.0.0>bar": "2.0.0",
            "foo@>1.0.0>bar": "2.0.0",
            "foo@>=1.0.0 <2.0.0": ">=2.0.0"
          }
        }
      }`;
      const res = await npmExtract.extractPackageFile(
        content,
        'package.json',
        defaultExtractConfig,
      );
      expect(res).toMatchObject({
        deps: [
          {
            currentValue: '2.0.0',
            datasource: 'npm',
            depName: 'foo>bar',
            depType: 'pnpm.overrides',
            packageName: 'bar',
            prettyDepType: 'overrides',
          },
          {
            currentValue: '2.0.0',
            datasource: 'npm',
            depName: 'foo@1.0.0',
            depType: 'pnpm.overrides',
            packageName: 'foo',
            prettyDepType: 'overrides',
          },
          {
            currentValue: '2.0.0',
            datasource: 'npm',
            depName: 'foo@>1.0.0',
            depType: 'pnpm.overrides',
            packageName: 'foo',
            prettyDepType: 'overrides',
          },
          {
            currentValue: '2.0.0',
            datasource: 'npm',
            depName: 'foo@>=1.0.0',
            depType: 'pnpm.overrides',
            packageName: 'foo',
            prettyDepType: 'overrides',
          },
          {
            currentValue: '2.0.0',
            datasource: 'npm',
            depName: 'foo@1.0.0>bar',
            depType: 'pnpm.overrides',
            packageName: 'bar',
            prettyDepType: 'overrides',
          },
          {
            currentValue: '2.0.0',
            datasource: 'npm',
            depName: 'foo@>1.0.0>bar',
            depType: 'pnpm.overrides',
            packageName: 'bar',
            prettyDepType: 'overrides',
          },
          {
            currentValue: '>=2.0.0',
            datasource: 'npm',
            depName: 'foo@>=1.0.0 <2.0.0',
            depType: 'pnpm.overrides',
            packageName: 'foo',
            prettyDepType: 'overrides',
          },
        ],
      });
    });
  });

  describe('.extractAllPackageFiles()', () => {
    it('runs', async () => {
      fs.readLocalFile.mockResolvedValueOnce(input02Content);
      const res = await extractAllPackageFiles(defaultExtractConfig, [
        'package.json',
        'invalid.json',
      ]);
      expect(res).toEqual([
        {
          deps: [
            {
              currentValue: '7.0.0',
              datasource: 'npm',
              depName: '@babel/core',
              depType: 'dependencies',
              prettyDepType: 'dependency',
            },
            {
              currentValue: '1.21.0',
              datasource: 'npm',
              depName: 'config',
              depType: 'dependencies',
              prettyDepType: 'dependency',
            },
            {
              currentValue: '0.7.0',
              datasource: 'npm',
              depName: 'express>cookie',
              packageName: 'cookie',
              depType: 'pnpm.overrides',
              prettyDepType: 'overrides',
            },
          ],
          extractedConstraints: {},
          managerData: {
            hasPackageManager: false,
            npmLock: undefined,
            packageJsonName: 'renovate',
            pnpmShrinkwrap: undefined,
            workspacesPackages: undefined,
            yarnLock: undefined,
            yarnZeroInstall: false,
          },
          npmrc: undefined,
          packageFile: 'package.json',
          packageFileVersion: '1.0.0',
          skipInstalls: true,
        },
      ]);
    });

    it('extracts pnpm workspace yaml files', async () => {
      fs.readLocalFile.mockResolvedValueOnce(codeBlock`
        packages:
          - pkg-a

        catalog:
          is-positive: 1.0.0
      `);
      const res = await extractAllPackageFiles(defaultExtractConfig, [
        'pnpm-workspace.yaml',
      ]);
      expect(res).toEqual([
        {
          deps: [
            {
              currentValue: '1.0.0',
              datasource: 'npm',
              depName: 'is-positive',
              depType: 'pnpm.catalog.default',
              prettyDepType: 'pnpm.catalog.default',
            },
          ],
          managerData: {
            pnpmShrinkwrap: undefined,
          },
          packageFile: 'pnpm-workspace.yaml',
        },
      ]);
    });
  });

  describe('.postExtract()', () => {
    it('runs', async () => {
      await expect(postExtract([])).resolves.not.toThrow();
    });
  });
});
