import { assert } from 'chai';

import { IObfuscationResult } from '../../../../../src/interfaces/IObfuscationResult';

import { NO_ADDITIONAL_NODES_PRESET } from '../../../../../src/options/presets/NoCustomNodes';

import { readFileAsString } from '../../../../helpers/readFileAsString';

import { JavaScriptObfuscator } from '../../../../../src/JavaScriptObfuscatorFacade';

describe('FunctionControlFlowTransformer', function () {
    this.timeout(100000);

    const variableMatch: string = '_0x([a-f0-9]){4,6}';
    const rootControlFlowStorageNodeMatch: string = `` +
        `var *${variableMatch} *= *\\{` +
            `'\\w{5}' *: *function *\\(${variableMatch}, *${variableMatch}\\) *\\{` +
                `return *${variableMatch} *\\+ *${variableMatch};` +
            `\\}` +
        `\\};` +
    ``;
    const innerControlFlowStorageNodeMatch: string = `` +
        `var *${variableMatch} *= *\\{` +
            `'\\w{5}' *: *function *\\(${variableMatch}, *${variableMatch}\\) *\\{` +
                `return *${variableMatch}\\['\\w{5}'\\]\\(${variableMatch}, *${variableMatch}\\);` +
            `\\}` +
        `\\};` +
    ``;

    describe('transformNode (functionNode: ESTree.Function): ESTree.Node', () => {
        describe('variant #1 - single `control flow storage` node with single item', () => {
            const regexp: RegExp = new RegExp(rootControlFlowStorageNodeMatch);

            let obfuscatedCode: string;

            before(() => {
                const code: string = readFileAsString(__dirname + '/fixtures/input-1.js');
                const obfuscationResult: IObfuscationResult = JavaScriptObfuscator.obfuscate(
                    code,
                    {
                        ...NO_ADDITIONAL_NODES_PRESET,
                        controlFlowFlattening: true,
                        controlFlowFlatteningThreshold: 1
                    }
                );

                obfuscatedCode = obfuscationResult.getObfuscatedCode();
            });

            it('should add `control flow storage` node to the obfuscated code', () => {
                assert.match(obfuscatedCode, regexp);
            });
        });

        describe('variant #2 - two `control flow storage` nodes: root and inner', () => {
            const expectedAppendToScopeThreshold: number = 0.5;

            const samplesCount: number = 1000;
            const delta: number = 0.1;

            const regExp1: RegExp = new RegExp(
                `\\(function\\(\\) *\\{ *${rootControlFlowStorageNodeMatch}`,
                'g'
            );
            const regExp2: RegExp = new RegExp(
                `function *${variableMatch} *\\(\\) *\\{ *${innerControlFlowStorageNodeMatch}`,
                'g'
            );

            let appendToScopeThreshold: number = 0;

            before(() => {
                const code: string = readFileAsString(__dirname + '/fixtures/input-2.js');

                let obfuscationResult: IObfuscationResult,
                    obfuscatedCode: string,
                    totalValue: number = 0;

                for (let i = 0; i < samplesCount; i++) {
                    obfuscationResult = JavaScriptObfuscator.obfuscate(
                        code,
                        {
                            ...NO_ADDITIONAL_NODES_PRESET,
                            controlFlowFlattening: true,
                            controlFlowFlatteningThreshold: 1
                        }
                    );
                    obfuscatedCode = obfuscationResult.getObfuscatedCode();

                    if (regExp1.test(obfuscatedCode)) {
                        totalValue += obfuscatedCode.match(regExp1)!.length;

                        if (regExp2.test(obfuscatedCode)) {
                            totalValue += obfuscatedCode.match(regExp2)!.length;
                        }
                    }
                }

                appendToScopeThreshold = (totalValue - samplesCount) / samplesCount;
            });

            it('should add two `control flow storage` nodes (root and inner) to the obfuscated code in different scopes', () => {
                assert.closeTo(appendToScopeThreshold, expectedAppendToScopeThreshold, delta);
            });
        });

        describe('variant #3 - single `control flow storage` node with multiple items', () => {
            const regexp: RegExp = new RegExp(
                `var *${variableMatch} *= *\\{` +
                    `'\\w{5}' *: *function *\\(${variableMatch}, *${variableMatch}\\) *\\{` +
                        `return *${variableMatch} *\\+ *${variableMatch};` +
                    `\\}, *` +
                    `'\\w{5}' *: *function *\\(${variableMatch}, *${variableMatch}\\) *\\{` +
                        `return *${variableMatch} *- *${variableMatch};` +
                    `\\}` +
                `\\};`
            );

            let obfuscatedCode: string;

            before(() => {
                const code: string = readFileAsString(__dirname + '/fixtures/multiple-items.js');
                const obfuscationResult: IObfuscationResult = JavaScriptObfuscator.obfuscate(
                    code,
                    {
                        ...NO_ADDITIONAL_NODES_PRESET,
                        controlFlowFlattening: true,
                        controlFlowFlatteningThreshold: 1
                    }
                );

                obfuscatedCode = obfuscationResult.getObfuscatedCode();
            });

            it('should add `control flow storage` node with multiple items to the obfuscated code', () => {
                assert.match(obfuscatedCode, regexp);
            });
        });

        describe('variant #4 - transformed node in the root block scope', () => {
            const regExp: RegExp = /^var *test *= *0x1 *\+ *0x2;$/;

            let obfuscatedCode: string;

            before(() => {
                const code: string = readFileAsString(__dirname + '/fixtures/root-block-scope-1.js');
                const obfuscationResult: IObfuscationResult = JavaScriptObfuscator.obfuscate(
                    code,
                    {
                        ...NO_ADDITIONAL_NODES_PRESET,
                        controlFlowFlattening: true,
                        controlFlowFlatteningThreshold: 1
                    }
                );

                obfuscatedCode = obfuscationResult.getObfuscatedCode();
            });

            it('should\'t add control flow storage node', () => {
                assert.match(obfuscatedCode, regExp);
            });
        });

        describe('variant #5 - transformed nodes not in the root block scope', () => {
            const expectedValue: number = 0;
            const samplesCount: number = 20;

            const regExp: RegExp = new RegExp(
                `var *[a-zA-Z]{6} *= *\\{` +
                    `'\\w{5}' *: *function *\\(${variableMatch}, *${variableMatch}\\) *\\{` +
                        `return *${variableMatch} *\\+ *${variableMatch};` +
                    `\\}` +
                `\\};`
            );

            const code: string = readFileAsString(__dirname + '/fixtures/root-block-scope-2.js');

            let totalValue: number = 0;

            before(() => {
                for (let i = 0; i < samplesCount; i++) {
                    const obfuscationResult: IObfuscationResult = JavaScriptObfuscator.obfuscate(
                        code,
                        {
                            ...NO_ADDITIONAL_NODES_PRESET,
                            controlFlowFlattening: true,
                            controlFlowFlatteningThreshold: 1
                        }
                    );
                    const obfuscatedCode: string = obfuscationResult.getObfuscatedCode();

                    if (regExp.test(obfuscatedCode)) {
                        totalValue++;
                    }
                }
            });

            it('should\'t add control flow storage node to the root block scope', () => {
                assert.equal(totalValue, expectedValue);
            });
        });

        describe('variant #6 - threshold is `0`', () => {
            const regexp: RegExp = /var *_0x([a-f0-9]){4,6} *= *0x1 *\+ *0x2;/;
            const controlFlowStorageRegExp: RegExp = new RegExp(rootControlFlowStorageNodeMatch);

            let obfuscatedCode: string;

            before(() => {
                const code: string = readFileAsString(__dirname + '/fixtures/zero-threshold.js');
                const obfuscationResult: IObfuscationResult = JavaScriptObfuscator.obfuscate(
                    code,
                    {
                        ...NO_ADDITIONAL_NODES_PRESET,
                        controlFlowFlattening: true,
                        controlFlowFlatteningThreshold: 0
                    }
                );

                obfuscatedCode = obfuscationResult.getObfuscatedCode();
            });

            it('shouldn\'t add call to control flow storage node to the obfuscated code', () => {
                assert.match(obfuscatedCode, regexp);
            });

            it('shouldn\'t add `control flow storage` node to the obfuscated code', () => {
                assert.notMatch(obfuscatedCode, controlFlowStorageRegExp);
            });
        });

        describe('arrow function expression', () => {
            describe('variant #1 - arrow function expression with body', () => {
                const regexp: RegExp = new RegExp(rootControlFlowStorageNodeMatch);

                let obfuscatedCode: string;

                before(() => {
                    const code: string = readFileAsString(__dirname + '/fixtures/arrow-function-expression-with-body.js');
                    const obfuscationResult: IObfuscationResult = JavaScriptObfuscator.obfuscate(
                        code,
                        {
                            ...NO_ADDITIONAL_NODES_PRESET,
                            controlFlowFlattening: true,
                            controlFlowFlatteningThreshold: 1
                        }
                    );

                    obfuscatedCode = obfuscationResult.getObfuscatedCode();
                });

                it('should add `control flow storage` node to the obfuscated code', () => {
                    assert.match(obfuscatedCode, regexp);
                });
            });

            describe('variant #2 - arrow function expression without body', () => {
                const regexp: RegExp = new RegExp(`var *${variableMatch} *= *\\(\\) *=> *0x1 *\\+ *0x2;`);

                let obfuscatedCode: string;

                before(() => {
                    const code: string = readFileAsString(__dirname + '/fixtures/arrow-function-expression-without-body.js');
                    const obfuscationResult: IObfuscationResult = JavaScriptObfuscator.obfuscate(
                        code,
                        {
                            ...NO_ADDITIONAL_NODES_PRESET,
                            controlFlowFlattening: true,
                            controlFlowFlatteningThreshold: 1
                        }
                    );

                    obfuscatedCode = obfuscationResult.getObfuscatedCode();
                });

                it('shouldn\'t add `control flow storage` node to the obfuscated code', () => {
                    assert.match(obfuscatedCode, regexp);
                });
            });
        });
    });
});
