package org.briarproject.blogs;

import org.briarproject.BriarTestCase;
import org.briarproject.api.FormatException;
import org.briarproject.api.blogs.Blog;
import org.briarproject.api.blogs.BlogFactory;
import org.briarproject.api.blogs.BlogPost;
import org.briarproject.api.blogs.BlogPostHeader;
import org.briarproject.api.clients.ClientHelper;
import org.briarproject.api.contact.Contact;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.contact.ContactManager;
import org.briarproject.api.data.BdfDictionary;
import org.briarproject.api.data.BdfEntry;
import org.briarproject.api.data.BdfList;
import org.briarproject.api.data.MetadataParser;
import org.briarproject.api.db.DatabaseComponent;
import org.briarproject.api.db.DbException;
import org.briarproject.api.db.Transaction;
import org.briarproject.api.event.BlogPostAddedEvent;
import org.briarproject.api.identity.Author;
import org.briarproject.api.identity.AuthorId;
import org.briarproject.api.identity.IdentityManager;
import org.briarproject.api.identity.LocalAuthor;
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.jmock.Expectations;
import org.jmock.Mockery;
import org.junit.Test;

import java.util.Collection;
import java.util.Collections;

import static org.briarproject.TestUtils.getRandomBytes;
import static org.briarproject.TestUtils.getRandomId;
import static org.briarproject.api.blogs.BlogConstants.KEY_AUTHOR;
import static org.briarproject.api.blogs.BlogConstants.KEY_AUTHOR_ID;
import static org.briarproject.api.blogs.BlogConstants.KEY_AUTHOR_NAME;
import static org.briarproject.api.blogs.BlogConstants.KEY_CONTENT_TYPE;
import static org.briarproject.api.blogs.BlogConstants.KEY_DESCRIPTION;
import static org.briarproject.api.blogs.BlogConstants.KEY_PUBLIC_KEY;
import static org.briarproject.api.blogs.BlogConstants.KEY_READ;
import static org.briarproject.api.blogs.BlogConstants.KEY_TIMESTAMP;
import static org.briarproject.api.blogs.BlogConstants.KEY_TIME_RECEIVED;
import static org.briarproject.api.identity.Author.Status.VERIFIED;
import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
import static org.briarproject.blogs.BlogManagerImpl.CLIENT_ID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

public class BlogManagerImplTest extends BriarTestCase {

	private final Mockery context = new Mockery();
	private final BlogManagerImpl blogManager;
	private final DatabaseComponent db = context.mock(DatabaseComponent.class);
	private final IdentityManager identityManager =
			context.mock(IdentityManager.class);
	private final ClientHelper clientHelper = context.mock(ClientHelper.class);
	private final MetadataParser metadataParser =
			context.mock(MetadataParser.class);
	private final ContactManager contactManager =
			context.mock(ContactManager.class);
	private final BlogFactory blogFactory = context.mock(BlogFactory.class);

	private final Blog blog1, blog2;
	private final Message message;
	private final MessageId messageId;

	public BlogManagerImplTest() {
		blogManager = new BlogManagerImpl(db, identityManager, clientHelper,
				metadataParser, contactManager, blogFactory);

		blog1 = getBlog("Test Blog 1", "Test Description 1");
		blog2 = getBlog("Test Blog 2", "Test Description 2");
		messageId = new MessageId(getRandomId());
		message = new Message(messageId, blog1.getId(), 42, getRandomBytes(42));
	}

	@Test
	public void testClientId() {
		assertEquals(CLIENT_ID, blogManager.getClientId());
	}

	@Test
	public void testCreateLocalState() throws DbException {
		final Transaction txn = new Transaction(null, false);
		final Collection<LocalAuthor> localAuthors =
				Collections.singletonList((LocalAuthor) blog1.getAuthor());

		final ContactId contactId = new ContactId(0);
		final Collection<ContactId> contactIds =
				Collections.singletonList(contactId);

		Contact contact = new Contact(contactId, blog2.getAuthor(),
				blog1.getAuthor().getId(), true, true);
		final Collection<Contact> contacts = Collections.singletonList(contact);

		context.checking(new Expectations() {{
			oneOf(db).getLocalAuthors(txn);
			will(returnValue(localAuthors));
			oneOf(blogFactory).createPersonalBlog(blog1.getAuthor());
			will(returnValue(blog1));
			oneOf(db).containsGroup(txn, blog1.getId());
			will(returnValue(false));
			oneOf(db).addGroup(txn, blog1.getGroup());
			oneOf(db).getContacts(txn, blog1.getAuthor().getId());
			will(returnValue(contactIds));
			oneOf(db).setVisibleToContact(txn, contactId, blog1.getId(), true);
			oneOf(db).getContacts(txn);
			will(returnValue(contacts));
			oneOf(blogFactory).createPersonalBlog(blog2.getAuthor());
			will(returnValue(blog2));
			oneOf(db).containsGroup(txn, blog2.getId());
			will(returnValue(false));
			oneOf(db).addGroup(txn, blog2.getGroup());
			oneOf(db).setVisibleToContact(txn, contactId, blog2.getId(), true);
			oneOf(db).getLocalAuthor(txn, blog1.getAuthor().getId());
			will(returnValue(blog1.getAuthor()));
			oneOf(blogFactory).createPersonalBlog(blog1.getAuthor());
			will(returnValue(blog1));
			oneOf(db).setVisibleToContact(txn, contactId, blog1.getId(), true);
		}});

		blogManager.createLocalState(txn);
	}

	@Test
	public void testRemovingContact() throws DbException {
		final Transaction txn = new Transaction(null, false);

		final ContactId contactId = new ContactId(0);
		Contact contact = new Contact(contactId, blog2.getAuthor(),
				blog1.getAuthor().getId(), true, true);

		context.checking(new Expectations() {{
			oneOf(blogFactory).createPersonalBlog(blog2.getAuthor());
			will(returnValue(blog2));
			oneOf(db).removeGroup(txn, blog2.getGroup());
		}});

		blogManager.removingContact(txn, contact);
	}

	@Test
	public void testAddingIdentity() throws DbException {
		final Transaction txn = new Transaction(null, false);
		Author a = blog1.getAuthor();
		final LocalAuthor localAuthor =
				new LocalAuthor(a.getId(), a.getName(), a.getPublicKey(),
						a.getPublicKey(), 0);

		context.checking(new Expectations() {{
			oneOf(blogFactory).createPersonalBlog(localAuthor);
			will(returnValue(blog1));
			oneOf(db).addGroup(txn, blog1.getGroup());
		}});

		blogManager.addingIdentity(txn, localAuthor);
	}

	@Test
	public void testRemovingIdentity() throws DbException {
		final Transaction txn = new Transaction(null, false);
		Author a = blog1.getAuthor();
		final LocalAuthor localAuthor =
				new LocalAuthor(a.getId(), a.getName(), a.getPublicKey(),
						a.getPublicKey(), 0);

		context.checking(new Expectations() {{
			oneOf(blogFactory).createPersonalBlog(localAuthor);
			will(returnValue(blog1));
			oneOf(db).removeGroup(txn, blog1.getGroup());
		}});

		blogManager.removingIdentity(txn, localAuthor);
	}

	@Test
	public void testIncomingMessage() throws DbException, FormatException {
		final Transaction txn = new Transaction(null, false);
		BdfList list = new BdfList();
		BdfDictionary author = authorToBdfDictionary(blog1.getAuthor());
		BdfDictionary meta = BdfDictionary.of(
				new BdfEntry(KEY_TIMESTAMP, 0),
				new BdfEntry(KEY_TIME_RECEIVED, 1),
				new BdfEntry(KEY_AUTHOR, author),
				new BdfEntry(KEY_CONTENT_TYPE, 0),
				new BdfEntry(KEY_READ, false),
				new BdfEntry(KEY_CONTENT_TYPE, "text/plain")
		);

		context.checking(new Expectations() {{
			oneOf(clientHelper).setMessageShared(txn, message, true);
			oneOf(identityManager)
					.getAuthorStatus(txn, blog1.getAuthor().getId());
			will(returnValue(VERIFIED));
		}});

		blogManager.incomingMessage(txn, message, list, meta);

		assertEquals(1, txn.getEvents().size());
		assertTrue(txn.getEvents().get(0) instanceof BlogPostAddedEvent);

		BlogPostAddedEvent e = (BlogPostAddedEvent) txn.getEvents().get(0);
		assertEquals(blog1.getId(), e.getGroupId());

		BlogPostHeader h = e.getHeader();
		assertEquals(1, h.getTimeReceived());
		assertEquals(messageId, h.getId());
		assertEquals(null, h.getParentId());
		assertEquals(VERIFIED, h.getAuthorStatus());
		assertEquals("text/plain", h.getContentType());
		assertEquals(blog1.getAuthor(), h.getAuthor());
	}

	@Test
	public void testAddBlog() throws DbException, FormatException {
		final Transaction txn = new Transaction(null, false);
		Author a = blog1.getAuthor();
		final LocalAuthor localAuthor =
				new LocalAuthor(a.getId(), a.getName(), a.getPublicKey(),
						a.getPublicKey(), 0);
		final BdfDictionary meta = BdfDictionary.of(
				new BdfEntry(KEY_DESCRIPTION, blog1.getDescription())
		);

		context.checking(new Expectations() {{
			oneOf(blogFactory)
					.createBlog(blog1.getName(), blog1.getDescription(),
							blog1.getAuthor());
			will(returnValue(blog1));
			oneOf(db).startTransaction(false);
			will(returnValue(txn));
			oneOf(db).addGroup(txn, blog1.getGroup());
			oneOf(clientHelper).mergeGroupMetadata(txn, blog1.getId(), meta);
			oneOf(db).endTransaction(txn);
		}});

		blogManager
				.addBlog(localAuthor, blog1.getName(), blog1.getDescription());
		assertTrue(txn.isComplete());
	}

	@Test
	public void testRemoveBlog() throws DbException, FormatException {
		final Transaction txn = new Transaction(null, false);

		context.checking(new Expectations() {{
			oneOf(db).startTransaction(false);
			will(returnValue(txn));
			oneOf(db).getGroup(txn, blog1.getId());
			will(returnValue(blog1.getGroup()));
			oneOf(clientHelper)
					.getGroupMetadataAsDictionary(txn, blog1.getId());
			oneOf(blogFactory).parseBlog(blog1.getGroup(), "");
			will(returnValue(blog1));
			oneOf(identityManager).getLocalAuthor(txn);
			will(returnValue(blog2.getAuthor()));
			oneOf(contactManager).contactExists(txn, blog1.getAuthor().getId(),
					blog2.getAuthor().getId());
			will(returnValue(false));
			oneOf(db).removeGroup(txn, blog1.getGroup());
			oneOf(db).endTransaction(txn);
		}});

		blogManager.removeBlog(blog1);
		assertTrue(txn.isComplete());
	}

	@Test
	public void testAddLocalPost() throws DbException, FormatException {
		final Transaction txn = new Transaction(null, true);
		final BlogPost post =
				new BlogPost(null, message, null, blog1.getAuthor(),
						"text/plain");
		BdfDictionary authorMeta = authorToBdfDictionary(blog1.getAuthor());
		final BdfDictionary meta = BdfDictionary.of(
				new BdfEntry(KEY_TIMESTAMP, message.getTimestamp()),
				new BdfEntry(KEY_AUTHOR, authorMeta),
				new BdfEntry(KEY_CONTENT_TYPE, "text/plain"),
				new BdfEntry(KEY_READ, true)
		);

		context.checking(new Expectations() {{
			oneOf(clientHelper).addLocalMessage(message, CLIENT_ID, meta, true);
			oneOf(db).startTransaction(true);
			will(returnValue(txn));
			oneOf(identityManager)
					.getAuthorStatus(txn, blog1.getAuthor().getId());
			will(returnValue(VERIFIED));
			oneOf(db).endTransaction(txn);
		}});

		blogManager.addLocalPost(post);
		assertTrue(txn.isComplete());

		assertEquals(1, txn.getEvents().size());
		assertTrue(txn.getEvents().get(0) instanceof BlogPostAddedEvent);

		BlogPostAddedEvent e = (BlogPostAddedEvent) txn.getEvents().get(0);
		assertEquals(blog1.getId(), e.getGroupId());

		BlogPostHeader h = e.getHeader();
		assertEquals(message.getTimestamp(), h.getTimeReceived());
		assertEquals(messageId, h.getId());
		assertEquals(null, h.getParentId());
		assertEquals(VERIFIED, h.getAuthorStatus());
		assertEquals("text/plain", h.getContentType());
		assertEquals(blog1.getAuthor(), h.getAuthor());
	}

	private Blog getBlog(String name, String desc) {
		final GroupId groupId = new GroupId(getRandomId());
		final Group group = new Group(groupId, CLIENT_ID, getRandomBytes(42));
		final AuthorId authorId = new AuthorId(getRandomId());
		final byte[] publicKey = getRandomBytes(MAX_PUBLIC_KEY_LENGTH);
		final byte[] privateKey = getRandomBytes(MAX_PUBLIC_KEY_LENGTH);
		final long created = System.currentTimeMillis();
		final LocalAuthor localAuthor =
				new LocalAuthor(authorId, "Author", publicKey, privateKey,
						created);
		return new Blog(group, name, desc, localAuthor);
	}

	private BdfDictionary authorToBdfDictionary(Author a) {
		return BdfDictionary.of(
				new BdfEntry(KEY_AUTHOR_ID, a.getId()),
				new BdfEntry(KEY_AUTHOR_NAME, a.getName()),
				new BdfEntry(KEY_PUBLIC_KEY, a.getPublicKey())
		);
	}

}
