'use strict';
/**
 * Tests for Punishments.
 *
 * @author mia-pi-git
 * @author Annika
 */
const assert = require('../assert');
const { makeUser, makeConnection } = require('../users-utils');
const { Punishments } = require('../../dist/server/punishments');

const TEST_PUNISHMENT_DURATION = 1000; // 1 second

describe("Punishments", () => {
	it("Should properly sort punishments by weight", () => {
		const list = [
			{ type: "LOCK", id: "User 1", expireTime: Date.now() + 1000, reason: '' },
			{ type: "SEMILOCK", id: "User 2", expireTime: Date.now() + 1000, reason: '' },
			{ type: "BAN", id: "", expireTime: Date.now() + 1000, reason: '' },
		];
		Punishments.byWeight(list);
		assert.equal(list[0].type, 'BAN');
	});
	it("Should prevent a user from having two punishments of the same type", () => {
		Punishments.userids.add('banmeplease', { type: 'BAN', expireTime: Date.now() + 30 * 1000, id: 'banmeplease', reason: '' });
		Punishments.userids.add("banmeplease", { type: 'BAN', expireTime: Date.now() + 30 * 1000, id: 'banmeplease', reason: 'ok' });
		assert.equal(Punishments.userids.get('banmeplease').length, 1);
	});
	it("Should overwrite the old reason when a user receives two of the same punishment", () => {
		Punishments.userids.add('banmeplease', { type: 'BAN', expireTime: Date.now() + 30 * 1000, id: 'banmeplease', reason: '' });
		Punishments.userids.add("banmeplease", { type: 'BAN', expireTime: Date.now() + 30 * 1000, id: 'banmeplease', reason: 'ok' });
		assert.equal(Punishments.userids.getByType('banmeplease', 'BAN').reason, 'ok');
	});
	it("Should properly filter out expiring punishments", () => {
		const punishments = [{ type: 'BAN', expireTime: Date.now() - 1000, id: 'banmeplease', reason: '' }];
		Punishments.userids.removeExpiring(punishments);
		assert.equal(punishments.length, 0);
	});
	it("Should be able to remove only one punishment from the list by passing an object", () => {
		const [expireTime, reason, id] = [Date.now() + 1000, '', 'banmeplease'];
		Punishments.userids.add(id, { type: 'BAN', expireTime, reason, id });
		Punishments.userids.add(id, { type: 'RICKROLL', expireTime, reason, id });
		Punishments.userids.deleteOne(id, { type: 'RICKROLL', expireTime, reason, id });
		assert.equal(Punishments.userids.get(id).length, 1);
	});

	it('should properly search for IP punishments by type', () => {
		const [expireTime, reason, id] = [Date.now() + 1000, '', 'banmeplease'];
		Punishments.ips.add('127.0.0.1', { type: 'BAN', expireTime, reason, id });
		Punishments.ips.add('127.0.0.1', { type: 'RICKROLL', expireTime, reason, id });
		Punishments.ips.add('127.0.*', { type: 'RANGEBAN', expireTime, reason, id });

		const allIPPunishments = Punishments.ipSearch('127.0.0.1');
		assert(Array.isArray(allIPPunishments));
		assert.equal(allIPPunishments.length, 3);

		const ban = Punishments.ipSearch('127.0.0.1', 'BAN');
		assert(!Array.isArray(ban));
		assert.equal(ban.type, 'BAN');

		const rickroll = Punishments.ipSearch('127.0.0.1', 'RICKROLL');
		assert(!Array.isArray(rickroll));
		assert.equal(rickroll.type, 'RICKROLL');
	});
});

describe('broader, more integrated Punishments tests', function () {
	before(() => {
		this.room = Rooms.get('lobby');

		this.parse = async function (message) {
			Chat.loadPlugins();
			const context = new Chat.CommandContext({
				message,
				room: this.room,
				user: this.user,
				connection: this.connection,
			});
			return context.parse();
		};
	});

	describe('room bans', () => {
		before(() => {
			this.user = makeUser("Roomban Me Please", '127.0.0.8');
			this.connection = this.user.connections[0];
			this.user.joinRoom(this.room.roomid, this.connection);
		});

		beforeEach(async () => Punishments.roomBan(this.room, this.user, Date.now() + TEST_PUNISHMENT_DURATION, this.user.id, false, 'test'));
		afterEach(() => Punishments.roomUnban(this.room, this.user.id));

		it('should force the user to leave the room and prevent them from rejoining', async () => {
			assert.equal(this.user.id in this.room.users, false, `user should not be in the room`);
			let joinAttempt = await this.user.tryJoinRoom(this.room, this.connection);
			assert.equal(joinAttempt, false, `user should be unable to join a room they are banned from`);

			Punishments.roomUnban(this.room, this.user.id);
			joinAttempt = await this.user.tryJoinRoom(this.room, this.connection);
			assert.equal(joinAttempt, true, `user should be able to join the room once they are unbanned`);
		});

		it.skip('should expire on its own', done => {
			assert(Punishments.hasRoomPunishType(this.room, 'roombanmeplease', 'ROOMBAN'), `should be in effect`);

			setTimeout(() => {
				assert(!Punishments.hasRoomPunishType(this.room, 'roombanmeplease', 'ROOMBAN'), `should have expired`);
				done();
			}, TEST_PUNISHMENT_DURATION);
		});
	});

	describe('locks (network) (slow)', () => {
		before(() => {
			this.user = makeUser("Lock Me Please", '127.0.0.3');
			this.connection = this.user.connections[0];
			this.user.joinRoom(this.room.roomid, this.connection);
		});

		beforeEach(async () => Punishments.lock(this.user, Date.now() + TEST_PUNISHMENT_DURATION, this.user.id, false, 'test'));
		afterEach(() => Punishments.unlock(this.user.id));

		it('should prevent users from chatting in rooms while they are locked', async () => {
			const initialLogLength = this.room.log.log.length;

			await this.parse("Hi! I'm a locked user!");
			assert.equal(this.room.log.log.length, initialLogLength, `user should be unable to successfully chat while locked`);

			Punishments.unlock(this.user.id);
			await this.parse("/msgroom lobby,Hi! I'm no longer locked!");
			// we can't just check the roomlog length because unlocking adds a |n| message to
			const lastMessage = this.room.log.log.pop();
			assert(lastMessage.endsWith(` Lock Me Please|Hi! I'm no longer locked!`), `user should have successfully sent a message after being locked`);
		});

		// This test relies on Chat#parse returning `false` when permission is denied.
		// I'm not sure if this is an intended feature, but the way Chat is currently implemented,
		// an `ErrorMessage` is the only time `false` will be returned by Chat#parse (unless a chat command returns it, which /msg does not).
		// If you are here because this test is failing, check if the above assumptions are still valid.
		// If they are not, the test should either be refactored to use another way of
		// determining whether a PM was successful (such as modifying Chat.sendPM), or skipped entirely.
		it('should prevent users from sending PMs other than to staff while they are locked', async () => {
			makeUser("Some Random Reg", '127.0.0.4');
			makeUser("Annika", '127.0.0.5').tempGroup = '~';

			let result = await this.parse("/msg Some Random Reg, Hi! I'm a locked user!");
			assert.equal(result, false, `user should be unable to successfully send PMs while locked`);

			result = await this.parse("/msg Annika, Hi! I'm a locked user!");
			assert.notEqual(result, false, `user should be able to send PMs to global staff while locked`);

			Punishments.unlock(this.user.id);
			result = await this.parse("/msg Annika, Hi! I'm a locked user!");
			assert.notEqual(result, false, `user should be able to send PMs after being unlocked`);
			result = await this.parse("/msg Some Random Reg, Hi! I'm a locked user!");
			assert.notEqual(result, false, `user should be able to send PMs after being unlocked`);
		});

		// expiry tests are skipped because they fail when another test fails.
		// I don't know why this happens; I'm guessing it's an ideosycrasy of Mocha.
		it.skip('should expire on its own', done => {
			assert(this.user.locked);
			assert(Punishments.hasPunishType(this.user.id, 'LOCK'));

			setTimeout(() => {
				assert(!this.user.locked);
				assert(!Punishments.hasPunishType(this.user.id, 'LOCK'));
				done();
			}, TEST_PUNISHMENT_DURATION);
		});
	});

	describe('namelocks (network) (slow)', () => {
		before(() => {
			this.user = makeUser("Namelock Me Please", '127.0.0.6');
			this.connection = this.user.connections[0];
			this.user.joinRoom(this.room.roomid, this.connection);
		});

		beforeEach(async () => Punishments.namelock(this.user, Date.now() + TEST_PUNISHMENT_DURATION, this.user.id, false, 'test'));
		afterEach(() => Punishments.unnamelock(this.user.id));

		it('should prevent the user from having a username', async () => {
			assert(this.user.id.startsWith('guest'));

			await this.user.rename('some other name', '', false, this.connection);
			assert(this.user.id.startsWith('guest'));
		});

		it.skip('should expire on its own', done => {
			assert(this.user.locked);
			assert(this.user.namelocked);
			assert(Punishments.hasPunishType('namelockmeplease', 'NAMELOCK'));

			setTimeout(() => {
				assert(!this.user.locked);
				assert(!this.user.namelocked);
				assert(!Punishments.hasPunishType('namelockmeplease', 'NAMELOCK'));
				done();
			}, TEST_PUNISHMENT_DURATION);
		});
	});

	describe('global bans (network) (slow)', () => {
		before(() => {
			this.user = makeUser("Ban Me Please", '127.0.0.7');
			this.connection = this.user.connections[0];
			this.user.joinRoom(this.room.roomid, this.connection);
		});

		beforeEach(async () => Punishments.ban(this.user, Date.now() + TEST_PUNISHMENT_DURATION, this.user.id, false, 'test'));
		afterEach(() => Punishments.unban(this.user.id));

		it('should disconnect the user and prevent them from reconnecting', () => {
			assert.equal(this.user.connected, false, `user should be disconnected`);

			const conn = makeConnection('127.0.0.7');
			assert(Punishments.checkIpBanned(conn), `IP should be banned`);
		});

		it.skip('should expire on its own', done => {
			assert(Punishments.checkIpBanned(makeConnection('127.0.0.7')), `IP should be banned`);
			assert(Punishments.hasPunishType('banmeplease', 'BAN'));

			setTimeout(() => {
				assert(!Punishments.checkIpBanned(makeConnection('127.0.0.7')), `IP should no longer be banned`);
				assert(!Punishments.hasPunishType('banmeplease', 'BAN'));
				done();
			}, TEST_PUNISHMENT_DURATION);
		});
	});
});
