/*
 * Copyright (C) 2024-present Puter Technologies Inc.
 *
 * This file is part of Puter.
 *
 * Puter is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published
 * by the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
// TODO: accessing these imports directly from a mod is not really
//       the way mods are intended to work; this is temporary until
//       we have these things registered in "useapi".
const {
    get_user,
    invalidate_cached_user,
    deleteUser,
} = require('../../../src/backend/src/helpers.js');
const { HLWrite } = require('../../../src/backend/src/filesystem/hl_operations/hl_write.js');
const { LLRead } = require('../../../src/backend/src/filesystem/ll_operations/ll_read.js');
const { Actor, UserActorType }
    = require('../../../src/backend/src/services/auth/Actor.js');
const { DB_WRITE } = require('../../../src/backend/src/services/database/consts.js');
const {
    RootNodeSelector,
    NodeChildSelector,
    NodePathSelector,
} = require('../../../src/backend/src/filesystem/node/selectors.js');
const { Context } = require('../../../src/backend/src/util/context.js');

class ShareTestService extends use.Service {
    static MODULES = {
        uuidv4: require('uuid').v4,
    };

    async _init () {
        const svc_commands = this.services.get('commands');
        this._register_commands(svc_commands);

        this.scenarios = require('./data/sharetest_scenarios');

        const svc_db = this.services.get('database');
        this.db = svc_db.get(svc_db.DB_WRITE, 'share-test');
    }

    _register_commands (commands) {
        commands.registerCommands('share-test', [
            {
                id: 'start',
                description: '',
                handler: async (_, log) => {
                    const results = await this.runit();

                    for ( const result of results ) {
                        log.log(`=== ${result.title} ===`);
                        if ( ! result.report ) {
                            log.log('\x1B[32;1mSUCCESS\x1B[0m');
                            continue;
                        }
                        log.log('\x1B[31;1mSTOPPED\x1B[0m at ' +
                            `${result.report.step}: ${
                                result.report.report.message}`);
                    }
                },
            },
        ]);
    }

    async runit () {
        await this.teardown_();
        await this.setup_();

        const results = [];

        for ( const scenario of this.scenarios ) {
            if ( ! scenario.title ) {
                scenario.title = scenario.sequence.map(step => step.title).join('; ');
            }
            results.push({
                title: scenario.title,
                report: await this.run_scenario_(scenario),
            });
        }

        await this.teardown_();
        return results;
    }

    async setup_ () {
        await this.create_test_user_('testuser_eric');
        await this.create_test_user_('testuser_stan');
        await this.create_test_user_('testuser_kyle');
        await this.create_test_user_('testuser_kenny');
    }
    async run_scenario_ (scenario) {
        let error;
        // Run sequence
        for ( const step of scenario.sequence ) {
            const method = this[`__scenario:${step.call}`];
            const user = await get_user({ username: step.as });
            const actor = await Actor.create(UserActorType, { user });
            const generated = { user, actor };
            const report = await Context.get().sub({ user, actor })
                .arun(async () => {
                    return await method.call(this, generated, step.with);
                });
            if ( report ) {
                error = { step: step.title, report };
                break;
            }
        }
        return error;
    }
    async teardown_ () {
        await this.delete_test_user_('testuser_eric');
        await this.delete_test_user_('testuser_stan');
        await this.delete_test_user_('testuser_kyle');
        await this.delete_test_user_('testuser_kenny');
    }

    async create_test_user_ (username) {
        await this.db.write(`
                INSERT INTO user (uuid, username, email, free_storage, password)
                VALUES (?, ?, ?, ?, ?)
            `,
        [
            this.modules.uuidv4(),
            username,
            `${username}@example.com`,
            1024 * 1024 * 500, // 500 MiB
            this.modules.uuidv4(),
        ]);
        const user = await get_user({ username });
        const svc_user = this.services.get('user');
        await svc_user.generate_default_fsentries({ user });
        invalidate_cached_user(user);
        return user;
    }

    async delete_test_user_ (username) {
        const user = await get_user({ username });
        if ( ! user ) return;
        await deleteUser(user.id);
    }

    // API for scenarios
    async ['__scenario:create-example-file'] (
        { actor, user },
        { name, contents },
    ) {
        const svc_fs = this.services.get('filesystem');
        const parent = await svc_fs.node(new NodePathSelector(`/${user.username}/Desktop`));
        console.log('test -> create-example-file',
                        user,
                        name,
                        contents);
        const buffer = Buffer.from(contents);
        const file = {
            size: buffer.length,
            name: name,
            type: 'application/octet-stream',
            buffer,
        };
        const hl_write = new HLWrite();
        await hl_write.run({
            actor,
            user,
            destination_or_parent: parent,
            specified_name: name,
            file,
        });
    }
    async ['__scenario:assert-no-access'] (
        { actor, user },
        { path },
    ) {
        const svc_fs = this.services.get('filesystem');
        const node = await svc_fs.node(new NodePathSelector(path));
        const ll_read = new LLRead();
        let expected_e; try {
            const stream = await ll_read.run({
                fsNode: node,
                actor,
            });
        } catch (e) {
            expected_e = e;
        }
        if ( ! expected_e ) {
            return { message: 'expected error, got none' };
        }
    }
    async ['__scenario:grant'] (
        { actor, user },
        { to, permission },
    ) {
        const svc_permission = this.services.get('permission');
        await svc_permission.grant_user_user_permission(actor, to, permission, {}, {});
    }
    async ['__scenario:assert-access'] (
        { actor, user },
        { path, level },
    ) {
        const svc_fs = this.services.get('filesystem');
        const svc_acl = this.services.get('acl');
        const node = await svc_fs.node(new NodePathSelector(path));
        const has_read = await svc_acl.check(actor, node, 'read');
        const has_write = await svc_acl.check(actor, node, 'write');

        if ( level !== 'write' && level !== 'read' ) {
            return {
                message: 'unexpected value for "level" parameter',
            };
        }

        if ( level === 'read' && has_write ) {
            return {
                message: 'expected read-only but actor can write',
            };
        }
        if ( level === 'read' && !has_read ) {
            return {
                message: 'expected read access but no read access',
            };
        }
        if ( level === 'write' && (!has_write || !has_read) ) {
            return {
                message: 'expected write access but no write access',
            };
        }
        if ( level === 'manage' && (!has_write || !has_read) ) {
            return {
                message: 'expected write access but no write access',
            };
        }
    }
}

module.exports = {
    ShareTestService,
};
