/**
* ownCloud
*
* @author Vincent Petry
* @copyright 2016 Vincent Petry <pvince81@owncloud.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* comment 3 of the License, or any later comment.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with this library.  If not, see <http://www.gnu.org/licenses/>.
*
*/

describe('OCA.Comments.CommentsTabView tests', function() {
	var view, fileInfoModel;
	var fetchStub;
	var avatarStub;
	var testComments;
	var clock;

	/**
	 * Creates a dummy message with the given length
	 *
	 * @param {int} len length
	 * @return {string} message
	 */
	function createMessageWithLength(len) {
		var bigMessage = '';
		for (var i = 0; i < len; i++) {
			bigMessage += 'a';
		}
		return bigMessage;
	}

	beforeEach(function() {
		clock = sinon.useFakeTimers(Date.UTC(2016, 1, 3, 10, 5, 9));
		fetchStub = sinon.stub(OCA.Comments.CommentCollection.prototype, 'fetchNext');
		avatarStub = sinon.stub($.fn, 'avatar');
		view = new OCA.Comments.CommentsTabView();
		fileInfoModel = new OCA.Files.FileInfoModel({
			id: 5,
			name: 'One.txt',
			mimetype: 'text/plain',
			permissions: 31,
			path: '/subdir',
			size: 123456789,
			etag: 'abcdefg',
			mtime: Date.UTC(2016, 1, 0, 0, 0, 0)
		});
		view.render();
		var comment1 = new OCA.Comments.CommentModel({
			id: 1,
			actorType: 'users',
			actorId: 'user1',
			actorDisplayName: 'User One',
			objectType: 'files',
			objectId: 5,
			message: 'First',
			creationDateTime: new Date(Date.UTC(2016, 1, 3, 10, 5, 0)).toUTCString()
		});
		var comment2 = new OCA.Comments.CommentModel({
			id: 2,
			actorType: 'users',
			actorId: 'user2',
			actorDisplayName: 'User Two',
			objectType: 'files',
			objectId: 5,
			message: 'Second\nNewline',
			creationDateTime: new Date(Date.UTC(2016, 1, 3, 10, 0, 0)).toUTCString()
		});
		var comment3 = new OCA.Comments.CommentModel({
			id: 3,
			actorId: 'anotheruser',
			actorDisplayName: 'Another User',
			actorType: 'users',
			verb: 'comment',
			message: 'Hail to thee, @macbeth. Yours faithfully, @banquo',
			creationDateTime: new Date(Date.UTC(2016, 1, 3, 10, 5, 9)).toUTCString(),
			mentions: {
				0: {
					mentionDisplayName: "Thane of Cawdor",
					mentionId: "macbeth",
					mentionTye: "user"
				},
				1: {
					mentionDisplayName: "Lord Banquo",
					mentionId: "banquo",
					mentionTye: "user"
				}
			}
		});

		testComments = [comment1, comment2, comment3];
	});
	afterEach(function() {
		view.remove();
		view = undefined;
		fetchStub.restore();
		avatarStub.restore();
		clock.restore();
	});
	describe('rendering', function() {
		it('reloads matching comments when setting file info model', function() {
			view.setFileInfo(fileInfoModel);
			expect(fetchStub.calledOnce).toEqual(true);
		});

		it('renders loading icon while fetching comments', function() {
			view.setFileInfo(fileInfoModel);
			view.collection.trigger('request');

			expect(view.$el.find('.loading').length).toEqual(1);
			expect(view.$el.find('.comments li').length).toEqual(0);
		});

		it('renders comments', function() {
			view.setFileInfo(fileInfoModel);
			view.collection.set(testComments);

			var $comments = view.$el.find('.comments>li');
			expect($comments.length).toEqual(3);
			var $item = $comments.eq(0);
			expect($item.find('.author').text()).toEqual('User One');
			expect($item.find('.date').text()).toEqual('seconds ago');
			expect($item.find('.message').text()).toEqual('First');

			$item = $comments.eq(1);
			expect($item.find('.author').text()).toEqual('User Two');
			expect($item.find('.date').text()).toEqual('5 minutes ago');
			expect($item.find('.message').html()).toEqual('Second<br>Newline');
		});

		it('renders comments from deleted user differently', function() {
			testComments[0].set('actorType', 'deleted_users', {silent: true});
			view.collection.set(testComments);

			var $item = view.$el.find('.comment[data-id=1]');
			expect($item.find('.author').text()).toEqual('[Deleted user]');
			expect($item.find('.avatar').attr('data-username')).not.toBeDefined();
		});

		it('renders mentioned user id to avatar and displayname', function() {
			view.collection.set(testComments);

			var $comment = view.$el.find('.comment[data-id=3] .message');
			expect($comment.length).toEqual(1);
			expect($comment.find('.avatar[data-user=macbeth]').length).toEqual(1);
			expect($comment.find('strong:first').text()).toEqual('Thane of Cawdor');
			expect($comment.find('.avatar[data-user=macbeth] ~ .contactsmenu-popover').length).toEqual(1);

			expect($comment.find('.avatar[data-user=banquo]').length).toEqual(1);
			expect($comment.find('.avatar[data-user=banquo] ~ strong').text()).toEqual('Lord Banquo');
			expect($comment.find('.avatar[data-user=banquo] ~ .contactsmenu-popover').length).toEqual(1);
		});

	});
	describe('more comments', function() {
		var hasMoreResultsStub;

		beforeEach(function() {
			view.collection.set(testComments);
			hasMoreResultsStub = sinon.stub(OCA.Comments.CommentCollection.prototype, 'hasMoreResults');
		});
		afterEach(function() {
			hasMoreResultsStub.restore();
		});

		it('shows "More comments" button when more comments are available', function() {
			hasMoreResultsStub.returns(true);
			view.collection.trigger('sync');

			expect(view.$el.find('.showMore').hasClass('hidden')).toEqual(false);
		});
		it('does not show "More comments" button when more comments are available', function() {
			hasMoreResultsStub.returns(false);
			view.collection.trigger('sync');

			expect(view.$el.find('.showMore').hasClass('hidden')).toEqual(true);
		});
		it('fetches and appends the next page when clicking the "More" button', function() {
			hasMoreResultsStub.returns(true);

			expect(fetchStub.notCalled).toEqual(true);

			view.$el.find('.showMore').trigger('click');

			expect(fetchStub.calledOnce).toEqual(true);
		});
		it('appends comment to the list when added to collection', function() {
			var comment4 = new OCA.Comments.CommentModel({
				id: 4,
				actorType: 'users',
				actorId: 'user3',
				actorDisplayName: 'User Three',
				objectType: 'files',
				objectId: 5,
				message: 'Third',
				creationDateTime: new Date(Date.UTC(2016, 1, 3, 5, 0, 0)).toUTCString()
			});

			view.collection.add(comment4);

			expect(view.$el.find('.comments>li').length).toEqual(4);

			var $item = view.$el.find('.comments>li').eq(3);
			expect($item.find('.author').text()).toEqual('User Three');
			expect($item.find('.date').text()).toEqual('5 hours ago');
			expect($item.find('.message').html()).toEqual('Third');
		});
	});
	describe('posting comments', function() {
		var createStub;
		var currentUserStub;
		var $newCommentForm;

		beforeEach(function() {
			view.collection.set(testComments);
			createStub = sinon.stub(OCA.Comments.CommentCollection.prototype, 'create');
			currentUserStub = sinon.stub(OC, 'getCurrentUser');
			currentUserStub.returns({
				uid: 'testuser',
				displayName: 'Test User'
			});

			$newCommentForm = view.$el.find('.newCommentForm');

			// Required for the absolute selector used to find the new comment
			// after a successful creation in _onSubmitSuccess.
			$('#testArea').append(view.$el);
		});
		afterEach(function() {
			createStub.restore();
			currentUserStub.restore();
		});

		it('creates a new comment when clicking post button', function() {
			$newCommentForm.find('.message').text('New message');
			$newCommentForm.submit();

			expect(createStub.calledOnce).toEqual(true);
			expect(createStub.lastCall.args[0]).toEqual({
				actorId: 'testuser',
				actorDisplayName: 'Test User',
				actorType: 'users',
				verb: 'comment',
				message: 'New message',
				creationDateTime: new Date(Date.UTC(2016, 1, 3, 10, 5, 9)).toUTCString()
			});
		});
		it('creates a new comment when typing enter', function() {
			$newCommentForm.find('.message').text('New message');
			var keydownEvent = new $.Event('keydown', {keyCode: 13});
			$newCommentForm.find('.message').trigger(keydownEvent);

			expect(createStub.calledOnce).toEqual(true);
			expect(createStub.lastCall.args[0]).toEqual({
				actorId: 'testuser',
				actorDisplayName: 'Test User',
				actorType: 'users',
				verb: 'comment',
				message: 'New message',
				creationDateTime: new Date(Date.UTC(2016, 1, 3, 10, 5, 9)).toUTCString()
			});
			expect(keydownEvent.isDefaultPrevented()).toEqual(true);
		});
		it('creates a new mention when typing enter in the autocomplete popover', function() {
			var autoCompleteStub = sinon.stub(view, '_onAutoComplete');
			autoCompleteStub.callsArgWith(1, [{"id":"userId", "label":"User Name", "source":"users"}]);

			// Force the autocomplete to be initialized
			view._initAutoComplete($newCommentForm.find('.message'));

			// PhantomJS does not seem to handle typing in a contenteditable, so
			// some tricks are needed to show the autocomplete popover.
			//
			// Instead of sending key events to type "@u" the characters are
			// programatically set in the input field.
			$newCommentForm.find('.message').text('Mention to @u');

			// When focusing on the input field the caret is not guaranteed to
			// be at the end; instead of calling "focus()" on the input field
			// the caret is explicitly set at the end of the input field, that
			// is, after "@u".
			var range = document.createRange();
			range.selectNodeContents($newCommentForm.find('.message')[0]);
			range.collapse(false);
			var selection = window.getSelection();
			selection.removeAllRanges();
			selection.addRange(range);

			// As PhantomJS does not handle typing in a contenteditable the key
			// typed here is in practice ignored by At.js, but despite that it
			// will cause the popover to be shown.
			$newCommentForm.find('.message').trigger(new $.Event('keydown', {keyCode: 's'}));
			$newCommentForm.find('.message').trigger(new $.Event('keyup', {keyCode: 's'}));

			expect(autoCompleteStub.calledOnce).toEqual(true);

			var keydownEvent = new $.Event('keydown', {keyCode: 13});
			$newCommentForm.find('.message').trigger(keydownEvent);

			expect(createStub.calledOnce).toEqual(false);
			expect($newCommentForm.find('.message').html()).toContain('Mention to <span');
			expect($newCommentForm.find('.message').html()).toContain('<span class="avatar"');
			expect($newCommentForm.find('.message').html()).toContain('<strong>User Name</strong>');
			expect($newCommentForm.find('.message').text()).not.toContain('@');
			// In this case the default behaviour is prevented by the
			// "onKeydown" event handler of At.js.
			expect(keydownEvent.isDefaultPrevented()).toEqual(true);
		});
		it('creates a new line when typing shift+enter', function() {
			$newCommentForm.find('.message').text('New message');
			var keydownEvent = new $.Event('keydown', {keyCode: 13, shiftKey: true});
			$newCommentForm.find('.message').trigger(keydownEvent);

			expect(createStub.calledOnce).toEqual(false);
			// PhantomJS does not seem to handle typing in a contenteditable, so
			// instead of looking for a new line the best that can be done is
			// checking that the default behaviour would have been executed.
			expect($newCommentForm.find('.message').text()).toContain('New message');
			expect(keydownEvent.isDefaultPrevented()).toEqual(false);
		});
		it('creates a new comment with mentions when clicking post button', function() {
			$newCommentForm.find('.message').text('New message @anotheruser');
			$newCommentForm.submit();

			var createStubExpectedData = {
				actorId: 'testuser',
				actorDisplayName: 'Test User',
				actorType: 'users',
				verb: 'comment',
				message: 'New message @anotheruser',
				creationDateTime: new Date(Date.UTC(2016, 1, 3, 10, 5, 9)).toUTCString()
			};

			expect(createStub.calledOnce).toEqual(true);
			expect(createStub.lastCall.args[0]).toEqual(createStubExpectedData);

			var model = new OCA.Comments.CommentModel(_.extend({id: 4}, createStubExpectedData));
			var fetchStub = sinon.stub(model, 'fetch');
			// simulate the fact that create adds the model to the collection
			view.collection.add(model, {at: 0});
			createStub.yieldTo('success', model);

			expect(fetchStub.calledOnce).toEqual(true);

			// simulate the fact that fetch sets the attribute
			model.set('mentions', {
				0: {
					mentionDisplayName: "Another User",
					mentionId: "anotheruser",
					mentionTye: "user"
				}
			});
			fetchStub.yieldTo('success', model);

			// comment was added to the list
			var $comment = view.$el.find('.comment[data-id=4]');
			expect($comment.length).toEqual(1);
			var $message = $comment.find('.message');
			expect($message.html()).toContain('New message');
			expect($message.find('.avatar').length).toEqual(1);
			expect($message.find('.avatar[data-user=anotheruser]').length).toEqual(1);
			expect($message.find('.avatar[data-user=anotheruser] ~ strong').text()).toEqual('Another User');
			expect($message.find('.avatar[data-user=anotheruser] ~ .contactsmenu-popover').length).toEqual(1);
		});
		it('does not create a comment if the field is empty', function() {
			$newCommentForm.find('.message').val('   ');
			$newCommentForm.submit();

			expect(createStub.notCalled).toEqual(true);
		});
		it('does not create a comment if the field length is too large', function() {
			var bigMessage = '';
			for (var i = 0; i < view._commentMaxLength * 2; i++) {
				bigMessage += 'a';
			}
			$newCommentForm.find('.message').val(bigMessage);
			$newCommentForm.submit();

			expect(createStub.notCalled).toEqual(true);
		});
		describe('limit indicator', function() {
			var tooltipStub;
			var $message;
			var $submitButton;

			beforeEach(function() {
				tooltipStub = sinon.stub($.fn, 'tooltip');
				$message = $newCommentForm.find('.message');
				$submitButton = $newCommentForm.find('.submit');
			});
			afterEach(function() {
				tooltipStub.restore();
			});

			it('does not displays tooltip when limit is far away', function() {
				$message.val(createMessageWithLength(3));
				$message.trigger('change');

				expect(tooltipStub.calledWith('show')).toEqual(false);
				expect($submitButton.prop('disabled')).toEqual(false);
				expect($message.hasClass('error')).toEqual(false);
			});
			it('displays tooltip when limit is almost reached', function() {
				$message.text(createMessageWithLength(view._commentMaxLength - 2));
				$message.trigger('change');

				expect(tooltipStub.calledWith('show')).toEqual(true);
				expect($submitButton.prop('disabled')).toEqual(false);
				expect($message.hasClass('error')).toEqual(false);
			});
			it('displays tooltip and disabled button when limit is exceeded', function() {
				$message.text(createMessageWithLength(view._commentMaxLength + 2));
				$message.trigger('change');

				expect(tooltipStub.calledWith('show')).toEqual(true);
				expect($submitButton.prop('disabled')).toEqual(true);
				expect($message.hasClass('error')).toEqual(true);
			});
		});
	});
	describe('editing comments', function() {
		var saveStub;
		var fetchStub;
		var currentUserStub;

		beforeEach(function() {
			saveStub = sinon.stub(OCA.Comments.CommentModel.prototype, 'save');
			fetchStub = sinon.stub(OCA.Comments.CommentModel.prototype, 'fetch');
			currentUserStub = sinon.stub(OC, 'getCurrentUser');
			currentUserStub.returns({
				uid: 'testuser',
				displayName: 'Test User'
			});
			view.collection.add({
				id: 1,
				actorId: 'testuser',
				actorDisplayName: 'Test User',
				actorType: 'users',
				verb: 'comment',
				message: 'New message',
				creationDateTime: new Date(Date.UTC(2016, 1, 3, 10, 5, 9)).toUTCString()
			});
			view.collection.add({
				id: 2,
				actorId: 'anotheruser',
				actorDisplayName: 'Another User',
				actorType: 'users',
				verb: 'comment',
				message: 'New message from another user',
				creationDateTime: new Date(Date.UTC(2016, 1, 3, 10, 5, 9)).toUTCString(),
			});
			view.collection.add({
				id: 3,
				actorId: 'testuser',
				actorDisplayName: 'Test User',
				actorType: 'users',
				verb: 'comment',
				message: 'Hail to thee, @macbeth. Yours faithfully, @banquo',
				creationDateTime: new Date(Date.UTC(2016, 1, 3, 10, 5, 9)).toUTCString(),
				mentions: {
					0: {
						mentionDisplayName: "Thane of Cawdor",
						mentionId: "macbeth",
						mentionTye: "user"
					},
					1: {
						mentionDisplayName: "Lord Banquo",
						mentionId: "banquo",
						mentionTye: "user"
					}
				}
			});
		});
		afterEach(function() {
			saveStub.restore();
			fetchStub.restore();
			currentUserStub.restore();
		});

		it('shows edit link for owner comments', function() {
			var $comment = view.$el.find('.comment[data-id=1]');
			expect($comment.length).toEqual(1);
			$comment.find('.action.more').trigger('click');
			expect($comment.find('.action.edit').length).toEqual(1);
		});

		it('does not show edit link for other user\'s comments', function() {
			var $comment = view.$el.find('.comment[data-id=2]');
			expect($comment.length).toEqual(1);
			$comment.find('.action.more').trigger('click');
			expect($comment.find('.action.edit').length).toEqual(0);
		});

		it('shows edit form when clicking edit', function() {
			var $comment = view.$el.find('.comment[data-id=1]');
			$comment.find('.action.more').trigger('click');
			$comment.find('.action.edit').trigger('click');

			expect($comment.hasClass('hidden')).toEqual(true);
			var $formRow = view.$el.find('.newCommentRow.comment[data-id=1]');
			expect($formRow.length).toEqual(1);
		});

		it('saves message and updates comment item when clicking save', function() {
			var $comment = view.$el.find('.comment[data-id=1]');
			$comment.find('.action.more').trigger('click');
			$comment.find('.action.edit').trigger('click');

			var $formRow = view.$el.find('.newCommentRow.comment[data-id=1]');
			expect($formRow.length).toEqual(1);

			$formRow.find('div.message').text('modified message');
			$formRow.find('form').submit();

			expect(saveStub.calledOnce).toEqual(true);
			expect(saveStub.lastCall.args[0]).toEqual({
				message: 'modified message'
			});

			var model = view.collection.get(1);
			// simulate the fact that save sets the attribute
			model.set('message', 'modified\nmessage');
			saveStub.yieldTo('success', model);
			view.collection.get(model);

			expect(fetchStub.called).toEqual(true);
			fetchStub.yieldTo('success', model);

			// original comment element is visible again
			expect($comment.hasClass('hidden')).toEqual(false);
			// and its message was updated
			expect($comment.find('.message').html()).toEqual('modified<br>message');

			// form row is gone
			$formRow = view.$el.find('.newCommentRow.comment[data-id=1]');
			expect($formRow.length).toEqual(0);
		});

		it('saves message and updates comment item with mentions when clicking save', function() {
			var $comment = view.$el.find('.comment[data-id=3]');
			$comment.find('.action.more').trigger('click');
			$comment.find('.action.edit').trigger('click');

			var $formRow = view.$el.find('.newCommentRow.comment[data-id=3]');
			expect($formRow.length).toEqual(1);

			$formRow.find('div.message').text('modified\nmessage @anotheruser');
			$formRow.find('form').submit();

			expect(saveStub.calledOnce).toEqual(true);
			expect(saveStub.lastCall.args[0]).toEqual({
				message: 'modified\nmessage @anotheruser'
			});

			var model = view.collection.get(3);
			// simulate the fact that save sets the attribute
			model.set('message', 'modified\nmessage @anotheruser');
			saveStub.yieldTo('success', model);

			expect(fetchStub.called).toEqual(true);

			// simulate the fact that fetch sets the attribute
			model.set('mentions', {
				0: {
					mentionDisplayName: "Another User",
					mentionId: "anotheruser",
					mentionTye: "user"
				}
			});
			fetchStub.yieldTo('success', model);

			// original comment element is visible again
			expect($comment.hasClass('hidden')).toEqual(false);
			// and its message was updated
			var $message = $comment.find('.message');
			expect($message.html()).toContain('modified<br>message');
			expect($message.find('.avatar').length).toEqual(1);
			expect($message.find('.avatar[data-user=anotheruser]').length).toEqual(1);
			expect($message.find('.avatar[data-user=anotheruser] ~ strong').text()).toEqual('Another User');
			expect($message.find('.avatar[data-user=anotheruser] ~ .contactsmenu-popover').length).toEqual(1);

			// form row is gone
			$formRow = view.$el.find('.newCommentRow.comment[data-id=3]');
			expect($formRow.length).toEqual(0);
		});

		it('restores original comment when cancelling', function() {
			var $comment = view.$el.find('.comment[data-id=1]');
			$comment.find('.action.more').trigger('click');
			$comment.find('.action.edit').trigger('click');

			var $formRow = view.$el.find('.newCommentRow.comment[data-id=1]');
			expect($formRow.length).toEqual(1);

			$formRow.find('textarea').val('modified\nmessage');
			$formRow.find('.cancel').trigger('click');

			expect(saveStub.notCalled).toEqual(true);

			// original comment element is visible again
			expect($comment.hasClass('hidden')).toEqual(false);
			// and its message was not updated
			expect($comment.find('.message').html()).toEqual('New message');

			// form row is gone
			$formRow = view.$el.find('.newCommentRow.comment[data-id=1]');
			expect($formRow.length).toEqual(0);
		});

		it('destroys model when clicking delete', function() {
			var destroyStub = sinon.stub(OCA.Comments.CommentModel.prototype, 'destroy');
			var $comment = view.$el.find('.comment[data-id=1]');
			$comment.find('.action.more').trigger('click');
			$comment.find('.action.delete').trigger('click');

			expect(destroyStub.calledOnce).toEqual(true);
			expect(destroyStub.thisValues[0].id).toEqual(1);

			destroyStub.yieldTo('success');

			// original comment element is gone
			$comment = view.$el.find('.comment[data-id=1]');
			expect($comment.length).toEqual(0);

			destroyStub.restore();
		});
		it('does not submit comment if the field is empty', function() {
			var $comment = view.$el.find('.comment[data-id=1]');
			$comment.find('.action.edit').trigger('click');
			$comment.find('.message').val('   ');
			$comment.find('form').submit();

			expect(saveStub.notCalled).toEqual(true);
		});
		it('does not submit comment if the field length is too large', function() {
			var $comment = view.$el.find('.comment[data-id=1]');
			$comment.find('.action.edit').trigger('click');
			$comment.find('.message').val(createMessageWithLength(view._commentMaxLength * 2));
			$comment.find('form').submit();

			expect(saveStub.notCalled).toEqual(true);
		});
	});
	describe('read marker', function() {
		var updateMarkerStub;

		beforeEach(function() {
			updateMarkerStub = sinon.stub(OCA.Comments.CommentCollection.prototype, 'updateReadMarker');
		});
		afterEach(function() {
			updateMarkerStub.restore();
		});

		it('resets the read marker after REPORT', function() {
			testComments[0].set('isUnread', true, {silent: true});
			testComments[1].set('isUnread', true, {silent: true});
			view.collection.set(testComments);
			view.collection.trigger('sync', 'REPORT');

			expect(updateMarkerStub.calledOnce).toEqual(true);
			expect(updateMarkerStub.lastCall.args[0]).toBeFalsy();
		});
		it('does not reset the read marker if there was no unread comments', function() {
			view.collection.set(testComments);
			view.collection.trigger('sync', 'REPORT');

			expect(updateMarkerStub.notCalled).toEqual(true);
		});
		it('does not reset the read marker when posting comments', function() {
			testComments[0].set('isUnread', true, {silent: true});
			testComments[1].set('isUnread', true, {silent: true});
			view.collection.set(testComments);
			view.collection.trigger('sync', 'POST');

			expect(updateMarkerStub.notCalled).toEqual(true);
		});
	});
});
