import FileTransfer from 'web-client/utilities/video-uploader/file-transfer';
import { RequestAbortedError } from 'web-client/utilities/error';
import RetryTimer from 'web-client/utilities/video-uploader/retry-timer';
import FakeFile from 'web-client/tests/helpers/fake-file';
import { module, test } from 'qunit';
import RSVP from 'rsvp';
import { assign } from 'ember-platform';
const { resolve, reject, defer } = RSVP;

module('Unit | Utils | file-transfer-test');

test('Successful upload', function(assert) {
  assert.expect(2);

  let adapter = new FakeAdapter([resolve, resolve, resolve]);
  let callbacks = [];

  let transmission = createFileTransfer({
    sendPart(part) {
      callbacks.push('sendPart');
      return adapter.sendPart(part);
    },

    onUploadStart() {
      callbacks.push('onUploadStart');
    }
  });

  return transmission.send().then(() => {
    assert.deepEqual(adapter.serializePartsSent(), [
      '1 - abc',
      '2 - def',
      '3 - g'
    ], 'sends the file in multiple parts');

    assert.deepEqual(callbacks, [
      'onUploadStart',
      'sendPart',
      'sendPart',
      'sendPart'
    ], 'Sends onUploadStart before sending any parts');
  });
});

test('Retrying a part', function(assert) {
  assert.expect(1);

  let adapter = new FakeAdapter([resolve, reject, resolve, resolve]);

  let transmission = createFileTransfer({
    sendPart(part) {
      return adapter.sendPart(part);
    }
  });

  return transmission.send().then(() => {
    assert.deepEqual(adapter.serializePartsSent(), [
      '1 - abc',
      '2 - def',
      '2 - def',
      '3 - g'
    ], 'retries the failed request');
  });
});

test('Aborting', function(assert) {
  assert.expect(3);

  let adapter = new FakeAdapter([
    resolve,
    () => {
      // The user initiates `abort`
      transmission.abort();
      return reject(new RequestAbortedError());
    },
    resolve
  ]);

  let transmission = createFileTransfer({
    sendPart(part) {
      return adapter.sendPart(part);
    }
  });

  return transmission.send().catch((e) => {
    assert.ok(e instanceof RequestAbortedError, 'rejected with a RequestAbortedError');

    assert.deepEqual(adapter.serializePartsSent(), [
      '1 - abc',
      '2 - def'
    ], 'stops after second part');

    assert.deepEqual(adapter.serializePartsAborted(), [
      '2 - def'
    ]);
  });
});

test('Reporting progress', function(assert) {
  assert.expect(1);

  let adapter = new FakeAdapter([resolve, resolve, resolve]);

  let transmission = createFileTransfer({
    sendPart(part) {
      return adapter.sendPart(part);
    },

    onProgress(event) {
      adapter.onProgress(event);
    }
  });

  return transmission.send().then(() => {
    assert.deepEqual(adapter.progressEvents, [
      { loaded: 2, total: 7 },
      { loaded: 5, total: 7 },
      { loaded: 6, total: 7 }
    ], 'reports progress of the entire file');
  });
});

test('Sending parallel requests', function(assert) {
  assert.expect(2);

  // Pause the first 2 requests
  let deferedResponses = [defer(), defer()];
  let adapter = new FakeAdapter([
    ...deferedResponses.map((r) => () => r.promise),
    resolve,
    resolve
  ]);

  let transmission = createFileTransfer({
    sendPart(part) {
      return adapter.sendPart(part);
    },
    partSize: 2,
    maxInFlightRequests: 2
  });

  let transmissionPromise = transmission.send();

  assert.deepEqual(adapter.serializePartsSent(), [
    '1 - ab',
    '2 - cd'
  ], 'sends two parts in parallel and waits');

  // Resolve pending responses and clear the existing partsSent
  deferedResponses.forEach((r) => r.resolve());
  adapter.partsSent = [];

  return transmissionPromise.then(() => {
    assert.deepEqual(adapter.serializePartsSent(), [
      '3 - ef',
      '4 - g'
    ], 'sends remaining parts after the previous requests resolve');
  });
});

test('Reporting progress for parallel requests', function(assert) {
  assert.expect(1);

  let adapter = new FakeAdapter([resolve, resolve, resolve]);

  let transmission = createFileTransfer({
    sendPart(part) {
      return adapter.sendPart(part);
    },
    onProgress(event) {
      adapter.onProgress(event);
    },
    maxInFlightRequests: 2
  });

  return transmission.send().then(() => {
    assert.deepEqual(adapter.progressEvents, [
      { loaded: 2, total: 7 }, // abc - 1 (inFlight)
      { loaded: 4, total: 7 }, // def - 1 (inFlight), abc - 1 (inFlight)
      { loaded: 5, total: 7 }  // abc (aknowledged),  def - 1 (inFlight), g - 1 (inFlight)
    ], 'reports progress of the entire file and inFlight requests');
  });
});

// Create a FileTransfer with default values
function createFileTransfer(options) {
  return new FileTransfer(assign({
    file: new FakeFile('abcdefg'),
    partSize: 3,
    retryTimer: new RetryTimer({
      minInterval: 0,
      maxInterval: 0,
      backoff: 0
    }),
    maxInFlightRequests: 1
  }, options));
}

class FakeAdapter {
  constructor(responses) {
    this.partsSent = [];
    this.partsAborted = [];
    this.progressEvents = [];
    this.responses = responses || [];
    this.pendingResponses = [];
  }

  sendPart(part) {
    if (!this.responses.length) {
      throw new Error('FakeAdapter has no more responses');
    }

    this.partsSent.push(part);

    // Give the part a callback to abort the xhr
    part.abort = () => {
      this.partsAborted.push(part);
    };

    // Report progress events with size - 1 to differentiate from a completed part
    part.onProgress({ loaded: part.file.size - 1, total: part.file.size });

    return this.responses.shift()(part);
  }

  onProgress(event) {
    this.progressEvents.push(event);
  }

  serializePartsSent() {
    return this.partsSent.map(filePartToString);
  }

  serializePartsAborted() {
    return this.partsAborted.map(filePartToString);
  }
}

// Serialize a filePart into a string for better test assertions
function filePartToString(filePart) {
  return `${filePart.number} - ${filePart.file.bytes}`;
}
