/* eslint-disable node-core/crypto-check */
'use strict';
// Refs: https://github.com/nodejs/node/issues/31733
const common = require('../common');
const assert = require('assert');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const stream = require('stream');
const tmpdir = require('../common/tmpdir');

class Sink extends stream.Writable {
  constructor() {
    super();
    this.chunks = [];
  }

  _write(chunk, encoding, cb) {
    this.chunks.push(chunk);
    cb();
  }
}

function direct(config) {
  const { cipher, key, iv, aad, authTagLength, plaintextLength } = config;
  const expected = Buffer.alloc(plaintextLength);

  const c = crypto.createCipheriv(cipher, key, iv, { authTagLength });
  c.setAAD(aad, { plaintextLength });
  const ciphertext = Buffer.concat([c.update(expected), c.final()]);

  const d = crypto.createDecipheriv(cipher, key, iv, { authTagLength });
  d.setAAD(aad, { plaintextLength });
  d.setAuthTag(c.getAuthTag());
  const actual = Buffer.concat([d.update(ciphertext), d.final()]);

  assert.deepStrictEqual(expected, actual);
}

function mstream(config) {
  const { cipher, key, iv, aad, authTagLength, plaintextLength } = config;
  const expected = Buffer.alloc(plaintextLength);

  const c = crypto.createCipheriv(cipher, key, iv, { authTagLength });
  c.setAAD(aad, { plaintextLength });

  const plain = new stream.PassThrough();
  const crypt = new Sink();
  const chunks = crypt.chunks;
  plain.pipe(c).pipe(crypt);
  plain.end(expected);

  crypt.on('close', common.mustCall(() => {
    const d = crypto.createDecipheriv(cipher, key, iv, { authTagLength });
    d.setAAD(aad, { plaintextLength });
    d.setAuthTag(c.getAuthTag());

    const crypt = new stream.PassThrough();
    const plain = new Sink();
    crypt.pipe(d).pipe(plain);
    for (const chunk of chunks) crypt.write(chunk);
    crypt.end();

    plain.on('close', common.mustCall(() => {
      const actual = Buffer.concat(plain.chunks);
      assert.deepStrictEqual(expected, actual);
    }));
  }));
}

function fstream(config) {
  const count = fstream.count++;
  const filename = (name) => path.join(tmpdir.path, `${name}${count}`);

  const { cipher, key, iv, aad, authTagLength, plaintextLength } = config;
  const expected = Buffer.alloc(plaintextLength);
  fs.writeFileSync(filename('a'), expected);

  const c = crypto.createCipheriv(cipher, key, iv, { authTagLength });
  c.setAAD(aad, { plaintextLength });

  const plain = fs.createReadStream(filename('a'));
  const crypt = fs.createWriteStream(filename('b'));
  plain.pipe(c).pipe(crypt);

  // Observation: 'close' comes before 'end' on |c|, which definitely feels
  // wrong. Switching to `c.on('end', ...)` doesn't fix the test though.
  crypt.on('close', common.mustCall(() => {
    // Just to drive home the point that decryption does actually work:
    // reading the file synchronously, then decrypting it, works.
    {
      const ciphertext = fs.readFileSync(filename('b'));
      const d = crypto.createDecipheriv(cipher, key, iv, { authTagLength });
      d.setAAD(aad, { plaintextLength });
      d.setAuthTag(c.getAuthTag());
      const actual = Buffer.concat([d.update(ciphertext), d.final()]);
      assert.deepStrictEqual(expected, actual);
    }

    const d = crypto.createDecipheriv(cipher, key, iv, { authTagLength });
    d.setAAD(aad, { plaintextLength });
    d.setAuthTag(c.getAuthTag());

    const crypt = fs.createReadStream(filename('b'));
    const plain = fs.createWriteStream(filename('c'));
    crypt.pipe(d).pipe(plain);

    plain.on('close', common.mustCall(() => {
      const actual = fs.readFileSync(filename('c'));
      assert.deepStrictEqual(expected, actual);
    }));
  }));
}
fstream.count = 0;

function test(config) {
  direct(config);
  mstream(config);
  fstream(config);
}

tmpdir.refresh();

// OK
test({
  cipher: 'aes-128-ccm',
  aad: Buffer.alloc(1),
  iv: Buffer.alloc(8),
  key: Buffer.alloc(16),
  authTagLength: 16,
  plaintextLength: 32768,
});

// Fails the fstream test.
test({
  cipher: 'aes-128-ccm',
  aad: Buffer.alloc(1),
  iv: Buffer.alloc(8),
  key: Buffer.alloc(16),
  authTagLength: 16,
  plaintextLength: 32769,
});
