import { codeBlock } from 'common-tags';
import upath from 'upath';
import { mockDeep } from 'vitest-mock-extended';
import { GlobalConfig } from '../../../config/global';
import type { RepoGlobalConfig } from '../../../config/types';
import { logger } from '../../../logger';
import * as docker from '../../../util/exec/docker';
import type { StatusResult } from '../../../util/git/types';
import * as _datasource from '../../datasource';
import type { UpdateArtifactsConfig, Upgrade } from '../types';
import { constructPipCompileCmd } from './artifacts';
import { extractHeaderCommand } from './common';
import { updateArtifacts } from '.';
import { envMock, mockExecAll, mockExecSequence } from '~test/exec-util';
import { Fixtures } from '~test/fixtures';
import { env, fs, git, partial } from '~test/util';

const datasource = vi.mocked(_datasource);

vi.mock('../../../util/exec/env');
vi.mock('../../../util/fs');
vi.mock('../../../util/host-rules', () => mockDeep());
vi.mock('../../../util/http');
vi.mock('../../datasource', () => mockDeep());

const requirementsWithUv = `# This file was autogenerated by uv via the following command:
#    uv pip compile --generate-hashes --output-file=requirements.txt --python-version=3.11 --no-emit-package=cffi --universal requirements.in
attrs==21.2.0 \
    --hash=sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1 \
    --hash=sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb
    # via -r requirements.in
setuptools==59.2.0 \
    --hash=sha256:157d21de9d055ab9e8ea3186d91e7f4f865e11f42deafa952d90842671fc2576 \
    --hash=sha256:4adde3d1e1c89bde1c643c64d89cdd94cbfd8c75252ee459d4500bccb9c7d05d
    # via -r requirements.in
`;

function getCommandInHeader(command: string) {
  return `#
# This file is autogenerated by pip-compile with Python 3.11
# by the following command:
#
#    ${command}
#
`;
}

function getCommandInUvHeader(command: string) {
  return `# This file was autogenerated by uv via the following command:
#    ${command}
`;
}

const simpleHeader = getCommandInHeader('pip-compile requirements.in');

const adminConfig: RepoGlobalConfig = {
  // `join` fixes Windows CI
  localDir: upath.join('/tmp/github/some/repo'),
  cacheDir: upath.join('/tmp/renovate/cache'),
  containerbaseDir: upath.join('/tmp/renovate/cache/containerbase'),
};
const dockerAdminConfig = {
  ...adminConfig,
  binarySource: 'docker',
  dockerSidecarImage: 'ghcr.io/containerbase/sidecar',
};

process.env.CONTAINERBASE = 'true';

const config: UpdateArtifactsConfig = {};
const lockMaintenanceConfig = { ...config, isLockFileMaintenance: true };

describe('modules/manager/pip-compile/artifacts', () => {
  beforeEach(() => {
    env.getChildProcessEnv.mockReturnValue({
      ...envMock.basic,
      LANG: 'en_US.UTF-8',
      LC_ALL: 'en_US',
    });
    GlobalConfig.set(adminConfig);
    docker.resetPrefetchedImages();
  });

  it('returns if no requirements.txt found', async () => {
    const execSnapshots = mockExecAll();
    expect(
      await updateArtifacts({
        packageFileName: 'requirements.in',
        updatedDeps: [],
        newPackageFileContent: '',
        config: {
          ...config,
          lockFiles: ['requirements.txt'],
        },
      }),
    ).toBeNull();
    expect(execSnapshots).toEqual([]);
  });

  it('returns null if all unchanged', async () => {
    fs.readLocalFile.mockResolvedValueOnce(simpleHeader);
    fs.readLocalFile.mockResolvedValueOnce('dependency==1.2.3');
    const execSnapshots = mockExecAll();
    fs.readLocalFile.mockResolvedValueOnce('new lock');
    expect(
      await updateArtifacts({
        packageFileName: 'requirements.in',
        updatedDeps: [],
        newPackageFileContent: 'some new content',
        config: {
          ...config,
          lockFiles: ['requirements.txt'],
        },
      }),
    ).toBeNull();
    expect(execSnapshots).toMatchObject([
      { cmd: 'pip-compile requirements.in' },
    ]);
  });

  it('returns null if no config.lockFiles', async () => {
    fs.readLocalFile.mockResolvedValueOnce(simpleHeader);
    fs.readLocalFile.mockResolvedValueOnce('dependency==1.2.3');
    fs.readLocalFile.mockResolvedValueOnce('new lock');
    expect(
      await updateArtifacts({
        packageFileName: 'requirements.in',
        updatedDeps: [],
        newPackageFileContent: 'some new content',
        config: {
          ...config,
        },
      }),
    ).toBeNull();
    expect(logger.warn).toHaveBeenCalledWith(
      { packageFileName: 'requirements.in' },
      'pip-compile: No lock files associated with a package file',
    );
  });

  it('returns updated requirements.txt', async () => {
    fs.readLocalFile.mockResolvedValueOnce(simpleHeader);
    fs.readLocalFile.mockResolvedValueOnce('dependency==1.2.3');
    const execSnapshots = mockExecAll();
    git.getRepoStatus.mockResolvedValue(
      partial<StatusResult>({
        modified: ['requirements.txt'],
      }),
    );
    fs.readLocalFile.mockResolvedValueOnce('new lock');
    expect(
      await updateArtifacts({
        packageFileName: 'requirements.in',
        updatedDeps: [],
        newPackageFileContent: 'some new content',
        config: {
          ...config,
          constraints: { python: '3.7' },
          lockFiles: ['requirements.txt'],
        },
      }),
    ).not.toBeNull();
    expect(execSnapshots).toMatchObject([
      { cmd: 'pip-compile requirements.in' },
    ]);
  });

  it('supports docker mode', async () => {
    GlobalConfig.set(dockerAdminConfig);
    // pip-tools
    datasource.getPkgReleases.mockResolvedValueOnce({
      releases: [{ version: '6.13.0' }],
    });
    const execSnapshots = mockExecAll();
    git.getRepoStatus.mockResolvedValue(
      partial<StatusResult>({
        modified: ['requirements.txt'],
      }),
    );
    fs.readLocalFile.mockResolvedValueOnce(simpleHeader);
    fs.readLocalFile.mockResolvedValueOnce('dependency==1.2.3');
    fs.ensureCacheDir.mockResolvedValueOnce('/tmp/renovate/cache/others/pip');
    expect(
      await updateArtifacts({
        packageFileName: 'requirements.in',
        updatedDeps: [],
        newPackageFileContent: 'some new content',
        config: {
          ...config,
          constraints: { python: '3.10.2' },
          lockFiles: ['requirements.txt'],
        },
      }),
    ).not.toBeNull();

    expect(execSnapshots).toMatchObject([
      { cmd: 'docker pull ghcr.io/containerbase/sidecar' },
      { cmd: 'docker ps --filter name=renovate_sidecar -aq' },
      {
        cmd:
          'docker run --rm --name=renovate_sidecar --label=renovate_child ' +
          '-v "/tmp/github/some/repo":"/tmp/github/some/repo" ' +
          '-v "/tmp/renovate/cache":"/tmp/renovate/cache" ' +
          '-e PIP_CACHE_DIR ' +
          '-e PIP_NO_INPUT ' +
          '-e PIP_KEYRING_PROVIDER ' +
          '-e PYTHON_KEYRING_BACKEND ' +
          '-e CONTAINERBASE_CACHE_DIR ' +
          '-w "/tmp/github/some/repo" ' +
          'ghcr.io/containerbase/sidecar ' +
          'bash -l -c "' +
          'install-tool python 3.10.2 ' +
          '&& ' +
          'install-tool pip-tools 6.13.0 ' +
          '&& ' +
          'pip-compile requirements.in' +
          '"',
      },
    ]);
  });

  it('supports install mode', async () => {
    GlobalConfig.set({ ...adminConfig, binarySource: 'install' });
    // pip-tools
    datasource.getPkgReleases.mockResolvedValueOnce({
      releases: [{ version: '6.13.0' }],
    });
    const execSnapshots = mockExecAll();
    git.getRepoStatus.mockResolvedValue(
      partial<StatusResult>({
        modified: ['requirements.txt'],
      }),
    );
    fs.readLocalFile.mockResolvedValueOnce(simpleHeader);
    fs.readLocalFile.mockResolvedValueOnce('dependency==1.2.3');
    expect(
      await updateArtifacts({
        packageFileName: 'requirements.in',
        updatedDeps: [],
        newPackageFileContent: 'some new content',
        config: {
          ...config,
          constraints: { python: '3.10.2' },
          lockFiles: ['requirements.txt'],
        },
      }),
    ).not.toBeNull();

    expect(execSnapshots).toMatchObject([
      { cmd: 'install-tool python 3.10.2' },
      { cmd: 'install-tool pip-tools 6.13.0' },
      {
        cmd: 'pip-compile requirements.in',
        options: { cwd: '/tmp/github/some/repo' },
      },
    ]);
  });

  it('installs Python version according to the lock file', async () => {
    GlobalConfig.set({ ...adminConfig, binarySource: 'install' });
    datasource.getPkgReleases.mockResolvedValueOnce({
      releases: [
        { version: '3.11.0' },
        { version: '3.11.1' },
        { version: '3.12.0' },
      ],
    });
    const execSnapshots = mockExecAll();
    git.getRepoStatus.mockResolvedValue(
      partial<StatusResult>({
        modified: ['requirements.txt'],
      }),
    );
    fs.readLocalFile.mockResolvedValueOnce(simpleHeader);
    expect(
      await updateArtifacts({
        packageFileName: 'requirements.in',
        updatedDeps: [],
        newPackageFileContent: 'some new content',
        config: {
          ...config,
          constraints: { pipTools: '6.13.0' },
          lockFiles: ['requirements.txt'],
        },
      }),
    ).not.toBeNull();

    expect(execSnapshots).toMatchObject([
      { cmd: 'install-tool python 3.11.1' },
      { cmd: 'install-tool pip-tools 6.13.0' },
      {
        cmd: 'pip-compile requirements.in',
        options: { cwd: '/tmp/github/some/repo' },
      },
    ]);
  });

  it('installs Python version according to the uv option', async () => {
    GlobalConfig.set({ ...adminConfig, binarySource: 'install' });
    datasource.getPkgReleases.mockResolvedValueOnce({
      releases: [
        { version: '3.11.0' },
        { version: '3.11.1' },
        { version: '3.12.0' },
      ],
    });
    const execSnapshots = mockExecAll();
    git.getRepoStatus.mockResolvedValue(
      partial<StatusResult>({
        modified: ['requirements.txt'],
      }),
    );
    fs.readLocalFile.mockResolvedValueOnce(
      getCommandInUvHeader(
        'uv pip compile --python-version=3.11 requirements.in',
      ),
    );
    expect(
      await updateArtifacts({
        packageFileName: 'requirements.in',
        updatedDeps: [],
        newPackageFileContent: 'some new content',
        config: {
          ...config,
          lockFiles: ['requirements.txt'],
          constraints: { uv: '0.5.27' },
        },
      }),
    ).not.toBeNull();

    expect(execSnapshots).toMatchObject([
      { cmd: 'install-tool python 3.11.1' },
      { cmd: 'install-tool uv 0.5.27' },
      {
        cmd: 'uv pip compile --python-version=3.11 requirements.in',
        options: { cwd: '/tmp/github/some/repo' },
      },
    ]);
  });

  it('install uv tools without constraints', async () => {
    GlobalConfig.set({ ...adminConfig, binarySource: 'install' });
    // python
    datasource.getPkgReleases.mockResolvedValueOnce({
      releases: [{ version: '3.12.0' }],
    });
    // uv
    datasource.getPkgReleases.mockResolvedValueOnce({
      releases: [{ version: '0.5.27' }],
    });
    const execSnapshots = mockExecAll();
    git.getRepoStatus.mockResolvedValue(
      partial<StatusResult>({
        modified: ['requirements.txt'],
      }),
    );
    fs.readLocalFile.mockResolvedValueOnce(
      getCommandInUvHeader('uv pip compile requirements.in'),
    );
    expect(
      await updateArtifacts({
        packageFileName: 'requirements.in',
        updatedDeps: [],
        newPackageFileContent: 'some new content',
        config: {
          ...config,
          lockFiles: ['requirements.txt'],
        },
      }),
    ).not.toBeNull();

    expect(execSnapshots).toMatchObject([
      { cmd: 'install-tool python 3.12.0' },
      { cmd: 'install-tool uv 0.5.27' },
      {
        cmd: 'uv pip compile requirements.in',
        options: { cwd: '/tmp/github/some/repo' },
      },
    ]);
  });

  it('installs latest Python version if no constraints and not in header', async () => {
    GlobalConfig.set({ ...adminConfig, binarySource: 'install' });
    datasource.getPkgReleases.mockResolvedValueOnce({
      releases: [
        { version: '3.11.0' },
        { version: '3.11.1' },
        { version: '3.12.0' },
      ],
    });
    const execSnapshots = mockExecAll();
    git.getRepoStatus.mockResolvedValue(
      partial<StatusResult>({
        modified: ['requirements.txt'],
      }),
    );
    // Before 6.2.0, pip-compile didn't include Python version in header
    const noPythonVersionHeader = codeBlock`
      #
      # This file is autogenerated by pip-compile
      # To update, run:
      #
      #    pip-compile requirements.in
      #
    `;
    fs.readLocalFile.mockResolvedValueOnce(noPythonVersionHeader);
    expect(
      await updateArtifacts({
        packageFileName: 'requirements.in',
        updatedDeps: [],
        newPackageFileContent: 'some new content',
        config: {
          ...config,
          constraints: { pipTools: '6.13.0' },
          lockFiles: ['requirements.txt'],
        },
      }),
    ).not.toBeNull();

    expect(execSnapshots).toMatchObject([
      { cmd: 'install-tool python 3.12.0' },
      { cmd: 'install-tool pip-tools 6.13.0' },
      {
        cmd: 'pip-compile requirements.in',
        options: { cwd: '/tmp/github/some/repo' },
      },
    ]);
  });

  it('catches errors', async () => {
    const execSnapshots = mockExecAll();
    fs.readLocalFile.mockResolvedValueOnce('Current requirements.txt');
    fs.readLocalFile.mockResolvedValueOnce('dependency==1.2.3');
    fs.writeLocalFile.mockImplementationOnce(() => {
      throw new Error('not found');
    });
    expect(
      await updateArtifacts({
        packageFileName: 'requirements.in',
        updatedDeps: [],
        newPackageFileContent: '{}',
        config: { ...config, lockFiles: ['requirements.txt'] },
      }),
    ).toEqual([
      {
        artifactError: { lockFile: 'requirements.txt', stderr: 'not found' },
      },
    ]);
    expect(execSnapshots).toEqual([]);
  });

  it('returns updated requirements.txt when doing lockfile maintenance', async () => {
    fs.readLocalFile.mockResolvedValueOnce(simpleHeader);
    fs.readLocalFile.mockResolvedValueOnce('dependency==1.2.3');
    const execSnapshots = mockExecAll();
    git.getRepoStatus.mockResolvedValue(
      partial<StatusResult>({
        modified: ['requirements.txt'],
      }),
    );
    fs.readLocalFile.mockResolvedValueOnce('New requirements.txt');
    expect(
      await updateArtifacts({
        packageFileName: 'requirements.in',
        updatedDeps: [],
        newPackageFileContent: '{}',
        config: { ...lockMaintenanceConfig, lockFiles: ['requirements.txt'] },
      }),
    ).not.toBeNull();
    expect(execSnapshots).toMatchObject([
      { cmd: 'pip-compile requirements.in' },
    ]);
  });

  it('uses --upgrade-package only for isLockfileUpdate', async () => {
    fs.readLocalFile.mockResolvedValueOnce(simpleHeader);
    fs.readLocalFile.mockResolvedValueOnce('dependency==1.2.3');
    const execSnapshots = mockExecAll();
    git.getRepoStatus.mockResolvedValue(
      partial<StatusResult>({
        modified: ['requirements.txt'],
      }),
    );
    fs.readLocalFile.mockResolvedValueOnce('New requirements.txt');
    expect(
      await updateArtifacts({
        packageFileName: 'requirements.in',
        updatedDeps: [
          { depName: 'foo', newVersion: '1.0.2', isLockfileUpdate: true },
          { depName: 'bar', newVersion: '2.0.0' },
        ] satisfies Upgrade[],
        newPackageFileContent: '{}',
        config: { ...lockMaintenanceConfig, lockFiles: ['requirements.txt'] },
      }),
    ).not.toBeNull();
    expect(execSnapshots).toMatchObject([
      {
        cmd: 'pip-compile requirements.in --upgrade-package=foo==1.0.2',
      },
    ]);
  });

  it('uses pip-compile version from config', async () => {
    fs.readLocalFile.mockResolvedValueOnce(simpleHeader);
    fs.readLocalFile.mockResolvedValueOnce('dependency==1.2.3');
    GlobalConfig.set(dockerAdminConfig);
    // pip-tools
    datasource.getPkgReleases.mockResolvedValueOnce({
      releases: [{ version: '6.13.0' }],
    });
    const execSnapshots = mockExecAll();
    git.getRepoStatus.mockResolvedValue(
      partial<StatusResult>({
        modified: ['requirements.txt'],
      }),
    );
    fs.readLocalFile.mockResolvedValueOnce('new lock');
    fs.ensureCacheDir.mockResolvedValueOnce('/tmp/renovate/cache/others/pip');
    expect(
      await updateArtifacts({
        packageFileName: 'requirements.in',
        updatedDeps: [],
        newPackageFileContent: 'some new content',
        config: {
          ...config,
          constraints: { python: '3.10.2', pipTools: '6.13.0' },
          lockFiles: ['requirements.txt'],
        },
      }),
    ).not.toBeNull();

    expect(execSnapshots).toMatchObject([
      { cmd: 'docker pull ghcr.io/containerbase/sidecar' },
      { cmd: 'docker ps --filter name=renovate_sidecar -aq' },
      {
        cmd:
          'docker run --rm --name=renovate_sidecar --label=renovate_child ' +
          '-v "/tmp/github/some/repo":"/tmp/github/some/repo" ' +
          '-v "/tmp/renovate/cache":"/tmp/renovate/cache" ' +
          '-e PIP_CACHE_DIR ' +
          '-e PIP_NO_INPUT ' +
          '-e PIP_KEYRING_PROVIDER ' +
          '-e PYTHON_KEYRING_BACKEND ' +
          '-e CONTAINERBASE_CACHE_DIR ' +
          '-w "/tmp/github/some/repo" ' +
          'ghcr.io/containerbase/sidecar ' +
          'bash -l -c "' +
          'install-tool python 3.10.2 ' +
          '&& ' +
          'install-tool pip-tools 6.13.0 ' +
          '&& ' +
          'pip-compile requirements.in' +
          '"',
      },
    ]);
  });

  describe('constructPipCompileCmd()', () => {
    afterEach(() => {
      delete process.env.PIP_INDEX_URL;
      delete process.env.PIP_EXTRA_INDEX_URL;
    });

    it('throws for garbage', () => {
      expect(() =>
        constructPipCompileCmd(
          extractHeaderCommand(
            Fixtures.get('requirementsNoHeaders.txt'),
            'subdir/requirements.txt',
          ),
        ),
      ).toThrow(/extract/);
    });

    it('returns extracted common arguments (like those featured in the README)', () => {
      expect(
        constructPipCompileCmd(
          extractHeaderCommand(
            Fixtures.get('requirementsWithHashes.txt'),
            'subdir/requirements.txt',
          ),
        ),
      ).toBe(
        'pip-compile --allow-unsafe --generate-hashes --no-emit-index-url --strip-extras --resolver=backtracking --output-file=requirements.txt requirements.in',
      );
    });

    it('returns extracted arguments for uv', () => {
      expect(
        constructPipCompileCmd(
          extractHeaderCommand(requirementsWithUv, 'subdir/requirements.txt'),
        ),
      ).toBe(
        'uv pip compile --generate-hashes --output-file=requirements.txt --python-version=3.11 --no-emit-package=cffi --universal requirements.in',
      );
    });

    it('returns --no-emit-index-url when credentials are found in PIP_INDEX_URL', () => {
      process.env.PIP_INDEX_URL = 'https://user:pass@example.com/pypi/simple';
      expect(
        constructPipCompileCmd(
          extractHeaderCommand(simpleHeader, 'subdir/requirements.txt'),
        ),
      ).toBe('pip-compile --no-emit-index-url requirements.in');
    });

    it('returns --no-emit-index-url when credentials are found in PIP_EXTRA_INDEX_URL', () => {
      process.env.PIP_EXTRA_INDEX_URL =
        'https://user:pass@example.com/pypi/simple';
      expect(
        constructPipCompileCmd(
          extractHeaderCommand(simpleHeader, 'subdir/requirements.txt'),
        ),
      ).toBe('pip-compile --no-emit-index-url requirements.in');
    });

    it('returns --no-emit-index-url when only a username is found in PIP_INDEX_URL', () => {
      process.env.PIP_INDEX_URL = 'https://user@example.com/pypi/simple';
      expect(
        constructPipCompileCmd(
          extractHeaderCommand(simpleHeader, 'subdir/requirements.txt'),
        ),
      ).toBe('pip-compile --no-emit-index-url requirements.in');
    });

    it('returns --no-emit-index-url when only a username is found in PIP_EXTRA_INDEX_URL', () => {
      process.env.PIP_EXTRA_INDEX_URL = 'https://user@example.com/pypi/simple';
      expect(
        constructPipCompileCmd(
          extractHeaderCommand(simpleHeader, 'subdir/requirements.txt'),
        ),
      ).toBe('pip-compile --no-emit-index-url requirements.in');
    });

    it('returns --no-emit-index-url when only a password is found in PIP_INDEX_URL', () => {
      process.env.PIP_INDEX_URL = 'https://:pass@example.com/pypi/simple';
      expect(
        constructPipCompileCmd(
          extractHeaderCommand(simpleHeader, 'subdir/requirements.txt'),
        ),
      ).toBe('pip-compile --no-emit-index-url requirements.in');
    });

    it('returns --no-emit-index-url when only a password is found in PIP_EXTRA_INDEX_URL', () => {
      process.env.PIP_EXTRA_INDEX_URL = 'https://:pass@example.com/pypi/simple';
      expect(
        constructPipCompileCmd(
          extractHeaderCommand(simpleHeader, 'subdir/requirements.txt'),
        ),
      ).toBe('pip-compile --no-emit-index-url requirements.in');
    });

    it('returns --no-emit-index-url when PIP_INDEX_URL is invalid', () => {
      process.env.PIP_INDEX_URL = 'invalid-url';
      expect(
        constructPipCompileCmd(
          extractHeaderCommand(simpleHeader, 'subdir/requirements.txt'),
        ),
      ).toBe('pip-compile --no-emit-index-url requirements.in');
    });

    it('returns --no-emit-index-url PIP_EXTRA_INDEX_URL is invalid', () => {
      process.env.PIP_EXTRA_INDEX_URL = 'invalid-url';
      expect(
        constructPipCompileCmd(
          extractHeaderCommand(simpleHeader, 'subdir/requirements.txt'),
        ),
      ).toBe('pip-compile --no-emit-index-url requirements.in');
    });

    it('returns --no-emit-index-url only once when its in the header and credentials are present in the environment', () => {
      process.env.PIP_EXTRA_INDEX_URL =
        'https://user:pass@example.com/pypi/simple';
      expect(
        constructPipCompileCmd(
          extractHeaderCommand(
            Fixtures.get('requirementsWithHashes.txt'),
            'subdir/requirements.txt',
          ),
        ),
      ).toBe(
        'pip-compile --allow-unsafe --generate-hashes --no-emit-index-url --strip-extras --resolver=backtracking --output-file=requirements.txt requirements.in',
      );
    });

    it('allow explicit --emit-index-url', () => {
      process.env.PIP_INDEX_URL = 'https://user:pass@example.com/pypi/simple';
      expect(
        constructPipCompileCmd(
          extractHeaderCommand(
            getCommandInHeader('pip-compile --emit-index-url requirements.in'),
            'subdir/requirements.txt',
          ),
        ),
      ).toBe('pip-compile --emit-index-url requirements.in');
    });

    it('throws on unknown arguments', () => {
      expect(() =>
        constructPipCompileCmd(
          extractHeaderCommand(
            Fixtures.get('requirementsWithUnknownArguments.txt'),
            'subdir/requirements.txt',
          ),
        ),
      ).toThrow(/supported/);
    });

    it('throws on custom command', () => {
      expect(() =>
        constructPipCompileCmd(
          extractHeaderCommand(
            Fixtures.get('requirementsCustomCommand.txt'),
            'subdir/requirements.txt',
          ),
        ),
      ).toThrow(/custom/);
    });

    it('add --upgrade-package to command if Upgrade[] passed', () => {
      expect(
        constructPipCompileCmd(
          extractHeaderCommand(
            getCommandInHeader(
              'pip-compile --output-file=requirements.txt requirements.in',
            ),
            'subdir/requirements.txt',
          ),
          [
            { depName: 'foo', newVersion: '1.0.2' },
            { depName: 'bar', newVersion: '2.0.0' },
          ] satisfies Upgrade[],
        ),
      ).toBe(
        'pip-compile --output-file=requirements.txt requirements.in --upgrade-package=foo==1.0.2 --upgrade-package=bar==2.0.0',
      );
    });

    it('reports errors when a lock file is unchanged', async () => {
      fs.readLocalFile.mockResolvedValue(simpleHeader);
      mockExecSequence([
        new Error('Oh noes!'),
        { stdout: 'This one worked', stderr: '' },
      ]);
      git.getRepoStatus.mockResolvedValue(
        partial<StatusResult>({
          modified: [],
        }),
      );
      const results = await updateArtifacts({
        packageFileName: 'requirements.in',
        updatedDeps: [],
        newPackageFileContent: 'some new content',
        config: {
          ...config,
          lockFiles: ['requirements1.txt', 'requirements2.txt'],
        },
      });
      expect(results).toMatchObject([
        {
          artifactError: { lockFile: 'requirements1.txt', stderr: 'Oh noes!' },
        },
      ]);
    });
  });
});
