/**
 * Copyright (c) HashiCorp, Inc.
 * SPDX-License-Identifier: BUSL-1.1
 */

import { module, test } from 'qunit';
import {
  parseCommand,
  logFromResponse,
  logFromError,
  formattedErrorFromInput,
  extractFlagsFromStrings,
  extractDataFromStrings,
} from 'vault/lib/console-helpers';

module('Unit | Lib | console helpers', function () {
  const testCommands = [
    {
      name: 'write with data',
      command: `vault write aws/config/root \
      access_key=AKIAJWVN5Z4FOFT7NLNA \
      secret_key=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 \
      region=us-east-1`,
      expected: {
        method: 'write',
        flagArray: [],
        path: 'aws/config/root',
        dataArray: [
          'access_key=AKIAJWVN5Z4FOFT7NLNA',
          'secret_key=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',
          'region=us-east-1',
        ],
      },
    },
    {
      name: 'write with space in a value',
      command: `vault write \
      auth/ldap/config \
      url=ldap://ldap.example.com:3268 \
      binddn="CN=ServiceViewDev,OU=Service Accounts,DC=example,DC=com" \
      bindpass="xxxxxxxxxxxxxxxxxxxxxxxxxx" \
      userdn="DC=example,DC=com" \
      groupdn="DC=example,DC=com" \
      insecure_tls=true \
      starttls=false
      `,
      expected: {
        method: 'write',
        flagArray: [],
        path: 'auth/ldap/config',
        dataArray: [
          'url=ldap://ldap.example.com:3268',
          'binddn=CN=ServiceViewDev,OU=Service Accounts,DC=example,DC=com',
          'bindpass=xxxxxxxxxxxxxxxxxxxxxxxxxx',
          'userdn=DC=example,DC=com',
          'groupdn=DC=example,DC=com',
          'insecure_tls=true',
          'starttls=false',
        ],
      },
    },
    {
      name: 'write with double quotes',
      command: `vault write \
      auth/token/create \
      policies="foo"
      `,
      expected: { method: 'write', flagArray: [], path: 'auth/token/create', dataArray: ['policies=foo'] },
    },
    {
      name: 'write with single quotes',
      command: `vault write \
      auth/token/create \
      policies='foo'
      `,
      expected: { method: 'write', flagArray: [], path: 'auth/token/create', dataArray: ['policies=foo'] },
    },
    {
      name: 'write with unmatched quotes',
      command: `vault write \
      auth/token/create \
      policies="'foo"
      `,
      expected: { method: 'write', flagArray: [], path: 'auth/token/create', dataArray: ["policies='foo"] },
    },
    {
      name: 'write with shell characters',
      /* eslint-disable no-useless-escape */
      command: `vault write  database/roles/api-prod db_name=apiprod creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" default_ttl=1h max_ttl=24h
      `,
      expected: {
        method: 'write',
        flagArray: [],
        path: 'database/roles/api-prod',
        dataArray: [
          'db_name=apiprod',
          `creation_statements=CREATE ROLE {{name}} WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO {{name}};`,
          'default_ttl=1h',
          'max_ttl=24h',
        ],
      },
    },

    {
      name: 'read with field',
      command: `vault read -field=access_key aws/creds/my-role`,
      expected: {
        method: 'read',
        flagArray: ['-field=access_key'],
        path: 'aws/creds/my-role',
        dataArray: [],
      },
    },
  ];

  testCommands.forEach(function (testCase) {
    test(`#parseCommand: ${testCase.name}`, function (assert) {
      const result = parseCommand(testCase.command);
      assert.deepEqual(result, testCase.expected);
    });
  });

  test('#parseCommand: invalid commands', function (assert) {
    assert.expect(1);
    const command = 'vault kv get foo';
    assert.throws(
      () => {
        parseCommand(command);
      },
      /invalid command/,
      'throws on invalid command'
    );
  });

  const testExtractCases = [
    {
      method: 'read',
      name: 'data fields',
      dataInput: [
        'access_key=AKIAJWVN5Z4FOFT7NLNA',
        'secret_key=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',
        'region=us-east-1',
      ],
      flagInput: [],
      expected: {
        data: {
          access_key: 'AKIAJWVN5Z4FOFT7NLNA',
          secret_key: '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',
          region: 'us-east-1',
        },
        flags: {},
      },
    },
    {
      method: 'read',
      name: 'repeated data and a flag',
      dataInput: ['allowed_domains=example.com', 'allowed_domains=foo.example.com'],
      flagInput: ['-wrap-ttl=2h'],
      expected: {
        data: {
          allowed_domains: ['example.com', 'foo.example.com'],
        },
        flags: {
          wrapTTL: '2h',
        },
      },
    },
    {
      method: 'read',
      name: 'triple data',
      dataInput: [
        'allowed_domains=example.com',
        'allowed_domains=foo.example.com',
        'allowed_domains=dev.example.com',
      ],
      flagInput: [],
      expected: {
        data: {
          allowed_domains: ['example.com', 'foo.example.com', 'dev.example.com'],
        },
        flags: {},
      },
    },
    {
      method: 'read',
      name: 'data with more than one equals sign',
      dataInput: ['foo=bar=baz', 'foo=baz=bop', 'some=value=val'],
      flagInput: [],
      expected: {
        data: {
          foo: ['bar=baz', 'baz=bop'],
          some: 'value=val',
        },
        flags: {},
      },
    },
    {
      method: 'read',
      name: 'data with empty values',
      dataInput: [`foo=`, 'some=thing'],
      flagInput: [],
      expected: {
        data: {
          foo: '',
          some: 'thing',
        },
        flags: {},
      },
    },
    {
      method: 'write',
      name: 'write with force flag',
      dataInput: [],
      flagInput: ['-force'],
      expected: {
        data: {},
        flags: {
          force: true,
        },
      },
    },
    {
      method: 'write',
      name: 'write with force short flag',
      dataInput: [],
      flagInput: ['-f'],
      expected: {
        data: {},
        flags: {
          force: true,
        },
      },
    },
    {
      method: 'write',
      name: 'write with GNU style force flag',
      dataInput: [],
      flagInput: ['--force'],
      expected: {
        data: {},
        flags: {
          force: true,
        },
      },
    },
  ];

  testExtractCases.forEach(function (testCase) {
    test(`#extractDataFromStrings: ${testCase.name}`, function (assert) {
      const data = extractDataFromStrings(testCase.dataInput);
      assert.deepEqual(data, testCase.expected.data, 'has expected data');
    });
    test(`#extractFlagsFromStrings: ${testCase.name}`, function (assert) {
      const flags = extractFlagsFromStrings(testCase.flagInput, testCase.method);
      assert.deepEqual(flags, testCase.expected.flags, 'has expected flags');
    });
  });

  const testResponseCases = [
    {
      name: 'write response, no content',
      args: [null, 'foo/bar', 'write', {}],
      expectedData: {
        type: 'success',
        content: 'Success! Data written to: foo/bar',
      },
    },
    {
      name: 'delete response, no content',
      args: [null, 'foo/bar', 'delete', {}],
      expectedData: {
        type: 'success',
        content: 'Success! Data deleted (if it existed) at: foo/bar',
      },
    },
    {
      name: 'read, no data, auth, wrap_info',
      args: [{ foo: 'bar', one: 'two' }, 'foo/bar', 'read', {}],
      expectedData: {
        type: 'object',
        content: { foo: 'bar', one: 'two' },
      },
    },
    {
      name: 'read with -format=json flag, no data, auth, wrap_info',
      args: [{ foo: 'bar', one: 'two' }, 'foo/bar', 'read', { format: 'json' }],
      expectedData: {
        type: 'json',
        content: { foo: 'bar', one: 'two' },
      },
    },
    {
      name: 'read with -field flag, no data, auth, wrap_info',
      args: [{ foo: 'bar', one: 'two' }, 'foo/bar', 'read', { field: 'one' }],
      expectedData: {
        type: 'text',
        content: 'two',
      },
    },
    {
      name: 'write, with content',
      args: [{ data: { one: 'two' } }, 'foo/bar', 'write', {}],
      expectedData: {
        type: 'object',
        content: { one: 'two' },
      },
    },
    {
      name: 'with wrap-ttl flag',
      args: [{ wrap_info: { one: 'two' } }, 'foo/bar', 'read', { wrapTTL: '1h' }],
      expectedData: {
        type: 'object',
        content: { one: 'two' },
      },
    },
    {
      name: 'with -format=json flag and wrap-ttl flag',
      args: [{ foo: 'bar', wrap_info: { one: 'two' } }, 'foo/bar', 'read', { format: 'json', wrapTTL: '1h' }],
      expectedData: {
        type: 'json',
        content: { foo: 'bar', wrap_info: { one: 'two' } },
      },
    },
    {
      name: 'with -format=json and -field flags',
      args: [{ foo: 'bar', data: { one: 'two' } }, 'foo/bar', 'read', { format: 'json', field: 'one' }],
      expectedData: {
        type: 'json',
        content: 'two',
      },
    },
    {
      name: 'with -format=json and -field, and -wrap-ttl flags',
      args: [
        { foo: 'bar', wrap_info: { one: 'two' } },
        'foo/bar',
        'read',
        { format: 'json', wrapTTL: '1h', field: 'one' },
      ],
      expectedData: {
        type: 'json',
        content: 'two',
      },
    },
    {
      name: 'with string field flag and wrap-ttl flag',
      args: [{ foo: 'bar', wrap_info: { one: 'two' } }, 'foo/bar', 'read', { field: 'one', wrapTTL: '1h' }],
      expectedData: {
        type: 'text',
        content: 'two',
      },
    },
    {
      name: 'with object field flag and wrap-ttl flag',
      args: [
        { foo: 'bar', wrap_info: { one: { two: 'three' } } },
        'foo/bar',
        'read',
        { field: 'one', wrapTTL: '1h' },
      ],
      expectedData: {
        type: 'object',
        content: { two: 'three' },
      },
    },
    {
      name: 'with response data and string field flag',
      args: [{ foo: 'bar', data: { one: 'two' } }, 'foo/bar', 'read', { field: 'one', wrapTTL: '1h' }],
      expectedData: {
        type: 'text',
        content: 'two',
      },
    },
    {
      name: 'with response data and object field flag ',
      args: [
        { foo: 'bar', data: { one: { two: 'three' } } },
        'foo/bar',
        'read',
        { field: 'one', wrapTTL: '1h' },
      ],
      expectedData: {
        type: 'object',
        content: { two: 'three' },
      },
    },
    {
      name: 'response with data',
      args: [{ foo: 'bar', data: { one: 'two' } }, 'foo/bar', 'read', {}],
      expectedData: {
        type: 'object',
        content: { one: 'two' },
      },
    },
    {
      name: 'with response data, field flag, and field missing',
      args: [{ foo: 'bar', data: { one: 'two' } }, 'foo/bar', 'read', { field: 'foo' }],
      expectedData: {
        type: 'error',
        content: 'Field "foo" not present in secret',
      },
    },
    {
      name: 'with response data and auth block',
      args: [{ data: { one: 'two' }, auth: { three: 'four' } }, 'auth/token/create', 'write', {}],
      expectedData: {
        type: 'object',
        content: { three: 'four' },
      },
    },
    {
      name: 'with -field and -format with an object field',
      args: [{ data: { one: { three: 'two' } } }, 'sys/mounts', 'read', { field: 'one', format: 'json' }],
      expectedData: {
        type: 'json',
        content: { three: 'two' },
      },
    },
    {
      name: 'with -field and -format with a string field',
      args: [{ data: { one: 'two' } }, 'sys/mounts', 'read', { field: 'one', format: 'json' }],
      expectedData: {
        type: 'json',
        content: 'two',
      },
    },
  ];

  testResponseCases.forEach(function (testCase) {
    test(`#logFromResponse: ${testCase.name}`, function (assert) {
      const data = logFromResponse(...testCase.args);
      assert.deepEqual(data, testCase.expectedData);
    });
  });

  const testErrorCases = [
    {
      name: 'AdapterError write',
      args: [{ httpStatus: 404, path: 'v1/sys/foo', errors: [{}] }, 'sys/foo', 'write'],
      expectedContent: 'Error writing to: sys/foo.\nURL: v1/sys/foo\nCode: 404',
    },
    {
      name: 'AdapterError read',
      args: [{ httpStatus: 404, path: 'v1/sys/foo', errors: [{}] }, 'sys/foo', 'read'],
      expectedContent: 'Error reading from: sys/foo.\nURL: v1/sys/foo\nCode: 404',
    },
    {
      name: 'AdapterError list',
      args: [{ httpStatus: 404, path: 'v1/sys/foo', errors: [{}] }, 'sys/foo', 'list'],
      expectedContent: 'Error listing: sys/foo.\nURL: v1/sys/foo\nCode: 404',
    },
    {
      name: 'AdapterError delete',
      args: [{ httpStatus: 404, path: 'v1/sys/foo', errors: [{}] }, 'sys/foo', 'delete'],
      expectedContent: 'Error deleting at: sys/foo.\nURL: v1/sys/foo\nCode: 404',
    },
    {
      name: 'VaultError single error',
      args: [{ httpStatus: 404, path: 'v1/sys/foo', errors: ['no client token'] }, 'sys/foo', 'delete'],
      expectedContent: 'Error deleting at: sys/foo.\nURL: v1/sys/foo\nCode: 404\nErrors:\n  no client token',
    },
    {
      name: 'VaultErrors multiple errors',
      args: [
        { httpStatus: 404, path: 'v1/sys/foo', errors: ['no client token', 'this is an error'] },
        'sys/foo',
        'delete',
      ],
      expectedContent:
        'Error deleting at: sys/foo.\nURL: v1/sys/foo\nCode: 404\nErrors:\n  no client token\n  this is an error',
    },
  ];

  testErrorCases.forEach(function (testCase) {
    test(`#logFromError: ${testCase.name}`, function (assert) {
      const data = logFromError(...testCase.args);
      assert.deepEqual(
        data,
        { type: 'error', content: testCase.expectedContent },
        'returns the expected data'
      );
    });
  });

  const testCommandCases = [
    {
      name: 'errors when command does not include a path',
      args: [],
      expectedContent: 'A path is required to make a request.',
    },
    {
      name: 'errors when write command does not include data and does not have force tag',
      args: ['foo/bar', 'write', {}, []],
      expectedContent: 'Must supply data or use -force',
    },
  ];

  testCommandCases.forEach(function (testCase) {
    test(`#formattedErrorFromInput: ${testCase.name}`, function (assert) {
      const data = formattedErrorFromInput(...testCase.args);

      assert.deepEqual(
        data,
        { type: 'error', content: testCase.expectedContent },
        'returns the pcorrect data'
      );
    });
  });
});
