import * as querystring from "querystring";
import supertest from "supertest";

import User from "../src/auth/models/User.js";
import UserEmail from "../src/auth/models/UserEmail.js";
import UserNameComponent from "../src/auth/models/UserNameComponent.js";
import UserPasswordComponent from "../src/auth/password/UserPasswordComponent.js";
import useApp from "./_app.js";
import {authAppProvider, followMagicLinkFromMail, testLogout} from "./_authentication_common.js";
import {popEmail} from "./_mail_server.js";

const app = useApp(authAppProvider());

let agent: supertest.SuperTest<supertest.Test>;

beforeAll(() => {
    agent = supertest(app().getExpressApp());
});

test('Approval Mode', () => {
    expect(User.isApprovalMode()).toStrictEqual(false);
});

describe('Register with username and password (password)', () => {
    let cookies: string[];
    let csrf: string;

    test('General case', async () => {
        const res = await agent.get('/csrf').expect(200);
        cookies = res.get('Set-Cookie');
        csrf = res.text;

        // Register user
        await agent.post('/auth/register')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                auth_method: 'password',
                identifier: 'entrapta',
                password: 'darla_is_cute',
                password_confirmation: 'darla_is_cute',
                terms: 'on',
            })
            .expect(302)
            .expect('Location', '/');

        // Verify saved user
        const user = await User.select()
            .where('name', 'entrapta')
            .first();

        expect(user).toBeDefined();
        expect(user?.as(UserNameComponent).getName()).toStrictEqual('entrapta');
        await expect(user?.as(UserPasswordComponent).verifyPassword('darla_is_cute')).resolves.toStrictEqual(true);

        // Proof must not be revoked
        await agent.get('/has-any-password-auth-proof')
            .set('Cookie', cookies)
            .expect(200);
    });

    test('Can\'t register when logged in', async () => {
        await agent.post('/auth/register')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                auth_method: 'password',
                identifier: 'entrapta2',
                password: 'darla_is_cute',
                password_confirmation: 'darla_is_cute',
                terms: 'on',
            })
            .expect(302)
            .expect('Location', '/');

        const user2 = await User.select()
            .where('name', 'entrapta2')
            .first();
        expect(user2).toBeNull();
    });

    test('Logout', async () => {
        await testLogout(agent, cookies, csrf);
    });

    test('Cannot register taken username', async () => {
        // Check that there is no hordak in DB
        expect(await User.select()
            .where('name', 'hordak')
            .count()).toStrictEqual(0);

        const res1 = await agent.get('/csrf').expect(200);
        const cookies = res1.get('Set-Cookie');
        const csrf = res1.text;

        // Register user
        await agent.post('/auth/register')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                auth_method: 'password',
                identifier: 'hordak',
                password: 'horde_prime_will_rise',
                password_confirmation: 'horde_prime_will_rise',
                terms: 'on',
            })
            .expect(302)
            .expect('Location', '/');
        await testLogout(agent, cookies, csrf);

        // Verify saved user
        expect(await User.select()
            .where('name', 'hordak')
            .count()).toStrictEqual(1);


        // Attempt register same user
        const res = await agent.post('/auth/register')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                auth_method: 'password',
                identifier: 'hordak',
                password: 'horde_prime_will_rise_unless',
                password_confirmation: 'horde_prime_will_rise_unless',
                terms: 'on',
            })
            .expect(400);
        // username field should be translated from identifier
        expect(res.body.messages?.identifier?.name).toStrictEqual('AlreadyExistsValidationError');

        // Verify nothing changed
        expect(await User.select()
            .where('name', 'hordak')
            .count()).toStrictEqual(1);
    });
});

describe('Register with email (magic_link)', () => {
    test('General case', async () => {
        const res = await agent.get('/csrf').expect(200);
        const cookies = res.get('Set-Cookie');
        const csrf = res.text;

        await agent.post('/auth/register?' + querystring.stringify({redirect_uri: '/redirect-uri'}))
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                auth_method: 'magic_link',
                identifier: 'glimmer@example.org',
                name: 'glimmer',
            })
            .expect(302)
            .expect('Location', '/magic/lobby?redirect_uri=%2Fredirect-uri');

        await followMagicLinkFromMail(agent, cookies);

        // Proof must not be revoked
        await agent.get('/has-any-magic-link')
            .set('Cookie', cookies)
            .expect(200);

        await testLogout(agent, cookies, csrf);

        // Verify saved user
        const user = await User.select()
            .with('mainEmail')
            .where('name', 'glimmer')
            .first();

        expect(user).toBeDefined();

        const email = user?.mainEmail.getOrFail();
        expect(email).toBeDefined();
        expect(email?.email).toStrictEqual('glimmer@example.org');

        expect(user?.as(UserNameComponent).getName()).toStrictEqual('glimmer');
        await expect(user?.as(UserPasswordComponent).verifyPassword('')).resolves.toStrictEqual(false);
    });

    test('Cannot register without specifying username', async () => {
        let res = await agent.get('/csrf').expect(200);
        const cookies = res.get('Set-Cookie');
        const csrf = res.text;

        res = await agent.post('/auth/register')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                auth_method: 'magic_link',
                identifier: 'no_user_name@example.org',
            })
            .expect(400);
        expect(res.body.messages?.name?.name).toStrictEqual('UndefinedValueValidationError');

        expect(await popEmail()).toBeNull();
    });

    test('Cannot register taken username', async () => {
        const res = await agent.get('/csrf').expect(200);
        const cookies = res.get('Set-Cookie');
        const csrf = res.text;

        await agent.post('/auth/register')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                auth_method: 'magic_link',
                identifier: 'angella@example.org',
                name: 'angella',
            })
            .expect(302)
            .expect('Location', '/magic/lobby');

        await followMagicLinkFromMail(agent, cookies);

        await testLogout(agent, cookies, csrf);

        // Verify saved user
        const user = await User.select()
            .with('mainEmail')
            .where('name', 'angella')
            .first();

        expect(user).toBeDefined();

        // Attempt register with another mail but same username
        await agent.post('/auth/register')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                auth_method: 'magic_link',
                identifier: 'angella_something_else@example.org',
                name: 'angella',
            })
            .expect(400);

        expect(await popEmail()).toBeNull();
    });

    test('Cannot register taken email', async () => {
        const res = await agent.get('/csrf').expect(200);
        const cookies = res.get('Set-Cookie');
        const csrf = res.text;

        await agent.post('/auth/register')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                auth_method: 'magic_link',
                identifier: 'bow@example.org',
                name: 'bow',
            })
            .expect(302)
            .expect('Location', '/magic/lobby');

        await followMagicLinkFromMail(agent, cookies);

        await testLogout(agent, cookies, csrf);

        // Verify saved user
        const user = await User.select()
            .with('mainEmail')
            .where('name', 'bow')
            .first();

        expect(user).toBeDefined();

        // Attempt register with another mail but same username
        await agent.post('/auth/register')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                auth_method: 'magic_link',
                identifier: 'bow@example.org',
                name: 'bow2',
            })
            .expect(400);

        expect(await popEmail()).toBeNull();
    });
});

describe('Authenticate with username and password (password)', () => {
    test('Force auth_method', async () => {
        let res = await agent.get('/csrf').expect(200);
        const cookies = res.get('Set-Cookie');
        const csrf = res.text;

        // Not authenticated
        await agent.get('/is-auth').set('Cookie', cookies).expect(401);

        // Bad password
        res = await agent.post('/auth/login')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                identifier: 'entrapta',
                password: 'darla_is_not_cute',
                auth_method: 'password',
            })
            .expect(400);
        expect(res.body.messages?.password?.name).toStrictEqual('InvalidFormatValidationError');

        // Authenticate
        await agent.post('/auth/login?' + querystring.stringify({redirect_uri: '/redirect-uri'}))
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                identifier: 'entrapta',
                password: 'darla_is_cute',
                auth_method: 'password',
            })
            .expect(302)
            .expect('Location', '/redirect-uri');

        await testLogout(agent, cookies, csrf);
    });

    test('Automatic auth_method', async () => {
        let res = await agent.get('/csrf').expect(200);
        const cookies = res.get('Set-Cookie');
        const csrf = res.text;

        // Not authenticated
        await agent.get('/is-auth').set('Cookie', cookies).expect(401);

        // Bad password
        res = await agent.post('/auth/login')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                identifier: 'entrapta',
                password: 'darla_is_not_cute',
            })
            .expect(400);
        expect(res.body.messages?.password?.name).toStrictEqual('InvalidFormatValidationError');

        // Authenticate
        await agent.post('/auth/login')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                identifier: 'entrapta',
                password: 'darla_is_cute',
            })
            .expect(302)
            .expect('Location', '/');

        await testLogout(agent, cookies, csrf);
    });

    test('Non-existing username', async () => {
        let res = await agent.get('/csrf').expect(200);
        const cookies = res.get('Set-Cookie');
        const csrf = res.text;

        // Not authenticated
        await agent.get('/is-auth').set('Cookie', cookies).expect(401);

        // Authenticate
        res = await agent.post('/auth/login')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                identifier: 'i_do_not_exist',
                password: 'there_is_no_point',
                auth_method: 'password',
            })
            .expect(400);
        expect(res.body.messages?.identifier?.name).toStrictEqual('UnknownRelationValidationError');

        // Authenticate (automatic method)
        res = await agent.post('/auth/login')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                identifier: 'i_do_not_exist',
                password: 'there_is_no_point',
                auth_method: 'password',
            })
            .expect(400);
        expect(res.body.messages?.identifier?.name).toStrictEqual('UnknownRelationValidationError');
    });

    test('No password user', async () => {
        let res = await agent.get('/csrf').expect(200);
        const cookies = res.get('Set-Cookie');
        const csrf = res.text;

        // Not authenticated
        await agent.get('/is-auth').set('Cookie', cookies).expect(401);

        // Authenticate
        res = await agent.post('/auth/login')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                identifier: 'glimmer',
                password: '',
                auth_method: 'password',
            })
            .expect(400);
        expect(res.body.messages?.password?.name).toStrictEqual('InvalidFormatValidationError');

        // Authenticate (automatic method)
        res = await agent.post('/auth/login')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                identifier: 'glimmer',
                password: '',
            })
            .expect(400);
        expect(res.body.messages?.password?.name).toStrictEqual('InvalidFormatValidationError');

        // Authenticate without password
        res = await agent.post('/auth/login')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                identifier: 'angella',
                auth_method: 'password',
            })
            .expect(400);
        expect(res.body.messages?.password?.name).toStrictEqual('UndefinedValueValidationError');

        // Authenticate without password (automatic method)
        res = await agent.post('/auth/login')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                identifier: 'angella',
            })
            .expect(400);
        expect(res.body.messages?.password?.name).toStrictEqual('UndefinedValueValidationError');
    });
});

describe('Authenticate with email (magic_link)', () => {
    test('Force auth_method', async () => {
        const res = await agent.get('/csrf').expect(200);
        const cookies = res.get('Set-Cookie');
        const csrf = res.text;

        // Not authenticated
        await agent.get('/is-auth').set('Cookie', cookies).expect(401);

        // Authenticate
        await agent.post('/auth/login?' + querystring.stringify({redirect_uri: '/redirect-uri'}))
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                identifier: 'glimmer@example.org',
                auth_method: 'magic_link',
            })
            .expect(302)
            .expect('Location', '/magic/lobby?redirect_uri=%2Fredirect-uri');

        await followMagicLinkFromMail(agent, cookies);

        await testLogout(agent, cookies, csrf);
    });

    test('Automatic auth_method', async () => {
        const res = await agent.get('/csrf').expect(200);
        const cookies = res.get('Set-Cookie');
        const csrf = res.text;

        // Not authenticated
        await agent.get('/is-auth').set('Cookie', cookies).expect(401);

        // Authenticate
        await agent.post('/auth/login')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                identifier: 'angella@example.org',
            })
            .expect(302)
            .expect('Location', '/magic/lobby');

        await followMagicLinkFromMail(agent, cookies);

        await testLogout(agent, cookies, csrf);
    });

    test('Non-existing email (forced auth_method)', async () => {
        let res = await agent.get('/csrf').expect(200);
        const cookies = res.get('Set-Cookie');
        const csrf = res.text;

        // Not authenticated
        await agent.get('/is-auth').set('Cookie', cookies).expect(401);

        // Authenticate
        res = await agent.post('/auth/login')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                identifier: 'i_do_not_exist@invalid.org',
                auth_method: 'magic_link',
            })
            .expect(400);
        expect(res.body.messages?.identifier?.name).toStrictEqual('UnknownRelationValidationError');
        await agent.get('/is-auth').set('Cookie', cookies).expect(401);
    });

    test('Non-existing email (automatic auth_method)', async () => {
        let res = await agent.get('/csrf').expect(200);
        const cookies = res.get('Set-Cookie');
        const csrf = res.text;

        // Not authenticated
        await agent.get('/is-auth').set('Cookie', cookies).expect(401);

        // Authenticate
        res = await agent.post('/auth/login')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                identifier: 'i_do_not_exist@invalid.org',
            })
            .expect(400);
        expect(res.body.messages?.identifier?.name).toStrictEqual('UnknownRelationValidationError');
        await agent.get('/is-auth').set('Cookie', cookies).expect(401);
    });
});

describe('Authenticate with email and password (password)', () => {
    test('Prepare user', async () => {
        const res = await agent.get('/csrf').expect(200);
        const cookies = res.get('Set-Cookie');
        const csrf = res.text;

        await agent.post('/auth/register')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                auth_method: 'magic_link',
                identifier: 'double-trouble@example.org',
                name: 'double-trouble',
            })
            .expect(302)
            .expect('Location', '/magic/lobby');

        await followMagicLinkFromMail(agent, cookies);

        await testLogout(agent, cookies, csrf);

        // Verify saved user
        const user = await User.select()
            .with('mainEmail')
            .where('name', 'double-trouble')
            .first();

        await user?.as(UserPasswordComponent).setPassword('trick-or-treat');
        await user?.save();

        expect(user).toBeDefined();

        const email = user?.mainEmail.getOrFail();
        expect(email).toBeDefined();
        expect(email?.email).toStrictEqual('double-trouble@example.org');

        expect(user?.as(UserNameComponent).getName()).toStrictEqual('double-trouble');
        await expect(user?.as(UserPasswordComponent).verifyPassword('trick-or-treat')).resolves.toStrictEqual(true);
    });

    test('Force auth_method', async () => {
        let res = await agent.get('/csrf').expect(200);
        const cookies = res.get('Set-Cookie');
        const csrf = res.text;

        // Not authenticated
        await agent.get('/is-auth').set('Cookie', cookies).expect(401);

        // Bad password
        res = await agent.post('/auth/login')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                identifier: 'double-trouble@example.org',
                password: 'i_have_no_imagination',
                auth_method: 'password',
            })
            .expect(400);
        expect(res.body.messages?.password?.name).toStrictEqual('InvalidFormatValidationError');

        // Authenticate
        await agent.post('/auth/login')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                identifier: 'double-trouble@example.org',
                password: 'trick-or-treat',
                auth_method: 'password',
            })
            .expect(302)
            .expect('Location', '/');

        await testLogout(agent, cookies, csrf);
    });

    test('Automatic auth_method', async () => {
        let res = await agent.get('/csrf').expect(200);
        const cookies = res.get('Set-Cookie');
        const csrf = res.text;

        // Not authenticated
        await agent.get('/is-auth').set('Cookie', cookies).expect(401);

        // Bad password
        res = await agent.post('/auth/login')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                identifier: 'double-trouble@example.org',
                password: 'i_have_no_imagination',
            })
            .expect(400);
        expect(res.body.messages?.password?.name).toStrictEqual('InvalidFormatValidationError');

        // Authenticate
        await agent.post('/auth/login')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                identifier: 'double-trouble@example.org',
                password: 'trick-or-treat',
            })
            .expect(302)
            .expect('Location', '/');

        await testLogout(agent, cookies, csrf);
    });
});


describe('Change password', () => {
    let cookies: string[], csrf: string;
    test('Prepare user', async () => {
        const res = await agent.get('/csrf').expect(200);
        cookies = res.get('Set-Cookie');
        csrf = res.text;

        await agent.post('/auth/register')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                auth_method: 'magic_link',
                identifier: 'aang@example.org',
                name: 'aang',
            })
            .expect(302)
            .expect('Location', '/magic/lobby');

        await followMagicLinkFromMail(agent, cookies);
    });

    test('Set password to blank from blank', async () => {
        await agent.post('/account/change-password')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                'current_password': '',
                'new_password': '',
                'new_password_confirmation': '',
            })
            .expect(400);

        const user = await User.select()
            .where('name', 'aang')
            .first();
        expect(user).toBeDefined();
        expect(user?.as(UserPasswordComponent).hasPassword()).toBeFalsy();
        expect(await user?.as(UserPasswordComponent).verifyPassword('')).toBeFalsy();
        expect(await user?.as(UserPasswordComponent).verifyPassword('bad_password')).toBeFalsy();
    });

    test('Set password to something from blank', async () => {
        await agent.post('/account/change-password')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                'current_password': '',
                'new_password': 'a_very_strong_password',
                'new_password_confirmation': 'a_very_strong_password',
            })
            .expect(302)
            .expect('Location', '/account/');

        const user = await User.select()
            .where('name', 'aang')
            .first();
        expect(user).toBeDefined();
        expect(user?.as(UserPasswordComponent).hasPassword()).toBeTruthy();
        expect(await user?.as(UserPasswordComponent).verifyPassword('')).toBeFalsy();
        expect(await user?.as(UserPasswordComponent).verifyPassword('bad_password')).toBeFalsy();
        expect(await user?.as(UserPasswordComponent).verifyPassword('a_very_strong_password')).toBeTruthy();
    });

    test('Set password to blank from something', async () => {
        await agent.post('/account/change-password')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                'current_password': 'a_very_strong_password',
                'new_password': '',
            })
            .expect(400);

        const user = await User.select()
            .where('name', 'aang')
            .first();
        expect(user).toBeDefined();
        expect(user?.as(UserPasswordComponent).hasPassword()).toBeTruthy();
        expect(await user?.as(UserPasswordComponent).verifyPassword('')).toBeFalsy();
        expect(await user?.as(UserPasswordComponent).verifyPassword('bad_password')).toBeFalsy();
        expect(await user?.as(UserPasswordComponent).verifyPassword('a_very_strong_password')).toBeTruthy();
    });

    test('Set password to something from something', async () => {
        await agent.post('/account/change-password')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                'current_password': 'a_very_strong_password',
                'new_password': 'a_very_strong_password_but_different',
                'new_password_confirmation': 'a_very_strong_password_but_different',
            })
            .expect(302)
            .expect('Location', '/account/');

        const user = await User.select()
            .where('name', 'aang')
            .first();
        expect(user).toBeDefined();
        expect(user?.as(UserPasswordComponent).hasPassword()).toBeTruthy();
        expect(await user?.as(UserPasswordComponent).verifyPassword('')).toBeFalsy();
        expect(await user?.as(UserPasswordComponent).verifyPassword('bad_password')).toBeFalsy();
        expect(await user?.as(UserPasswordComponent).verifyPassword('a_very_strong_password')).toBeFalsy();
        expect(await user?.as(UserPasswordComponent).verifyPassword('a_very_strong_password_but_different')).toBeTruthy();
    });

    test('Set password to unconfirmed', async () => {
        await agent.post('/account/change-password')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                'current_password': 'a_very_strong_password_but_different',
                'new_password': 'a_very_strong_password',
                'new_password_confirmation': '',
            })
            .expect(400);

        await agent.post('/account/change-password')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                'current_password': 'a_very_strong_password_but_different',
                'new_password': 'a_very_strong_password',
                'new_password_confirmation': 'a_very_strong_password_but_different2',
            })
            .expect(400);

        const user = await User.select()
            .where('name', 'aang')
            .first();
        expect(user).toBeDefined();
        expect(user?.as(UserPasswordComponent).hasPassword()).toBeTruthy();
        expect(await user?.as(UserPasswordComponent).verifyPassword('bad_password')).toBeFalsy();
        expect(await user?.as(UserPasswordComponent).verifyPassword('a_very_strong_password')).toBeFalsy();
        expect(await user?.as(UserPasswordComponent).verifyPassword('a_very_strong_password_but_different')).toBeTruthy();
        expect(await user?.as(UserPasswordComponent).verifyPassword('a_very_strong_password_but_different2')).toBeFalsy();
    });

    test('Set password to too short password', async () => {
        await agent.post('/account/change-password')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                'current_password': 'a_very_strong_password_but_different',
                'new_password': '123',
                'new_password_confirmation': '123',
            })
            .expect(400);

        const user = await User.select()
            .where('name', 'aang')
            .first();
        expect(user).toBeDefined();
        expect(user?.as(UserPasswordComponent).hasPassword()).toBeTruthy();
        expect(await user?.as(UserPasswordComponent).verifyPassword('a_very_strong_password_but_different')).toBeTruthy();
        expect(await user?.as(UserPasswordComponent).verifyPassword('123')).toBeFalsy();
    });

    test('Remove password', async () => {
        let user = await User.select()
            .where('name', 'aang')
            .first();
        expect(user).toBeDefined();
        expect(user?.as(UserPasswordComponent).hasPassword()).toBe(true);

        await agent.post('/account/remove-password')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
            })
            .expect(302)
            .expect('Location', '/magic/lobby?redirect_uri=%2Faccount%2F');

        await followMagicLinkFromMail(agent, cookies, '/account/');

        user = await User.select()
            .where('name', 'aang')
            .first();
        expect(user).toBeDefined();
        expect(user?.as(UserPasswordComponent).hasPassword()).toBe(false);
    });

    test('Can\'t remove password without contact email', async () => {
        const res = await agent.get('/csrf').expect(200);
        cookies = res.get('Set-Cookie');
        csrf = res.text;

        await agent.post('/auth/register')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                auth_method: 'password',
                identifier: 'eleanor',
                password: 'this_is_a_very_strong_password',
                password_confirmation: 'this_is_a_very_strong_password',
                terms: 'on',
            })
            .expect(302)
            .expect('Location', '/');

        let user = await User.select()
            .where('name', 'eleanor')
            .first();
        expect(user).toBeDefined();
        expect(user?.as(UserPasswordComponent).hasPassword()).toBe(true);

        await agent.post('/account/remove-password')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
            })
            .expect(302)
            .expect('Location', '/account/');

        user = await User.select()
            .where('name', 'eleanor')
            .first();
        expect(user).toBeDefined();
        expect(user?.as(UserPasswordComponent).hasPassword()).toBe(true);
    });
});

describe('Change username', () => {
    let cookies: string[], csrf: string;
    test('Prepare user', async () => {
        const res = await agent.get('/csrf').expect(200);
        cookies = res.get('Set-Cookie');
        csrf = res.text;

        await agent.post('/auth/register')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                auth_method: 'password',
                identifier: 'richard',
                password: 'a_very_strong_password',
                password_confirmation: 'a_very_strong_password',
                terms: 'on',
            })
            .expect(302)
            .expect('Location', '/');
    });

    test('Initial value of name_changed_at should be the same as created_at', async () => {
        const user = await User.select().where('name', 'richard').first();
        expect(user).toBeDefined();
        expect(user?.as(UserNameComponent).getNameChangedAt()?.getTime()).toBe(user?.created_at?.getTime());
    });

    test('Cannot change username just after registration', async () => {
        expect(await User.select().where('name', 'richard').count()).toBe(1);

        await agent.post('/account/change-name')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                name: 'robert',
            })
            .expect(302)
            .expect('Location', '/account/');

        expect(await User.select().where('name', 'richard').count()).toBe(1);
    });

    test('Can change username after hold period', async () => {
        // Set last username change to older date
        const user = await User.select().where('name', 'richard').first();
        if (user) {
            user.as(UserNameComponent).forgetNameChangeDate();
            await user.save();
        }

        expect(await User.select().where('name', 'richard').count()).toBe(1);
        expect(await User.select().where('name', 'robert').count()).toBe(0);

        await agent.post('/account/change-name')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                name: 'robert',
            })
            .expect(302)
            .expect('Location', '/account/');

        expect(await User.select().where('name', 'richard').count()).toBe(0);
        expect(await User.select().where('name', 'robert').count()).toBe(1);
    });

    test('Cannot change username just after changing username', async () => {
        expect(await User.select().where('name', 'robert').count()).toBe(1);
        expect(await User.select().where('name', 'bebert').count()).toBe(0);

        await agent.post('/account/change-name')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                name: 'bebert',
            })
            .expect(302)
            .expect('Location', '/account/');

        expect(await User.select().where('name', 'robert').count()).toBe(1);
        expect(await User.select().where('name', 'bebert').count()).toBe(0);
    });
});

describe('Manage email addresses', () => {

    async function testMainSecondaryState(main: string, secondary: string) {
        const user = await User.select('main_email_id').where('name', 'katara').first();

        const mainEmail = await UserEmail.select().where('email', main).first();
        expect(mainEmail).not.toBeNull();
        expect(user?.main_email_id).toBe(mainEmail?.id);

        const secondaryEmail = await UserEmail.select().where('email', secondary).first();
        expect(secondaryEmail).not.toBeNull();
        expect(user?.main_email_id).not.toBe(secondaryEmail?.id);

        return secondaryEmail;
    }

    let cookies: string[], csrf: string;
    test('Prepare user', async () => {
        const res = await agent.get('/csrf').expect(200);
        cookies = res.get('Set-Cookie');
        csrf = res.text;

        await agent.post('/auth/register')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                auth_method: 'magic_link',
                identifier: 'katara@example.org',
                name: 'katara',
            })
            .expect(302)
            .expect('Location', '/magic/lobby');

        await followMagicLinkFromMail(agent, cookies);

        // Add fake, not owned secondary email
        const user = User.create({
            name: 'not_katara',
        });
        await user.save();
        await UserEmail.create({
            email: 'not_katara@example.org',
            user_id: user.id,
        }).save();
    });

    describe('Add', () => {
        test('Add invalid email addresses', async () => {
            await agent.post('/account/add-email')
                .set('Cookie', cookies)
                .send({
                    csrf: csrf,
                })
                .expect(400);
            await agent.post('/account/add-email')
                .set('Cookie', cookies)
                .send({
                    csrf: csrf,
                    email: '',
                })
                .expect(400);
            await agent.post('/account/add-email')
                .set('Cookie', cookies)
                .send({
                    csrf: csrf,
                    email: 'katara@example.org',
                })
                .expect(400);

            expect(await UserEmail.select().where('email', 'katara@example.org').count()).toBe(1);
        });

        test('Add valid email addresses', async () => {
            const expectedUserId = (await User.select('id').where('name', 'katara').first())?.id;

            for (const email of [
                'katara2@example.org',
                'katara3@example.org',
                'katara4@example.org',
            ]) {
                await agent.post('/account/add-email')
                    .set('Cookie', cookies)
                    .send({
                        csrf: csrf,
                        email: email,
                    })
                    .expect(302)
                    .expect('Location', '/magic/lobby?redirect_uri=%2Faccount%2F');

                await followMagicLinkFromMail(agent, cookies, '/account/');

                const userEmail = await UserEmail.select().where('email', email).first();
                expect(userEmail).not.toBeNull();
                expect(userEmail?.user_id).toBe(expectedUserId);
            }
        });
    });

    describe('Set main', () => {
        test('Set main email address as main email address', async () => {
            await testMainSecondaryState('katara@example.org', 'katara3@example.org');

            // Set secondary as main
            await agent.post('/account/set-main-email')
                .set('Cookie', cookies)
                .send({
                    csrf: csrf,
                    id: (await UserEmail.select().where('email', 'katara@example.org').first())?.id,
                })
                .expect(400);

            await testMainSecondaryState('katara@example.org', 'katara3@example.org');
        });

        test('Set secondary email address as main email address', async () => {
            const beforeSecondaryEmail = await testMainSecondaryState('katara@example.org', 'katara3@example.org');

            // Set secondary as main
            await agent.post('/account/set-main-email')
                .set('Cookie', cookies)
                .send({
                    csrf: csrf,
                    id: beforeSecondaryEmail?.id,
                })
                .expect(302)
                .expect('Location', '/account/');

            await testMainSecondaryState('katara3@example.org', 'katara@example.org');
        });

        test('Set non-owned address as main email address', async () => {
            const beforeSecondaryEmail = await testMainSecondaryState('katara3@example.org', 'not_katara@example.org');

            // Set secondary as main
            await agent.post('/account/set-main-email')
                .set('Cookie', cookies)
                .send({
                    csrf: csrf,
                    id: beforeSecondaryEmail?.id,
                })
                .expect(403);

            await testMainSecondaryState('katara3@example.org', 'not_katara@example.org');
        });

        test('Set non-existing address as main email address', async () => {
            await testMainSecondaryState('katara3@example.org', 'katara4@example.org');

            // Set secondary as main
            await agent.post('/account/set-main-email')
                .set('Cookie', cookies)
                .send({
                    csrf: csrf,
                    id: 999,
                })
                .expect(404);

            await testMainSecondaryState('katara3@example.org', 'katara4@example.org');
        });
    });

    describe('Remove', () => {
        test('Remove secondary email address', async () => {
            expect(await UserEmail.select().where('email', 'katara2@example.org').count()).toBe(1);

            // Set secondary as main
            await agent.post('/account/remove-email')
                .set('Cookie', cookies)
                .send({
                    csrf: csrf,
                    id: (await UserEmail.select().where('email', 'katara2@example.org').first())?.id,
                })
                .expect(302)
                .expect('Location', '/account/');

            expect(await UserEmail.select().where('email', 'katara2@example.org').count()).toBe(0);
        });

        test('Remove non-owned email address', async () => {
            expect(await UserEmail.select().where('email', 'not_katara@example.org').count()).toBe(1);

            // Set secondary as main
            await agent.post('/account/remove-email')
                .set('Cookie', cookies)
                .send({
                    csrf: csrf,
                    id: (await UserEmail.select().where('email', 'not_katara@example.org').first())?.id,
                })
                .expect(403);

            expect(await UserEmail.select().where('email', 'not_katara@example.org').count()).toBe(1);
        });

        test('Remove non-existing email address', async () => {
            // Set secondary as main
            await agent.post('/account/remove-email')
                .set('Cookie', cookies)
                .send({
                    csrf: csrf,
                    id: 999,
                })
                .expect(404);
        });

        test('Remove main email address', async () => {
            expect(await UserEmail.select().where('email', 'katara3@example.org').count()).toBe(1);

            // Set secondary as main
            await agent.post('/account/remove-email')
                .set('Cookie', cookies)
                .send({
                    csrf: csrf,
                    id: (await UserEmail.select().where('email', 'katara3@example.org').first())?.id,
                })
                .expect(400);

            expect(await UserEmail.select().where('email', 'katara3@example.org').count()).toBe(1);
        });
    });
});

describe('Session persistence', () => {
    let cookies: string[], csrf: string;

    test('Not persistent at registration', async () => {
        let res = await agent.get('/csrf').expect(200);
        cookies = res.get('Set-Cookie');
        csrf = res.text;

        await agent.post('/auth/register')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                auth_method: 'magic_link',
                identifier: 'zuko@example.org',
                name: 'zuko',
            })
            .expect(302)
            .expect('Location', '/magic/lobby');

        await followMagicLinkFromMail(agent, cookies);

        expect(cookies[0]).toMatch(/^connect\.sid=.+; Path=\/; SameSite=Strict$/);

        res = await agent.get('/csrf')
            .set('Cookie', cookies)
            .expect(200);
        expect(res.get('Set-Cookie')[0]).toMatch(/^connect\.sid=.+; Path=\/; Expires=.+; SameSite=Strict$/);

        // Logout
        await agent.post('/auth/logout')
            .set('Cookie', cookies)
            .send({csrf})
            .expect(302)
            .expect('Location', '/');
    });

    test('Login with persistence', async () => {
        await agent.post('/auth/login')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                auth_method: 'magic_link',
                identifier: 'zuko@example.org',
                persist_session: 'on',
            })
            .expect(302)
            .expect('Location', '/magic/lobby');

        await followMagicLinkFromMail(agent, cookies);

        const res = await agent.get('/csrf')
            .set('Cookie', cookies)
            .expect(200);
        expect(res.get('Set-Cookie')[0]).toMatch(/^connect\.sid=.+; Path=\/; Expires=.+; SameSite=Strict$/);

        // Logout
        await agent.post('/auth/logout')
            .set('Cookie', cookies)
            .send({csrf})
            .expect(302)
            .expect('Location', '/');
    });

    test('Login without persistence', async () => {
        await agent.post('/auth/login')
            .set('Cookie', cookies)
            .send({
                csrf: csrf,
                auth_method: 'magic_link',
                identifier: 'zuko@example.org',
                persist_session: undefined,
            })
            .expect(302)
            .expect('Location', '/magic/lobby');

        await followMagicLinkFromMail(agent, cookies);

        const res = await agent.get('/csrf')
            .set('Cookie', cookies)
            .expect(200);
        expect(res.get('Set-Cookie')[0]).toMatch(/^connect\.sid=.+; Path=\/; SameSite=Strict$/);

        // Logout
        await agent.post('/auth/logout')
            .set('Cookie', cookies)
            .send({csrf})
            .expect(302)
            .expect('Location', '/');
    });
});