const FileSender = window.FileSender;
const FileReceiver = window.FileReceiver;
const FakeFile = window.FakeFile;
const assert = window.assert;
const server = window.server;
const hexToArray = window.hexToArray;
const arrayToHex = window.arrayToHex;
const sinon = window.sinon;

let file;
let encryptedIV;
let fileHash;
let secretKey;
let originalBlob;

describe('File Sender', function() {
  before(function() {
    server.respondImmediately = true;
    server.respondWith('POST', '/upload', function(request) {
      const reader = new FileReader();
      reader.readAsArrayBuffer(request.requestBody.get('data'));

      reader.onload = function(event) {
        file = this.result;
      };

      const responseObj = JSON.parse(request.requestHeaders['X-File-Metadata']);
      request.respond(
        200,
        { 'Content-Type': 'application/json' },
        JSON.stringify({
          url: 'some url',
          id: responseObj.id,
          delete: responseObj.delete
        })
      );
    });
  });

  it('Should get a loading event emission', function() {
    const file = new FakeFile('hello_world.txt', ['This is some data.']);
    const fs = new FileSender(file);
    let testLoading = true;

    fs.on('loading', isStillLoading => {
      assert(!(!testLoading && isStillLoading));
      testLoading = isStillLoading;
    });

    return fs
      .upload()
      .then(info => {
        assert(info);
        assert(!testLoading);
      })
      .catch(err => {
        console.log(err, err.stack);
        assert.fail();
      });
  });

  it('Should get a hashing event emission', function() {
    const file = new FakeFile('hello_world.txt', ['This is some data.']);
    const fs = new FileSender(file);
    let testHashing = true;

    fs.on('hashing', isStillHashing => {
      assert(!(!testHashing && isStillHashing));
      testHashing = isStillHashing;
    });

    return fs
      .upload()
      .then(info => {
        assert(info);
        assert(!testHashing);
      })
      .catch(err => {
        console.log(err, err.stack);
        assert.fail();
      });
  });

  it('Should get a encrypting event emission', function() {
    const file = new FakeFile('hello_world.txt', ['This is some data.']);
    const fs = new FileSender(file);
    let testEncrypting = true;

    fs.on('encrypting', isStillEncrypting => {
      assert(!(!testEncrypting && isStillEncrypting));
      testEncrypting = isStillEncrypting;
    });

    return fs
      .upload()
      .then(info => {
        assert(info);
        assert(!testEncrypting);
      })
      .catch(err => {
        console.log(err, err.stack);
        assert.fail();
      });
  });

  it('Should encrypt a file properly', function(done) {
    const newFile = new FakeFile('hello_world.txt', ['This is some data.']);
    const fs = new FileSender(newFile);
    fs.upload().then(info => {
      const key = info.secretKey;
      secretKey = info.secretKey;
      const IV = info.fileId;
      encryptedIV = info.fileId;

      const readRaw = new FileReader();
      readRaw.onload = function(event) {
        const rawArray = new Uint8Array(this.result);
        originalBlob = rawArray;

        window.crypto.subtle.digest('SHA-256', rawArray).then(hash => {
          fileHash = hash;
          window.crypto.subtle
            .importKey(
              'jwk',
              {
                kty: 'oct',
                k: key,
                alg: 'A128GCM',
                ext: true
              },
              {
                name: 'AES-GCM'
              },
              true,
              ['encrypt', 'decrypt']
            )
            .then(cryptoKey => {
              window.crypto.subtle
                .encrypt(
                  {
                    name: 'AES-GCM',
                    iv: hexToArray(IV),
                    additionalData: hash,
                    tagLength: 128
                  },
                  cryptoKey,
                  rawArray
                )
                .then(encrypted => {
                  assert(
                    new Uint8Array(encrypted).toString() ===
                      new Uint8Array(file).toString()
                  );
                  done();
                });
            });
        });
      };

      readRaw.readAsArrayBuffer(newFile);
    });
  });
});

describe('File Receiver', function() {
  class FakeXHR {
    constructor() {
      this.response = file;
      this.status = 200;
    }

    static setup() {
      FakeXHR.prototype.open = sinon.spy();
      FakeXHR.prototype.send = function() {
        this.onload();
      };

      FakeXHR.prototype.originalXHR = window.XMLHttpRequest;

      FakeXHR.prototype.getResponseHeader = function() {
        return JSON.stringify({
          aad: arrayToHex(new Uint8Array(fileHash)),
          filename: 'hello_world.txt',
          id: encryptedIV
        });
      };
      window.XMLHttpRequest = FakeXHR;
    }

    static restore() {
      // originalXHR is a sinon FakeXMLHttpRequest, since
      // fakeServer.create() is called in frontend.bundle.js
      window.XMLHttpRequest.prototype.originalXHR.restore();
    }
  }

  const cb = function(done) {
    if (
      file === undefined ||
      encryptedIV === undefined ||
      fileHash === undefined ||
      secretKey === undefined
    ) {
      assert.fail(
        'Please run file sending tests before trying to receive the files.'
      );
      done();
    }

    FakeXHR.setup();
    done();
  };

  before(cb);

  after(function() {
    FakeXHR.restore();
  });

  it('Should decrypt properly', function() {
    const fr = new FileReceiver();
    location.hash = secretKey;
    return fr
      .download()
      .then(([decrypted, name]) => {
        assert(name);
        assert(
          new Uint8Array(decrypted).toString() ===
            new Uint8Array(originalBlob).toString()
        );
      })
      .catch(err => {
        console.log(err, err.stack);
        assert.fail();
      });
  });

  it('Should emit decrypting events', function() {
    const fr = new FileReceiver();
    location.hash = secretKey;

    let testDecrypting = true;

    fr.on('decrypting', isStillDecrypting => {
      assert(!(!testDecrypting && isStillDecrypting));
      testDecrypting = isStillDecrypting;
    });

    fr.on('safe', isSafe => {
      assert(isSafe);
    });

    return fr
      .download()
      .then(([decrypted, name]) => {
        assert(decrypted);
        assert(name);
        assert(!testDecrypting);
      })
      .catch(err => {
        console.log(err, err.stack);
        assert.fail();
      });
  });

  it('Should emit hashing events', function() {
    const fr = new FileReceiver();
    location.hash = secretKey;

    let testHashing = true;

    fr.on('hashing', isStillHashing => {
      assert(!(!testHashing && isStillHashing));
      testHashing = isStillHashing;
    });

    fr.on('safe', isSafe => {
      assert(isSafe);
    });

    return fr
      .download()
      .then(([decrypted, name]) => {
        assert(decrypted);
        assert(name);
        assert(!testHashing);
      })
      .catch(err => {
        assert.fail();
      });
  });

  it('Should catch fraudulent checksums', function(done) {
    // Use the secret key and file hash of the previous file to encrypt,
    // which has a different hash than this one (different strings).
    const newFile = new FakeFile('hello_world.txt', [
      'This is some data, with a changed hash.'
    ]);
    const readRaw = new FileReader();

    readRaw.onload = function(event) {
      const plaintext = new Uint8Array(this.result);
      window.crypto.subtle
        .importKey(
          'jwk',
          {
            kty: 'oct',
            k: secretKey,
            alg: 'A128GCM',
            ext: true
          },
          {
            name: 'AES-GCM'
          },
          true,
          ['encrypt', 'decrypt']
        )
        .then(key => {
          // The file hash used here is the hash of the fake
          // file from the previous test; it's a phony checksum.
          return window.crypto.subtle.encrypt(
            {
              name: 'AES-GCM',
              iv: hexToArray(encryptedIV),
              additionalData: fileHash,
              tagLength: 128
            },
            key,
            plaintext
          );
        })
        .then(encrypted => {
          file = encrypted;
          const fr = new FileReceiver();
          location.hash = secretKey;

          fr.on('unsafe', isUnsafe => {
            assert(isUnsafe);
          });

          fr.on('safe', () => {
            // This event should not be emitted.
            assert.fail();
          });

          fr
            .download()
            .then(() => {
              assert.fail();
              done();
            })
            .catch(err => {
              assert(1);
              done();
            });
        });
    };
    readRaw.readAsArrayBuffer(newFile);
  });

  it('Should not decrypt with an incorrect checksum', function() {
    FakeXHR.prototype.getResponseHeader = function() {
      return JSON.stringify({
        aad: 'some_bad_hashz',
        filename: 'hello_world.txt',
        id: encryptedIV
      });
    };

    const fr = new FileReceiver();
    location.hash = secretKey;

    return fr
      .download()
      .then(([decrypted, name]) => {
        assert(decrypted);
        assert(name);
        assert.fail();
      })
      .catch(err => {
        assert(1);
      });
  });
});