const test = require('tap').test;
const Looks = require('../../src/blocks/scratch3_looks');
const Runtime = require('../../src/engine/runtime');
const Sprite = require('../../src/sprites/sprite.js');
const RenderedTarget = require('../../src/sprites/rendered-target.js');
const util = {
    target: {
        currentCostume: 0, // Internally, current costume is 0 indexed
        getCostumes: function () {
            return this.sprite.costumes;
        },
        sprite: {
            costumes: [
                {name: 'first name'},
                {name: 'second name'},
                {name: 'third name'}
            ]
        },
        _customState: {},
        getCustomState: () => util.target._customState
    }
};

const fakeRuntime = {
    getTargetForStage: () => util.target, // Just return the dummy target above.
    on: () => {} // Stub out listener methods used in constructor.
};
const blocks = new Looks(fakeRuntime);

/**
 * Test which costume index the `switch costume`
 * block will jump to given an argument and array
 * of costume names. Works for backdrops if isStage is set.
 *
 * @param {string[]} costumes List of costume names as strings
 * @param {string|number|boolean} arg The argument to provide to the block.
 * @param {number} [currentCostume=1] The 1-indexed default costume for the sprite to start at.
 * @param {boolean} [isStage=false] Whether the sprite is the stage
 * @return {number} The 1-indexed costume index on which the sprite lands.
 */
const testCostume = (costumes, arg, currentCostume = 1, isStage = false) => {
    const rt = new Runtime();
    const looks = new Looks(rt);

    const sprite = new Sprite(null, rt);
    const target = new RenderedTarget(sprite, rt);

    sprite.costumes = costumes.map(name => ({name: name}));
    target.currentCostume = currentCostume - 1; // Convert to 0-indexed.

    if (isStage) {
        target.isStage = true;
        rt.addTarget(target);
        looks.switchBackdrop({BACKDROP: arg}, {target});
    } else {
        looks.switchCostume({COSTUME: arg}, {target});
    }

    return target.currentCostume + 1; // Convert to 1-indexed.
};


/**
 * Test which backdrop index the `switch backdrop`
 * block will jump to given an argument and array
 * of backdrop names.
 *
 * @param {string[]} backdrops List of backdrop names as strings
 * @param {string|number|boolean} arg The argument to provide to the block.
 * @param {number} [currentCostume=1] The 1-indexed default backdrop for the stage to start at.
 * @return {number} The 1-indexed backdrop index on which the stage lands.
 */
const testBackdrop = (backdrops, arg, currentCostume = 1) => testCostume(backdrops, arg, currentCostume, true);

test('switch costume block runs correctly', t => {
    // Non-existant costumes do nothing
    t.strictEqual(testCostume(['a', 'b', 'c', 'd'], 'e', 3), 3);

    // Numeric arguments are always the costume index
    // String arguments are treated as costume names, and coerced to
    // a costume index as a fallback
    t.strictEqual(testCostume(['a', 'b', 'c', '2'], 2), 2);
    t.strictEqual(testCostume(['a', 'b', 'c', '2'], '2'), 4);
    t.strictEqual(testCostume(['a', 'b', 'c'], '2'), 2);

    // 'previous costume' and 'next costume' increment/decrement
    t.strictEqual(testCostume(['a', 'b', 'c', 'd'], 'previous costume', 3), 2);
    t.strictEqual(testCostume(['a', 'b', 'c', 'd'], 'next costume', 2), 3);

    // 'previous costume' and 'next costume' can be overriden
    t.strictEqual(testCostume(['a', 'previous costume', 'c', 'd'], 'previous costume'), 2);
    t.strictEqual(testCostume(['next costume', 'b', 'c', 'd'], 'next costume'), 1);

    // NaN, Infinity, and true are the first costume
    t.strictEqual(testCostume(['a', 'b', 'c', 'd'], NaN, 2), 1);
    t.strictEqual(testCostume(['a', 'b', 'c', 'd'], true, 2), 1);
    t.strictEqual(testCostume(['a', 'b', 'c', 'd'], Infinity, 2), 1);
    t.strictEqual(testCostume(['a', 'b', 'c', 'd'], -Infinity, 2), 1);

    // 'previous backdrop' and 'next backdrop' have no effect
    t.strictEqual(testCostume(['a', 'b', 'c', 'd'], 'previous backdrop', 3), 3);
    t.strictEqual(testCostume(['a', 'b', 'c', 'd'], 'next backdrop', 3), 3);

    // Strings with no digits are not numeric
    t.strictEqual(testCostume(['a', 'b', 'c', 'd'], '    ', 2), 2);

    // False is 0 (the last costume)
    t.strictEqual(testCostume(['a', 'b', 'c', 'd'], false), 4);

    // Booleans are costume names where possible.
    t.strictEqual(testCostume(['a', 'true', 'false', 'd'], false), 3);
    t.strictEqual(testCostume(['a', 'true', 'false', 'd'], true), 2);

    // Costume indices should wrap around.
    t.strictEqual(testCostume(['a', 'b', 'c', 'd'], -1), 3);
    t.strictEqual(testCostume(['a', 'b', 'c', 'd'], -4), 4);
    t.strictEqual(testCostume(['a', 'b', 'c', 'd'], 10), 2);

    t.end();
});

test('switch backdrop block runs correctly', t => {
    // Non-existant backdrops do nothing
    t.strictEqual(testBackdrop(['a', 'b', 'c', 'd'], 'e', 3), 3);

    // Difference between string and numeric arguments
    t.strictEqual(testBackdrop(['a', 'b', 'c', '2'], 2), 2);
    t.strictEqual(testBackdrop(['a', 'b', 'c', '2'], '2'), 4);

    // 'previous backdrop' and 'next backdrop' increment/decrement
    t.strictEqual(testBackdrop(['a', 'b', 'c', 'd'], 'previous backdrop', 3), 2);
    t.strictEqual(testBackdrop(['a', 'b', 'c', 'd'], 'next backdrop', 2), 3);

    // 'previous backdrop', 'previous backdrop', 'random backdrop' can be overriden
    // Test is deterministic since 'random backdrop' will not pick the same backdrop as currently selected
    t.strictEqual(testBackdrop(['a', 'previous backdrop', 'c', 'd'], 'previous backdrop', 4), 2);
    t.strictEqual(testBackdrop(['next backdrop', 'b', 'c', 'd'], 'next backdrop', 3), 1);
    t.strictEqual(testBackdrop(['random backdrop', 'b', 'c', 'd'], 'random backdrop'), 1);

    // NaN, Infinity, and true are the first costume
    t.strictEqual(testBackdrop(['a', 'b', 'c', 'd'], NaN, 2), 1);
    t.strictEqual(testBackdrop(['a', 'b', 'c', 'd'], true, 2), 1);
    t.strictEqual(testBackdrop(['a', 'b', 'c', 'd'], Infinity, 2), 1);
    t.strictEqual(testBackdrop(['a', 'b', 'c', 'd'], -Infinity, 2), 1);

    // 'previous costume' and 'next costume' have no effect
    t.strictEqual(testBackdrop(['a', 'b', 'c', 'd'], 'previous costume', 3), 3);
    t.strictEqual(testBackdrop(['a', 'b', 'c', 'd'], 'next costume', 3), 3);

    // Strings with no digits are not numeric
    t.strictEqual(testBackdrop(['a', 'b', 'c', 'd'], '    ', 2), 2);

    // False is 0 (the last costume)
    t.strictEqual(testBackdrop(['a', 'b', 'c', 'd'], false), 4);

    // Booleans are backdrop names where possible.
    t.strictEqual(testBackdrop(['a', 'true', 'false', 'd'], false), 3);
    t.strictEqual(testBackdrop(['a', 'true', 'false', 'd'], true), 2);

    // Backdrop indices should wrap around.
    t.strictEqual(testBackdrop(['a', 'b', 'c', 'd'], -1), 3);
    t.strictEqual(testBackdrop(['a', 'b', 'c', 'd'], -4), 4);
    t.strictEqual(testBackdrop(['a', 'b', 'c', 'd'], 10), 2);

    t.end();
});

test('getCostumeNumberName returns 1-indexed costume number', t => {
    util.target.currentCostume = 0; // This is 0-indexed.
    const args = {NUMBER_NAME: 'number'};
    const number = blocks.getCostumeNumberName(args, util);
    t.strictEqual(number, 1);
    t.end();
});

test('getCostumeNumberName can return costume name', t => {
    util.target.currentCostume = 0; // This is 0-indexed.
    const args = {NUMBER_NAME: 'name'};
    const name = blocks.getCostumeNumberName(args, util);
    t.strictEqual(name, 'first name');
    t.end();
});

test('getBackdropNumberName returns 1-indexed costume number', t => {
    util.target.currentCostume = 2; // This is 0-indexed.
    const args = {NUMBER_NAME: 'number'};
    const number = blocks.getBackdropNumberName(args, util);
    t.strictEqual(number, 3);
    t.end();
});

test('getBackdropNumberName can return costume name', t => {
    util.target.currentCostume = 2; // This is 0-indexed.
    const args = {NUMBER_NAME: 'name'};
    const number = blocks.getBackdropNumberName(args, util);
    t.strictEqual(number, 'third name');
    t.end();
});

test('numbers should be rounded properly in say/think', t => {
    const rt = new Runtime();
    const looks = new Looks(rt);

    let expectedSayString;

    rt.addListener('SAY', () => {
        const bubbleState = util.target.getCustomState(Looks.STATE_KEY);
        t.strictEqual(bubbleState.text, expectedSayString);
    });

    expectedSayString = '3.14';
    looks.say({MESSAGE: 3.14159}, util, 'say bubble should round to 2 decimal places');
    looks.think({MESSAGE: 3.14159}, util, 'think bubble should round to 2 decimal places');

    expectedSayString = '3';
    looks.say({MESSAGE: 3}, util, 'say bubble should not add decimal places to integers');
    looks.think({MESSAGE: 3}, util, 'think bubble should not add decimal places to integers');

    expectedSayString = '3.10';
    looks.say({MESSAGE: 3.1}, util, 'say bubble should round to 2 decimal places, even if only 1 is needed');
    looks.think({MESSAGE: 3.1}, util, 'think bubble should round to 2 decimal places, even if only 1 is needed');

    expectedSayString = '0.00125';
    looks.say({MESSAGE: 0.00125}, util, 'say bubble should not round if it would display small numbers as 0');
    looks.think({MESSAGE: 0.00125}, util, 'think bubble should not round if it would display small numbers as 0');

    expectedSayString = '1.99999';
    looks.say({MESSAGE: '1.99999'}, util, 'say bubble should not round strings');
    looks.think({MESSAGE: '1.99999'}, util, 'think bubble should not round strings');

    t.end();
});

test('clamp graphic effects', t => {
    const rt = new Runtime();
    const looks = new Looks(rt);
    const expectedValues = {
        brightness: {high: 100, low: -100},
        ghost: {high: 100, low: 0},
        color: {high: 500, low: -500},
        fisheye: {high: 500, low: -500},
        whirl: {high: 500, low: -500},
        pixelate: {high: 500, low: -500},
        mosaic: {high: 500, low: -500}
    };
    const args = [
        {EFFECT: 'brightness', VALUE: 500, CLAMP: 'high'},
        {EFFECT: 'brightness', VALUE: -500, CLAMP: 'low'},
        {EFFECT: 'ghost', VALUE: 500, CLAMP: 'high'},
        {EFFECT: 'ghost', VALUE: -500, CLAMP: 'low'},
        {EFFECT: 'color', VALUE: 500, CLAMP: 'high'},
        {EFFECT: 'color', VALUE: -500, CLAMP: 'low'},
        {EFFECT: 'fisheye', VALUE: 500, CLAMP: 'high'},
        {EFFECT: 'fisheye', VALUE: -500, CLAMP: 'low'},
        {EFFECT: 'whirl', VALUE: 500, CLAMP: 'high'},
        {EFFECT: 'whirl', VALUE: -500, CLAMP: 'low'},
        {EFFECT: 'pixelate', VALUE: 500, CLAMP: 'high'},
        {EFFECT: 'pixelate', VALUE: -500, CLAMP: 'low'},
        {EFFECT: 'mosaic', VALUE: 500, CLAMP: 'high'},
        {EFFECT: 'mosaic', VALUE: -500, CLAMP: 'low'}
    ];

    util.target.setEffect = function (effectName, actualValue) {
        const clamp = actualValue > 0 ? 'high' : 'low';
        rt.emit(effectName + clamp, effectName, actualValue);
    };

    for (const arg of args) {
        rt.addListener(arg.EFFECT + arg.CLAMP, (effectName, actualValue) => {
            const expected = expectedValues[arg.EFFECT][arg.CLAMP];
            t.strictEqual(actualValue, expected);
        });

        looks.setEffect(arg, util);
    }
    t.end();
});
