/* global libsignal, textsecure */

'use strict';

const {
  SecretSessionCipher,
  createCertificateValidator,
  _createSenderCertificateFromBuffer,
  _createServerCertificateFromBuffer,
} = window.Signal.Metadata;
const {
  bytesFromString,
  stringFromBytes,
  arrayBufferToBase64,
} = window.Signal.Crypto;

function InMemorySignalProtocolStore() {
  this.store = {};
}

function toString(thing) {
  if (typeof thing === 'string') {
    return thing;
  }
  return arrayBufferToBase64(thing);
}

InMemorySignalProtocolStore.prototype = {
  Direction: {
    SENDING: 1,
    RECEIVING: 2,
  },

  getIdentityKeyPair() {
    return Promise.resolve(this.get('identityKey'));
  },
  getLocalRegistrationId() {
    return Promise.resolve(this.get('registrationId'));
  },
  put(key, value) {
    if (
      key === undefined ||
      value === undefined ||
      key === null ||
      value === null
    )
      throw new Error('Tried to store undefined/null');
    this.store[key] = value;
  },
  get(key, defaultValue) {
    if (key === null || key === undefined)
      throw new Error('Tried to get value for undefined/null key');
    if (key in this.store) {
      return this.store[key];
    }

    return defaultValue;
  },
  remove(key) {
    if (key === null || key === undefined)
      throw new Error('Tried to remove value for undefined/null key');
    delete this.store[key];
  },

  isTrustedIdentity(identifier, identityKey) {
    if (identifier === null || identifier === undefined) {
      throw new Error('tried to check identity key for undefined/null key');
    }
    if (!(identityKey instanceof ArrayBuffer)) {
      throw new Error('Expected identityKey to be an ArrayBuffer');
    }
    const trusted = this.get(`identityKey${identifier}`);
    if (trusted === undefined) {
      return Promise.resolve(true);
    }
    return Promise.resolve(toString(identityKey) === toString(trusted));
  },
  loadIdentityKey(identifier) {
    if (identifier === null || identifier === undefined) {
      throw new Error('Tried to get identity key for undefined/null key');
    }
    return Promise.resolve(this.get(`identityKey${identifier}`));
  },
  saveIdentity(identifier, identityKey) {
    if (identifier === null || identifier === undefined) {
      throw new Error('Tried to put identity key for undefined/null key');
    }
    const address = libsignal.SignalProtocolAddress.fromString(identifier);

    const existing = this.get(`identityKey${address.getName()}`);
    this.put(`identityKey${address.getName()}`, identityKey);

    if (existing && toString(identityKey) !== toString(existing)) {
      return Promise.resolve(true);
    }

    return Promise.resolve(false);
  },

  /* Returns a prekeypair object or undefined */
  loadPreKey(keyId) {
    let res = this.get(`25519KeypreKey${keyId}`);
    if (res !== undefined) {
      res = { pubKey: res.pubKey, privKey: res.privKey };
    }
    return Promise.resolve(res);
  },
  storePreKey(keyId, keyPair) {
    return Promise.resolve(this.put(`25519KeypreKey${keyId}`, keyPair));
  },
  removePreKey(keyId) {
    return Promise.resolve(this.remove(`25519KeypreKey${keyId}`));
  },

  /* Returns a signed keypair object or undefined */
  loadSignedPreKey(keyId) {
    let res = this.get(`25519KeysignedKey${keyId}`);
    if (res !== undefined) {
      res = { pubKey: res.pubKey, privKey: res.privKey };
    }
    return Promise.resolve(res);
  },
  storeSignedPreKey(keyId, keyPair) {
    return Promise.resolve(this.put(`25519KeysignedKey${keyId}`, keyPair));
  },
  removeSignedPreKey(keyId) {
    return Promise.resolve(this.remove(`25519KeysignedKey${keyId}`));
  },

  loadSession(identifier) {
    return Promise.resolve(this.get(`session${identifier}`));
  },
  storeSession(identifier, record) {
    return Promise.resolve(this.put(`session${identifier}`, record));
  },
  removeSession(identifier) {
    return Promise.resolve(this.remove(`session${identifier}`));
  },
  removeAllSessions(identifier) {
    // eslint-disable-next-line no-restricted-syntax
    for (const id in this.store) {
      if (id.startsWith(`session${identifier}`)) {
        delete this.store[id];
      }
    }
    return Promise.resolve();
  },
};

describe('SecretSessionCipher', () => {
  it('successfully roundtrips', async () => {
    const aliceStore = new InMemorySignalProtocolStore();
    const bobStore = new InMemorySignalProtocolStore();

    await _initializeSessions(aliceStore, bobStore);

    const aliceIdentityKey = await aliceStore.getIdentityKeyPair();

    const trustRoot = await libsignal.Curve.async.generateKeyPair();
    const senderCertificate = await _createSenderCertificateFor(
      trustRoot,
      '+14151111111',
      1,
      aliceIdentityKey.pubKey,
      31337
    );
    const aliceCipher = new SecretSessionCipher(aliceStore);

    const ciphertext = await aliceCipher.encrypt(
      new libsignal.SignalProtocolAddress('+14152222222', 1),
      senderCertificate,
      bytesFromString('smert za smert')
    );

    const bobCipher = new SecretSessionCipher(bobStore);

    const decryptResult = await bobCipher.decrypt(
      createCertificateValidator(trustRoot.pubKey),
      ciphertext,
      31335
    );

    assert.strictEqual(
      stringFromBytes(decryptResult.content),
      'smert za smert'
    );
    assert.strictEqual(decryptResult.sender.toString(), '+14151111111.1');
  });

  it('fails when untrusted', async () => {
    const aliceStore = new InMemorySignalProtocolStore();
    const bobStore = new InMemorySignalProtocolStore();

    await _initializeSessions(aliceStore, bobStore);

    const aliceIdentityKey = await aliceStore.getIdentityKeyPair();

    const trustRoot = await libsignal.Curve.async.generateKeyPair();
    const falseTrustRoot = await libsignal.Curve.async.generateKeyPair();
    const senderCertificate = await _createSenderCertificateFor(
      falseTrustRoot,
      '+14151111111',
      1,
      aliceIdentityKey.pubKey,
      31337
    );
    const aliceCipher = new SecretSessionCipher(aliceStore);

    const ciphertext = await aliceCipher.encrypt(
      new libsignal.SignalProtocolAddress('+14152222222', 1),
      senderCertificate,
      bytesFromString('и вот я')
    );

    const bobCipher = new SecretSessionCipher(bobStore);

    try {
      await bobCipher.decrypt(
        createCertificateValidator(trustRoot.pubKey),
        ciphertext,
        31335
      );
      throw new Error('It did not fail!');
    } catch (error) {
      assert.strictEqual(error.message, 'Invalid signature');
    }
  });

  it('fails when expired', async () => {
    const aliceStore = new InMemorySignalProtocolStore();
    const bobStore = new InMemorySignalProtocolStore();

    await _initializeSessions(aliceStore, bobStore);

    const aliceIdentityKey = await aliceStore.getIdentityKeyPair();

    const trustRoot = await libsignal.Curve.async.generateKeyPair();
    const senderCertificate = await _createSenderCertificateFor(
      trustRoot,
      '+14151111111',
      1,
      aliceIdentityKey.pubKey,
      31337
    );
    const aliceCipher = new SecretSessionCipher(aliceStore);

    const ciphertext = await aliceCipher.encrypt(
      new libsignal.SignalProtocolAddress('+14152222222', 1),
      senderCertificate,
      bytesFromString('и вот я')
    );

    const bobCipher = new SecretSessionCipher(bobStore);

    try {
      await bobCipher.decrypt(
        createCertificateValidator(trustRoot.pubKey),
        ciphertext,
        31338
      );
      throw new Error('It did not fail!');
    } catch (error) {
      assert.strictEqual(error.message, 'Certificate is expired');
    }
  });

  it('fails when wrong identity', async () => {
    const aliceStore = new InMemorySignalProtocolStore();
    const bobStore = new InMemorySignalProtocolStore();

    await _initializeSessions(aliceStore, bobStore);

    const trustRoot = await libsignal.Curve.async.generateKeyPair();
    const randomKeyPair = await libsignal.Curve.async.generateKeyPair();
    const senderCertificate = await _createSenderCertificateFor(
      trustRoot,
      '+14151111111',
      1,
      randomKeyPair.pubKey,
      31337
    );
    const aliceCipher = new SecretSessionCipher(aliceStore);

    const ciphertext = await aliceCipher.encrypt(
      new libsignal.SignalProtocolAddress('+14152222222', 1),
      senderCertificate,
      bytesFromString('smert za smert')
    );

    const bobCipher = new SecretSessionCipher(bobStore);

    try {
      await bobCipher.decrypt(
        createCertificateValidator(trustRoot.puKey),
        ciphertext,
        31335
      );
      throw new Error('It did not fail!');
    } catch (error) {
      assert.strictEqual(error.message, 'Invalid public key');
    }
  });

  // private SenderCertificate _createCertificateFor(
  //   ECKeyPair trustRoot
  //   String sender
  //   int deviceId
  //   ECPublicKey identityKey
  //   long expires
  // )
  async function _createSenderCertificateFor(
    trustRoot,
    sender,
    deviceId,
    identityKey,
    expires
  ) {
    const serverKey = await libsignal.Curve.async.generateKeyPair();

    const serverCertificateCertificateProto = new textsecure.protobuf.ServerCertificate.Certificate();
    serverCertificateCertificateProto.id = 1;
    serverCertificateCertificateProto.key = serverKey.pubKey;
    const serverCertificateCertificateBytes = serverCertificateCertificateProto
      .encode()
      .toArrayBuffer();

    const serverCertificateSignature = await libsignal.Curve.async.calculateSignature(
      trustRoot.privKey,
      serverCertificateCertificateBytes
    );

    const serverCertificateProto = new textsecure.protobuf.ServerCertificate();
    serverCertificateProto.certificate = serverCertificateCertificateBytes;
    serverCertificateProto.signature = serverCertificateSignature;
    const serverCertificate = _createServerCertificateFromBuffer(
      serverCertificateProto.encode().toArrayBuffer()
    );

    const senderCertificateCertificateProto = new textsecure.protobuf.SenderCertificate.Certificate();
    senderCertificateCertificateProto.sender = sender;
    senderCertificateCertificateProto.senderDevice = deviceId;
    senderCertificateCertificateProto.identityKey = identityKey;
    senderCertificateCertificateProto.expires = expires;
    senderCertificateCertificateProto.signer = textsecure.protobuf.ServerCertificate.decode(
      serverCertificate.serialized
    );
    const senderCertificateBytes = senderCertificateCertificateProto
      .encode()
      .toArrayBuffer();

    const senderCertificateSignature = await libsignal.Curve.async.calculateSignature(
      serverKey.privKey,
      senderCertificateBytes
    );

    const senderCertificateProto = new textsecure.protobuf.SenderCertificate();
    senderCertificateProto.certificate = senderCertificateBytes;
    senderCertificateProto.signature = senderCertificateSignature;
    return _createSenderCertificateFromBuffer(
      senderCertificateProto.encode().toArrayBuffer()
    );
  }

  // private void _initializeSessions(
  //   SignalProtocolStore aliceStore, SignalProtocolStore bobStore)
  async function _initializeSessions(aliceStore, bobStore) {
    const aliceAddress = new libsignal.SignalProtocolAddress('+14152222222', 1);
    await aliceStore.put(
      'identityKey',
      await libsignal.Curve.generateKeyPair()
    );
    await bobStore.put('identityKey', await libsignal.Curve.generateKeyPair());

    await aliceStore.put('registrationId', 57);
    await bobStore.put('registrationId', 58);

    const bobPreKey = await libsignal.Curve.async.generateKeyPair();
    const bobIdentityKey = await bobStore.getIdentityKeyPair();
    const bobSignedPreKey = await libsignal.KeyHelper.generateSignedPreKey(
      bobIdentityKey,
      2
    );

    const bobBundle = {
      identityKey: bobIdentityKey.pubKey,
      registrationId: 1,
      preKey: {
        keyId: 1,
        publicKey: bobPreKey.pubKey,
      },
      signedPreKey: {
        keyId: 2,
        publicKey: bobSignedPreKey.keyPair.pubKey,
        signature: bobSignedPreKey.signature,
      },
    };
    const aliceSessionBuilder = new libsignal.SessionBuilder(
      aliceStore,
      aliceAddress
    );
    await aliceSessionBuilder.processPreKey(bobBundle);

    await bobStore.storeSignedPreKey(2, bobSignedPreKey.keyPair);
    await bobStore.storePreKey(1, bobPreKey);
  }
});
