package org.briarproject.privategroup;

import org.briarproject.ValidatorTestCase;
import org.briarproject.api.FormatException;
import org.briarproject.api.clients.BdfMessageContext;
import org.briarproject.api.data.BdfDictionary;
import org.briarproject.api.data.BdfList;
import org.briarproject.api.identity.Author;
import org.briarproject.api.identity.AuthorId;
import org.briarproject.api.privategroup.PrivateGroup;
import org.briarproject.api.privategroup.PrivateGroupFactory;
import org.briarproject.api.privategroup.invitation.GroupInvitationFactory;
import org.briarproject.api.sync.InvalidMessageException;
import org.briarproject.api.sync.MessageId;
import org.jmock.Expectations;
import org.junit.Test;

import java.security.GeneralSecurityException;

import static org.briarproject.TestUtils.getRandomBytes;
import static org.briarproject.TestUtils.getRandomId;
import static org.briarproject.TestUtils.getRandomString;
import static org.briarproject.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
import static org.briarproject.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH;
import static org.briarproject.api.privategroup.GroupMessageFactory.SIGNING_LABEL_JOIN;
import static org.briarproject.api.privategroup.GroupMessageFactory.SIGNING_LABEL_POST;
import static org.briarproject.api.privategroup.MessageType.JOIN;
import static org.briarproject.api.privategroup.MessageType.POST;
import static org.briarproject.api.privategroup.PrivateGroupConstants.GROUP_SALT_LENGTH;
import static org.briarproject.api.privategroup.PrivateGroupConstants.MAX_GROUP_POST_BODY_LENGTH;
import static org.briarproject.api.privategroup.invitation.GroupInvitationFactory.SIGNING_LABEL_INVITE;
import static org.briarproject.privategroup.GroupConstants.KEY_INITIAL_JOIN_MSG;
import static org.briarproject.privategroup.GroupConstants.KEY_MEMBER_ID;
import static org.briarproject.privategroup.GroupConstants.KEY_MEMBER_NAME;
import static org.briarproject.privategroup.GroupConstants.KEY_MEMBER_PUBLIC_KEY;
import static org.briarproject.privategroup.GroupConstants.KEY_PARENT_MSG_ID;
import static org.briarproject.privategroup.GroupConstants.KEY_PREVIOUS_MSG_ID;
import static org.briarproject.privategroup.GroupConstants.KEY_READ;
import static org.briarproject.privategroup.GroupConstants.KEY_TIMESTAMP;
import static org.briarproject.privategroup.GroupConstants.KEY_TYPE;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

public class GroupMessageValidatorTest extends ValidatorTestCase {

	private final PrivateGroupFactory privateGroupFactory =
			context.mock(PrivateGroupFactory.class);
	private final GroupInvitationFactory groupInvitationFactory =
			context.mock(GroupInvitationFactory.class);


	private final String creatorName = "Member Name";
	private final String memberName = "Member Name";
	private final byte[] creatorKey = getRandomBytes(MAX_PUBLIC_KEY_LENGTH);
	private final byte[] memberKey = getRandomBytes(MAX_PUBLIC_KEY_LENGTH);
	private final byte[] creatorSignature =
			getRandomBytes(MAX_SIGNATURE_LENGTH);
	private final byte[] signature = getRandomBytes(MAX_SIGNATURE_LENGTH);
	private final Author member =
			new Author(new AuthorId(getRandomId()), memberName, memberKey);
	private final Author creator =
			new Author(new AuthorId(getRandomId()), creatorName, creatorKey);
	private final long inviteTimestamp = 42L;
	private final PrivateGroup privateGroup =
			new PrivateGroup(group, "Private Group Name", creator,
					getRandomBytes(GROUP_SALT_LENGTH));
	private final BdfList token = BdfList.of("token");
	private MessageId parentId = new MessageId(getRandomId());
	private MessageId previousMsgId = new MessageId(getRandomId());
	private String postContent = "Post text";

	private GroupMessageValidator validator =
			new GroupMessageValidator(privateGroupFactory, clientHelper,
					metadataEncoder, clock, authorFactory,
					groupInvitationFactory);

	@Test(expected = FormatException.class)
	public void testRejectTooShortMemberName() throws Exception {
		BdfList list = BdfList.of(JOIN.getInt(), "", memberKey, null,
				signature);
		validator.validateMessage(message, group, list);
	}

	@Test(expected = FormatException.class)
	public void testRejectTooLongMemberName() throws Exception {
		BdfList list = BdfList.of(JOIN.getInt(),
				getRandomString(MAX_AUTHOR_NAME_LENGTH + 1), memberKey, null,
				signature);
		validator.validateMessage(message, group, list);
	}

	@Test(expected = FormatException.class)
	public void testRejectTooShortMemberKey() throws Exception {
		BdfList list = BdfList.of(JOIN.getInt(), memberName, new byte[0], null,
				signature);
		validator.validateMessage(message, group, list);
	}

	@Test(expected = FormatException.class)
	public void testRejectTooLongMemberKey() throws Exception {
		BdfList list = BdfList.of(JOIN.getInt(), memberName,
				getRandomBytes(MAX_PUBLIC_KEY_LENGTH + 1), null,
				signature);
		validator.validateMessage(message, group, list);
	}

	@Test(expected = FormatException.class)
	public void testRejectNonRawMemberKey() throws Exception {
		BdfList list =
				BdfList.of(JOIN.getInt(), memberName, "non raw key", null,
						signature);
		validator.validateMessage(message, group, list);
	}

	// JOIN message

	@Test(expected = FormatException.class)
	public void testRejectsTooShortJoinMessage() throws Exception {
		BdfList list = BdfList.of(JOIN.getInt(), creatorName, creatorKey,
				null);
		validator.validateMessage(message, group, list);
	}

	@Test(expected = FormatException.class)
	public void testRejectsTooLongJoinMessage() throws Exception {
		expectCreateAuthor(creator);
		BdfList list = BdfList.of(JOIN.getInt(), creatorName, creatorKey,
				null, signature, "");
		validator.validateMessage(message, group, list);
	}

	@Test(expected = FormatException.class)
	public void testRejectsJoinMessageWithNonListInvitation() throws Exception {
		expectCreateAuthor(creator);
		expectParsePrivateGroup();
		BdfList list = BdfList.of(JOIN.getInt(), creatorName, creatorKey,
				"not a list", signature);
		validator.validateMessage(message, group, list);
	}

	@Test
	public void testAcceptCreatorJoinMessage() throws Exception {
		final BdfList invite = null;
		expectJoinMessage(creator, invite, true, true);
		BdfList list = BdfList.of(JOIN.getInt(), creatorName, creatorKey,
				invite, signature);
		BdfMessageContext messageContext =
				validator.validateMessage(message, group, list);
		assertMessageContext(messageContext, creator);
		assertTrue(messageContext.getDictionary()
				.getBoolean(KEY_INITIAL_JOIN_MSG));
	}

	@Test(expected = InvalidMessageException.class)
	public void testRejectsMemberJoinMessageWithoutInvitation()
			throws Exception {
		expectCreateAuthor(member);
		expectParsePrivateGroup();
		BdfList list = BdfList.of(JOIN.getInt(), memberName, memberKey, null,
				signature);
		validator.validateMessage(message, group, list);
	}

	@Test(expected = FormatException.class)
	public void testRejectsJoinMessageWithTooShortInvitation() throws Exception {
		BdfList invite = BdfList.of(inviteTimestamp);
		expectCreateAuthor(member);
		expectParsePrivateGroup();
		BdfList list = BdfList.of(JOIN.getInt(), memberName, memberKey, invite,
				signature);
		validator.validateMessage(message, group, list);
	}

	@Test(expected = FormatException.class)
	public void testRejectsJoinMessageWithTooLongInvitation() throws Exception {
		BdfList invite = BdfList.of(inviteTimestamp, creatorSignature, "");
		expectCreateAuthor(member);
		expectParsePrivateGroup();
		BdfList list = BdfList.of(JOIN.getInt(), memberName, memberKey, invite,
				signature);
		validator.validateMessage(message, group, list);
	}

	@Test(expected = InvalidMessageException.class)
	public void testRejectsJoinMessageWithEqualInvitationTime()
			throws Exception {
		BdfList invite = BdfList.of(message.getTimestamp(), creatorSignature);
		expectCreateAuthor(member);
		expectParsePrivateGroup();
		BdfList list = BdfList.of(JOIN.getInt(), memberName, memberKey, invite,
				signature);
		validator.validateMessage(message, group, list);
	}

	@Test(expected = InvalidMessageException.class)
	public void testRejectsJoinMessageWithLaterInvitationTime()
			throws Exception {
		BdfList invite =
				BdfList.of(message.getTimestamp() + 1, creatorSignature);
		expectCreateAuthor(member);
		expectParsePrivateGroup();
		BdfList list = BdfList.of(JOIN.getInt(), memberName, memberKey, invite,
				signature);
		validator.validateMessage(message, group, list);
	}

	@Test(expected = FormatException.class)
	public void testRejectsJoinMessageWithNonRawCreatorSignature()
			throws Exception {
		BdfList invite = BdfList.of(inviteTimestamp, "non-raw signature");
		expectCreateAuthor(member);
		expectParsePrivateGroup();
		BdfList list = BdfList.of(JOIN.getInt(), memberName, memberKey, invite,
				signature);
		validator.validateMessage(message, group, list);
	}

	@Test(expected = FormatException.class)
	public void testRejectsJoinMessageWithTooShortCreatorSignature()
			throws Exception {
		BdfList invite = BdfList.of(inviteTimestamp, new byte[0]);
		expectCreateAuthor(member);
		expectParsePrivateGroup();
		BdfList list = BdfList.of(JOIN.getInt(), memberName, memberKey, invite,
				signature);
		validator.validateMessage(message, group, list);
	}

	@Test(expected = FormatException.class)
	public void testRejectsJoinMessageWithTooLongCreatorSignature()
			throws Exception {
		BdfList invite = BdfList.of(inviteTimestamp,
				getRandomBytes(MAX_SIGNATURE_LENGTH + 1));
		expectCreateAuthor(member);
		expectParsePrivateGroup();
		BdfList list = BdfList.of(JOIN.getInt(), memberName, memberKey, invite,
				signature);
		validator.validateMessage(message, group, list);
	}

	@Test(expected = InvalidMessageException.class)
	public void testRejectsJoinMessageWithInvalidCreatorSignature()
			throws Exception {
		BdfList invite = BdfList.of(inviteTimestamp, creatorSignature);
		expectJoinMessage(member, invite, false, true);
		BdfList list = BdfList.of(JOIN.getInt(), memberName, memberKey, invite,
				signature);
		validator.validateMessage(message, group, list);
	}

	@Test(expected = InvalidMessageException.class)
	public void testRejectsJoinMessageWithInvalidMemberSignature()
			throws Exception {
		BdfList invite = BdfList.of(inviteTimestamp, creatorSignature);
		expectJoinMessage(member, invite, true, false);
		BdfList list = BdfList.of(JOIN.getInt(), memberName, memberKey, invite,
				signature);
		validator.validateMessage(message, group, list);
	}

	@Test
	public void testAcceptMemberJoinMessage() throws Exception {
		BdfList invite = BdfList.of(inviteTimestamp, creatorSignature);
		expectJoinMessage(member, invite, true, true);
		BdfList list = BdfList.of(JOIN.getInt(), memberName, memberKey, invite,
				signature);
		BdfMessageContext messageContext =
				validator.validateMessage(message, group, list);
		assertMessageContext(messageContext, member);
		assertFalse(messageContext.getDictionary()
				.getBoolean(KEY_INITIAL_JOIN_MSG));
	}

	private void expectCreateAuthor(final Author member) {
		context.checking(new Expectations() {{
			oneOf(authorFactory)
					.createAuthor(member.getName(), member.getPublicKey());
			will(returnValue(member));
		}});
	}

	private void expectParsePrivateGroup() throws Exception {
		context.checking(new Expectations() {{
			oneOf(privateGroupFactory).parsePrivateGroup(group);
			will(returnValue(privateGroup));
		}});
	}

	private void expectJoinMessage(final Author member, final BdfList invite,
			final boolean creatorSigValid, final boolean memberSigValid)
			throws Exception {
		final BdfList signed =
				BdfList.of(group.getId(), message.getTimestamp(), JOIN.getInt(),
						member.getName(), member.getPublicKey(), invite);
		expectCreateAuthor(member);
		expectParsePrivateGroup();
		context.checking(new Expectations() {{
			if (invite != null) {
				oneOf(groupInvitationFactory)
						.createInviteToken(creator.getId(), member.getId(),
								privateGroup.getId(), inviteTimestamp);
				will(returnValue(token));
				oneOf(clientHelper)
						.verifySignature(SIGNING_LABEL_INVITE, creatorSignature,
								creatorKey, token);
				if (!memberSigValid)
					will(throwException(new GeneralSecurityException()));
			}
			if (memberSigValid) {
				oneOf(clientHelper)
						.verifySignature(SIGNING_LABEL_JOIN, signature,
								member.getPublicKey(), signed);
				if (!creatorSigValid)
					will(throwException(new GeneralSecurityException()));
			}
		}});
	}

	// POST Message

	@Test(expected = FormatException.class)
	public void testRejectsTooShortPost() throws Exception {
		BdfList list =
				BdfList.of(POST.getInt(), memberName, memberKey, parentId,
						previousMsgId, postContent);
		expectCreateAuthor(member);
		validator.validateMessage(message, group, list);
	}

	@Test(expected = FormatException.class)
	public void testRejectsTooLongPost() throws Exception {
		BdfList list =
				BdfList.of(POST.getInt(), memberName, memberKey, parentId,
						previousMsgId, postContent, signature, "");
		validator.validateMessage(message, group, list);
	}

	@Test(expected = FormatException.class)
	public void testRejectsPostWithNonRawParentId() throws Exception {
		BdfList list =
				BdfList.of(POST.getInt(), memberName, memberKey, "non-raw",
						previousMsgId, postContent, signature);
		expectCreateAuthor(member);
		validator.validateMessage(message, group, list);
	}

	@Test(expected = FormatException.class)
	public void testRejectsPostWithTooShortParentId() throws Exception {
		BdfList list =
				BdfList.of(POST.getInt(), memberName, memberKey,
						getRandomBytes(MessageId.LENGTH - 1), previousMsgId,
						postContent, signature);
		expectCreateAuthor(member);
		validator.validateMessage(message, group, list);
	}

	@Test(expected = FormatException.class)
	public void testRejectsPostWithTooLongParentId() throws Exception {
		BdfList list =
				BdfList.of(POST.getInt(), memberName, memberKey,
						getRandomBytes(MessageId.LENGTH + 1), previousMsgId,
						postContent, signature);
		expectCreateAuthor(member);
		validator.validateMessage(message, group, list);
	}

	@Test(expected = FormatException.class)
	public void testRejectsPostWithNonRawPreviousMsgId() throws Exception {
		BdfList list =
				BdfList.of(POST.getInt(), memberName, memberKey, parentId,
						"non-raw", postContent, signature);
		expectCreateAuthor(member);
		validator.validateMessage(message, group, list);
	}

	@Test(expected = FormatException.class)
	public void testRejectsPostWithTooShortPreviousMsgId() throws Exception {
		BdfList list =
				BdfList.of(POST.getInt(), memberName, memberKey, parentId,
						getRandomBytes(MessageId.LENGTH - 1),
						postContent, signature);
		expectCreateAuthor(member);
		validator.validateMessage(message, group, list);
	}

	@Test(expected = FormatException.class)
	public void testRejectsPostWithTooLongPreviousMsgId() throws Exception {
		BdfList list =
				BdfList.of(POST.getInt(), memberName, memberKey, parentId,
						getRandomBytes(MessageId.LENGTH + 1),
						postContent, signature);
		expectCreateAuthor(member);
		validator.validateMessage(message, group, list);
	}

	@Test(expected = FormatException.class)
	public void testRejectsPostWithEmptyContent() throws Exception {
		postContent = "";
		BdfList list =
				BdfList.of(POST.getInt(), memberName, memberKey, parentId,
						previousMsgId, postContent, signature);
		expectCreateAuthor(member);
		validator.validateMessage(message, group, list);
	}

	@Test(expected = FormatException.class)
	public void testRejectsPostWithTooLongContent() throws Exception {
		postContent = getRandomString(MAX_GROUP_POST_BODY_LENGTH + 1);
		BdfList list =
				BdfList.of(POST.getInt(), memberName, memberKey, parentId,
						previousMsgId, postContent, signature);
		expectCreateAuthor(member);
		validator.validateMessage(message, group, list);
	}

	@Test(expected = FormatException.class)
	public void testRejectsPostWithNonStringContent() throws Exception {
		BdfList list =
				BdfList.of(POST.getInt(), memberName, memberKey, parentId,
						previousMsgId, getRandomBytes(5), signature);
		expectCreateAuthor(member);
		validator.validateMessage(message, group, list);
	}

	@Test(expected = FormatException.class)
	public void testRejectsPostWithEmptySignature() throws Exception {
		BdfList list =
				BdfList.of(POST.getInt(), memberName, memberKey, parentId,
						previousMsgId, postContent, new byte[0]);
		expectCreateAuthor(member);
		validator.validateMessage(message, group, list);
	}

	@Test(expected = FormatException.class)
	public void testRejectsPostWithTooLongSignature() throws Exception {
		BdfList list =
				BdfList.of(POST.getInt(), memberName, memberKey, parentId,
						previousMsgId, postContent,
						getRandomBytes(MAX_SIGNATURE_LENGTH + 1));
		expectCreateAuthor(member);
		validator.validateMessage(message, group, list);
	}

	@Test(expected = FormatException.class)
	public void testRejectsPostWithNonRawSignature() throws Exception {
		BdfList list =
				BdfList.of(POST.getInt(), memberName, memberKey, parentId,
						previousMsgId, postContent, "non-raw");
		expectCreateAuthor(member);
		validator.validateMessage(message, group, list);
	}

	@Test(expected = InvalidMessageException.class)
	public void testRejectsPostWithInvalidSignature() throws Exception {
		BdfList list =
				BdfList.of(POST.getInt(), memberName, memberKey, parentId,
						previousMsgId, postContent, signature);
		expectPostMessage(member, false);
		validator.validateMessage(message, group, list);
	}

	@Test
	public void testAcceptPost() throws Exception {
		BdfList list =
				BdfList.of(POST.getInt(), memberName, memberKey, parentId,
						previousMsgId, postContent, signature);
		expectPostMessage(member, true);
		BdfMessageContext messageContext =
				validator.validateMessage(message, group, list);
		assertMessageContext(messageContext, member);
		assertEquals(previousMsgId.getBytes(),
				messageContext.getDictionary().getRaw(KEY_PREVIOUS_MSG_ID));
		assertEquals(parentId.getBytes(),
				messageContext.getDictionary().getRaw(KEY_PARENT_MSG_ID));
	}

	@Test
	public void testAcceptTopLevelPost() throws Exception {
		parentId = null;
		BdfList list =
				BdfList.of(POST.getInt(), memberName, memberKey, parentId,
						previousMsgId, postContent, signature);
		expectPostMessage(member, true);
		BdfMessageContext messageContext =
				validator.validateMessage(message, group, list);
		assertMessageContext(messageContext, member);
		assertEquals(previousMsgId.getBytes(),
				messageContext.getDictionary().getRaw(KEY_PREVIOUS_MSG_ID));
		assertFalse(
				messageContext.getDictionary().containsKey(KEY_PARENT_MSG_ID));
	}

	private void expectPostMessage(final Author member, final boolean sigValid)
			throws Exception {
		final BdfList signed =
				BdfList.of(group.getId(), message.getTimestamp(), POST.getInt(),
						member.getName(), member.getPublicKey(),
						parentId == null ? null : parentId.getBytes(),
						previousMsgId == null ? null : previousMsgId.getBytes(),
						postContent);
		expectCreateAuthor(member);
		context.checking(new Expectations() {{
			oneOf(clientHelper)
					.verifySignature(SIGNING_LABEL_POST, signature,
							member.getPublicKey(), signed);
			if (!sigValid) will(throwException(new GeneralSecurityException()));
		}});
	}

	private void assertMessageContext(BdfMessageContext c, Author member)
			throws FormatException {
		BdfDictionary d = c.getDictionary();
		assertTrue(message.getTimestamp() == d.getLong(KEY_TIMESTAMP));
		assertFalse(d.getBoolean(KEY_READ));
		assertEquals(member.getId().getBytes(), d.getRaw(KEY_MEMBER_ID));
		assertEquals(member.getName(), d.getString(KEY_MEMBER_NAME));
		assertEquals(member.getPublicKey(), d.getRaw(KEY_MEMBER_PUBLIC_KEY));

		// assert message dependencies
		if (d.getLong(KEY_TYPE) == POST.getInt()) {
			assertTrue(c.getDependencies().contains(previousMsgId));
			if (parentId != null) {
				assertTrue(c.getDependencies().contains(parentId));
			} else {
				assertFalse(c.getDependencies().contains(parentId));
			}
		} else {
			assertEquals(JOIN.getInt(), d.getLong(KEY_TYPE).intValue());
		}
	}

}