const t = require('tap')
const { load: loadMockNpm } = require('../fixtures/mock-npm')
const BaseCommand = require('../../lib/base-cmd.js')
const Definition = require('@npmcli/config/lib/definitions/definition.js')

t.test('flags() method with command definitions', async t => {
  const { npm } = await loadMockNpm(t, {
    config: {
      mountain: 'kilimanjaro',
    },
  })

  class TestCommand extends BaseCommand {
    static name = 'test-command'
    static description = 'Test command'
    static params = ['mountain']

    static definitions = [
      new Definition('mountain', {
        type: String,
        default: 'everest',
        description: 'Your favorite mountain',
        usage: '--mountain=<mountain>',
      }),
    ]

    async exec () {
      return this.flags()
    }
  }

  const command = new TestCommand(npm)
  const [flags] = await command.exec()

  t.ok(flags, 'flags() returns an object')
  t.equal(flags.mountain, 'kilimanjaro', 'includes config value when set')
})

t.test('flags() method with default values', async t => {
  const { npm } = await loadMockNpm(t)

  class TestCommand extends BaseCommand {
    static name = 'test-command'
    static description = 'Test command'
    static params = ['mountain']

    static definitions = [
      new Definition('mountain', {
        type: String,
        default: 'everest',
        description: 'Your favorite mountain',
        usage: '--mountain=<mountain>',
      }),
    ]

    async exec () {
      return this.flags()
    }
  }

  const command = new TestCommand(npm)
  const [flags] = await command.exec()

  t.equal(flags.mountain, 'everest', 'uses default value when not set')
})

t.test('flags() method filters unknown options', async t => {
  const { npm } = await loadMockNpm(t, {
    // npm.config.argv would have both known and unknown flags parsed
    config: {
      mountain: 'denali',
    },
  })

  class TestCommand extends BaseCommand {
    static name = 'test-command'
    static description = 'Test command'
    static params = ['mountain']

    static definitions = [
      new Definition('mountain', {
        type: String,
        default: 'everest',
        description: 'Your favorite mountain',
        usage: '--mountain=<mountain>',
      }),
    ]

    async exec () {
      return this.flags()
    }
  }

  const command = new TestCommand(npm)
  const [flags] = await command.exec()

  t.equal(flags.mountain, 'denali', 'includes known flag')
  t.notOk(flags.bug, 'filters out unknown flags')
  t.same(Object.keys(flags), ['mountain'], 'only includes defined keys')
})

t.test('flags() method with no definitions', async t => {
  const { npm } = await loadMockNpm(t)

  class TestCommand extends BaseCommand {
    static name = 'test-command'
    static description = 'Test command'

    async exec () {
      return this.flags()
    }
  }

  const command = new TestCommand(npm)
  const [flags] = await command.exec()

  t.same(flags, {}, 'returns empty object when no definitions')
})

t.test('flags() throws error for unknown flags', async t => {
  const { npm } = await loadMockNpm(t, {
    npm: { argv: ['test-command', '--unknown-flag'] },
  })

  class TestCommand extends BaseCommand {
    static name = 'test-command'
    static description = 'Test command'
    static params = ['mountain']

    static definitions = [
      new Definition('mountain', {
        type: String,
        default: 'everest',
        description: 'Your favorite mountain',
        usage: '--mountain=<mountain>',
      }),
    ]

    async exec () {
      return this.flags()
    }
  }

  const command = new TestCommand(npm)
  await t.rejects(
    command.exec(),
    {
      code: 'EUNKNOWNCONFIG',
      message: /Unknown cli config:[\s\S]*--unknown-flag/,
    },
    'throws EUNKNOWNCONFIG for unknown flag'
  )
})

t.test('flags() maps alias to main key', async t => {
  const { npm } = await loadMockNpm(t)

  class TestCommand extends BaseCommand {
    static name = 'test-command'
    static description = 'Test command'
    static params = ['mountain']

    static definitions = [
      new Definition('mountain', {
        type: String,
        default: 'everest',
        description: 'Your favorite mountain',
        usage: '--mountain=<mountain>',
        alias: ['peak'],
      }),
    ]

    async exec () {
      return this.flags()
    }
  }

  // Use the alias --peak instead of --mountain
  npm.config.argv = ['node', 'npm', 'test-command', '--peak=denali']

  const command = new TestCommand(npm)
  const [flags] = await command.exec()

  t.equal(flags.mountain, 'denali', 'alias value is mapped to main key')
  t.notOk('peak' in flags, 'alias key is not present in flags')
})

t.test('flags() throws error when both main key and alias are provided', async t => {
  const { npm } = await loadMockNpm(t)

  class TestCommand extends BaseCommand {
    static name = 'test-command'
    static description = 'Test command'
    static params = ['mountain']

    static definitions = [
      new Definition('mountain', {
        type: String,
        default: 'everest',
        description: 'Your favorite mountain',
        usage: '--mountain=<mountain>',
        alias: ['peak'],
      }),
    ]

    async exec () {
      return this.flags()
    }
  }

  // Provide both --mountain and --peak (its alias)
  npm.config.argv = ['node', 'npm', 'test-command', '--mountain=everest', '--peak=denali']

  const command = new TestCommand(npm)
  await t.rejects(
    command.exec(),
    { message: /Please provide only one of --mountain or --peak/ },
    'throws error when main key and alias are both provided'
  )
})

t.test('getUsage() with no params and no definitions', async t => {
  class TestCommand extends BaseCommand {
    static name = 'test-command'
    static description = 'Test command description'
    static usage = ['']
  }

  const usage = TestCommand.describeUsage

  t.ok(usage.includes('Test command description'), 'includes description')
  t.ok(usage.includes('npm test-command'), 'includes usage line')
  t.notOk(usage.includes('Options:'), 'does not include Options section')
})

t.test('getUsage() with both params and definitions', async t => {
  class TestCommand extends BaseCommand {
    static name = 'test-command'
    static description = 'Test command description'
    static params = ['mountain', 'river']

    static definitions = [
      new Definition('mountain', {
        type: String,
        default: 'everest',
        description: 'Your favorite mountain',
        usage: '--mountain=<mountain>',
      }),
      new Definition('river', {
        type: String,
        default: 'nile',
        description: 'Your favorite river',
        usage: '--river=<river>',
      }),
    ]
  }

  const usage = TestCommand.describeUsage

  t.ok(usage.includes('Test command description'), 'includes description')
  t.ok(usage.includes('Options:'), 'includes Options section')
  t.ok(usage.includes('--mountain'), 'includes mountain flag')
  t.ok(usage.includes('--river'), 'includes river flag')
})

t.test('getUsage() with subcommand without description', async t => {
  class SubCommandWithDesc extends BaseCommand {
    static name = 'with-desc'
    static description = 'Subcommand with description'
  }

  class SubCommandNoDesc extends BaseCommand {
    static name = 'no-desc'
    // No description
  }

  class TestCommand extends BaseCommand {
    static name = 'test-command'
    static description = 'Test command description'
    static subcommands = {
      'with-desc': SubCommandWithDesc,
      'no-desc': SubCommandNoDesc,
    }
  }

  const usage = TestCommand.describeUsage

  t.ok(usage.includes('Subcommands:'), 'includes Subcommands section')
  t.ok(usage.includes('with-desc'), 'includes subcommand with description')
  t.ok(usage.includes('Subcommand with description'), 'includes the description text')
  t.ok(usage.includes('no-desc'), 'includes subcommand without description')
})

t.test('getUsage() with definition without description', async t => {
  class TestCommand extends BaseCommand {
    static name = 'test-command'
    static description = 'Test command description'
    static params = ['mountain', 'river']

    static definitions = [
      new Definition('mountain', {
        type: String,
        default: 'everest',
        description: 'Your favorite mountain',
        usage: '--mountain=<mountain>',
      }),
      new Definition('river', {
        type: String,
        default: 'nile',
        description: '', // Empty description
        usage: '--river=<river>',
      }),
    ]
  }

  const usage = TestCommand.describeUsage

  t.ok(usage.includes('Options:'), 'includes Options section')
  t.ok(usage.includes('--mountain'), 'includes mountain flag in options')
  t.ok(usage.includes('Your favorite mountain'), 'includes mountain description')
  t.ok(usage.includes('[--river=<river>]'), 'includes river in usage line')
  t.notOk(usage.includes('  --river'), 'does not include river flag description section')
})

t.test('flags() handles definition with multiple aliases', async t => {
  const { npm } = await loadMockNpm(t)

  class TestCommand extends BaseCommand {
    static name = 'test-command'
    static description = 'Test command'

    static definitions = [
      new Definition('mountain', {
        type: String,
        default: 'everest',
        description: 'Your favorite mountain',
        usage: '--mountain=<mountain>',
        alias: ['peak', 'summit'], // Multiple aliases
      }),
    ]

    async exec () {
      return this.flags()
    }
  }

  // Use the second alias --summit
  npm.config.argv = ['node', 'npm', 'test-command', '--summit=denali']

  const command = new TestCommand(npm)
  const [flags] = await command.exec()

  t.equal(flags.mountain, 'denali', 'second alias value is mapped to main key')
  t.notOk('summit' in flags, 'alias key is not present in flags')
  t.notOk('peak' in flags, 'other alias key is not present in flags')
})

t.test('flags() handles definition with short as array', async t => {
  const { npm } = await loadMockNpm(t)

  class TestCommand extends BaseCommand {
    static name = 'test-command'
    static description = 'Test command'

    static definitions = [
      new Definition('mountain', {
        type: String,
        default: 'everest',
        description: 'Your favorite mountain',
        usage: '--mountain=<mountain>',
        short: ['m', 'M'], // Short as array
      }),
    ]

    async exec () {
      return this.flags()
    }
  }

  // Use the short flag -m
  npm.config.argv = ['node', 'npm', 'test-command', '-m', 'denali']

  const command = new TestCommand(npm)
  const [flags] = await command.exec()

  t.equal(flags.mountain, 'denali', 'short flag value is parsed correctly')
})

t.test('flags() returns defaults when argv is empty', async t => {
  const { npm } = await loadMockNpm(t)

  class TestCommand extends BaseCommand {
    static name = 'test-command'
    static description = 'Test command'

    static definitions = [
      new Definition('mountain', {
        type: String,
        default: 'everest',
        description: 'Your favorite mountain',
        usage: '--mountain=<mountain>',
      }),
    ]

    async exec () {
      return this.flags()
    }
  }

  // Set argv to empty array
  npm.config.argv = []

  const command = new TestCommand(npm)
  const [flags, remains] = await command.exec()

  t.equal(flags.mountain, 'everest', 'returns default value when argv is empty')
  t.same(remains, [], 'remains is empty array')
})

t.test('flags() throws error for multiple unknown flags with pluralization', async t => {
  const { npm } = await loadMockNpm(t, {
    npm: { argv: ['test-command', '--unknown-one', '--unknown-two'] },
  })

  class TestCommand extends BaseCommand {
    static name = 'test-command'
    static description = 'Test command'

    static definitions = [
      new Definition('mountain', {
        type: String,
        default: 'everest',
        description: 'Your favorite mountain',
        usage: '--mountain=<mountain>',
      }),
    ]

    async exec () {
      return this.flags()
    }
  }

  // Provide multiple unknown flags
  npm.config.argv = ['node', 'npm', 'test-command', '--unknown-one', '--unknown-two']

  const command = new TestCommand(npm)
  await t.rejects(
    command.exec(),
    {
      code: 'EUNKNOWNCONFIG',
      message: /Unknown cli configs:[\s\S]*--unknown-one[\s\S]*--unknown-two/,
    },
    'throws EUNKNOWNCONFIG with pluralized "configs" for multiple unknown flags'
  )
})

t.test('base exec() method returns undefined', async t => {
  const { npm } = await loadMockNpm(t)

  class TestCommand extends BaseCommand {
    static name = 'test-command'
    static description = 'Test command'
    // Intentionally not overriding exec() to test the base implementation
  }

  const command = new TestCommand(npm)
  const result = await command.exec()

  t.equal(result, undefined, 'base exec() returns undefined')
})

t.test('flags() removes unknown positional warning when value is consumed by command definition', async t => {
  // Pass raw argv to loadMockNpm so warnings are generated during config.load()
  // The global config sees --id as unknown (boolean), so "abc123" becomes a positional
  // and queues a warning. But command-specific definitions should consume it.
  const { npm, logs } = await loadMockNpm(t, {
    argv: ['--id', 'abc123'],
  })

  class TestCommand extends BaseCommand {
    static name = 'test-command'
    static description = 'Test command'
    static params = ['id']

    static definitions = [
      new Definition('id', {
        type: String,
        default: null,
        description: 'An identifier',
        usage: '--id=<id>',
      }),
    ]

    async exec () {
      return this.flags()
    }
  }

  // Set up argv for command execution (mock-npm prepends the command)
  npm.config.argv = ['node', 'npm', 'test-command', '--id', 'abc123']

  const command = new TestCommand(npm)
  const [flags, remains] = await command.exec()

  // The flag should be properly parsed
  t.equal(flags.id, 'abc123', 'id flag is properly parsed')
  t.same(remains, [], 'no remaining positionals')

  // Check that no warning about "abc123" being parsed as positional was logged
  const warningLogs = logs.warn
  const positionalWarnings = warningLogs.filter(msg =>
    msg.includes('abc123') && msg.includes('parsed as a normal command line argument')
  )
  t.equal(positionalWarnings.length, 0, 'no warning about abc123 being a positional')
})

t.test('flags() keeps unknown positional warning when multiple values follow unknown flag', async t => {
  // Pass raw argv to loadMockNpm so warnings are generated during config.load()
  // Both "abc123" and "def456" are seen as positionals by global config because --id is unknown
  // nopt only warns about "abc123" (the immediate next value after unknown flag)
  // Command definition consumes "abc123" for --id, "def456" remains as true positional
  const { npm, logs } = await loadMockNpm(t, {
    argv: ['--id', 'abc123', 'def456'],
  })

  class TestCommand extends BaseCommand {
    static name = 'test-command'
    static description = 'Test command'
    static params = ['id']

    static definitions = [
      new Definition('id', {
        type: String,
        default: null,
        description: 'An identifier',
        usage: '--id=<id>',
      }),
    ]

    async exec () {
      return this.flags()
    }
  }

  // Set up argv for command execution
  npm.config.argv = ['node', 'npm', 'test-command', '--id', 'abc123', 'def456']

  const command = new TestCommand(npm)
  const [flags, remains] = await command.exec()

  // The flag should be properly parsed
  t.equal(flags.id, 'abc123', 'id flag is properly parsed')
  t.same(remains, ['def456'], 'def456 remains as positional')

  // Check that warning about "abc123" was removed (consumed by --id)
  const warningLogs = logs.warn
  const abc123Warnings = warningLogs.filter(msg =>
    msg.includes('abc123') && msg.includes('parsed as a normal command line argument')
  )
  t.equal(abc123Warnings.length, 0, 'no warning about abc123 (consumed by --id)')
})

t.test('flags() does not remove unknown positional warning when value is in remains', async t => {
  // This tests the else branch where remainsSet.has(unknownPos) is true
  // When a value is a true positional (in remains), we should NOT remove its warning
  // The warning should be logged (not suppressed)
  const { npm, logs } = await loadMockNpm(t, {
    argv: ['truepositional'],
  })

  class TestCommand extends BaseCommand {
    static name = 'test-command'
    static description = 'Test command'
    static params = ['id']

    static definitions = [
      new Definition('id', {
        type: String,
        default: null,
        description: 'An identifier',
        usage: '--id=<id>',
      }),
    ]

    async exec () {
      return this.flags()
    }
  }

  // Manually queue a warning for a value that will be in remains
  npm.config.queueWarning('unknown:truepositional', 'config', 'truepositional was parsed as positional')

  // Set up argv for command execution with the value as a true positional
  npm.config.argv = ['node', 'npm', 'test-command', 'truepositional']

  const command = new TestCommand(npm)
  const [flags, remains] = await command.exec()

  // The positional should remain
  t.same(remains, ['truepositional'], 'truepositional is in remains')
  t.equal(flags.id, null, 'id flag uses default')

  // Check that the warning WAS logged (not removed before logWarnings())
  // Because the value is in remains, it's a true positional and should warn
  const warningLogs = logs.warn
  const positionalWarnings = warningLogs.filter(msg =>
    msg.includes('truepositional') && msg.includes('parsed as positional')
  )
  t.equal(positionalWarnings.length, 1, 'warning for truepositional was logged')
})

t.test('flags() throws error for extra positional arguments beyond expected count', async t => {
  // When a command specifies static positionals = N, extra positionals should throw an error
  const { npm } = await loadMockNpm(t, {
    argv: ['pkg1', 'extra1', 'extra2'],
  })

  class TestCommand extends BaseCommand {
    static name = 'test-command'
    static description = 'Test command'
    static positionals = 1 // expects only 1 positional
    static params = ['id']

    static definitions = [
      new Definition('id', {
        type: String,
        default: null,
        description: 'An identifier',
        usage: '--id=<id>',
      }),
    ]

    async exec () {
      return this.flags()
    }
  }

  // Set up argv for command execution with multiple positionals
  npm.config.argv = ['node', 'npm', 'test-command', 'pkg1', 'extra1', 'extra2']

  const command = new TestCommand(npm)

  // Should throw error for extra positional
  await t.rejects(
    command.exec(),
    { message: 'Unknown positional arguments: extra1, extra2' },
    'throws error for extra positionals'
  )
})

t.test('flags() does not throw when positionals is null (unlimited)', async t => {
  // When static positionals is null, any number of positionals is allowed without error
  const { npm } = await loadMockNpm(t, {
    argv: ['pkg1', 'extra1', 'extra2'],
  })

  class TestCommand extends BaseCommand {
    static name = 'test-command'
    static description = 'Test command'
    static positionals = null // unlimited/unchecked
    static params = ['id']

    static definitions = [
      new Definition('id', {
        type: String,
        default: null,
        description: 'An identifier',
        usage: '--id=<id>',
      }),
    ]

    async exec () {
      return this.flags()
    }
  }

  // Set up argv for command execution with multiple positionals
  npm.config.argv = ['node', 'npm', 'test-command', 'pkg1', 'extra1', 'extra2']

  const command = new TestCommand(npm)
  const [flags, remains] = await command.exec()

  // All positionals should remain - no error thrown
  t.same(remains, ['pkg1', 'extra1', 'extra2'], 'all positionals are in remains')
  t.equal(flags.id, null, 'id flag uses default')
})

t.test('validateCli throws aggregated error for unknown file configs', async t => {
  const { npm } = await loadMockNpm(t, {
    homeDir: {
      '.npmrc': [
        'bogus-user-key=yes',
        '@scope:bogus-scoped=no',
      ].join('\n'),
    },
  })

  class TestCommand extends BaseCommand {
    static name = 'test-command'
    static description = 'Test command'
  }

  const command = new TestCommand(npm)
  await t.rejects(
    (async () => command.validateCli())(),
    {
      code: 'EUNKNOWNCONFIG',
      message: /Unknown npm configuration keys:[\s\S]*bogus-user-key[\s\S]*bogus-scoped[\s\S]*npm help npmrc/,
    },
    'throws EUNKNOWNCONFIG aggregating plain and scoped keys'
  )
})

t.test('validateCli uses singular "key" when only one unknown file config', async t => {
  const { npm } = await loadMockNpm(t, {
    homeDir: {
      '.npmrc': 'only-one-bad-key=yes',
    },
  })

  class TestCommand extends BaseCommand {
    static name = 'test-command'
    static description = 'Test command'
  }

  const command = new TestCommand(npm)
  await t.rejects(
    (async () => command.validateCli())(),
    {
      code: 'EUNKNOWNCONFIG',
      message: /Unknown npm configuration key:\n/,
    },
    'singular phrasing when only one unknown key'
  )
})

t.test('validateCli bypasses checks when skipConfigValidation is set', async t => {
  const { npm } = await loadMockNpm(t, {
    homeDir: {
      '.npmrc': 'bogus-user-key=yes',
    },
  })

  class TestCommand extends BaseCommand {
    static name = 'test-command'
    static description = 'Test command'
    static skipConfigValidation = true
  }

  const command = new TestCommand(npm)
  t.doesNotThrow(() => command.validateCli(), 'skipConfigValidation bypasses unknown-file-config check')
})

t.test('validateCli combines cli and file unknowns into a single error', async t => {
  const { npm } = await loadMockNpm(t, {
    homeDir: {
      '.npmrc': 'bogus-user-key=yes',
    },
    npm: { argv: ['test-command', '--unknown-cli'] },
  })

  class TestCommand extends BaseCommand {
    static name = 'test-command'
    static description = 'Test command'
    async exec () {
      return this.flags()
    }
  }

  const command = new TestCommand(npm)
  let err
  try {
    command.validateCli()
    t.fail('expected throw')
  } catch (e) {
    err = e
  }
  t.equal(err.code, 'EUNKNOWNCONFIG')
  t.match(err.message, /Unknown cli config:[\s\S]*--unknown-cli/,
    'message contains the cli section')
  t.match(err.message, /Unknown npm configuration key:[\s\S]*bogus-user-key/,
    'message contains the file section')
  t.match(err.unknownConfigs, [
    { where: 'cli', key: 'unknown-cli' },
    { where: 'user', key: 'bogus-user-key' },
  ], 'structured payload contains both cli and file entries')
})

t.test('validateCli does not throw on unknown env (npm_config_*) configs', async t => {
  // Env unknowns warn but never error — they are excluded from getUnknownConfigs() by default.
  // This test locks in the carve-out at the validateCli boundary so the env→warn behavior cannot silently flip if the filter changes.
  const { npm } = await loadMockNpm(t, {
    globals: {
      'process.env.npm_config_bogus_env': 'yes',
    },
  })

  class TestCommand extends BaseCommand {
    static name = 'test-command'
    static description = 'Test command'
  }

  const command = new TestCommand(npm)
  t.doesNotThrow(() => command.validateCli(),
    'unknown env-set config does not throw at validateCli')
  // sanity: the unknown was in fact seen by Config, just filtered from getUnknownConfigs()
  t.ok(
    npm.config.getUnknownConfigs('env').some(u => u.key === 'bogus-env'),
    'env unknown is still tracked internally'
  )
})

t.test('flags() throws with scoped (nerfdart) display for unknown cli config', async t => {
  const { npm } = await loadMockNpm(t, {
    argv: ['--@scope:bogus=yes'],
  })

  class TestCommand extends BaseCommand {
    static name = 'test-command'
    static description = 'Test command'

    async exec () {
      return this.flags()
    }
  }

  npm.config.argv = ['node', 'npm', 'test-command', '--@scope:bogus=yes']
  const command = new TestCommand(npm)
  await t.rejects(
    command.exec(),
    {
      code: 'EUNKNOWNCONFIG',
      message: /Unknown cli config:[\s\S]*--bogus \(@scope:bogus\)/,
    },
    'scoped display form uses baseKey (key) layout, throws EUNKNOWNCONFIG'
  )
})

t.test('flags() throws singular "argument" for a single extra positional', async t => {
  const { npm } = await loadMockNpm(t, {
    argv: ['pkg1', 'extra1'],
  })

  class TestCommand extends BaseCommand {
    static name = 'test-command'
    static description = 'Test command'
    static positionals = 1

    async exec () {
      return this.flags()
    }
  }

  npm.config.argv = ['node', 'npm', 'test-command', 'pkg1', 'extra1']
  const command = new TestCommand(npm)
  await t.rejects(
    command.exec(),
    { message: 'Unknown positional argument: extra1' },
    'singular phrasing when only one extra positional'
  )
})
