// Utility Packages
const debug = require('@tryghost/debug')('test');
const Promise = require('bluebird');
const _ = require('lodash');
const fs = require('fs-extra');
const path = require('path');
const os = require('os');
const uuid = require('uuid');
const KnexMigrator = require('knex-migrator');
const knexMigrator = new KnexMigrator();

// Ghost Internals
const config = require('../../core/shared/config');
const boot = require('../../core/boot');
const db = require('../../core/server/data/db');
const models = require('../../core/server/models');
const urlService = require('../../core/frontend/services/url');
const settingsService = require('../../core/server/services/settings');
const frontendSettingsService = require('../../core/frontend/services/settings');
const web = require('../../core/server/web');
const themeService = require('../../core/server/services/themes');
const limits = require('../../core/server/services/limits');

// Other Test Utilities
const configUtils = require('./configUtils');
const dbUtils = require('./db-utils');
const urlServiceUtils = require('./url-service-utils');
const redirects = require('./redirects');
const context = require('./fixtures/context');

let ghostServer;
let existingData = {};
let totalStartTime = 0;

/**
 * Because we use ObjectID we don't know the ID of fixtures ahead of time
 * This function fetches all of our fixtures and exposes them so that tests can use them
 * @TODO: Optimize this by making it optional / selective
 */
const exposeFixtures = async () => {
    const fixturePromises = {
        roles: models.Role.findAll({columns: ['id']}),
        users: models.User.findAll({columns: ['id', 'email']}),
        tags: models.Tag.findAll({columns: ['id']}),
        apiKeys: models.ApiKey.findAll({withRelated: 'integration'})
    };
    const keys = Object.keys(fixturePromises);
    existingData = {};

    return Promise
        .all(Object.values(fixturePromises))
        .then((results) => {
            for (let i = 0; i < keys.length; i += 1) {
                existingData[keys[i]] = results[i].toJSON(context.internal);
            }
        })
        .catch((err) => {
            console.error('Unable to expose fixtures', err); // eslint-disable-line no-console
            process.exit(1);
        });
};

const prepareContentFolder = (options) => {
    const contentFolderForTests = options.contentFolder;

    /**
     * We never use the root content folder for testing!
     * We use a tmp folder.
     */
    configUtils.set('paths:contentPath', contentFolderForTests);

    fs.ensureDirSync(contentFolderForTests);
    fs.ensureDirSync(path.join(contentFolderForTests, 'data'));
    fs.ensureDirSync(path.join(contentFolderForTests, 'themes'));
    fs.ensureDirSync(path.join(contentFolderForTests, 'images'));
    fs.ensureDirSync(path.join(contentFolderForTests, 'logs'));
    fs.ensureDirSync(path.join(contentFolderForTests, 'adapters'));
    fs.ensureDirSync(path.join(contentFolderForTests, 'settings'));

    if (options.copyThemes) {
        // Copy all themes into the new test content folder. Default active theme is always casper. If you want to use a different theme, you have to set the active theme (e.g. stub)
        fs.copySync(path.join(__dirname, 'fixtures', 'themes'), path.join(contentFolderForTests, 'themes'));
    }

    if (options.redirectsFile) {
        redirects.setupFile(contentFolderForTests, options.redirectsFileExt);
    }

    if (options.copySettings) {
        fs.copySync(path.join(__dirname, 'fixtures', 'settings', 'routes.yaml'), path.join(contentFolderForTests, 'settings', 'routes.yaml'));
    }
};

// CASE: Ghost Server is Running
// In this case we need to reset things so it's as though Ghost just booted:
// - truncate database
// - re-run default fixtures
// - reload affected services
const restartModeGhostStart = async () => {
    debug('Reload Mode');
    // Teardown truncates all tables and also calls urlServiceUtils.reset();
    await dbUtils.teardown();

    // The tables have been truncated, this runs the fixture init task (init file 2) to re-add our default fixtures
    await knexMigrator.init({only: 2});
    debug('init done');

    // Reset the settings cache
    await settingsService.init();
    debug('settings done');

    // Reload the frontend
    await frontendSettingsService.init();
    await themeService.init();
    debug('frontend done');

    // Reload the URL service & wait for it to be ready again
    // @TODO: why/how is this different to urlService.resetGenerators?
    urlServiceUtils.reset();
    urlServiceUtils.init();
    await urlServiceUtils.isFinished();
    debug('routes done');
    // @TODO: why does this happen _after_ URL service
    web.shared.middlewares.customRedirects.reload();

    // Reload limits service
    limits.init();
};

const bootGhost = async () => {
    ghostServer = await boot();
};

// CASE: Ghost Server needs Starting
// In this case we need to ensure that Ghost is started cleanly:
// - ensure the DB is reset
// - CASE: If we are in force start mode the server is already running so we
//      - stop the server (if we are in force start mode it will be running)
//      - reload affected services - just settings and not the frontend!?
// - Start Ghost: Uses OLD Boot process
const freshModeGhostStart = async (options) => {
    if (options.forceStart) {
        debug('Forced Restart Mode');
    } else {
        debug('Fresh Start Mode');
    }

    // Reset the DB
    await knexMigrator.reset({force: true});

    // Stop the server (forceStart Mode)
    await stopGhost();

    // Reset the settings cache and disable listeners so they don't get triggered further
    settingsService.shutdown();

    // Do a full database initialisation
    await knexMigrator.init();

    if (config.get('database:client') === 'sqlite3') {
        await db.knex.raw('PRAGMA journal_mode = TRUNCATE;');
    }

    await settingsService.init();

    // Reset the URL service generators
    // @TODO: Prob B: why/how is this different to urlService.reset?
    // @TODO: why would we do this on a fresh boot?!
    urlService.resetGenerators();

    // Actually boot Ghost
    await bootGhost(options);

    // Wait for the URL service to be ready, which happens after bootYou
    await urlServiceUtils.isFinished();
};

const startGhost = async (options) => {
    const startTime = Date.now();
    debug('Start Ghost');
    options = _.merge({
        redirectsFile: true,
        redirectsFileExt: '.json',
        forceStart: false,
        copyThemes: true,
        copySettings: true,
        contentFolder: path.join(os.tmpdir(), uuid.v4(), 'ghost-test'),
        subdir: false
    }, options);

    // Ensure we have tmp content folders populated ready for testing
    // @TODO: tidy up the tmp folders after tests
    prepareContentFolder(options);

    if (ghostServer && ghostServer.httpServer && !options.forceStart) {
        await restartModeGhostStart(options);
    } else {
        await freshModeGhostStart(options);
    }

    // Expose fixture data, wrap-up and return
    await exposeFixtures();

    // Reporting
    const totalTime = Date.now() - startTime;
    totalStartTime += totalTime;
    debug(`Started Ghost in ${totalTime / 1000}s`);
    debug(`Accumulated start time is ${totalStartTime / 1000}s`);
    return ghostServer;
};

const stopGhost = async () => {
    if (ghostServer && ghostServer.httpServer) {
        await ghostServer.stop();
        delete require.cache[require.resolve('../../core/app')];
        urlService.resetGenerators();
    }
};

module.exports = {
    startGhost,
    stopGhost,
    getExistingData: () => {
        return existingData;
    }
};