"use strict";

define(['ably', 'shared_helper', 'async'], function(Ably, helper, async) {
	var currentTime, rest, exports = {},
		_exports = {},
		loadTestData = helper.loadTestData,
		testClientId = 'clientId',
		utils = helper.Utils,
		i = utils.inspect,
		displayError = helper.displayError,
		closeAndFinish = helper.closeAndFinish,
		monitorConnection = helper.monitorConnection,
		testOnAllTransports = helper.testOnAllTransports,
		BufferUtils = Ably.Realtime.BufferUtils,
		Crypto = Ably.Realtime.Crypto,
		Message = Ably.Realtime.Message,
		displayError = helper.displayError,
		testResourcesPath = helper.testResourcesPath,
		msgpack = (typeof(window) == 'object') ? Ably.msgpack : require('msgpack-js'),
		mixin = helper.Utils.mixin,
		utils = helper.Utils,
		noop = function() {},
		createPM = Ably.Realtime.ProtocolMessage.fromDeserialized,
		simulateDroppedConnection = helper.simulateDroppedConnection,
		availableTransports = helper.availableTransports;

	exports.setuptmp = function(test) {
		test.expect(1);
		helper.setupApp(function(err) {
			if(err) {
				test.ok(false, helper.displayError(err));
				test.done();
				return;
			}

			rest = helper.AblyRest();
			rest.time(function(err, time) {
				if(err) {
					test.ok(false, helper.displayError(err));
				} else {
					currentTime = time;
					test.ok(true, 'Obtained time via REST');
				}
				test.done();
			});
		});
	};

	function requestSync(realtime, channelName) {
		realtime.connection.connectionManager.activeProtocol.transport.send({
			action: 16,
			channel: channelName
		});
	}

	function attachChannels(channels, callback) {
		async.map(channels, function(channel, cb) { channel.attach(cb); }, callback);
	}

	function compareMessage(one, two) {
		if(one.encoding != two.encoding) return false;
		if(typeof(one.data) == 'string' && typeof(two.data) == 'string') {
			return one.data == two.data;
		}
		if(BufferUtils.isBuffer(one.data) && BufferUtils.isBuffer(two.data)) {
			return (BufferUtils.bufferCompare(one.data, two.data) === 0);
		}
		return JSON.stringify(one.data) == JSON.stringify(two.data);
	}


	function testEachFixture(test, filename, channelName, testsPerFixture, fixtureTest) {
		if(!Crypto) {
			test.ok(false, 'Encryption not supported');
			test.done();
			return;
		}

		loadTestData(testResourcesPath + filename, function(err, testData) {
			if(err) {
				test.ok(false, 'Unable to get test assets; err = ' + displayError(err));
				return;
			}
			var realtime = helper.AblyRealtime();
			var key = BufferUtils.base64Decode(testData.key);
			var iv = BufferUtils.base64Decode(testData.iv);
			var channel = realtime.channels.get(channelName, {cipher: {key: key, iv: iv}});

			test.expect(testData.items.length * testsPerFixture);
			for(var i = 0; i < testData.items.length; i++) {
				var item = testData.items[i];

				/* read messages from test data */
				var testMessage = Message.fromValues(item.encoded);
				var encryptedMessage = Message.fromValues(item.encrypted);
				/* decode (ie remove any base64 encoding). Will throw when
				 * it gets to the cipher part of the encoding, so wrap in try/catch */
				try { Message.decode(testMessage); } catch(_) {}
				try { Message.decode(encryptedMessage); } catch(_) {}
				/* reset channel cipher, to ensure it uses the given iv */
				channel.setOptions({cipher: {key: key, iv: iv}});

				fixtureTest(channel.channelOptions, testMessage, encryptedMessage, item.msgpack);
			}
			closeAndFinish(test, realtime);
		});
	}

	var publishIntervalHelper = function(currentMessageNum, channel, dataFn, onPublish){
			return function(currentMessageNum) {
				console.log('sending: ' + currentMessageNum);
				channel.publish('event0', dataFn(), function() {
					console.log('publish callback called');
					onPublish();
				});
			};
		},
		publishAtIntervals = function(numMessages, channel, dataFn, onPublish){
			for(var i = numMessages; i > 0; i--) {
				setTimeout(publishIntervalHelper(i, channel, dataFn, onPublish), 2*i);
			}
		};


	function _multiple_send(test, text, iterations, delay) {
		if(!Crypto) {
			test.ok(false, 'Encryption not supported');
			test.done();
			return;
		}

		var realtime = helper.AblyRealtime({ useBinaryProtocol: !text});
		test.expect(iterations + 3);
		var channelName = 'multiple_send_' + (text ? 'text_' : 'binary_') + iterations + '_' + delay,
			channel = realtime.channels.get(channelName),
			messageText = 'Test message (' + channelName + ')';

		Crypto.generateRandomKey(128, function(err, key) {
			channel.setOptions({cipher: {key: key}}, function(err) {
				if(err) {
					test.ok(false, 'Unable to set channel options; err = ' + displayError(err));
					closeAndFinish(test, realtime);
					return;
				}
				test.equal(channel.channelOptions.cipher.algorithm, 'aes');
				test.equal(channel.channelOptions.cipher.keyLength, 128);
				function sendAll(sendCb) {
					var sent = 0;
					var sendOnce = function() {
						channel.publish('event0', messageText);
						if(++sent == iterations) {
							sendCb(null);
							return;
						}
						setTimeout(sendOnce, delay);
					};
					sendOnce();
				}
				function recvAll(recvCb) {
					var received = 0;
					channel.subscribe('event0', function(msg) {
						test.ok(msg.data == messageText);
						if(++received == iterations)
							recvCb(null);
					});
				}
				async.parallel([sendAll, recvAll], function(err) {
					if(err) {
						test.ok(false, 'Error sending messages; err = ' + displayError(err));
					}
					test.ok('Verify all messages received');
					closeAndFinish(test, realtime);
				});
			});
		});
	}

	function extractClientIds(presenceSet) {
		return utils.arrMap(presenceSet, function(presmsg) {
			return presmsg.clientId;
		}).sort();
	}

	function extractMember(presenceSet, clientId) {
		return helper.arrFind(presenceSet, function(member) {
			return member.clientId === clientId;
		});
	}

	/***************************************************/

	function extractClientIds(presenceSet) {
		return utils.arrMap(presenceSet, function(presmsg) {
			return presmsg.clientId;
		}).sort();
	}

	function extractMember(presenceSet, clientId) {
		return helper.arrFind(presenceSet, function(member) {
			return member.clientId === clientId;
		});
	}

	var rest, authToken, authToken2;
	var testClientId = 'testclient', testClientId2 = 'testclient2';

	var createListenerChannel = function(channelName, callback) {
		var channel, realtime;
		try {
			realtime = helper.AblyRealtime();
			realtime.connection.on('connected', function() {
				console.log("Listener connected");
				channel = realtime.channels.get(channelName);
				channel.attach(function(err) {
					console.log("Listener attached to channel " + channelName);
					callback(err, realtime, channel);
				});
			});
		} catch(err) {
			callback(err, realtime);
		}
	};

	var listenerFor = function(eventName, expectedClientId) {
		return function(test, channel, callback) {
			var presenceHandler = function(presmsg) {
				if(this.event === eventName) {
					test.ok(true, 'Presence ' + eventName + ' received');
					if(expectedClientId !== undefined) {
						test.equal(presmsg.clientId, expectedClientId, 'Verify correct clientId');
					}
					channel.presence.unsubscribe(presenceHandler);
					callback();
				}
			};
			channel.presence.subscribe(presenceHandler);
		};
	};

	var runTestWithEventListener = function(test, channel, eventListener, testRunner) {
		try {
			createListenerChannel(channel, function(err, listenerRealtime, presenceChannel){
				if(err) {
					test.ok(false, displayError(err));
					closeAndFinish(test, listenerRealtime);
					return;
				}
				console.log("presenceChannel:", presenceChannel.name);

				async.parallel([
					function(cb) {
						eventListener(test, presenceChannel, cb);
					},
					testRunner
				], function(err, res) {
					console.log("in callback, err = ", err);
					if(err) {
						test.ok(false, displayError(err));
					}
					// testRunner might or might not call back with an open realtime
					var openConnections = (res[1] && res[1].close) ?
						[listenerRealtime, res[1]] :
						listenerRealtime;
					closeAndFinish(test, openConnections);
				});
			});
		} catch(e) {
			test.ok(false, 'test failed with exception: ' + e.stack);
			test.done();
		}
	};

	exports.queued = function(test){
		test.expect(150);
		var txRealtime, rxRealtime;
		try {
			txRealtime = helper.AblyRealtime({ autoConnect: false, log: {level: 4} });
			rxRealtime = helper.AblyRealtime({log: {level: 4}});
			var txChannel = txRealtime.channels.get('publishQueued_' + String(Math.random()).substr(2));
			var rxChannel = rxRealtime.channels.get(txChannel.name);

		async.series([
			function(cb) {
				rxRealtime.connection.once('connected', function() { cb(); });
			},
			function(cb) {
				rxChannel.attach(function(err) { cb(err); });
			},
			function(cb) {
				async.parallel([
					function(parCb) {
						var expectedMsgNum = 0;
						rxChannel.subscribe('event', function(msg) {
							var num = msg.data.num;
							test.ok(true, 'Received event ' + num);
							test.equal(expectedMsgNum, num, 'Event ' + num + ' was in the right order');
							expectedMsgNum++;
							if(num === 49) parCb();
						});
					},
					function(parCb) {
						var ackd = 0;
						var publish = function(i) {
							/* Publish without attaching -- transient publishes */
							txChannel.publish('event', {num: i}, function(err) {
								test.ok(!err, 'successfully published ' + i + (err ? ' err was ' + displayError(err) : ''));
								ackd++;
								if(ackd === 50) parCb();
							});
							if(i < 49) {
								setTimeout(function() {
									publish(i + 1);
								}, 20);
							}
						};
						publish(0);
					},
					function(parCb) {
						txRealtime.connection.once('connected', function() { parCb(); });
						txRealtime.connection.connect();
					}
				], cb);
			}
		], function() {
				closeAndFinish(test, [rxRealtime, txRealtime]);
			});
		} catch(e) {
			test.ok(false, 'test failed with exception: ' + e.stack);
			closeAndFinish(test, [rxRealtime, txRealtime]);
		}
	};

	return module.exports = helper.withTimeout(exports);
});
