'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 groups = require('../../src/groups');
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');

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

	describe('topic already in multiple categories', () => {
		let tid;
		let cid1;
		let cid2;
		let uid;

		before(async () => {
			({ cid: cid1 } = await categories.create({ name: utils.generateUUID().slice(0, 8) }));
			uid = await user.create({ username: utils.generateUUID().slice(0, 8) });
			const { topicData } = await topics.post({
				uid,
				cid: cid1,
				title: utils.generateUUID(),
				content: utils.generateUUID(),
			});
			tid = topicData.tid;

			// Add topic to another category's zset
			const crosspostCategory = await categories.create({ name: utils.generateUUID().slice(0, 8) });
			cid2 = crosspostCategory.cid;
			await db.sortedSetAdd(`cid:${crosspostCategory.cid}:tids`, topicData.timestamp, tid);
		});

		it('should contain the topic in both categories when requested', async () => {
			const tids1 = await categories.getTopicIds({
				uid,
				cid: cid1,
				start: 0,
				stop: 1,
			});

			const tids2 = await categories.getTopicIds({
				uid,
				cid: cid2,
				start: 0,
				stop: 1,
			});

			assert.deepStrictEqual(tids1, tids2);
		});
	});

	describe('crosspost', () => {
		let tid;
		let cid1;
		let cid2;
		let uid;

		before(async () => {
			({ cid: cid1 } = await categories.create({ name: utils.generateUUID().slice(0, 8) }));
			const crosspostCategory = await categories.create({ name: utils.generateUUID().slice(0, 8) });
			cid2 = crosspostCategory.cid;
			uid = await user.create({ username: utils.generateUUID().slice(0, 8) });
			const { topicData } = await topics.post({
				uid,
				cid: cid1,
				title: utils.generateUUID(),
				content: utils.generateUUID(),
			});
			tid = topicData.tid;
		});

		it('should not allow a spider (uid -1) to crosspost', async () => {
			await assert.rejects(
				topics.crossposts.add(tid, cid2, -1),
				{ message: '[[error:not-allowed]]' }
			);
		});

		it('should not allow a crosspost if privilege is missing', async () => {
			await privileges.categories.rescind(['groups:topics:crosspost'], cid2, 'registered-users');
			await assert.rejects(
				topics.crossposts.add(tid, cid2, uid),
				{ message: '[[error:not-allowed]]' }
			);
			await privileges.categories.give(['groups:topics:crosspost'], cid2, 'registered-users');
		});

		it('should successfully crosspost to another cid', async () => {
			const crossposts = await topics.crossposts.add(tid, cid2, uid);

			assert(Array.isArray(crossposts));
			assert.strictEqual(crossposts.length, 1);

			const actual = crossposts[0];
			assert.deepStrictEqual({
				uid: actual.uid,
				tid: actual.tid,
				cid: actual.cid,
			}, {
				uid,
				tid,
				cid: cid2,
			});
		});

		it('should show the tid in both categories when requested', async () => {
			const tids1 = await categories.getTopicIds({
				uid,
				cid: cid1,
				start: 0,
				stop: 1,
			});

			const tids2 = await categories.getTopicIds({
				uid,
				cid: cid2,
				start: 0,
				stop: 1,
			});

			assert.deepStrictEqual(tids1, tids2);
		});

		it('should throw on cross-posting again when already cross-posted', async () => {
			await assert.rejects(
				topics.crossposts.add(tid, cid2, uid),
				{ message: '[[error:topic-already-crossposted]]' },
			);
		});
	});

	describe('uncrosspost', () => {
		let tid;
		let cid1;
		let cid2;
		let uid;

		before(async () => {
			({ cid: cid1 } = await categories.create({ name: utils.generateUUID().slice(0, 8) }));
			const crosspostCategory = await categories.create({ name: utils.generateUUID().slice(0, 8) });
			cid2 = crosspostCategory.cid;
			uid = await user.create({ username: utils.generateUUID().slice(0, 8) });
			const { topicData } = await topics.post({
				uid,
				cid: cid1,
				title: utils.generateUUID(),
				content: utils.generateUUID(),
			});
			tid = topicData.tid;

			await topics.crossposts.add(tid, cid2, uid);
		});

		it('should not let another user uncrosspost', async () => {
			const uid2 = await user.create({ username: utils.generateUUID().slice(0, 8) });
			await assert.rejects(
				topics.crossposts.remove(tid, cid2, uid2),
				{ message: '[[error:invalid-data]]' },
			);
		});

		it('should successfully uncrosspost from a cid', async () => {
			const crossposts = await topics.crossposts.remove(tid, cid2, uid);

			assert(Array.isArray(crossposts));
			assert.strictEqual(crossposts.length, 0);
		});

		it('should not contain the topic in the category the topic was uncrossposted from', async () => {
			const tids = await categories.getTopicIds({
				uid,
				cid: cid2,
				start: 0,
				stop: 1,
			});

			assert(!tids.includes(tid));
		});

		it('should throw on uncrossposting if already uncrossposted', async () => {
			await assert.rejects(
				topics.crossposts.remove(tid, cid2, uid),
				{ message: '[[error:invalid-data]]' },
			);
		});
	});

	describe('uncrosspost (as administrator)', () => {
		let tid;
		let cid1;
		let cid2;
		let uid;
		let privUid;

		before(async () => {
			({ cid: cid1 } = await categories.create({ name: utils.generateUUID().slice(0, 8) }));
			const crosspostCategory = await categories.create({ name: utils.generateUUID().slice(0, 8) });
			cid2 = crosspostCategory.cid;
			uid = await user.create({ username: utils.generateUUID().slice(0, 8) });
			privUid = await user.create({ username: utils.generateUUID().slice(0, 8) });
			await groups.join('administrators', privUid);

			const { topicData } = await topics.post({
				uid,
				cid: cid1,
				title: utils.generateUUID(),
				content: utils.generateUUID(),
			});
			tid = topicData.tid;

			await topics.crossposts.add(tid, cid2, uid);
		});

		it('should successfully uncrosspost from a cid', async () => {
			const crossposts = await topics.crossposts.remove(tid, cid2, privUid);

			assert(Array.isArray(crossposts));
			assert.strictEqual(crossposts.length, 0);
		});
	});

	describe('uncrosspost (as global moderator)', () => {
		let tid;
		let cid1;
		let cid2;
		let uid;
		let privUid;

		before(async () => {
			({ cid: cid1 } = await categories.create({ name: utils.generateUUID().slice(0, 8) }));
			const crosspostCategory = await categories.create({ name: utils.generateUUID().slice(0, 8) });
			cid2 = crosspostCategory.cid;
			uid = await user.create({ username: utils.generateUUID().slice(0, 8) });
			privUid = await user.create({ username: utils.generateUUID().slice(0, 8) });
			await groups.join('Global Moderators', privUid);

			const { topicData } = await topics.post({
				uid,
				cid: cid1,
				title: utils.generateUUID(),
				content: utils.generateUUID(),
			});
			tid = topicData.tid;

			await topics.crossposts.add(tid, cid2, uid);
		});

		it('should successfully uncrosspost from a cid', async () => {
			const crossposts = await topics.crossposts.remove(tid, cid2, privUid);

			assert(Array.isArray(crossposts));
			assert.strictEqual(crossposts.length, 0);
		});
	});

	describe('uncrosspost (as category moderator)', () => {
		let tid;
		let cid1;
		let cid2;
		let uid;
		let privUid;

		before(async () => {
			({ cid: cid1 } = await categories.create({ name: utils.generateUUID().slice(0, 8) }));
			const crosspostCategory = await categories.create({ name: utils.generateUUID().slice(0, 8) });
			cid2 = crosspostCategory.cid;
			uid = await user.create({ username: utils.generateUUID().slice(0, 8) });
			privUid = await user.create({ username: utils.generateUUID().slice(0, 8) });

			const { topicData } = await topics.post({
				uid,
				cid: cid1,
				title: utils.generateUUID(),
				content: utils.generateUUID(),
			});
			tid = topicData.tid;

			await topics.crossposts.add(tid, cid2, uid);
		});

		it('should fail to uncrosspost if not mod of passed-in category', async () => {
			await privileges.categories.give(['moderate'], cid1, [privUid]);
			await assert.rejects(
				topics.crossposts.remove(tid, cid2, privUid),
				{ message: '[[error:invalid-data]]' },
			);
		});

		it('should successfully uncrosspost from a cid if proper mod', async () => {
			await privileges.categories.give(['moderate'], cid2, [privUid]);
			const crossposts = await topics.crossposts.remove(tid, cid2, privUid);

			assert(Array.isArray(crossposts));
			assert.strictEqual(crossposts.length, 0);
		});
	});

	describe('Deletion', () => {
		let tid;
		let cid1;
		let cid2;
		let uid;

		before(async () => {
			({ cid: cid1 } = await categories.create({ name: utils.generateUUID().slice(0, 8) }));
			const crosspostCategory = await categories.create({ name: utils.generateUUID().slice(0, 8) });
			cid2 = crosspostCategory.cid;
			uid = await user.create({ username: utils.generateUUID().slice(0, 8) });
			const { topicData } = await topics.post({
				uid,
				cid: cid1,
				title: utils.generateUUID(),
				content: utils.generateUUID(),
			});
			tid = topicData.tid;

			await topics.crossposts.add(tid, cid2, uid);
			await topics.delete(tid, uid);
		});

		it('should maintain crossposts when topic is deleted', async () => {
			const crossposts = await topics.crossposts.get(tid);
			assert(Array.isArray(crossposts));
			assert.strictEqual(crossposts.length, 1);
		});
	});

	describe('Purging', () => {
		let tid;
		let cid1;
		let cid2;
		let uid;

		before(async () => {
			({ cid: cid1 } = await categories.create({ name: utils.generateUUID().slice(0, 8) }));
			const crosspostCategory = await categories.create({ name: utils.generateUUID().slice(0, 8) });
			cid2 = crosspostCategory.cid;
			uid = await user.create({ username: utils.generateUUID().slice(0, 8) });
			const { topicData } = await topics.post({
				uid,
				cid: cid1,
				title: utils.generateUUID(),
				content: utils.generateUUID(),
			});
			tid = topicData.tid;

			await topics.crossposts.add(tid, cid2, uid);
			await topics.purge(tid, uid);
		});

		it('should remove crossposts when topic is purged', async () => {
			const crossposts = await topics.crossposts.get(tid);
			assert(Array.isArray(crossposts));
			assert.strictEqual(crossposts.length, 0);
		});
	});

	describe('category sync; integration with', () => {
		let cid;
		let remoteCid;
		let pid;
		let post;

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

		before(async () => {
			({ cid } = await categories.create({ name: utils.generateUUID().slice(0, 8) }));
			({ id: remoteCid } = helpers.mocks.group());
			({ id: pid, note: post } = helpers.mocks.note({
				audience: [remoteCid],
			}));

			// Mock a group follow/accept
			const timestamp = Date.now();
			await Promise.all([
				db.sortedSetAdd(`cid:${cid}:following`, timestamp, remoteCid),
				db.sortedSetAdd(`followersRemote:${remoteCid}`, timestamp, `cid|${cid}`),
			]);
		});

		it('should automatically cross-post the topic when the remote category announces', async () => {
			const { activity: body } = helpers.mocks.announce({
				actor: remoteCid,
				object: post,
			});

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

			const tid = await posts.getPostField(pid, 'tid');
			const crossposts = await topics.crossposts.get(tid);

			assert.strictEqual(crossposts.length, 1);

			const actual = crossposts[0];
			assert.deepStrictEqual({
				uid: actual.uid,
				tid: actual.tid,
				cid: actual.cid,
			}, {
				uid: 0,
				tid,
				cid: cid,
			});
		});
	});

	describe('auto-categorization; integration with', () => {
		let cid;
		let remoteCid;
		let pid;
		let post;

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

		before(async () => {
			const preferredUsername = utils.generateUUID().slice(0, 8);
			({ cid } = await categories.create({ name: utils.generateUUID().slice(0, 8) }));
			({ id: remoteCid } = helpers.mocks.group({
				preferredUsername,
			}));
			({ id: pid, note: post } = helpers.mocks.note({
				audience: [remoteCid],
				tag: [
					{
						type: 'Hashtag',
						name: `#${preferredUsername}`,
					},
				],
			}));

			await activitypub.rules.add('hashtag', preferredUsername, cid);
		});

		it('note assertion should automatically cross-post', async () => {
			await activitypub.notes.assert(0, pid, { skipChecks: true });

			const tid = await posts.getPostField(pid, 'tid');
			const crossposts = await topics.crossposts.get(tid);
			assert.strictEqual(crossposts.length, 1);

			const actual = crossposts[0];
			assert.deepStrictEqual({
				uid: actual.uid,
				tid: actual.tid,
				cid: actual.cid,
			}, {
				uid: 0,
				tid,
				cid: cid,
			});
		});
	});

	describe('ActivityPub effects (or lack thereof)', () => {
		describe('local canonical category', () => {
			let tid;
			let cid1;
			let cid2;
			let uid;
			let pid;

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

			before(async () => {
				({ cid: cid1 } = await categories.create({ name: utils.generateUUID().slice(0, 8) }));
				const crosspostCategory = await categories.create({ name: utils.generateUUID().slice(0, 8) });
				cid2 = crosspostCategory.cid;
				uid = await user.create({ username: utils.generateUUID().slice(0, 8) });
				const { topicData } = await topics.post({
					uid,
					cid: cid1,
					title: utils.generateUUID(),
					content: utils.generateUUID(),
				});
				tid = topicData.tid;
				pid = topicData.mainPid,

				// Add some remote followers
				await Promise.all([cid1, cid2].map(async (cid) => {
					const {activity} = helpers.mocks.follow({
						object: {
							id: `${nconf.get('url')}/category/${cid}`,
						},
					});
					await activitypub.inbox.follow({
						body: activity,
					});
				}));

				activitypub._sent.clear();
			});

			afterEach(() => {
				activitypub._sent.clear();
			});

			it('should not federate out any events on crosspost', async () => {
				await topics.crossposts.add(tid, cid2, uid);
				assert.strictEqual(activitypub._sent.size, 0);
			});

			it('should not federate out anything on uncrosspost', async () => {
				await topics.crossposts.remove(tid, cid2, uid);
				assert.strictEqual(activitypub._sent.size, 0);
			});

			it('should only federate an Announce on a remote reply from the canonical cid', async () => {
				const { note: object } = helpers.mocks.note({
					audience: `${nconf.get('url')}/category/${cid1}`,
					inReplyTo: `${nconf.get('url')}/post/${pid}`,
				});
				const { activity } = helpers.mocks.create(object);
				await activitypub.inbox.create({
					body: activity,
				});

				assert.strictEqual(activitypub._sent.size, 1);

				const actual = Array.from(activitypub._sent).pop()[1];
				assert.deepStrictEqual({
					type: actual.payload.type,
					actor: actual.payload.actor,
					object: actual.payload.object,
				}, {
					type: 'Announce',
					actor: `${nconf.get('url')}/category/${cid1}`,
					object: activity,
				});
			});

			it('should only federate an Announce on a remote like from the canonical cid', async () => {
				const { activity: body } = helpers.mocks.like({
					object: {
						id: `${nconf.get('url')}/post/${pid}`,
					},
				});
				await activitypub.inbox.like({ body });

				assert.strictEqual(activitypub._sent.size, 1);

				const actual = Array.from(activitypub._sent).pop()[1];
				assert.deepStrictEqual({
					type: actual.payload.type,
					actor: actual.payload.actor,
					object: actual.payload.object,
				}, {
					type: 'Announce',
					actor: `${nconf.get('url')}/category/${cid1}`,
					object: body,
				});
			});
		});

		describe('remote canonical category', () => {
			let tid;
			let cid;
			let remoteCid;
			let uid;
			let pid;

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

			before(async () => {
				({ id: remoteCid } = helpers.mocks.group());
				await activitypub.actors.assertGroup(remoteCid);
				({ cid } = await categories.create({ name: utils.generateUUID().slice(0, 8) }));
				uid = await user.create({ username: utils.generateUUID().slice(0, 8) });

				({ id: pid } = helpers.mocks.note());
				await activitypub.notes.assert(0, pid, { skipChecks: 1, cid: remoteCid });

				tid = await posts.getPostField(pid, 'tid');

				await topics.crossposts.add(tid, cid, uid);
			});

			it('should properly address the remote category when federating out a local reply', async () => {
				const postData = await topics.reply({
					uid,
					cid,
					tid,
					content: utils.generateUUID(),
				});
				const mocked = await activitypub.mocks.notes.public(postData);
				assert(mocked.to.includes(remoteCid));
			});
		});
	});
});