import type { MockInstance } from 'vitest';
import type { RequiredConfig } from '../../../../config/types';
import { logger } from '../../../../logger';
import * as env from './env';
import type { ParseConfigOptions } from './types';

describe('workers/global/config/parse/env', () => {
  describe('.getConfig(env)', () => {
    it('returns empty env', async () => {
      expect(await env.getConfig({})).toEqual({ hostRules: [] });
    });

    it('supports boolean true', async () => {
      const envParam: NodeJS.ProcessEnv = { RENOVATE_CONFIG_MIGRATION: 'true' };
      expect((await env.getConfig(envParam)).configMigration).toBeTrue();
    });

    it('supports boolean false', async () => {
      const envParam: NodeJS.ProcessEnv = {
        RENOVATE_CONFIG_MIGRATION: 'false',
      };
      expect((await env.getConfig(envParam)).configMigration).toBeFalse();
    });

    it('throws exception for invalid boolean value', async () => {
      const envParam: NodeJS.ProcessEnv = {
        RENOVATE_CONFIG_MIGRATION: 'badvalue',
      };
      await expect(env.getConfig(envParam)).rejects.toThrow(
        Error(
          "Invalid boolean value: expected 'true' or 'false', but got 'badvalue'",
        ),
      );
    });

    delete process.env.RENOVATE_CONFIG_MIGRATION;

    it('supports list single', async () => {
      const envParam: NodeJS.ProcessEnv = { RENOVATE_LABELS: 'a' };
      expect((await env.getConfig(envParam)).labels).toEqual(['a']);
    });

    it('supports list multiple', async () => {
      const envParam: NodeJS.ProcessEnv = { RENOVATE_LABELS: 'a,b,c' };
      expect((await env.getConfig(envParam)).labels).toEqual(['a', 'b', 'c']);
    });

    it('supports list multiple without blank items', async () => {
      const envParam: NodeJS.ProcessEnv = { RENOVATE_LABELS: 'a,b,c,' };
      expect((await env.getConfig(envParam)).labels).toEqual(['a', 'b', 'c']);
    });

    it('supports string', async () => {
      const envParam: NodeJS.ProcessEnv = { RENOVATE_TOKEN: 'a' };
      expect((await env.getConfig(envParam)).token).toBe('a');
    });

    it('coerces string newlines', async () => {
      const envParam: NodeJS.ProcessEnv = {
        RENOVATE_GIT_PRIVATE_KEY: 'abc\\ndef',
      };
      expect((await env.getConfig(envParam)).gitPrivateKey).toBe('abc\ndef');
    });

    it('supports custom prefixes', async () => {
      const envParam: NodeJS.ProcessEnv = {
        ENV_PREFIX: 'FOOBAR_',
        FOOBAR_TOKEN: 'abc',
      };
      const res = await env.getConfig(envParam);
      expect(res).toMatchObject({ token: 'abc' });
    });

    it('supports json', async () => {
      const envParam: NodeJS.ProcessEnv = {
        RENOVATE_LOCK_FILE_MAINTENANCE: '{}',
      };
      expect((await env.getConfig(envParam)).lockFileMaintenance).toEqual({});
    });

    it('supports arrays of objects', async () => {
      const envParam: NodeJS.ProcessEnv = {
        RENOVATE_HOST_RULES: JSON.stringify([{ foo: 'bar' }]),
      };
      const res = await env.getConfig(envParam);
      expect(res).toMatchObject({ hostRules: [{ foo: 'bar' }] });
    });

    test.each`
      envArg                                           | config
      ${{ RENOVATE_RECREATE_CLOSED: 'true' }}          | ${{ recreateWhen: 'always' }}
      ${{ RENOVATE_RECREATE_CLOSED: 'false' }}         | ${{ recreateWhen: 'auto' }}
      ${{ RENOVATE_RECREATE_WHEN: 'auto' }}            | ${{ recreateWhen: 'auto' }}
      ${{ RENOVATE_RECREATE_WHEN: 'always' }}          | ${{ recreateWhen: 'always' }}
      ${{ RENOVATE_RECREATE_WHEN: 'never' }}           | ${{ recreateWhen: 'never' }}
      ${{ RENOVATE_BASE_BRANCHES: '["main", "dev"]' }} | ${{ baseBranchPatterns: ['main', 'dev'] }}
    `('"$envArg" -> $config', async ({ envArg, config }) => {
      expect(await env.getConfig(envArg)).toMatchObject(config);
    });

    it('skips misconfigured arrays', async () => {
      const envName = 'RENOVATE_HOST_RULES';
      const val = JSON.stringify('foobar');
      const envParam: NodeJS.ProcessEnv = { [envName]: val };

      const res = await env.getConfig(envParam);

      expect(res).toEqual({ hostRules: [] });
      expect(logger.debug).toHaveBeenLastCalledWith(
        { val, envName },
        'Could not parse object array',
      );
    });

    it('skips garbage array values', async () => {
      const envName = 'RENOVATE_HOST_RULES';
      const val = '!@#';
      const envParam: NodeJS.ProcessEnv = { [envName]: val };

      const res = await env.getConfig(envParam);

      expect(res).toEqual({ hostRules: [] });
      expect(logger.debug).toHaveBeenLastCalledWith(
        { val, envName },
        'Could not parse environment variable',
      );
    });

    it('supports GitHub token', async () => {
      const envParam: NodeJS.ProcessEnv = {
        RENOVATE_TOKEN: 'github.com token',
      };
      expect(await env.getConfig(envParam)).toMatchSnapshot({
        token: 'github.com token',
      });
    });

    it('supports GitHub custom endpoint', async () => {
      const envParam: NodeJS.ProcessEnv = {
        RENOVATE_ENDPOINT: 'a ghe endpoint',
      };
      expect(await env.getConfig(envParam)).toMatchSnapshot({
        endpoint: 'a ghe endpoint',
      });
    });

    it('supports GitHub custom endpoint and github.com', async () => {
      const envParam: NodeJS.ProcessEnv = {
        GITHUB_COM_TOKEN: 'a github.com token',
        RENOVATE_ENDPOINT: 'a ghe endpoint',
        RENOVATE_TOKEN: 'a ghe token',
      };
      expect(await env.getConfig(envParam)).toMatchSnapshot({
        endpoint: 'a ghe endpoint',
        hostRules: [
          {
            hostType: 'github',
            matchHost: 'github.com',
            token: 'a github.com token',
          },
        ],
        token: 'a ghe token',
      });
    });

    it('supports GitHub fine-grained PATs', async () => {
      const envParam: NodeJS.ProcessEnv = {
        GITHUB_COM_TOKEN: 'github_pat_XXXXXX',
        RENOVATE_TOKEN: 'a github.com token',
      };
      expect(await env.getConfig(envParam)).toEqual({
        token: 'a github.com token',
        hostRules: [
          {
            hostType: 'github',
            matchHost: 'github.com',
            token: 'github_pat_XXXXXX',
          },
        ],
      });
    });

    it('supports RENOVATE_ prefixed github com token', async () => {
      const envParam: NodeJS.ProcessEnv = {
        RENOVATE_GITHUB_COM_TOKEN: 'github_pat_XXXXXX',
        RENOVATE_TOKEN: 'a github.com token',
      };
      expect(await env.getConfig(envParam)).toEqual({
        token: 'a github.com token',
        hostRules: [
          {
            hostType: 'github',
            matchHost: 'github.com',
            token: 'github_pat_XXXXXX',
          },
        ],
      });
    });

    it('GITHUB_COM_TOKEN takes precedence over RENOVATE_GITHUB_COM_TOKEN', async () => {
      const envParam: NodeJS.ProcessEnv = {
        GITHUB_COM_TOKEN: 'github_pat_XXXXXX',
        RENOVATE_GITHUB_COM_TOKEN: 'github_pat_YYYYYY',
        RENOVATE_TOKEN: 'a github.com token',
      };
      expect(await env.getConfig(envParam)).toEqual({
        token: 'a github.com token',
        hostRules: [
          {
            hostType: 'github',
            matchHost: 'github.com',
            token: 'github_pat_XXXXXX',
          },
        ],
      });
    });

    it('supports GitHub custom endpoint and gitlab.com', async () => {
      const envParam: NodeJS.ProcessEnv = {
        RENOVATE_ENDPOINT: 'a ghe endpoint',
        RENOVATE_TOKEN: 'a ghe token',
      };
      expect(await env.getConfig(envParam)).toMatchSnapshot({
        endpoint: 'a ghe endpoint',
        token: 'a ghe token',
      });
    });

    it('supports GitLab token', async () => {
      const envParam: NodeJS.ProcessEnv = {
        RENOVATE_PLATFORM: 'gitlab',
        RENOVATE_TOKEN: 'a gitlab.com token',
      };
      expect(await env.getConfig(envParam)).toMatchSnapshot({
        platform: 'gitlab',
        token: 'a gitlab.com token',
      });
    });

    it('supports GitLab custom endpoint', async () => {
      const envParam: NodeJS.ProcessEnv = {
        RENOVATE_PLATFORM: 'gitlab',
        RENOVATE_TOKEN: 'a gitlab token',
        RENOVATE_ENDPOINT: 'a gitlab endpoint',
      };
      expect(await env.getConfig(envParam)).toMatchSnapshot({
        endpoint: 'a gitlab endpoint',
        platform: 'gitlab',
        token: 'a gitlab token',
      });
    });

    it('supports Azure DevOps', async () => {
      const envParam: NodeJS.ProcessEnv = {
        RENOVATE_PLATFORM: 'azure',
        RENOVATE_TOKEN: 'an Azure DevOps token',
        RENOVATE_ENDPOINT: 'an Azure DevOps endpoint',
      };
      expect(await env.getConfig(envParam)).toMatchSnapshot({
        endpoint: 'an Azure DevOps endpoint',
        platform: 'azure',
        token: 'an Azure DevOps token',
      });
    });

    it('supports Bitbucket token', async () => {
      const envParam: NodeJS.ProcessEnv = {
        RENOVATE_PLATFORM: 'bitbucket',
        RENOVATE_ENDPOINT: 'a bitbucket endpoint',
        RENOVATE_USERNAME: 'some-username',
        RENOVATE_PASSWORD: 'app-password',
      };
      expect(await env.getConfig(envParam)).toMatchSnapshot({
        platform: 'bitbucket',
        endpoint: 'a bitbucket endpoint',
        username: 'some-username',
        password: 'app-password',
      });
    });

    it('supports Bitbucket username/password', async () => {
      const envParam: NodeJS.ProcessEnv = {
        RENOVATE_PLATFORM: 'bitbucket',
        RENOVATE_ENDPOINT: 'a bitbucket endpoint',
        RENOVATE_USERNAME: 'some-username',
        RENOVATE_PASSWORD: 'app-password',
      };
      expect(await env.getConfig(envParam)).toMatchSnapshot({
        endpoint: 'a bitbucket endpoint',
        hostRules: [],
        password: 'app-password',
        platform: 'bitbucket',
        username: 'some-username',
      });
    });

    it('merges full config from env', async () => {
      const envParam: NodeJS.ProcessEnv = {
        RENOVATE_CONFIG: '{"enabled":false,"token":"foo"}',
        RENOVATE_TOKEN: 'a',
      };
      const config = await env.getConfig(envParam);
      expect(config.enabled).toBeFalse();
      expect(config.token).toBe('a');
    });

    it('massages converted experimental env vars', async () => {
      const envParam: NodeJS.ProcessEnv = {
        RENOVATE_X_MERGE_CONFIDENCE_API_BASE_URL: 'some-url', // converted
        RENOVATE_X_MERGE_CONFIDENCE_SUPPORTED_DATASOURCES: '["docker"]', // converted
        RENOVATE_X_AUTODISCOVER_REPO_SORT: 'alpha',
        RENOVATE_X_DOCKER_MAX_PAGES: '10',
        RENOVATE_AUTODISCOVER_REPO_ORDER: 'desc',
        RENOVATE_X_DELETE_CONFIG_FILE: 'true',
        RENOVATE_X_S3_ENDPOINT: 'endpoint',
        RENOVATE_X_S3_PATH_STYLE: 'true',
      };
      const config = await env.getConfig(envParam);
      expect(config).toMatchObject({
        mergeConfidenceEndpoint: 'some-url',
        mergeConfidenceDatasources: ['docker'],
        autodiscoverRepoSort: 'alpha',
        autodiscoverRepoOrder: 'desc',
        dockerMaxPages: 10,
        deleteConfigFile: true,
        s3Endpoint: 'endpoint',
        s3PathStyle: true,
      });
    });

    describe('RENOVATE_CONFIG tests', () => {
      let processExit: MockInstance<(code?: number | string | null) => never>;

      beforeAll(() => {
        processExit = vi
          .spyOn(process, 'exit')
          .mockImplementation((() => void 0) as never);
      });

      afterAll(() => {
        processExit.mockRestore();
      });

      it('crashes', async () => {
        const envParam: NodeJS.ProcessEnv = { RENOVATE_CONFIG: '!@#' };
        processExit.mockImplementationOnce(() => {
          throw new Error('terminate function to simulate process.exit call');
        });

        await expect(env.getConfig(envParam)).toReject();
        expect(processExit).toHaveBeenCalledWith(1);
      });

      it('migrates RENOVATE_CONFIG', async () => {
        const envParam: NodeJS.ProcessEnv = {
          RENOVATE_CONFIG: '{"automerge":"any","token":"foo"}',
        };
        const config = await env.getConfig(envParam);
        expect(logger.warn).toHaveBeenCalledTimes(1);
        expect(config.automerge).toBeTrue();
      });

      it('warns if config in RENOVATE_CONFIG is invalid', async () => {
        const envParam: NodeJS.ProcessEnv = {
          RENOVATE_CONFIG: '{"enabled":"invalid-value","prTitle":"something"}',
        };
        await env.getConfig(envParam);
        expect(logger.warn).toHaveBeenCalledTimes(2);
      });
    });

    describe('migrations', () => {
      it('renames migrated variables', async () => {
        const envParam: NodeJS.ProcessEnv = {
          RENOVATE_GIT_LAB_AUTOMERGE: 'true',
        };
        const config = await env.getConfig(envParam);
        expect(config.platformAutomerge).toBe(true);
      });
    });
  });

  describe('.getEnvName(definition)', () => {
    it('returns empty', () => {
      const option: ParseConfigOptions = {
        name: 'foo',
        env: false,
      };
      expect(env.getEnvName(option)).toBe('');
    });

    it('returns existing env', () => {
      const option: ParseConfigOptions = {
        name: 'foo',
        env: 'FOO',
      };
      expect(env.getEnvName(option)).toBe('FOO');
    });

    it('generates RENOVATE_ env', () => {
      const option: ParseConfigOptions = {
        name: 'oneTwoThree',
      };
      expect(env.getEnvName(option)).toBe('RENOVATE_ONE_TWO_THREE');
    });

    it('dryRun boolean true', async () => {
      const envParam: NodeJS.ProcessEnv = {
        RENOVATE_DRY_RUN: 'true',
      };
      const config = await env.getConfig(envParam);
      expect(config.dryRun).toBe('full');
    });

    it('dryRun boolean false', async () => {
      const envParam: NodeJS.ProcessEnv = {
        RENOVATE_DRY_RUN: 'false',
      };
      const config = await env.getConfig(envParam);
      expect(config.dryRun).toBeUndefined();
    });

    it('dryRun null', async () => {
      const envParam: NodeJS.ProcessEnv = {
        RENOVATE_DRY_RUN: 'null',
      };
      const config = await env.getConfig(envParam);
      expect(config.dryRun).toBeUndefined();
    });

    it('requireConfig boolean true', async () => {
      const envParam: NodeJS.ProcessEnv = {
        RENOVATE_REQUIRE_CONFIG: 'true' as RequiredConfig,
      };
      const config = await env.getConfig(envParam);
      expect(config.requireConfig).toBe('required');
    });

    it('requireConfig boolean false', async () => {
      const envParam: NodeJS.ProcessEnv = {
        RENOVATE_REQUIRE_CONFIG: 'false' as RequiredConfig,
      };
      const config = await env.getConfig(envParam);
      expect(config.requireConfig).toBe('optional');
    });

    it('platformCommit boolean true', async () => {
      const envParam: NodeJS.ProcessEnv = {
        RENOVATE_PLATFORM_COMMIT: 'true',
      };
      const config = await env.getConfig(envParam);
      expect(config.platformCommit).toBe('enabled');
    });

    it('platformCommit boolean false', async () => {
      const envParam: NodeJS.ProcessEnv = {
        RENOVATE_PLATFORM_COMMIT: 'false',
      };
      const config = await env.getConfig(envParam);
      expect(config.platformCommit).toBe('disabled');
    });
  });
});
