import Ember from 'ember';
import { moduleFor, test } from 'ember-qunit';
import {
  setup as setupMirage, teardown as teardownMirage
} from '../../helpers/setup-mirage-for-integration';
import { setOwnerInjection } from 'web-client/utilities/owner-injection';
import serialize from '../../../mirage/utils/serialize';

const { get, run, RSVP, getOwner } = Ember;

let service;
const API_STREAMS_URL = 'http://api.twitch.tv/kraken/streams';

moduleFor('service:recommended-channels', {
  integration: true,
  beforeEach() {
    setupMirage(this);
    let owner = getOwner(this);
    setOwnerInjection(owner.ownerInjection());

    service = this.subject({
      watchedChannels: [],
      followedChannels: [],
      promotedChannels: []
    });
  },

  afterEach() {
    teardownMirage(this);
    service = null;
  }
});

// Create an array of {count} channel ids
function createChannelIds(count) {
  let ids = [];
  for (let i = 0; i < count; i++) {
    ids.push(`channel${i}`);
  }
  return ids;
}
// Create an array of stream objects for the given array of channelIds
function createStreamsForChannels(server, channelIds) {
  return channelIds.map((channelId) => {
    let channel = server.create('channel', {name: channelId});
    let stream = server.create('stream', {channel});
    return serialize(server, stream).stream;
  });
}

// Stub requests for /streams?channel=id1,id2,...
// Pushes the requested `channel` query params in order into
// `requestedChannelBatches`
function stubStreamRequests(server, streams, requestedChannelBatches, options={}) {
  server.get(API_STREAMS_URL, function (schema, request) {
    let channelIds = request.queryParams.channel.split(',');

    requestedChannelBatches.push(channelIds);

    let returnStreams = streams.filter((stream) => {
      return channelIds.indexOf(stream.channel.name) !== -1;
    });
    if (options.reverse) {
      returnStreams.reverse();
    }

    return { streams: returnStreams };
  });
}

test('#_recommendedChannels updates when watchedChannelIds changes', function (assert) {
  assert.expect(2);
  let done = assert.async();
  let stubbedStreams = ['stream1', 'stream2'];

  let fetchedIds = [];
  service._fetchStreams = (ids) => {
    fetchedIds = ids;
    return RSVP.resolve(stubbedStreams);
  };

  run(() => {
    service.set('watchedChannels', [{id: 'id1'}, {id: 'id2'}]);
    service.get('recommendedChannels'); // recommendedChannels is lazy-loaded so need to call get to trigger the request to _fetchStreams
  });

  run.next(() => {
    assert.deepEqual(fetchedIds, ['id1', 'id2'],
                     'channel ids passed to _fetchStreams');

    let streams = service.get('recommendedChannels');
    assert.deepEqual(streams, stubbedStreams,
                     'sets recommendedChannels to result of _fetchStreams');

    done();
  });
});

test('#_fetchStreams fetches channels until maxChannels live streams found when all channels have live streams', function (assert) {
  assert.expect(6);
  let done = assert.async();
  let maxChannels = 5;
  let batchCount = 2;
  let watchedChannelIds = createChannelIds(10);
  let requestedChannelBatches = [];
  let streams = createStreamsForChannels(this.server, [
    'channel0', 'channel1', 'channel2', 'channel3', 'channel4', 'channel5'
  ]);
  assert.equal(streams.length, maxChannels + 1,
               'precond - more channel streams than maxChannels');
  stubStreamRequests(this.server, streams, requestedChannelBatches);
  service.setProperties({maxChannels, batchCount});

  service._fetchStreams(watchedChannelIds).then((fetchedStreams) => {
    assert.equal(fetchedStreams.length, maxChannels, 'limits to maxChannels streams');
    assert.deepEqual(requestedChannelBatches, [
      ['channel0', 'channel1'],
      ['channel2', 'channel3'],
      ['channel4', 'channel5']
    ], `fetches streams in groups of batchCount (${batchCount}) channels`);

    assert.deepEqual(fetchedStreams.map((stream) => stream.channel.name), [
      'channel0', 'channel1', 'channel2', 'channel3', 'channel4'
    ], 'streams match requested channels in order');

    assert.deepEqual(fetchedStreams.map((stream) => stream.id), [
      'channel0', 'channel1', 'channel2', 'channel3', 'channel4'
    ], 'stream.id property matches channel.name');

    assert.ok(fetchedStreams.every(stream => !!get(stream, 'channelHref')),
              'every stream has a `channelHref` property');

    done();
  });
});

test('#_fetchStreams when some channels have no live stream', function (assert) {
  assert.expect(2);
  let done = assert.async();
  let maxChannels = 5;
  let batchCount = 2;
  let watchedChannelIds = createChannelIds(10);
  let requestedChannelBatches = [];
  let streams = createStreamsForChannels(this.server, [
    // no live streams for channels 0-4
    'channel5', 'channel6', 'channel7', 'channel8', 'channel9'
  ]);
  stubStreamRequests(this.server, streams, requestedChannelBatches);
  service.setProperties({maxChannels, batchCount});

  service._fetchStreams(watchedChannelIds).then((fetchedStreams) => {
    assert.deepEqual(requestedChannelBatches, [
      ['channel0', 'channel1'],
      ['channel2', 'channel3'],
      ['channel4', 'channel5'],
      ['channel6', 'channel7'],
      ['channel8', 'channel9']
    ], `fetches streams in groups of batchCount (${batchCount}) channels`);

    assert.deepEqual(fetchedStreams.map((stream) => stream.channel.name), [
      'channel5', 'channel6', 'channel7', 'channel8', 'channel9'
    ], 'streams match requested channels in order');

    done();
  });
});

test('#_fetchStreams sorts out-of-order streams by channel id position', function (assert) {
  assert.expect(2);
  let done = assert.async();
  let maxChannels = 5;
  let batchCount = 2;
  let watchedChannelIds = createChannelIds(10);
  let requestedChannelBatches = [];
  let streams = createStreamsForChannels(this.server, [
    'channel0', 'channel1', 'channel2', 'channel3', 'channel4', 'channel5'
  ]);
  let options = { reverse: true }; // simulate out-of-order responses
  stubStreamRequests(this.server, streams, requestedChannelBatches, options);
  service.setProperties({maxChannels, batchCount});

  service._fetchStreams(watchedChannelIds).then((fetchedStreams) => {
    assert.deepEqual(requestedChannelBatches, [
      ['channel0', 'channel1'],
      ['channel2', 'channel3'],
      ['channel4', 'channel5']
    ], `fetches streams in groups of batchCount (${batchCount}) channels`);

    assert.deepEqual(fetchedStreams.map((stream) => stream.channel.name), [
      'channel0', 'channel1', 'channel2', 'channel3', 'channel4'
    ], 'streams match requested channels in order');

    done();
  });
});

test('#_fetchStreams when < maxChannels streams are live', function (assert) {
  assert.expect(3);
  let done = assert.async();
  let maxChannels = 5;
  let batchCount = 2;
  let watchedChannelIds = createChannelIds(10);
  let requestedChannelBatches = [];
  let streamList = createStreamsForChannels(this.server, [
    // only 2 total channels with live streams
    'channel0', 'channel1'
  ]);
  stubStreamRequests(this.server, streamList, requestedChannelBatches);
  service.setProperties({maxChannels, batchCount});

  service._fetchStreams(watchedChannelIds).then((streams) => {
    assert.equal(streams.length, 2, 'finds only 2 streams');

    assert.deepEqual(requestedChannelBatches, [
      // requests all channels because maxChannels streams are never found
      ['channel0', 'channel1'],
      ['channel2', 'channel3'],
      ['channel4', 'channel5'],
      ['channel6', 'channel7'],
      ['channel8', 'channel9']
    ], `fetches streams in groups of batchCount (${batchCount}) channels`);

    assert.deepEqual(streams.map((stream) => stream.channel.name), [
      'channel0', 'channel1'
    ], 'streams match requested channels in order');

    done();
  });
});

test('`_uniqueChannels` removes unique channels based on channel.name', function (assert) {
  assert.expect(2);

  let result;

  run(() => {
    let recommendedChannels = [{id: 'r1', channel: {name: 'duplicate'}}];
    let followedChannels = {content: [{id: 'f1', channel: {name: 'duplicate'}}]};
    let promotedChannels = {content: []};
    let allChannels = [].concat(recommendedChannels, followedChannels.content, promotedChannels.content);

    result = service._uniqueChannels(allChannels).mapBy('id');
    assert.equal(result.join(), 'r1');
    assert.equal(result.length, 1);
  });
});

/*
  this function concats channels to fit the following rules
  we want to end up with 3 channels
  r: 1, f: 2
  r: 1, f: 1, p: 1
  r: 0, f: 1, p: 2
  r: 0, f: 0, p: 3
*/

test('`authenticatedChannels` computes proper combination of channels', function (assert) {
  assert.expect(8);

  let result;

  service._uniqueChannels = function (channels) {
    return channels;
  };

  run(() => {
    service.setProperties({
      recommendedChannels: [{id: 'r1'}],
      followedChannels: {content: [{id: 'f1'}, {id: 'f2'}]},
      promotedChannels: {content: []}
    });

    result = service.get('authenticatedChannels').mapBy('id');
    assert.equal(result.join(), 'r1,f1,f2');
    assert.equal(result.length, 3);
  });

  run(() => {
    service.setProperties({
      recommendedChannels: [{id: 'r1'}],
      followedChannels: {content: [{id: 'f1'}]},
      promotedChannels: {content: [{id: 'p1'}]}
    });
    result = service.get('authenticatedChannels').mapBy('id');
    assert.equal(result.join(), 'r1,f1,p1');
    assert.equal(result.length, 3);
  });

  run(() => {
    service.setProperties({
      recommendedChannels: [],
      followedChannels: {content: [{id: 'f1'}]},
      promotedChannels: {content: [{id: 'p1'}, {id: 'p2'}]}
    });
    result = service.get('authenticatedChannels').mapBy('id');
    assert.equal(result.join(), 'f1,p1,p2');
    assert.equal(result.length, 3);
  });

  run(() => {
    service.setProperties({
      recommendedChannels: [],
      followedChannels: {content: []},
      promotedChannels: {content: [{id: 'p1'}, {id: 'p2'}, {id: 'p3'}]}
    });
    result = service.get('authenticatedChannels').mapBy('id');
    assert.equal(result.join(), 'p1,p2,p3');
    assert.equal(result.length, 3);
  });
});

/*
  nonAuthenticatedChannels
  this function concats channels to fit the following rules
  we ideally want 3-10 recommendedChannels
  less than 3 and we start padding with promotedChannels
  r: 4, p: 0
  r: 3, p: 0
  r: 2, p: 1
  r: 1, p: 2
  r: 0, p: 3
*/

test('`nonAuthenticatedChannels` computes proper combination of channels', function (assert) {
  assert.expect(12);
  let result;

  service._uniqueChannels = function (channels) {
    return channels;
  };

  run(() => {
    service.setProperties({
      recommendedChannels: [
        {id: 'r1'}, {id: 'r2'}, {id: 'r3'}, {id: 'r4'}, {id: 'r5'},
        {id: 'r6'}, {id: 'r7'}, {id: 'r8'}, {id: 'r9'}, {id: 'r10'},
        {id: 'r11'}
      ],
      promotedChannels: {content: [{id: 'p1'}, {id: 'p2'}, {id: 'p3'}]}
    });
    result = service.get('nonAuthenticatedChannels').mapBy('id');
    assert.equal(result.join(), 'r1,r2,r3,r4,r5,r6,r7,r8,r9,r10');
    assert.equal(result.length, 10);
  });

  run(() => {
    service.setProperties({
      recommendedChannels: [{id: 'r1'}, {id: 'r2'}, {id: 'r3'}, {id: 'r4'}],
      promotedChannels: {content: [{id: 'p1'}, {id: 'p2'}, {id: 'p3'}]}
    });
    result = service.get('nonAuthenticatedChannels').mapBy('id');
    assert.equal(result.join(), 'r1,r2,r3,r4');
    assert.equal(result.length, 4);
  });

  run(() => {
    service.setProperties({
      recommendedChannels: [{id: 'r1'}, {id: 'r2'}, {id: 'r3'}],
      promotedChannels: {content: [{id: 'p1'}, {id: 'p2'}, {id: 'p3'}]}
    });
    result = service.get('nonAuthenticatedChannels').mapBy('id');
    assert.equal(result.join(), 'r1,r2,r3');
    assert.equal(result.length, 3);
  });

  run(() => {
    service.setProperties({
      recommendedChannels: [{id: 'r1'}, {id: 'r2'}],
      promotedChannels: {content: [{id: 'p1'}, {id: 'p2'}, {id: 'p3'}]}
    });
    result = service.get('nonAuthenticatedChannels').mapBy('id');
    assert.equal(result.join(), 'r1,r2,p1');
    assert.equal(result.length, 3);
  });

  run(() => {
    service.setProperties({
      recommendedChannels: [{id: 'r1'}],
      promotedChannels: {content: [{id: 'p1'}, {id: 'p2'}, {id: 'p3'}]}
    });
    result = service.get('nonAuthenticatedChannels').mapBy('id');
    assert.equal(result.join(), 'r1,p1,p2');
    assert.equal(result.length, 3);
  });

  run(() => {
    service.setProperties({
      recommendedChannels: [],
      promotedChannels: {content: [{id: 'p1'}, {id: 'p2'}, {id: 'p3'}]}
    });
    result = service.get('nonAuthenticatedChannels').mapBy('id');
    assert.equal(result.join(), 'p1,p2,p3');
    assert.equal(result.length, 3);
  });
});
