package org.briarproject.introduction;

import org.briarproject.BriarTestCase;
import org.briarproject.TestUtils;
import org.briarproject.api.FormatException;
import org.briarproject.api.TransportId;
import org.briarproject.api.clients.ClientHelper;
import org.briarproject.api.data.BdfDictionary;
import org.briarproject.api.data.BdfEntry;
import org.briarproject.api.data.BdfList;
import org.briarproject.api.data.MetadataEncoder;
import org.briarproject.api.clients.SessionId;
import org.briarproject.api.sync.ClientId;
import org.briarproject.api.sync.Group;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.Message;
import org.briarproject.api.sync.MessageId;
import org.briarproject.api.system.Clock;
import org.briarproject.system.SystemClock;
import org.jmock.Mockery;
import org.junit.Test;

import java.io.IOException;

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.introduction.IntroductionConstants.ACCEPT;
import static org.briarproject.api.introduction.IntroductionConstants.E_PUBLIC_KEY;
import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID;
import static org.briarproject.api.introduction.IntroductionConstants.MSG;
import static org.briarproject.api.introduction.IntroductionConstants.NAME;
import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY;
import static org.briarproject.api.introduction.IntroductionConstants.SESSION_ID;
import static org.briarproject.api.introduction.IntroductionConstants.TIME;
import static org.briarproject.api.introduction.IntroductionConstants.TRANSPORT;
import static org.briarproject.api.introduction.IntroductionConstants.TYPE;
import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ABORT;
import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ACK;
import static org.briarproject.api.introduction.IntroductionConstants.TYPE_REQUEST;
import static org.briarproject.api.introduction.IntroductionConstants.TYPE_RESPONSE;
import static org.briarproject.api.properties.TransportPropertyConstants.MAX_PROPERTY_LENGTH;
import static org.briarproject.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;

public class IntroductionValidatorTest extends BriarTestCase {

	private final Mockery context = new Mockery();
	private final Group group;
	private final Message message;
	private final IntroductionValidator validator;
	private final Clock clock = new SystemClock();

	public IntroductionValidatorTest() {
		GroupId groupId = new GroupId(TestUtils.getRandomId());
		ClientId clientId = new ClientId(TestUtils.getRandomId());
		byte[] descriptor = TestUtils.getRandomBytes(12);
		group = new Group(groupId, clientId, descriptor);

		MessageId messageId = new MessageId(TestUtils.getRandomId());
		long timestamp = System.currentTimeMillis();
		byte[] raw = TestUtils.getRandomBytes(123);
		message = new Message(messageId, group.getId(), timestamp, raw);


		ClientHelper clientHelper = context.mock(ClientHelper.class);
		MetadataEncoder metadataEncoder = context.mock(MetadataEncoder.class);
		validator = new IntroductionValidator(clientHelper, metadataEncoder,
				clock);
		context.assertIsSatisfied();
	}

	//
	// Introduction Requests
	//

	@Test
	public void testValidateProperIntroductionRequest() throws IOException {
		final byte[] sessionId = TestUtils.getRandomId();
		final String name = TestUtils.getRandomString(MAX_AUTHOR_NAME_LENGTH);
		final byte[] publicKey = TestUtils.getRandomBytes(MAX_PUBLIC_KEY_LENGTH);
		final String text = TestUtils.getRandomString(MAX_MESSAGE_BODY_LENGTH);

		BdfList body = BdfList.of(TYPE_REQUEST, sessionId,
				name, publicKey, text);

		final BdfDictionary result =
				validator.validateMessage(message, group, body)
						.getDictionary();

		assertEquals(Long.valueOf(TYPE_REQUEST), result.getLong(TYPE));
		assertEquals(sessionId, result.getRaw(SESSION_ID));
		assertEquals(name, result.getString(NAME));
		assertEquals(publicKey, result.getRaw(PUBLIC_KEY));
		assertEquals(text, result.getString(MSG));
		context.assertIsSatisfied();
	}

	@Test(expected = FormatException.class)
	public void testValidateIntroductionRequestWithNoName() throws IOException {
		BdfDictionary msg = getValidIntroductionRequest();

		// no NAME is message
		BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID),
					msg.getRaw(PUBLIC_KEY));
		if (msg.containsKey(MSG)) body.add(msg.getString(MSG));

		validator.validateMessage(message, group, body);
	}

	@Test(expected = FormatException.class)
	public void testValidateIntroductionRequestWithLongName() throws IOException {
		// too long NAME in message
		BdfDictionary msg = getValidIntroductionRequest();
		msg.put(NAME, msg.get(NAME) + "x");
		BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID),
				msg.getString(NAME), msg.getRaw(PUBLIC_KEY));
		if (msg.containsKey(MSG)) body.add(msg.getString(MSG));

		validator.validateMessage(message, group, body);
	}

	@Test(expected = FormatException.class)
	public void testValidateIntroductionRequestWithWrongType()
			throws IOException {
		// wrong message type
		BdfDictionary msg = getValidIntroductionRequest();
		msg.put(TYPE, 324234);

		BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID),
				msg.getString(NAME), msg.getRaw(PUBLIC_KEY));
		if (msg.containsKey(MSG)) body.add(msg.getString(MSG));
		validator.validateMessage(message, group, body);
	}

	private BdfDictionary getValidIntroductionRequest() throws FormatException {
		byte[] sessionId = TestUtils.getRandomId();
		String name = TestUtils.getRandomString(MAX_AUTHOR_NAME_LENGTH);
		byte[] publicKey = TestUtils.getRandomBytes(MAX_PUBLIC_KEY_LENGTH);
		String text = TestUtils.getRandomString(MAX_MESSAGE_BODY_LENGTH);

		BdfDictionary msg = new BdfDictionary();
		msg.put(TYPE, TYPE_REQUEST);
		msg.put(SESSION_ID, sessionId);
		msg.put(NAME, name);
		msg.put(PUBLIC_KEY, publicKey);
		msg.put(MSG, text);

		return msg;
	}

	//
	// Introduction Responses
	//

	@Test
	public void testValidateIntroductionAcceptResponse() throws IOException {
		byte[] groupId = TestUtils.getRandomId();
		byte[] sessionId = TestUtils.getRandomId();
		long time = clock.currentTimeMillis();
		byte[] publicKey = TestUtils.getRandomBytes(MAX_PUBLIC_KEY_LENGTH);
		String transportId = TestUtils
				.getRandomString(TransportId.MAX_TRANSPORT_ID_LENGTH);
		BdfDictionary tProps = BdfDictionary.of(
				new BdfEntry(TestUtils.getRandomString(MAX_PROPERTY_LENGTH),
						TestUtils.getRandomString(MAX_PROPERTY_LENGTH))
		);
		BdfDictionary tp = BdfDictionary.of(
				new BdfEntry(transportId, tProps)
		);

		BdfDictionary msg = new BdfDictionary();
		msg.put(TYPE, TYPE_RESPONSE);
		msg.put(GROUP_ID, groupId);
		msg.put(SESSION_ID, sessionId);
		msg.put(ACCEPT, true);
		msg.put(TIME, time);
		msg.put(E_PUBLIC_KEY, publicKey);
		msg.put(TRANSPORT, tp);

		BdfList body = BdfList.of(TYPE_RESPONSE, msg.getRaw(SESSION_ID),
				msg.getBoolean(ACCEPT), msg.getLong(TIME),
				msg.getRaw(E_PUBLIC_KEY), msg.getDictionary(TRANSPORT));

		final BdfDictionary result =
				validator.validateMessage(message, group, body).getDictionary();

		assertEquals(Long.valueOf(TYPE_RESPONSE), result.getLong(TYPE));
		assertEquals(sessionId, result.getRaw(SESSION_ID));
		assertEquals(true, result.getBoolean(ACCEPT));
		assertEquals(publicKey, result.getRaw(E_PUBLIC_KEY));
		assertEquals(tp, result.getDictionary(TRANSPORT));
		context.assertIsSatisfied();
	}

	@Test
	public void testValidateIntroductionDeclineResponse()
			throws IOException {
		BdfDictionary msg = getValidIntroductionResponse(false);
		BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID),
				msg.getBoolean(ACCEPT));

		BdfDictionary result = validator.validateMessage(message, group, body)
				.getDictionary();

		assertFalse(result.getBoolean(ACCEPT));
		context.assertIsSatisfied();
	}

	@Test(expected = FormatException.class)
	public void testValidateIntroductionResponseWithoutAccept()
			throws IOException {
		BdfDictionary msg = getValidIntroductionResponse(false);
		BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID));

		validator.validateMessage(message, group, body);
	}

	@Test(expected = FormatException.class)
	public void testValidateIntroductionResponseWithBrokenTp()
			throws IOException {
		BdfDictionary msg = getValidIntroductionResponse(true);
		BdfDictionary tp = msg.getDictionary(TRANSPORT);
		tp.put(TestUtils
				.getRandomString(TransportId.MAX_TRANSPORT_ID_LENGTH), "X");
		msg.put(TRANSPORT, tp);

		BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID),
				msg.getBoolean(ACCEPT), msg.getLong(TIME),
				msg.getRaw(E_PUBLIC_KEY), msg.getDictionary(TRANSPORT));

		validator.validateMessage(message, group, body);
	}

	@Test(expected = FormatException.class)
	public void testValidateIntroductionResponseWithoutPublicKey()
			throws IOException {
		BdfDictionary msg = getValidIntroductionResponse(true);

		BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID),
				msg.getBoolean(ACCEPT), msg.getLong(TIME),
				msg.getDictionary(TRANSPORT));

		validator.validateMessage(message, group, body);
	}

	private BdfDictionary getValidIntroductionResponse(boolean accept)
			throws FormatException {

		byte[] groupId = TestUtils.getRandomId();
		byte[] sessionId = TestUtils.getRandomId();
		long time = clock.currentTimeMillis();
		byte[] publicKey = TestUtils.getRandomBytes(MAX_PUBLIC_KEY_LENGTH);
		String transportId = TestUtils
				.getRandomString(TransportId.MAX_TRANSPORT_ID_LENGTH);
		BdfDictionary tProps = BdfDictionary.of(
				new BdfEntry(TestUtils.getRandomString(MAX_PROPERTY_LENGTH),
						TestUtils.getRandomString(MAX_PROPERTY_LENGTH))
		);
		BdfDictionary tp = BdfDictionary.of(
				new BdfEntry(transportId, tProps)
		);

		BdfDictionary msg = new BdfDictionary();
		msg.put(TYPE, TYPE_RESPONSE);
		msg.put(GROUP_ID, groupId);
		msg.put(SESSION_ID, sessionId);
		msg.put(ACCEPT, accept);
		if (accept) {
			msg.put(TIME, time);
			msg.put(E_PUBLIC_KEY, publicKey);
			msg.put(TRANSPORT, tp);
		}

		return msg;
	}

	//
	// Introduction ACK
	//

	@Test
	public void testValidateProperIntroductionAck() throws IOException {
		final byte[] sessionId = TestUtils.getRandomId();

		BdfDictionary msg = new BdfDictionary();
		msg.put(TYPE, TYPE_ACK);
		msg.put(SESSION_ID, sessionId);

		BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID));

		BdfDictionary result =
				validator.validateMessage(message, group, body).getDictionary();

		assertEquals(Long.valueOf(TYPE_ACK), result.getLong(TYPE));
		assertEquals(sessionId, result.getRaw(SESSION_ID));
		context.assertIsSatisfied();
	}

	@Test(expected = FormatException.class)
	public void testValidateTooLongIntroductionAck() throws IOException {
		BdfDictionary msg = BdfDictionary.of(
				new BdfEntry(TYPE, TYPE_ACK),
				new BdfEntry(SESSION_ID, TestUtils.getRandomId()),
				new BdfEntry("garbage", TestUtils.getRandomString(255))
		);
		BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID),
				msg.getString("garbage"));

		validator.validateMessage(message, group, body);
	}

	@Test(expected = FormatException.class)
	public void testValidateIntroductionAckWithLongSessionId() throws IOException {
		BdfDictionary msg = BdfDictionary.of(
				new BdfEntry(TYPE, TYPE_ACK),
				new BdfEntry(SESSION_ID, new byte[SessionId.LENGTH + 1])
		);
		BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID));

		validator.validateMessage(message, group, body);
	}

	//
	// Introduction Abort
	//

	@Test
	public void testValidateProperIntroductionAbort() throws IOException {
		byte[] sessionId = TestUtils.getRandomId();

		BdfDictionary msg = new BdfDictionary();
		msg.put(TYPE, TYPE_ABORT);
		msg.put(SESSION_ID, sessionId);

		BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID));

		BdfDictionary result =
				validator.validateMessage(message, group, body).getDictionary();

		assertEquals(Long.valueOf(TYPE_ABORT), result.getLong(TYPE));
		assertEquals(sessionId, result.getRaw(SESSION_ID));
		context.assertIsSatisfied();
	}

	@Test(expected = FormatException.class)
	public void testValidateTooLongIntroductionAbort() throws IOException {
		BdfDictionary msg = BdfDictionary.of(
				new BdfEntry(TYPE, TYPE_ABORT),
				new BdfEntry(SESSION_ID, TestUtils.getRandomId()),
				new BdfEntry("garbage", TestUtils.getRandomString(255))
		);
		BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID),
				msg.getString("garbage"));

		validator.validateMessage(message, group, body);
	}

}
