import { createUserAndSession, beforeAllDb, afterAllTests, beforeEachDb, models, checkThrowAsync, expectThrow, createUser, expectHttpError } from '../utils/testing/testUtils';
import { EmailSender, UserFlagType } from '../services/database/types';
import { ErrorBadRequest, ErrorUnprocessableEntity } from '../utils/errors';
import { betaUserDateRange, stripeConfig } from '../utils/stripe';
import { accountByType, AccountType } from './UserModel';
import { failedPaymentFinalAccount, failedPaymentWarningInterval } from './SubscriptionModel';
import { stripePortalUrl } from '../utils/urlUtils';
import { Day } from '../utils/time';
import config from '../config';

describe('UserModel', () => {

	beforeAll(async () => {
		const envValues = {
			MFA_ENCRYPTION_KEY: '42bb51d708f1f7bfe43f074b03dfcd5f6c10fe337744d2e73deb4ee46d2b1038',
		};

		await beforeAllDb('UserModel', { envValues });
	});

	afterAll(async () => {
		await afterAllTests();
	});

	beforeEach(async () => {
		await beforeEachDb();
	});

	test('should validate user objects', async () => {
		const { user: user1 } = await createUserAndSession(2, false);
		const { user: user2 } = await createUserAndSession(3, false);

		let error = null;

		// Email must be set
		error = await checkThrowAsync(async () => await models().user().save({ email: '', password: '1234546' }));
		expect(error instanceof ErrorUnprocessableEntity).toBe(true);

		// Password must be set
		error = await checkThrowAsync(async () => await models().user().save({ email: 'newone@example.com', password: '' }));
		expect(error instanceof ErrorUnprocessableEntity).toBe(true);

		// email must be set
		error = await checkThrowAsync(async () => await models().user().save({ id: user1.id, email: '' }));
		expect(error instanceof ErrorUnprocessableEntity).toBe(true);

		// password must be set
		error = await checkThrowAsync(async () => await models().user().save({ id: user1.id, password: '' }));
		expect(error instanceof ErrorUnprocessableEntity).toBe(true);

		// there is already a user with this email
		error = await checkThrowAsync(async () => await models().user().save({ id: user1.id, email: user2.email }));
		expect(error instanceof ErrorUnprocessableEntity).toBe(true);

		// check that the email is valid
		error = await checkThrowAsync(async () => await models().user().save({ id: user1.id, email: 'ohno' }));
		expect(error instanceof ErrorUnprocessableEntity).toBe(true);

		// check that the email is not too long
		error = await checkThrowAsync(async () => await models().user().save({ id: user1.id, email: `${'long'.repeat(100)}@example.com` }));
		expect(error instanceof ErrorUnprocessableEntity).toBe(true);

		// check that the full name is not too long
		error = await checkThrowAsync(async () => await models().user().save({ id: user1.id, full_name: 'long'.repeat(400) }));
		expect(error instanceof ErrorUnprocessableEntity).toBe(true);

		// should not throw if updating with valid data
		expect(
			await checkThrowAsync(async () => await models().user().save({ id: user1.id, full_name: 'Example', email: 'new_email@example.com' })),
		).toBe(null);
	});

	test('should push an email when creating a new user', async () => {
		const { user: user1 } = await createUserAndSession(1);
		const { user: user2 } = await createUserAndSession(2);

		const emails = await models().email().all();
		expect(emails.length).toBe(2);
		expect(emails.find(e => e.recipient_email === user1.email)).toBeTruthy();
		expect(emails.find(e => e.recipient_email === user2.email)).toBeTruthy();

		const email = emails[0];
		expect(email.subject.trim()).toBeTruthy();
		expect(email.body.includes('/confirm?token=')).toBeTruthy();
		expect(email.sender_id).toBe(EmailSender.NoReply);
		expect(email.sent_success).toBe(0);
		expect(email.sent_time).toBe(0);
		expect(email.error).toBe('');
	});

	test('should send a beta reminder email', async () => {
		stripeConfig().enabled = true;
		const { user: user1 } = await createUserAndSession(1, false, { email: 'toto@example.com' });
		const range = betaUserDateRange();

		await models().user().save({
			id: user1.id,
			created_time: range[0],
			account_type: AccountType.Pro,
		});

		Date.now = jest.fn(() => range[0] + 6912000 * 1000); // 80 days later

		await models().user().handleBetaUserEmails();

		expect((await models().email().all()).length).toBe(2);

		{
			const email = (await models().email().all()).pop();
			expect(email.recipient_email).toBe('toto@example.com');
			expect(email.subject.indexOf('10 days') > 0).toBe(true);
			expect(email.body.indexOf('10 days') > 0).toBe(true);
			expect(email.body.indexOf('toto%40example.com') > 0).toBe(true);
			expect(email.body.indexOf('account_type=2') > 0).toBe(true);
		}

		await models().user().handleBetaUserEmails();

		// It should not send a second email
		expect((await models().email().all()).length).toBe(2);

		Date.now = jest.fn(() => range[0] + 7603200 * 1000); // 88 days later

		await models().user().handleBetaUserEmails();

		expect((await models().email().all()).length).toBe(3);

		{
			const email = (await models().email().all()).pop();
			expect(email.subject.indexOf('2 days') > 0).toBe(true);
			expect(email.body.indexOf('2 days') > 0).toBe(true);
		}

		await models().user().handleBetaUserEmails();

		expect((await models().email().all()).length).toBe(3);

		stripeConfig().enabled = false;
	});

	test('should disable beta account once expired', async () => {
		stripeConfig().enabled = true;
		const { user: user1 } = await createUserAndSession(1, false, { email: 'toto@example.com' });
		const range = betaUserDateRange();
		await models().user().save({
			id: user1.id,
			created_time: range[0],
			account_type: AccountType.Pro,
		});

		Date.now = jest.fn(() => range[0] + 8640000 * 1000); // 100 days later

		await models().user().handleBetaUserEmails();

		expect((await models().email().all()).length).toBe(4);
		const email = (await models().email().all()).pop();
		expect(email.subject.indexOf('beta account is expired') > 0).toBe(true);

		const reloadedUser = await models().user().load(user1.id);
		expect(reloadedUser.can_upload).toBe(0);

		const userFlag = await models().userFlag().byUserId(user1.id, UserFlagType.AccountWithoutSubscription);
		expect(userFlag).toBeTruthy();
		stripeConfig().enabled = false;
	});

	test('should disable upload and send an email if payment failed recently', async () => {
		stripeConfig().enabled = true;

		const { user: user1 } = await models().subscription().saveUserAndSubscription('toto@example.com', 'Toto', AccountType.Basic, 'usr_111', 'sub_111');
		await models().subscription().saveUserAndSubscription('tutu@example.com', 'Tutu', AccountType.Basic, 'usr_222', 'sub_222');

		const sub = await models().subscription().byUserId(user1.id);

		const now = Date.now();
		const paymentFailedTime = now - failedPaymentWarningInterval - 10;
		await models().subscription().save({
			id: sub.id,
			last_payment_time: now - failedPaymentWarningInterval * 2,
			last_payment_failed_time: paymentFailedTime,
		});

		await models().user().handleFailedPaymentSubscriptions();

		{
			const user1 = await models().user().loadByEmail('toto@example.com');
			expect(user1.can_upload).toBe(0);

			const email = (await models().email().all()).pop();
			expect(email.key).toBe(`payment_failed_upload_disabled_${paymentFailedTime}`);
			expect(email.body).toContain(stripePortalUrl());
			expect(email.body).toContain('14 days');
		}

		const beforeEmailCount = (await models().email().all()).length;
		await models().user().handleFailedPaymentSubscriptions();
		const afterEmailCount = (await models().email().all()).length;
		expect(beforeEmailCount).toBe(afterEmailCount);

		{
			const user2 = await models().user().loadByEmail('tutu@example.com');
			expect(user2.can_upload).toBe(1);
		}

		stripeConfig().enabled = false;
	});

	test('should disable the account and send an email if payment failed for good', async () => {
		stripeConfig().enabled = true;

		const { user: user1 } = await models().subscription().saveUserAndSubscription('toto@example.com', 'Toto', AccountType.Basic, 'usr_111', 'sub_111');

		const sub = await models().subscription().byUserId(user1.id);

		const now = Date.now();
		const paymentFailedTime = now - failedPaymentFinalAccount - 10;
		await models().subscription().save({
			id: sub.id,
			last_payment_time: now - failedPaymentFinalAccount * 2,
			last_payment_failed_time: paymentFailedTime,
		});

		await models().user().handleFailedPaymentSubscriptions();

		{
			const user1 = await models().user().loadByEmail('toto@example.com');
			expect(user1.enabled).toBe(0);

			const email = (await models().email().all()).pop();
			expect(email.key).toBe(`payment_failed_account_disabled_${paymentFailedTime}`);
			expect(email.body).toContain(stripePortalUrl());
		}

		stripeConfig().enabled = false;
	});

	test('should send emails and flag accounts when it is over the size limit', async () => {
		const { user: user1 } = await createUserAndSession(1);
		const { user: user2 } = await createUserAndSession(2);

		await models().user().save({
			id: user1.id,
			account_type: AccountType.Basic,
			total_item_size: Math.round(accountByType(AccountType.Basic).max_total_item_size * 0.85),
		});

		await models().user().save({
			id: user2.id,
			account_type: AccountType.Pro,
			total_item_size: Math.round(accountByType(AccountType.Pro).max_total_item_size * 0.2),
		});

		const emailBeforeCount = (await models().email().all()).length;

		await models().user().handleOversizedAccounts();

		const emailAfterCount = (await models().email().all()).length;

		expect(emailAfterCount).toBe(emailBeforeCount + 1);

		const email = (await models().email().all()).pop();
		expect(email.recipient_id).toBe(user1.id);
		expect(email.subject).toContain('80%');

		{
			// Running it again should not send a second email
			await models().user().handleOversizedAccounts();
			expect((await models().email().all()).length).toBe(emailBeforeCount + 1);
		}

		{
			// Now check that the 100% email is sent too

			for (const u of [user2]) {
				const user = await models().user().load(u.id);

				await models().user().save({
					id: user.id,
					total_item_size: Math.round(accountByType(user.account_type).max_total_item_size * 1.1),
				});

				// User upload should be enabled at this point
				expect((await models().user().load(user.id)).can_upload).toBe(1);

				const emailBeforeCount = (await models().email().all()).length;
				await models().user().handleOversizedAccounts();
				const emailAfterCount = (await models().email().all()).length;

				// User upload should be disabled
				expect((await models().user().load(user.id)).can_upload).toBe(0);
				expect(await models().userFlag().byUserId(user.id, UserFlagType.AccountOverLimit)).toBeTruthy();

				expect(emailAfterCount).toBe(emailBeforeCount + 1);
				const email = (await models().email().all()).pop();

				expect(email.recipient_id).toBe(user.id);
				expect(email.subject).toContain('100%');

				// Running it again should not send a second email
				await models().user().handleOversizedAccounts();
				expect((await models().email().all()).length).toBe(emailBeforeCount + 1);
			}
		}
	});

	test('should not send emails when an account\'s size limit has been manually increased', async () => {
		const { user: user1 } = await createUserAndSession(1);

		const totalSize = Math.round(accountByType(AccountType.Basic).max_total_item_size * 0.85);
		await models().user().save({
			id: user1.id,
			account_type: AccountType.Basic,
			total_item_size: totalSize,
			max_total_item_size: totalSize * 2,
		});

		const emailBeforeCount = (await models().email().all()).length;

		await models().user().handleOversizedAccounts();

		const emailAfterCount = (await models().email().all()).length;
		expect(emailAfterCount).toBe(emailBeforeCount);
	});

	test('should get the user public key', async () => {
		const { user: user1 } = await createUserAndSession(1);
		const { user: user2 } = await createUserAndSession(2);
		const { user: user3 } = await createUserAndSession(3);
		const { user: user4 } = await createUserAndSession(4);

		// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
		const syncInfo1: any = {
			'version': 3,
			'e2ee': {
				'value': false,
				'updatedTime': 0,
			},
			'ppk': {
				'value': {
					publicKey: 'PUBLIC_KEY_1',
					privateKey: {
						encryptionMode: 4,
						ciphertext: 'PRIVATE_KEY',
					},
				},
				'updatedTime': 0,
			},
		};

		// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
		const syncInfo2: any = JSON.parse(JSON.stringify(syncInfo1));
		syncInfo2.ppk.value.publicKey = 'PUBLIC_KEY_2';

		// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
		const syncInfo3: any = JSON.parse(JSON.stringify(syncInfo1));
		delete syncInfo3.ppk;

		await models().item().saveFromRawContent(user1, {
			body: Buffer.from(JSON.stringify(syncInfo1)),
			name: 'info.json',
		});

		await models().item().saveFromRawContent(user2, {
			body: Buffer.from(JSON.stringify(syncInfo2)),
			name: 'info.json',
		});

		await models().item().saveFromRawContent(user3, {
			body: Buffer.from(JSON.stringify(syncInfo3)),
			name: 'info.json',
		});

		expect((await models().user().publicPrivateKey(user1.id)).publicKey).toBe('PUBLIC_KEY_1');
		expect((await models().user().publicPrivateKey(user2.id)).publicKey).toBe('PUBLIC_KEY_2');
		expect((await models().user().publicPrivateKey(user3.id))).toBeFalsy();

		await expectThrow(async () => models().user().publicPrivateKey(user4.id));
	});

	test('should remove flag when account goes under the limit', async () => {
		const { user: user1 } = await createUserAndSession(1);

		await models().user().save({
			id: user1.id,
			account_type: AccountType.Basic,
			total_item_size: Math.round(accountByType(AccountType.Basic).max_total_item_size * 1.1),
		});

		await models().user().handleOversizedAccounts();
		expect(await models().userFlag().byUserId(user1.id, UserFlagType.AccountOverLimit)).toBeTruthy();

		await models().user().save({
			id: user1.id,
			total_item_size: Math.round(accountByType(AccountType.Basic).max_total_item_size * 0.5),
		});

		await models().user().handleOversizedAccounts();
		expect(await models().userFlag().byUserId(user1.id, UserFlagType.AccountOverLimit)).toBeFalsy();
	});

	test('should disable and enable users', async () => {
		const { user: user1 } = await createUserAndSession(1);
		const { user: user2 } = await createUserAndSession(2);

		jest.useFakeTimers();

		const t0 = new Date('2022-01-01').getTime();
		jest.setSystemTime(t0);

		await models().userFlag().add(user1.id, UserFlagType.ManuallyDisabled);

		expect((await models().user().load(user1.id)).enabled).toBe(0);
		expect((await models().user().load(user2.id)).enabled).toBe(1);

		const t1 = new Date('2022-02-01').getTime();
		jest.setSystemTime(t1);

		// If we run the user deletion service at this point, it should add the
		// disabled account
		await models().userDeletion().autoAdd(10, 10 * Day, t1 + 3 * Day);
		expect(await models().userDeletion().count()).toBe(1);

		// If we make the account enabled again, the user should be immediately
		// removed from the queue
		await models().userFlag().remove(user1.id, UserFlagType.ManuallyDisabled);
		expect(await models().userDeletion().count()).toBe(0);

		await models().userFlag().add(user1.id, UserFlagType.ManuallyDisabled);

		const t2 = new Date('2022-03-01').getTime();
		jest.setSystemTime(t2);

		// Should be added again
		await models().userDeletion().autoAdd(10, 10 * Day, t2 + 3 * Day);
		expect(await models().userDeletion().count()).toBe(1);

		const t3 = new Date('2022-04-01').getTime();
		jest.setSystemTime(t3);

		// Now if the service were to run, the user deletion would start and it
		// should no longer be possible to remove it from the queue. And it
		// shouldn't be possible to enable the user either.
		const job = await models().userDeletion().next();
		expect(job.user_id).toBe(user1.id);
		await models().userDeletion().start(job.id);

		await models().userFlag().add(user1.id, UserFlagType.ManuallyDisabled);
		expect((await models().user().load(user1.id)).enabled).toBe(0);
	});

	test('should throw an error if the password being saved seems to be hashed', async () => {
		const passwordSimilarToHash = '$2a$10';

		const user = await models().user().save({
			email: 'test@example.com',
			password: '111111',
		});

		const error = await checkThrowAsync(async () => await models().user().save({ id: user.id, password: passwordSimilarToHash }));

		expect(error.message).toBe(`Unable to save user because password already seems to be hashed. User id: ${user.id}`);
		expect(error instanceof ErrorBadRequest).toBe(true);
	});

	test('should not allow the creation of user records with invalid email address', async () => {

		const error = await checkThrowAsync(
			async () => await models().user().save({
				email: 'invalid_email.com',
			}),
		);

		expect(error.message).toBe('Should include @ in email address, email: invalid_email.com');
		expect(error instanceof ErrorUnprocessableEntity).toBe(true);
	});

	test('should generate a notification when MFA is enabled', async () => {
		const user = await models().user().save({
			email: 'test@example.com',
			password: '111111',
		});
		// cSpell:disable
		await models().user().enableMFA(user.id, 'KUKNC5O2CGOLU6EVKT2PRAHE3AK7MKY3', '');
		// cSpell:enable

		const notifications = await models().notification().allUnreadByUserId(user.id);
		expect(notifications.length).toBe(1);
		expect(notifications[0].message).toBe('Multi-factor authentication has been enabled for your account. Please remember to copy and save your recovery codes');
	});

	test('should create a isNewlyCreated keyValue record after MFA is enabled', async () => {
		const user = await models().user().save({
			email: 'test@example.com',
			password: '111111',
		});
		// cSpell:disable
		await models().user().enableMFA(user.id, 'KUKNC5O2CGOLU6EVKT2PRAHE3AK7MKY3', '');
		// cSpell:enable

		const isNewlyCreated = await models().keyValue().value(`RecoveryCode::isNewlyCreated::${user.id}`);
		expect(isNewlyCreated).toBe(1);
	});

	test('should delete all other sessions when MFA is enabled', async () => {
		const user = await models().user().save({
			email: 'test@example.com',
			password: '111111',
		});
		await models().session().createUserSession(user.id);
		await models().session().createApplicationSession(user.id, '00000000-0000-0000-0000-000000000001');
		await models().session().createApplicationSession(user.id, '00000000-0000-0000-0000-000000000002');

		const session = await models().session().createUserSession(user.id);

		let sessions = await models().session().all();
		expect(sessions.length).toBe(4);
		// cSpell:disable
		await models().user().enableMFA(user.id, 'KUKNC5O2CGOLU6EVKT2PRAHE3AK7MKY3', session.id);
		// cSpell:enable
		sessions = await models().session().all();
		expect(sessions.length).toBe(1);
		expect(sessions[0].id).toBe(session.id);
	});

	test('should throw error if password is empty', async () => {
		await expectHttpError(() => models().user().isPasswordValid('', undefined as string), 400);
	});

	test('should delete all applications when password is reset', async () => {
		const user = await models().user().save({
			email: 'test@example.com',
			password: '111111',
		});

		await models().application().createPreLoginRecord('random-string', '');
		await models().application().onAuthorizeUse('random-string', user.id);
		await models().application().createPreLoginRecord('random-string2', '');
		await models().application().onAuthorizeUse('random-string2', user.id);

		let applications = await models().application().all();
		expect(applications.length).toBe(2);

		const url = await models().user().generateLinkForPasswordReset(user.id);
		const token = url.split('?token=')[1];
		await models().user().resetPassword(token, { password: '111111', password2: '111111' });

		applications = await models().application().all();
		expect(applications.length).toBe(0);
	});

	test('should not log in an user using a email/password combo when the local auth is disabled', async () => {
		config().LOCAL_AUTH_ENABLED = false;

		const user = await createUser();

		expect(await models().user().login(user.email, '123456')).toBe(null);
	});

	test('should not change user properties managed by SAML', async () => {
		const user1 = await createUser(1);

		config().SAML_ENABLED = true;

		try {
			await models().user().save({
				id: user1.id,
				is_external: 1,
				full_name: 'testing',
			}, { skipValidation: true });

			const previousUser = await models().user().load(user1.id);

			await models().user().save({
				id: user1.id,
				full_name: 'testing mod',
				email: 'testingmod@example.com',
				password: 'mod',
				max_item_size: 12345,
			});

			const modUser = await models().user().load(user1.id);

			expect(modUser.full_name).toBe(previousUser.full_name);
			expect(modUser.email).toBe(previousUser.email);
			expect(modUser.password).toBe(previousUser.password);
			expect(modUser.max_item_size).toBe(12345);
		} finally {
			config().SAML_ENABLED = false;
		}
	});

	test('should generate a unique SSO code', async () => {
		const createExternalUser = async (index: number) => {
			const user = await createUser(index);
			return await models().user().save({
				id: user.id,
				is_external: 1,
			}, { skipValidation: true });
		};
		const user1 = await createExternalUser(1);
		const user2 = await createExternalUser(2);
		config().SAML_ENABLED = true;

		try {
			await models().user().generateSsoCode(user1);
			await models().user().generateSsoCode(user2);

			const code1 = (await models().user().load(user1.id)).sso_auth_code;
			const code2 = (await models().user().load(user2.id)).sso_auth_code;
			expect(code1).not.toBe(code2);
		} finally {
			config().SAML_ENABLED = false;
		}
	});

});
