import '../../spec_helper'

import path from 'path'
import os from 'os'
import snapshot from 'snap-shot-it'
import stripAnsi from 'strip-ansi'
import minimist from 'minimist'
import type { ParsedCypressArgv } from '../../../lib/util/args'
import { toObject, normalizeBackslashes, toArray } from '../../../lib/util/args'
import * as getWindowsProxyUtil from '../../../lib/util/get-windows-proxy'

const getCwd = () => process.cwd()

describe('lib/util/args', () => {
  describe('minimist behavior', () => {
    it('casts numbers by default', () => {
      const options = minimist(['--ci-build-id', '1e100'])

      expect(options).to.deep.equal({
        _: [],
        'ci-build-id': 1e+100,
      })
    })

    it('does not cast strings if specified', () => {
      const options = minimist(['--ci-build-id', '1e100'], {
        string: ['ci-build-id'],
      })

      expect(options).to.deep.equal({
        _: [],
        'ci-build-id': '1e100',
      })
    })

    it('does not cast alias strings if specified', () => {
      const options = minimist(['--ciBuildId', '1e100'], {
        string: ['ci-build-id'],
        alias: {
          'ci-build-id': 'ciBuildId',
        },
      })

      expect(options).to.deep.equal({
        _: [],
        'ci-build-id': '1e100',
        ciBuildId: '1e100',
      })
    })
  })

  describe('--smoke-test', () => {
    it('sets pong to ping', function () {
      const options = toObject(['--smoke-test', '--ping=123'])

      expect(options.pong).to.eq(123)
    })
  })

  describe('normalizeBackslashes', () => {
    it('sets non-string properties to undefined', () => {
      const input = {
        // string properties
        project: true,
        appPath: '/foo/bar',
        // unknown properties will be preserved
        somethingElse: 42,
      }
      const output = normalizeBackslashes(input)

      expect(output).to.deep.equal({
        appPath: '/foo/bar',
        somethingElse: 42,
      })
    })

    it('handles empty project path string', () => {
      const input = {
        project: '',
      }
      const output = normalizeBackslashes(input)

      // empty project path remains
      expect(output).to.deep.equal(input)
    })
  })

  describe('--project', () => {
    it('sets projectRoot', function () {
      const projectRoot = path.resolve(getCwd(), './foo/bar')
      const options = toObject(['--project', './foo/bar'])

      expect(options.projectRoot).to.eq(projectRoot)
    })

    it('is undefined if not specified', function () {
      const options = toObject([])

      expect(options.projectRoot).to.eq(undefined)
    })

    it('handles bool project parameter', function () {
      const options = toObject(['--project', true] as unknown as string[])

      expect(options.projectRoot).to.eq(undefined)
    })
  })

  describe('--run-project', () => {
    it('sets projectRoot', function () {
      const projectRoot = path.resolve(getCwd(), '/baz')
      const options = toObject(['--run-project', '/baz'])

      expect(options.projectRoot).to.eq(projectRoot)
    })

    it('strips single double quote from the end', function () {
      // https://github.com/cypress-io/cypress/issues/535
      // NPM does not pass correctly options that end with backslash
      const options = toObject(['--run-project', 'C:\\foo"'])

      expect(options.runProject).to.eq('C:\\foo')
    })

    it('does not strip if there are multiple double quotes', function () {
      const options = toObject(['--run-project', '"foo bar"'])

      expect(options.runProject).to.eq('"foo bar"')
    })
  })

  describe('--spec', () => {
    it('converts to array', function () {
      const options = toObject(['--run-project', 'foo', '--spec', 'cypress/integration/a.js,cypress/integration/b.js,cypress/integration/c.js'])

      expect(options.spec[0]).to.eq(`${getCwd()}/cypress/integration/a.js`)
      expect(options.spec[1]).to.eq(`${getCwd()}/cypress/integration/b.js`)

      expect(options.spec[2]).to.eq(`${getCwd()}/cypress/integration/c.js`)
    })

    it('discards wrapping single quotes', function () {
      const options = toObject(['--run-project', 'foo', '--spec', '\'cypress/integration/foo_spec.js\''])

      expect(options.spec[0]).to.eq(`${getCwd()}/cypress/integration/foo_spec.js`)
    })

    it('throws if argument cannot be parsed', function () {
      expect(() => {
        return toObject(['--run-project', 'foo', '--spec', {}] as unknown as string[])
      }).to.throw

      try {
        return toObject(['--run-project', 'foo', '--spec', {}] as unknown as string[])
      } catch (err: unknown) {
        return snapshot('invalid spec error', stripAnsi((err as Error).message))
      }
    })

    it('should be correctly parsing globs with lists & ranges', function () {
      const options = toObject(['--spec', 'cypress/integration/{[!a]*.spec.js,sub1,{sub2,sub3/sub4}}/*.js'])

      expect(options.spec[0]).to.eq(`${getCwd()}/cypress/integration/{[!a]*.spec.js,sub1,{sub2,sub3/sub4}}/*.js`)
    })

    it('should be correctly parsing globs with a mix of lists, ranges & regular paths', function () {
      const options = toObject(['--spec', 'cypress/integration/{[!a]*.spec.js,sub1,{sub2,sub3/sub4}}/*.js,cypress/integration/foo.spec.js'])

      expect(options.spec[0]).to.eq(`${getCwd()}/cypress/integration/{[!a]*.spec.js,sub1,{sub2,sub3/sub4}}/*.js`)
      expect(options.spec[1]).to.eq(`${getCwd()}/cypress/integration/foo.spec.js`)
    })

    it('should be correctly parsing single glob with range', function () {
      const options = toObject(['--spec', 'cypress/integration/[a-c]*/**'])

      expect(options.spec[0]).to.eq(`${getCwd()}/cypress/integration/[a-c]*/**`)
    })

    it('should be correctly parsing single glob with list', function () {
      const options = toObject(['--spec', 'cypress/integration/{a,b,c}/*.js'])

      expect(options.spec[0]).to.eq(`${getCwd()}/cypress/integration/{a,b,c}/*.js`)
    })

    // https://github.com/cypress-io/cypress/issues/20794
    it('does not split at filename with glob pattern', function () {
      const options = toObject(['--spec', 'cypress/integration/foo/bar/[baz]/test.ts,cypress/integration/foo1/bar/[baz]/test.ts,cypress/integration/foo2/bar/baz/test.ts,cypress/integration/foo3/bar/baz/foo4.ts'])

      expect(options.spec[0]).to.eq(`${getCwd()}/cypress/integration/foo/bar/[baz]/test.ts`)
      expect(options.spec[1]).to.eq(`${getCwd()}/cypress/integration/foo1/bar/[baz]/test.ts`)
      expect(options.spec[2]).to.eq(`${getCwd()}/cypress/integration/foo2/bar/baz/test.ts`)
      expect(options.spec[3]).to.eq(`${getCwd()}/cypress/integration/foo3/bar/baz/foo4.ts`)
    })

    // https://github.com/cypress-io/cypress/issues/20794
    it('correctly splits at comma with glob pattern', function () {
      const options = toObject(['--spec', 'cypress/integration/foo/bar/baz/test.ts,cypress/integration/foo1/bar/[baz]/test.ts,cypress/integration/foo2/bar/baz/test.ts,cypress/integration/foo3/bar/baz/foo4.ts'])

      expect(options.spec[0]).to.eq(`${getCwd()}/cypress/integration/foo/bar/baz/test.ts`)
      expect(options.spec[1]).to.eq(`${getCwd()}/cypress/integration/foo1/bar/[baz]/test.ts`)
      expect(options.spec[2]).to.eq(`${getCwd()}/cypress/integration/foo2/bar/baz/test.ts`)
      expect(options.spec[3]).to.eq(`${getCwd()}/cypress/integration/foo3/bar/baz/foo4.ts`)
    })

    // https://github.com/cypress-io/cypress/issues/20794
    it('correctly splits at comma with escaped glob pattern', function () {
      const options = toObject(['--spec', 'cypress/integration/foo/bar/\[baz\]/test.ts,cypress/integration/foo1/bar/\[baz1\]/test.ts,cypress/integration/foo2/bar/baz/test.ts,cypress/integration/foo3/bar/baz/foo4.ts'])

      expect(options.spec[0]).to.eq(`${getCwd()}/cypress/integration/foo/bar/\[baz\]/test.ts`)
      expect(options.spec[1]).to.eq(`${getCwd()}/cypress/integration/foo1/bar/\[baz1\]/test.ts`)
      expect(options.spec[2]).to.eq(`${getCwd()}/cypress/integration/foo2/bar/baz/test.ts`)
      expect(options.spec[3]).to.eq(`${getCwd()}/cypress/integration/foo3/bar/baz/foo4.ts`)
    })
  })

  describe('--tag', () => {
    it('converts to array', function () {
      const options = toObject(['--run-project', 'foo', '--tag', 'nightly,production,build'])

      expect(options.tag[0]).to.eq('nightly')
      expect(options.tag[1]).to.eq('production')

      expect(options.tag[2]).to.eq('build')
    })
  })

  describe('--auto-cancel-after-failures', () => {
    it('converts to integer', function () {
      const options = toObject(['--auto-cancel-after-failures', '4'])

      expect(options.autoCancelAfterFailures).to.eq(4)
    })

    it('converts to false', function () {
      const options = toObject(['--auto-cancel-after-failures', 'false'])

      expect(options.autoCancelAfterFailures).to.eq(false)
    })

    it('handles value 0', function () {
      const options = toObject(['--auto-cancel-after-failures', '0'])

      expect(options.autoCancelAfterFailures).to.eq(0)
    })

    it('throws error when a string is set', function () {
      try {
        return toObject(['--auto-cancel-after-failures', 'foo'])
      } catch (err: unknown) {
        return snapshot('invalid --auto-cancel-after-failures error', stripAnsi((err as Error).message))
      }
    })

    it('throws error when true is set', function () {
      try {
        return toObject(['--auto-cancel-after-failures', 'true'])
      } catch (err: unknown) {
        return snapshot('invalid --auto-cancel-after-failures (true) error', stripAnsi((err as Error).message))
      }
    })

    it('throws error when a negative value is set', function () {
      try {
        return toObject(['--auto-cancel-after-failures', '-1'])
      } catch (err: unknown) {
        return snapshot('invalid --auto-cancel-after-failures (negative value) error', stripAnsi((err as Error).message))
      }
    })

    it('throws error when a decimal value is set', function () {
      try {
        return toObject(['--auto-cancel-after-failures', '1.5'])
      } catch (err: unknown) {
        return snapshot('invalid --auto-cancel-after-failures (decimal value) error', stripAnsi((err as Error).message))
      }
    })
  })

  describe('--port', () => {
    it('converts to Number', function () {
      const options = toObject(['--port', '8080'])

      expect(options.config.port).to.eq(8080)
    })
  })

  describe('--env', () => {
    it('converts to object literal', function () {
      const options = toObject(['--env', 'foo=bar,version=0.12.1,host=localhost:8888,bar=qux='])

      expect(options.config.env).to.deep.eq({
        foo: 'bar',
        version: '0.12.1',
        host: 'localhost:8888',
        bar: 'qux=',
      })
    })

    it('throws if env string cannot be parsed', function () {
      expect(() => {
        return toObject(['--env', 'nonono'])
      }).to.throw

      // now look at the error
      try {
        return toObject(['--env', 'nonono'])
      } catch (err: unknown) {
        return snapshot('invalid env error', stripAnsi((err as Error).message))
      }
    })

    // https://github.com/cypress-io/cypress/issues/6891
    it('handles values containing exponential operators', function () {
      const options = toObject(['--env', 'foo=bar,hash=769e98018'])

      expect(options.config.env).to.deep.eq({
        foo: 'bar',
        hash: '769e98018',
      })
    })

    // https://github.com/cypress-io/cypress/issues/6810
    it('handles values that are arrays', function () {
      const options = toObject(['--env', 'foo="[bar1,bar2,bar3]"'])

      expect(options.config.env).to.deep.eq({
        foo: '[bar1|bar2|bar3]',
      })
    })
  })

  describe('--reporterOptions', () => {
    it('converts to object literal', function () {
      const reporterOpts = {
        mochaFile: 'path/to/results.xml',
        testCaseSwitchClassnameAndName: true,
        suiteTitleSeparatedBy: '.=|',
      }

      const options = toObject(['--reporterOptions', JSON.stringify(reporterOpts)])

      expect(options.config.reporterOptions).to.deep.eq(reporterOpts)
    })

    it('converts nested objects with mixed assignment usage', function () {
      const reporterOpts = {
        reporterEnabled: 'JSON, Spec',
        jsonReporterOptions: {
          toConsole: true,
        },
      }

      // as a full blown object
      let options = toObject(['--reporterOptions', JSON.stringify(reporterOpts)])

      expect(options.config.reporterOptions).to.deep.eq(reporterOpts)

      // as mixed usage
      const nestedJSON = JSON.stringify(reporterOpts.jsonReporterOptions)

      options = toObject([
        '--reporterOptions',
        `reporterEnabled=JSON,jsonReporterOptions=${nestedJSON}`,
      ])

      expect(options.config.reporterOptions).to.deep.eq({
        reporterEnabled: 'JSON',
        jsonReporterOptions: {
          toConsole: true,
        },
      })
    })

    it('throws if reporter string cannot be parsed', function () {
      expect(() => {
        return toObject(['--reporterOptions', 'abc'])
      }).to.throw

      // now look at the error
      try {
        return toObject(['--reporterOptions', 'abc'])
      } catch (err: unknown) {
        return snapshot('invalid reporter options error', stripAnsi((err as Error).message))
      }
    })
  })

  describe('--runner-ui', () => {
    it('converts to boolean', function () {
      const options = toObject(['--runner-ui', 'false'])

      expect(options.runnerUi).to.eq(false)
    })

    it('is undefined if not specified', function () {
      const options = toObject([])

      expect(options.runnerUi).to.eq(undefined)
    })
  })

  describe('--config', () => {
    it('converts to object literal', function () {
      const options = toObject(['--config', 'pageLoadTimeout=10000,waitForAnimations=false'])

      expect(options.config.pageLoadTimeout).eq(10000)

      expect(options.config.waitForAnimations).eq(false)
    })

    it('converts straight JSON stringification', function () {
      const config = {
        pageLoadTimeout: 10000,
        waitForAnimations: false,
      }

      const options = toObject(['--config', JSON.stringify(config)])

      expect(options.config).to.deep.eq(config)
    })

    it('converts nested usage with JSON stringification', function () {
      const config = {
        pageLoadTimeout: 10000,
        waitForAnimations: false,
        blockHosts: ['one.com', 'www.two.io'],
        hosts: {
          'foobar.com': '127.0.0.1',
        },
      }

      // as a full blown object
      let options = toObject(['--config', JSON.stringify(config)])

      expect(options.config).to.deep.eq(config)

      // as mixed usage
      const hosts = JSON.stringify(config.hosts)
      const blockHosts = JSON.stringify(config.blockHosts)

      options = toObject([
        '--config',
        [
          'pageLoadTimeout=10000',
          'waitForAnimations=false',
          `hosts=${hosts}`,
          `blockHosts=${blockHosts}`,
        ].join(','),

      ])

      expect(options.config).to.deep.eq(config)
    })

    it('allows config properties', function () {
      const options = toObject(['--config', 'foo=bar,port=1111,supportFile=path/to/support_file'])

      expect(options.config.port).to.eq(1111)
      const cfg = options.config as Record<string, unknown>
      const e2e = cfg.e2e as Record<string, unknown>
      const component = cfg.component as Record<string, unknown>

      expect(e2e.supportFile).to.eq('path/to/support_file')
      expect(component.supportFile).to.eq('path/to/support_file')

      expect(options).not.to.have.property('foo')
      expect(options.config).not.to.have.property('supportFile')
    })

    it('overrides port in config', function () {
      let options = toObject(['--port', '2222', '--config', 'port=3333'])

      expect(options.config.port).to.eq(2222)

      options = toObject(['--port', '2222'])

      expect(options.config.port).to.eq(2222)
    })

    it('throws if config string cannot be parsed', function () {
      expect(() => {
        return toObject(['--config', 'xyz'])
      }).to.throw

      // now look at the error
      try {
        return toObject(['--config', 'xyz'])
      } catch (err: unknown) {
        return snapshot('invalid config error', stripAnsi((err as Error).message))
      }
    })
  })

  describe('.toArray', () => {
    let obj: { config: { foo: string }, project: string }

    beforeEach(function () {
      obj = { config: { foo: 'bar' }, project: 'foo/bar' }
    })

    it('rejects values which have an corresponding underscore\'d key', function () {
      expect(toArray(obj)).to.deep.eq([
        `--config=${JSON.stringify({ foo: 'bar' })}`,
        '--project=foo/bar',
      ])
    })

    it('does not pick disallowed args', () => {
      const result = toArray({ doNotPick: 'forbidden' })

      expect(result).to.be.empty
    })

    it('does pick allowed args', () => {
      const result = toArray({
        apiKey: 'apiKey',
        appPath: 'appPath',
        browser: 'browser',
        ci: 'ci',
        ciBuildId: 'ciBuildId',
        userNodePath: 'userNodePath',
        userNodeVersion: 'userNodeVersion',
        config: 'config',
        configFile: 'configFile',
        cwd: 'cwd',
        env: 'env',
        execPath: 'execPath',
        exit: 'exit',
        exitWithCode: 'exitWithCode',
        group: 'group',
        headed: 'headed',
        inspectBrk: 'inspectBrk',
        key: 'key',
        mode: 'mode',
        outputPath: 'outputPath',
        parallel: 'parallel',
        ping: 'ping',
        port: 'port',
        project: 'project',
        proxySource: 'proxySource',
        quiet: 'quiet',
        record: 'record',
        reporter: 'reporter',
        reporterOptions: 'reporterOptions',
        returnPkg: 'returnPkg',
        runMode: 'runMode',
        runProject: 'runProject',
        smokeTest: 'smokeTest',
        spec: 'spec',
        tag: 'tag',
        testingType: 'testingType',
        updating: 'updating',
        version: 'version',
      })

      expect(result).to.deep.eq([
        '--apiKey=apiKey',
        '--appPath=appPath',
        '--browser=browser',
        '--ci=ci',
        '--ciBuildId=ciBuildId',
        '--config=config',
        '--configFile=configFile',
        '--cwd=cwd',
        '--env=env',
        '--execPath=execPath',
        '--exit=exit',
        '--exitWithCode=exitWithCode',
        '--group=group',
        '--headed=headed',
        '--inspectBrk=inspectBrk',
        '--key=key',
        '--mode=mode',
        '--outputPath=outputPath',
        '--parallel=parallel',
        '--ping=ping',
        '--port=port',
        '--project=project',
        '--proxySource=proxySource',
        '--quiet=quiet',
        '--record=record',
        '--reporter=reporter',
        '--reporterOptions=reporterOptions',
        '--returnPkg=returnPkg',
        '--runMode=runMode',
        '--runProject=runProject',
        '--smokeTest=smokeTest',
        '--spec=spec',
        '--tag=tag',
        '--testingType=testingType',
        '--updating=updating',
        '--userNodePath=userNodePath',
        '--userNodeVersion=userNodeVersion',
        '--version=version',
      ])
    })
  })

  describe('.toObject', () => {
    let hosts: { a: string, b: string }
    let blockHosts: string[]
    let specs: string[]
    let env: { foo: string, baz: string, bar: string }
    let config: Record<string, unknown>
    let obj: ParsedCypressArgv

    beforeEach(function () {
      hosts = { a: 'b', b: 'c' }
      blockHosts = ['a.com', 'b.com']
      specs = [
        path.join(getCwd(), 'foo'),
        path.join(getCwd(), 'bar'),
        path.join(getCwd(), 'baz'),
      ]

      env = {
        foo: 'bar',
        baz: 'quux',
        bar: 'foo=quz',
      }

      config = {
        env,
        hosts,
        requestTimeout: 1234,
        blockHosts,
        reporterOptions: {
          foo: 'bar',
        },
      }

      const s = (str: unknown) => {
        return JSON.stringify(str)
      }

      obj = toObject([
        '--env=foo=bar,baz=quux,bar=foo=quz',
        '--config',
        `requestTimeout=1234,blockHosts=${s(blockHosts)},hosts=${s(hosts)}`,
        '--reporter-options=foo=bar',
        '--spec=foo,bar,baz',
      ])
    })

    it('coerces booleans', function () {
      expect(toObject(['--foo=true']).foo).be.true
      expect(toObject(['--no-record']).record).to.be.false

      expect(toObject(['--record=false']).record).to.be.false
    })

    it('backs up env, config, reporterOptions, spec', function () {
      expect(obj).to.deep.eq({
        cwd: getCwd(),
        _: [],
        config,
        invokedFromCli: false,
        spec: specs,
      })
    })

    it('can transpose back to an array', function () {
      const mergedConfig = JSON.stringify({
        requestTimeout: config.requestTimeout,
        blockHosts,
        hosts,
        env,
        reporterOptions: {
          foo: 'bar',
        },
      })

      const args = toArray(obj)

      expect(args).to.deep.eq([
        `--config=${mergedConfig}`,
        `--cwd=${getCwd()}`,
        `--spec=${JSON.stringify(specs)}`,
      ])

      expect(toObject(args)).to.deep.eq({
        cwd: getCwd(),
        _: [],
        invokedFromCli: true,
        config,
        spec: specs,
      })
    })

    it('does not coerce --ci-build-id', function () {
      const result = toObject(['--ci-build-id', '1e100'])

      expect(result).to.deep.equal({
        ciBuildId: '1e100',
        cwd: getCwd(),
        _: [],
        invokedFromCli: false,
        config: {},
      })
    })

    it('moves testing-type specific config options', function () {
      const result = toObject(['--config', '{"baseUrl": "http://foobar.com", "specPattern":"**/*.test.js"}'])

      expect(result).to.deep.equal({
        cwd: getCwd(),
        _: [],
        invokedFromCli: false,
        config: {
          e2e: { baseUrl: 'http://foobar.com', specPattern: '**/*.test.js' },
          component: { specPattern: '**/*.test.js' },
        },
      })
    })
  })

  describe('--updating', () => {
    // updating from 0.13.9 will omit the appPath + execPath so we must
    // handle these missing arguments manually
    it('slurps up appPath + execPath if updating and these are omitted', () => {
      const argv = [
        '/private/var/folders/wr/3xdzqnq16lz5r1j_xtl443580000gn/T/cypress/Cypress.app/Contents/MacOS/Cypress',
        '/Applications/Cypress.app',
        '/Applications/Cypress.app',
        '--updating',
      ]

      expect(toObject(argv)).to.deep.eq({
        cwd: getCwd(),
        _: [
          '/private/var/folders/wr/3xdzqnq16lz5r1j_xtl443580000gn/T/cypress/Cypress.app/Contents/MacOS/Cypress',
          '/Applications/Cypress.app',
          '/Applications/Cypress.app',
        ],
        config: {},
        appPath: '/Applications/Cypress.app',
        execPath: '/Applications/Cypress.app',
        invokedFromCli: false,
        updating: true,
      })
    })

    it('does not slurp up appPath + execPath if updating and these are already present in args', () => {
      const argv = [
        '/private/var/folders/wr/3xdzqnq16lz5r1j_xtl443580000gn/T/cypress/Cypress.app/Contents/MacOS/Cypress',
        '/Applications/Cypress.app1',
        '/Applications/Cypress.app2',
        '--app-path=a',
        '--exec-path=e',
        '--updating',
      ]

      expect(toObject(argv)).to.deep.eq({
        cwd: getCwd(),
        _: [
          '/private/var/folders/wr/3xdzqnq16lz5r1j_xtl443580000gn/T/cypress/Cypress.app/Contents/MacOS/Cypress',
          '/Applications/Cypress.app1',
          '/Applications/Cypress.app2',
        ],
        config: {},
        appPath: 'a',
        execPath: 'e',
        invokedFromCli: false,
        updating: true,
      })
    })
  })

  describe('with proxy', () => {
    beforeEach(function () {
      // originalEnv is set in spec_helper.js
      process.env = this.originalEnv
      delete process.env.HTTP_PROXY
      delete process.env.HTTPS_PROXY
      delete process.env.NO_PROXY
      delete process.env.http_proxy
      delete process.env.https_proxy

      return delete process.env.no_proxy
    })

    it('sets options from environment', function () {
      process.env.HTTP_PROXY = 'http://foo-bar.baz:123'
      process.env.NO_PROXY = 'a,b,c'
      const options = toObject([])

      expect(options.proxySource).to.be.undefined
      expect(options.proxyServer).to.eq(process.env.HTTP_PROXY)
      expect(options.proxyServer).to.eq('http://foo-bar.baz:123')
      expect(options.proxyBypassList).to.eq('a,b,c,127.0.0.1,::1,localhost')

      expect(process.env.HTTPS_PROXY).to.eq(process.env.HTTP_PROXY)
    })

    it('loads from Windows registry if not defined', function () {
      sinon.stub(getWindowsProxyUtil, 'getWindowsProxy').returns({
        httpProxy: 'http://quux.quuz',
        noProxy: 'd,e,f',
      })

      sinon.stub(os, 'platform').returns('win32')
      const options = toObject([])

      expect(options.proxySource).to.eq('win32')
      expect(options.proxyServer).to.eq('http://quux.quuz')
      expect(options.proxyServer).to.eq(process.env.HTTP_PROXY)
      expect(options.proxyServer).to.eq(process.env.HTTPS_PROXY)
      expect(options.proxyBypassList).to.eq('d,e,f,127.0.0.1,::1,localhost')

      expect(options.proxyBypassList).to.eq(process.env.NO_PROXY)
    });

    ['', 'false', '0'].forEach((override) => {
      it(`doesn't load from Windows registry if HTTP_PROXY overridden with string '${override}'`, function () {
        sinon.stub(getWindowsProxyUtil, 'getWindowsProxy').returns()
        sinon.stub(os, 'platform').returns('win32')
        process.env.HTTP_PROXY = override
        const options = toObject([])

        expect(getWindowsProxyUtil.getWindowsProxy).to.not.called
        expect(options.proxySource).to.be.undefined
        expect(options.proxyServer).to.be.undefined
        expect(options.proxyBypassList).to.be.undefined
        expect(process.env.HTTP_PROXY).to.be.undefined
        expect(process.env.HTTPS_PROXY).to.be.undefined

        expect(process.env.NO_PROXY).to.eq('127.0.0.1,::1,localhost')
      })
    })

    it(`doesn't mess with env vars if Windows registry doesn't have proxy`, function () {
      sinon.stub(getWindowsProxyUtil, 'getWindowsProxy').returns()
      sinon.stub(os, 'platform').returns('win32')
      const options = toObject([])

      expect(options.proxySource).to.be.undefined
      expect(options.proxyServer).to.be.undefined
      expect(options.proxyBypassList).to.be.undefined
      expect(process.env.HTTP_PROXY).to.be.undefined
      expect(process.env.HTTPS_PROXY).to.be.undefined

      expect(process.env.NO_PROXY).to.eq('127.0.0.1,::1,localhost')
    })

    it('sets a default NO_PROXY', function () {
      process.env.HTTP_PROXY = 'http://foo-bar.baz:123'
      const options = toObject([])

      expect(options.proxySource).to.be.undefined
      expect(options.proxyServer).to.eq(process.env.HTTP_PROXY)
      expect(options.proxyBypassList).to.eq('127.0.0.1,::1,localhost')

      expect(options.proxyBypassList).to.eq(process.env.NO_PROXY)
    })

    it('does not add localhost to NO_PROXY if NO_PROXY contains <-loopback>', function () {
      process.env.HTTP_PROXY = 'http://foo-bar.baz:123'
      process.env.NO_PROXY = 'a,b,c,<-loopback>,d'
      const options = toObject([])

      expect(options.proxySource).to.be.undefined
      expect(options.proxyServer).to.eq(process.env.HTTP_PROXY)
      expect(options.proxyBypassList).to.eq('a,b,c,<-loopback>,d')

      expect(options.proxyBypassList).to.eq(process.env.NO_PROXY)
    })

    it('sets a default localhost NO_PROXY if NO_PROXY = \'\'', function () {
      process.env.HTTP_PROXY = 'http://foo-bar.baz:123'
      process.env.NO_PROXY = ''
      const options = toObject([])

      expect(options.proxySource).to.be.undefined
      expect(options.proxyServer).to.eq(process.env.HTTP_PROXY)
      expect(options.proxyBypassList).to.eq('127.0.0.1,::1,localhost')

      expect(options.proxyBypassList).to.eq(process.env.NO_PROXY)
    })

    it('does not set a default localhost NO_PROXY if NO_PROXY = \'<-loopback>\'', function () {
      process.env.HTTP_PROXY = 'http://foo-bar.baz:123'
      process.env.NO_PROXY = '<-loopback>'
      const options = toObject([])

      expect(options.proxySource).to.be.undefined
      expect(options.proxyServer).to.eq(process.env.HTTP_PROXY)
      expect(options.proxyBypassList).to.eq('<-loopback>')

      expect(options.proxyBypassList).to.eq(process.env.NO_PROXY)
    })

    it('copies lowercase proxy vars to uppercase', function () {
      process.env.http_proxy = 'http://foo-bar.baz:123'
      process.env.https_proxy = 'https://foo-bar.baz:123'
      process.env.no_proxy = 'http://no-proxy.holla'
      expect(process.env.HTTP_PROXY).to.be.undefined
      expect(process.env.HTTPS_PROXY).to.be.undefined
      expect(process.env.NO_PROXY).to.be.undefined

      const options = toObject([])

      expect(process.env.HTTP_PROXY).to.eq('http://foo-bar.baz:123')
      expect(process.env.HTTPS_PROXY).to.eq('https://foo-bar.baz:123')
      expect(process.env.NO_PROXY).to.eq('http://no-proxy.holla,127.0.0.1,::1,localhost')
      expect(options.proxySource).to.be.undefined
      expect(options.proxyServer).to.eq(process.env.HTTP_PROXY)

      expect(options.proxyBypassList).to.eq(process.env.NO_PROXY)
    })

    it('can use npm_config_proxy', function () {
      process.env.npm_config_proxy = 'http://foo-bar.baz:123'
      expect(process.env.HTTP_PROXY).to.be.undefined

      const options = toObject([])

      expect(process.env.HTTP_PROXY).to.eq('http://foo-bar.baz:123')
      expect(process.env.HTTPS_PROXY).to.eq('http://foo-bar.baz:123')
      expect(process.env.NO_PROXY).to.eq('127.0.0.1,::1,localhost')
      expect(options.proxySource).to.be.undefined
      expect(options.proxyServer).to.eq(process.env.HTTP_PROXY)

      expect(options.proxyBypassList).to.eq(process.env.NO_PROXY)
    })

    it('can override npm_config_proxy with falsy HTTP_PROXY', function () {
      process.env.npm_config_proxy = 'http://foo-bar.baz:123'
      process.env.HTTP_PROXY = ''

      const options = toObject([])

      expect(process.env.HTTP_PROXY).to.be.undefined
      expect(process.env.HTTPS_PROXY).to.be.undefined
      expect(process.env.NO_PROXY).to.eq('127.0.0.1,::1,localhost')
      expect(options.proxySource).to.be.undefined
      expect(options.proxyServer).to.eq(process.env.HTTP_PROXY)

      expect(options.proxyBypassList).to.be.undefined
    })
  })
})
