'use strict';

const assert = require('assert');
const nconf = require('nconf');

const db = require('../mocks/databasemock');
const meta = require('../../src/meta');
const install = require('../../src/install');
const user = require('../../src/user');
const categories = require('../../src/categories');
const topics = require('../../src/topics');
const posts = require('../../src/posts');
const privileges = require('../../src/privileges');
const activitypub = require('../../src/activitypub');
const utils = require('../../src/utils');

const helpers = require('./helpers');

describe('Inbox', () => {
	before(async () => {
		meta.config.activitypubEnabled = 1;
		await install.giveWorldPrivileges();
	});

	describe('Inbox handling', () => {
		describe('helper self-check', () => {
			it('should generate a Like activity', () => {
				const object = utils.generateUUID();
				const { id: actor } = helpers.mocks.person();
				const { activity } = helpers.mocks.like({
					object,
					actor,
				});

				assert.deepStrictEqual(activity, {
					'@context': 'https://www.w3.org/ns/activitystreams',
					id: `${helpers.mocks._baseUrl}/like/${encodeURIComponent(object)}`,
					type: 'Like',
					actor,
					object,
				});
			});

			it('should generate an Announce activity wrapping a Like activity', () => {
				const object = utils.generateUUID();
				const { id: actor } = helpers.mocks.person();
				const { activity: like } = helpers.mocks.like({
					object,
					actor,
				});
				const { id: gActor } = helpers.mocks.group();
				const { activity } = helpers.mocks.announce({
					actor: gActor,
					object: like,
				});

				assert.deepStrictEqual(activity, {
					'@context': 'https://www.w3.org/ns/activitystreams',
					id: `${helpers.mocks._baseUrl}/announce/${encodeURIComponent(like.id)}`,
					type: 'Announce',
					to: [ 'https://www.w3.org/ns/activitystreams#Public' ],
					cc: [
						`${gActor}/followers`,
					],
					actor: gActor,
					object: like,
				});
			});
		});

		describe('from a banned account', () => {
			before(async function () {
				const { id: bannedUid } = helpers.mocks.person();
				this.bannedUid = bannedUid;

				const { note } = helpers.mocks.note({
					attributedTo: bannedUid,
				});

				const uid = await user.create({ username: utils.generateUUID() });
				const { id: boosterUid } = helpers.mocks.person();
				await db.sortedSetAdd(`followersRemote:${boosterUid}`, Date.now(), uid);
				const { activity } = helpers.mocks.announce({
					actor: boosterUid,
					object: note,
				});
				this.activity = activity;
				this.pid = note.id;

				await user.bans.ban(bannedUid, 0, 'testing');
			});

			it('should list the remote user as banned, when queried', async function () {
				const isBanned = await user.bans.isBanned(this.bannedUid);
				assert.strictEqual(isBanned, true);
			});

			// Can't actually test the middleware because I can't sign a request for a test

			it('should not assert a note if authored by a banned user (boosted by third-party)', async function () {
				await activitypub.inbox.announce({
					body: this.activity,
				});

				const exists = await posts.exists(this.pid);
				assert.strictEqual(exists, false);
			});
		});

		describe('Create', () => {
			let uid;

			before(async () => {
				uid = await user.create({ username: utils.generateUUID() });
			});

			describe('(Note)', () => {
				it('should create a new topic in cid -1', async () => {
					const { note, id } = helpers.mocks.note();
					const { activity } = helpers.mocks.create(note);

					await db.sortedSetAdd(`followersRemote:${note.attributedTo}`, Date.now(), uid);
					await activitypub.inbox.create({ body: activity });

					assert(await posts.exists(id));

					const cid = await posts.getCidByPid(id);
					assert.strictEqual(cid, -1);
				});

				it('should not append to the tids_read sorted set', async () => {
					const { note, id } = helpers.mocks.note();
					const { activity } = helpers.mocks.create(note);

					await db.sortedSetAdd(`followersRemote:${note.attributedTo}`, Date.now(), uid);
					await activitypub.inbox.create({ body: activity });

					const exists = await db.exists(`uid:${note.attributedTo}:tids_read`);
					assert(!exists);
				});

				it('should create a new topic in a remote category if addressed (category same-origin)', async () => {
					const { id: remoteCid } = helpers.mocks.group();
					const { note, id } = helpers.mocks.note({
						audience: [remoteCid],
					});
					const { activity } = helpers.mocks.create(note);

					await activitypub.inbox.create({ body: activity });

					assert(await posts.exists(id));

					const cid = await posts.getCidByPid(id);
					assert.strictEqual(cid, remoteCid);
				});

				it('should create a new topic in cid -1 if a non-same origin remote category is addressed', async function () {
					const { id: remoteCid } = helpers.mocks.group({
						id: `https://example.com/${utils.generateUUID()}`,
					});
					const { note, id } = helpers.mocks.note({
						audience: [remoteCid],
					});
					const { activity } = helpers.mocks.create(note);
					try {
						await activitypub.inbox.create({ body: activity });
					} catch (err) {
						assert(false);
					}

					assert(await posts.exists(id));
					const cid = await posts.getCidByPid(id);
					assert.strictEqual(cid, -1);
				});
			});
		});

		describe('Announce', () => {
			let cid;

			before(async () => {
				({ cid } = await categories.create({ name: utils.generateUUID() }));
			});

			describe('(Create)', () => {
				it('should create a new topic in a remote category if addressed', async () => {
					const { id: remoteCid } = helpers.mocks.group();
					const { id, note } = helpers.mocks.note({
						audience: [remoteCid],
					});
					let { activity } = helpers.mocks.create(note);
					({ activity } = helpers.mocks.announce({ actor: remoteCid, object: activity }));

					await activitypub.inbox.announce({ body: activity });

					assert(await posts.exists(id));

					const cid = await posts.getCidByPid(id);
					assert.strictEqual(cid, remoteCid);
				});
			});

			describe('(Create) or (Note) referencing local post', () => {
				let uid;
				let topicData;
				let postData;
				let localNote;
				let announces = 0;

				before(async () => {
					uid = await user.create({ username: utils.generateUUID().slice(0, 10) });
					({ topicData, postData } = await topics.post({
						cid,
						uid,
						title: utils.generateUUID(),
						content: utils.generateUUID(),
					}));
					localNote = await activitypub.mocks.notes.public(postData);
				});

				it('should increment announces counter when a remote user shares', async () => {
					const { id } = helpers.mocks.person();
					const { activity } = helpers.mocks.announce({
						actor: id,
						object: localNote,
						cc: [`${nconf.get('url')}/uid/${topicData.uid}`],
					});

					await activitypub.inbox.announce({ body: activity });
					announces += 1;

					const count = await posts.getPostField(topicData.mainPid, 'announces');
					assert.strictEqual(count, announces);
				});

				it('should contain the remote user announcer id in the post announces zset', async () => {
					const { id } = helpers.mocks.person();
					const { activity } = helpers.mocks.announce({
						actor: id,
						object: localNote,
						cc: [`${nconf.get('url')}/uid/${topicData.uid}`],
					});

					await activitypub.inbox.announce({ body: activity });
					announces += 1;

					const exists = await db.isSortedSetMember(`pid:${topicData.mainPid}:announces`, id);
					assert(exists);
				});

				it('should NOT increment announces counter when a remote category shares', async () => {
					const { id } = helpers.mocks.group();
					const { activity } = helpers.mocks.announce({
						actor: id,
						object: localNote,
						cc: [`${nconf.get('url')}/uid/${topicData.uid}`],
					});

					await activitypub.inbox.announce({ body: activity });

					const count = await posts.getPostField(topicData.mainPid, 'announces');
					assert.strictEqual(count, announces);
				});

				it('should NOT contain the remote category announcer id in the post announces zset', async () => {
					const { id } = helpers.mocks.group();
					const { activity } = helpers.mocks.announce({
						actor: id,
						object: localNote,
						cc: [`${nconf.get('url')}/uid/${topicData.uid}`],
					});

					await activitypub.inbox.announce({ body: activity });

					const exists = await db.isSortedSetMember(`pid:${topicData.mainPid}:announces`, id);
					assert(!exists);
				});
			});

			describe('(Note)', () => {
				it('should create a new topic in cid -1 if category not addressed', async () => {
					const { note } = helpers.mocks.note();
					await activitypub.actors.assert([note.attributedTo]);
					const { activity } = helpers.mocks.announce({
						object: note,
					});
					const uid = await user.create({ username: utils.generateUUID().slice(0, 10) });
					await db.sortedSetAdd(`followersRemote:${activity.actor}`, Date.now(), uid);

					const beforeCount = await db.sortedSetCard(`cid:-1:tids`);
					await activitypub.inbox.announce({ body: activity });
					const count = await db.sortedSetCard(`cid:-1:tids`);

					assert.strictEqual(count, beforeCount + 1);
				});

				it('should create a new topic in local category', async () => {
					const { note } = helpers.mocks.note({
						cc: [`${nconf.get('url')}/category/${cid}`],
					});
					await activitypub.actors.assert([note.attributedTo]);
					const { activity } = helpers.mocks.announce({
						object: note,
					});
					const uid = await user.create({ username: utils.generateUUID().slice(0, 10) });
					await db.sortedSetAdd(`followersRemote:${activity.actor}`, Date.now(), uid);

					const beforeCount = await db.sortedSetCard(`cid:${cid}:tids`);
					await activitypub.inbox.announce({ body: activity });
					const count = await db.sortedSetCard(`cid:${cid}:tids`);

					assert.strictEqual(count, beforeCount + 1);
				});
			});

			describe('(Like)', () => {
				it('should upvote a local post', async () => {
					const uid = await user.create({ username: utils.generateUUID().slice(0, 10) });
					const { postData } = await topics.post({
						cid,
						uid,
						title: utils.generateUUID(),
						content: utils.generateUUID(),
					});

					const { activity: like } = helpers.mocks.like({
						object: `${nconf.get('url')}/post/${postData.pid}`,
					});
					const { activity } = helpers.mocks.announce({
						object: like,
					});

					let { upvotes } = await posts.getPostFields(postData.pid, 'upvotes');
					assert.strictEqual(upvotes, 0);

					await activitypub.inbox.announce({ body: activity });
					({ upvotes } = await posts.getPostFields(postData.pid, 'upvotes'));
					assert.strictEqual(upvotes, 1);
				});

				it('should upvote an asserted remote post', async () => {
					const { id } = helpers.mocks.note();
					await activitypub.notes.assert(0, id, { skipChecks: true });
					const { activity: like } = helpers.mocks.like({
						object: id,
					});
					const { activity } = helpers.mocks.announce({
						object: like,
					});

					let { upvotes } = await posts.getPostFields(id, 'upvotes');
					assert.strictEqual(upvotes, 0);

					await activitypub.inbox.announce({ body: activity });

					({ upvotes } = await posts.getPostFields(id, 'upvotes'));
					assert.strictEqual(upvotes, 1);
				});
			});

			describe('(Update)', () => {
				it('should update a note\'s content', async () => {
					const { id: actor } = helpers.mocks.person();
					const { id, note } = helpers.mocks.note({ attributedTo: actor });
					await activitypub.notes.assert(0, id, { skipChecks: true });
					note.content = utils.generateUUID();
					const { activity: update } = helpers.mocks.update({ object: note });
					const { activity } = helpers.mocks.announce({ object: update });

					await activitypub.inbox.announce({ body: activity });

					const content = await posts.getPostField(id, 'content');
					assert.strictEqual(content, note.content);
				});
			});
		});

		describe('Like', () => {
			before(async function () {
				const uid = await user.create({ username: utils.generateUUID() });
				const { cid } = await categories.create({ name: utils.generateUUID() });
				this.cid = cid;
				const { postData } = await topics.post({
					uid,
					cid,
					title: utils.generateUUID(),
					content: utils.generateUUID(),
				});
				this.postData = postData;
				const object = await activitypub.mocks.notes.public(postData);
				const { activity } = helpers.mocks.like({ object });
				this.voterUid = activity.actor;
				await activitypub.inbox.like({ body: activity });
			});

			it('should increment a like for the post', async function () {
				const voted = await posts.hasVoted(this.postData.pid, this.voterUid);
				const count = await posts.getPostField(this.postData.pid, 'upvotes');
				assert(voted);
				assert.strictEqual(count, 1);
			});

			it('should not append to the uid upvotes zset', async function () {
				const exists = await db.exists(`uid:${this.voterUid}:upvote`);
				assert(!exists);
			});

			describe('with privilege revoked (from fediverse pseudo-user)', () => {
				before(async function () {
					await privileges.categories.rescind(['groups:posts:upvote'], this.cid, 'fediverse');
					const object = await activitypub.mocks.notes.public(this.postData);
					const { activity } = helpers.mocks.like({ object });
					this.voterUid = activity.actor;
					await activitypub.inbox.like({ body: activity });
				});

				after(async function () {
					await privileges.categories.give(['groups:posts:upvote'], this.cid, 'fediverse');
				});

				it('should not increment a like for the post', async function () {
					const { upvoted } = await posts.hasVoted(this.postData.pid, this.voterUid);
					const count = await posts.getPostField(this.postData.pid, 'upvotes');
					assert.strictEqual(upvoted, false);
					assert.strictEqual(count, 1);
				});
			});
		});
	});

	describe('Inbox Synchronization', () => {
		let cid;
		let uid;
		let topicData;

		before(async () => {
			({ cid } = await categories.create({ name: utils.generateUUID().slice(0, 8) }));
		});

		beforeEach(async () => {
			uid = await user.create({ username: utils.generateUUID().slice(0, 10) });
			({ topicData } = await topics.post({
				cid,
				uid,
				title: utils.generateUUID(),
				content: utils.generateUUID(),
			}));
		});

		it('should add a topic to a user\'s inbox if user is a recipient in OP', async () => {
			await db.setAdd(`post:${topicData.mainPid}:recipients`, [uid]);
			await activitypub.notes.syncUserInboxes(topicData.tid);
			const inboxed = await db.isSortedSetMember(`uid:${uid}:inbox`, topicData.tid);

			assert.strictEqual(inboxed, true);
		});

		it('should add a topic to a user\'s inbox if a user is a recipient in a reply', async () => {
			const uid = await user.create({ username: utils.generateUUID().slice(0, 10) });
			const { pid } = await topics.reply({
				tid: topicData.tid,
				uid,
				content: utils.generateUUID(),
			});
			await db.setAdd(`post:${pid}:recipients`, [uid]);
			await activitypub.notes.syncUserInboxes(topicData.tid);
			const inboxed = await db.isSortedSetMember(`uid:${uid}:inbox`, topicData.tid);

			assert.strictEqual(inboxed, true);
		});

		it('should maintain a list of recipients at the topic level', async () => {
			await db.setAdd(`post:${topicData.mainPid}:recipients`, [uid]);
			await activitypub.notes.syncUserInboxes(topicData.tid);
			const [isRecipient, count] = await Promise.all([
				db.isSetMember(`tid:${topicData.tid}:recipients`, uid),
				db.setCount(`tid:${topicData.tid}:recipients`),
			]);

			assert(isRecipient);
			assert.strictEqual(count, 1);
		});

		it('should add topic to a user\'s inbox if it is explicitly passed in as an argument', async () => {
			await activitypub.notes.syncUserInboxes(topicData.tid, uid);
			const inboxed = await db.isSortedSetMember(`uid:${uid}:inbox`, topicData.tid);

			assert.strictEqual(inboxed, true);
		});

		it('should remove a topic from a user\'s inbox if that user is no longer a recipient in any contained posts', async () => {
			await activitypub.notes.syncUserInboxes(topicData.tid, uid);
			await activitypub.notes.syncUserInboxes(topicData.tid);
			const inboxed = await db.isSortedSetMember(`uid:${uid}:inbox`, topicData.tid);

			assert.strictEqual(inboxed, false);
		});
	});
});
