'use strict';

const assert = require('assert');

const debug = false;

const {
  fixtureKey,
  mustCall,
  setup,
} = require('./common.js');
const {
  AgentProtocol,
  BaseAgent,
  utils: { parseKey },
} = require('../lib/index.js');

const serverCfg = { hostKeys: [ fixtureKey('ssh_host_rsa_key').raw ] };

const clientKey = fixtureKey('openssh_new_rsa');

{
  let getIdentitiesCount = 0;
  let signCount = 0;
  class MyAgent extends BaseAgent {
    getIdentities(cb) {
      assert.strictEqual(++getIdentitiesCount, 1);
      // Ensure that no private portion of the key is used by re-parsing the
      // public version of the key
      cb(null, [ parseKey(clientKey.key.getPublicSSH()) ]);
    }
    sign(pubKey, data, options, cb) {
      assert.strictEqual(++signCount, 1);
      assert.strictEqual(pubKey.getPublicPEM(), clientKey.key.getPublicPEM());
      const sig = clientKey.key.sign(data, options.hash);
      cb(null, sig);
    }
  }

  const username = 'Agent User';
  const { server } = setup(
    'Custom agent authentication',
    {
      client: { username, agent: new MyAgent() },
      server: serverCfg,

      debug,
    }
  );

  server.on('connection', mustCall((conn) => {
    let authAttempt = 0;
    conn.on('authentication', mustCall((ctx) => {
      assert(ctx.username === username,
             `Wrong username: ${ctx.username}`);
      switch (++authAttempt) {
        case 1:
          assert(ctx.method === 'none', `Wrong auth method: ${ctx.method}`);
          return ctx.reject();
        case 3:
          assert(ctx.signature, 'Missing publickey signature');
        // FALLTHROUGH
        case 2:
          assert(ctx.method === 'publickey',
                 `Wrong auth method: ${ctx.method}`);
          assert(ctx.key.algo === clientKey.key.type,
                 `Wrong key algo: ${ctx.key.algo}`);
          assert.deepStrictEqual(clientKey.key.getPublicSSH(),
                                 ctx.key.data,
                                 'Public key mismatch');
          break;
      }
      if (ctx.signature) {
        assert(clientKey.key.verify(ctx.blob, ctx.signature) === true,
               'Could not verify publickey signature');
      }
      ctx.accept();
    }, 3)).on('ready', mustCall(() => {
      assert.strictEqual(getIdentitiesCount, 1);
      assert.strictEqual(signCount, 1);
      conn.end();
    }));
  }));
}
{
  const client = new AgentProtocol(true);
  const server = new AgentProtocol(false);

  server.on('identities', mustCall((req) => {
    setImmediate(() => server.failureReply(req));
  }));
  client.getIdentities(mustCall((err, keys) => {
    assert(err, 'Missing expected error');
  }));

  client.pipe(server).pipe(client);
}
{
  const client = new AgentProtocol(true);
  const server = new AgentProtocol(false);

  server.on('identities', mustCall((req) => {
    const keys = [ clientKey.key ];
    server.getIdentitiesReply(req, keys);
  }));
  client.getIdentities(mustCall((err, keys) => {
    assert(!err, 'Unexpected error');
    assert.strictEqual(keys.length, 1);
    assert.strictEqual(keys[0].isPrivateKey(), false);
    assert.strictEqual(keys[0].getPublicPEM(), clientKey.key.getPublicPEM());
  }));

  client.pipe(server).pipe(client);
}
{
  const client = new AgentProtocol(true);
  const server = new AgentProtocol(false);
  const buf = Buffer.from('data to sign');

  server.on('sign', mustCall((req, pubKey, data, options) => {
    assert.strictEqual(pubKey.getPublicPEM(), clientKey.key.getPublicPEM());
    assert.deepStrictEqual(data, buf);
    assert.strictEqual(options.hash, undefined);
    server.failureReply(req);
  }));
  client.sign(clientKey.key.getPublicSSH(),
              buf,
              mustCall((err, signature) => {
    assert(err, 'Missing expected error');
  }));

  client.pipe(server).pipe(client);
}
{
  const client = new AgentProtocol(true);
  const server = new AgentProtocol(false);
  const buf = Buffer.from('data to sign');

  server.on('sign', mustCall((req, pubKey, data, options) => {
    assert.strictEqual(pubKey.getPublicPEM(), clientKey.key.getPublicPEM());
    assert.deepStrictEqual(data, buf);
    assert.strictEqual(options.hash, undefined);
    server.signReply(req, clientKey.key.sign(data));
  }));
  client.sign(clientKey.key.getPublicSSH(),
              buf,
              mustCall((err, signature) => {
    assert(!err, 'Unexpected error');
    const pubKey = parseKey(clientKey.key.getPublicSSH());
    assert.strictEqual(pubKey.verify(buf, signature), true);
  }));

  client.pipe(server).pipe(client);
}
{
  // Test that outstanding requests are handled upon unexpected closure of the
  // protocol stream

  const client = new AgentProtocol(true);
  const server = new AgentProtocol(false);

  server.on('identities', mustCall((req) => {
    server.destroy();
  }));
  client.getIdentities(mustCall((err) => {
    assert(err, 'Missing expected error');
  }));

  client.pipe(server).pipe(client);
}
