const test = require('tap').test;
const Sensing = require('../../src/blocks/scratch3_sensing');
const Runtime = require('../../src/engine/runtime');
const Sprite = require('../../src/sprites/sprite');
const RenderedTarget = require('../../src/sprites/rendered-target');
const BlockUtility = require('../../src/engine/block-utility');

test('getPrimitives', t => {
    const rt = new Runtime();
    const s = new Sensing(rt);
    t.type(s.getPrimitives(), 'object');
    t.end();
});

test('ask and answer with a hidden target', t => {
    const rt = new Runtime();
    const s = new Sensing(rt);
    const util = {target: {visible: false}};

    const expectedQuestion = 'a question';
    const expectedAnswer = 'the answer';

    // Test is written out of order because of promises, follow the (#) comments.
    rt.addListener('QUESTION', question => {
        // (2) Assert the question is correct, then emit the answer
        t.strictEqual(question, expectedQuestion);
        rt.emit('ANSWER', expectedAnswer);
    });

    // (1) Emit the question.
    const promise = s.askAndWait({QUESTION: expectedQuestion}, util);

    // (3) Ask block resolves after the answer is emitted.
    promise.then(() => {
        t.strictEqual(s.getAnswer(), expectedAnswer);
        t.end();
    });
});

test('ask and stop all dismisses question', t => {
    const rt = new Runtime();
    const s = new Sensing(rt);
    const util = {target: {visible: false}};

    const expectedQuestion = 'a question';

    let call = 0;

    rt.addListener('QUESTION', question => {
        if (call === 0) {
            // (2) Assert the question was passed.
            t.strictEqual(question, expectedQuestion);
        } else if (call === 1) {
            // (4) Assert the question was dismissed.
            t.strictEqual(question, null);
            t.end();
        }
        call += 1;
    });

    // (1) Emit the question.
    s.askAndWait({QUESTION: expectedQuestion}, util);
    // (3) Emit the stop all event.
    rt.stopAll();
});

test('ask and stop other scripts dismisses if it is the last question', t => {
    const rt = new Runtime();
    const s = new Sensing(rt);
    const util = {target: {visible: false, sprite: {}, getCustomState: () => ({})}, thread: {}};

    const expectedQuestion = 'a question';

    let call = 0;

    rt.addListener('QUESTION', question => {
        if (call === 0) {
            // (2) Assert the question was passed.
            t.strictEqual(question, expectedQuestion);
        } else if (call === 1) {
            // (4) Assert the question was dismissed.
            t.strictEqual(question, null);
            t.end();
        }
        call += 1;
    });

    // (1) Emit the questions.
    s.askAndWait({QUESTION: expectedQuestion}, util);
    // (3) Emit the stop for target event.
    rt.stopForTarget(util.target, util.thread);
});

test('ask and stop other scripts asks next question', t => {
    const rt = new Runtime();
    const s = new Sensing(rt);
    const util = {target: {visible: false, sprite: {}, getCustomState: () => ({})}, thread: {}};
    const util2 = {target: {visible: false, sprite: {}, getCustomState: () => ({})}, thread: {}};

    const expectedQuestion = 'a question';
    const nextQuestion = 'a followup';

    let call = 0;

    rt.addListener('QUESTION', question => {
        if (call === 0) {
            // (2) Assert the question was passed.
            t.strictEqual(question, expectedQuestion);
        } else if (call === 1) {
            // (4) Assert the next question was passed.
            t.strictEqual(question, nextQuestion);
            t.end();
        }
        call += 1;
    });

    // (1) Emit the questions.
    s.askAndWait({QUESTION: expectedQuestion}, util);
    s.askAndWait({QUESTION: nextQuestion}, util2);
    // (3) Emit the stop for target event.
    rt.stopForTarget(util.target, util.thread);
});

test('ask and answer with a visible target', t => {
    const rt = new Runtime();
    const s = new Sensing(rt);
    const util = {target: {visible: true}};

    const expectedQuestion = 'a question';
    const expectedAnswer = 'the answer';

    rt.removeAllListeners('SAY'); // Prevent say blocks from executing

    rt.addListener('SAY', (target, type, question) => {
        // Should emit SAY with the question
        t.strictEqual(question, expectedQuestion);
    });

    rt.addListener('QUESTION', question => {
        // Question should be blank for a visible target
        t.strictEqual(question, '');

        // Remove the say listener and add a new one to assert bubble is cleared
        // by setting say to empty string after answer is received.
        rt.removeAllListeners('SAY');
        rt.addListener('SAY', (target, type, text) => {
            t.strictEqual(text, '');
            t.end();
        });
        rt.emit('ANSWER', expectedAnswer);
    });

    s.askAndWait({QUESTION: expectedQuestion}, util);
});

test('answer gets reset when runtime is disposed', t => {
    const rt = new Runtime();
    const s = new Sensing(rt);
    const util = {target: {visible: false}};
    const expectedAnswer = 'the answer';

    rt.addListener('QUESTION', () => rt.emit('ANSWER', expectedAnswer));
    const promise = s.askAndWait({QUESTION: ''}, util);

    promise.then(() => t.strictEqual(s.getAnswer(), expectedAnswer))
        .then(() => rt.dispose())
        .then(() => {
            t.strictEqual(s.getAnswer(), '');
            t.end();
        });
});

test('set drag mode', t => {
    const runtime = new Runtime();
    runtime.requestTargetsUpdate = () => {}; // noop for testing
    const sensing = new Sensing(runtime);
    const s = new Sprite(null, runtime);
    const rt = new RenderedTarget(s, runtime);

    sensing.setDragMode({DRAG_MODE: 'not draggable'}, {target: rt});
    t.strictEqual(rt.draggable, false);

    sensing.setDragMode({DRAG_MODE: 'draggable'}, {target: rt});
    t.strictEqual(rt.draggable, true);

    t.end();
});

test('get loudness with caching', t => {
    const rt = new Runtime();
    const sensing = new Sensing(rt);

    // It should report -1 when audio engine is not available.
    t.strictEqual(sensing.getLoudness(), -1);

    // Stub the audio engine with its getLoudness function, and set up different
    // values to simulate it changing over time.
    const firstLoudness = 1;
    const secondLoudness = 2;
    let simulatedLoudness = firstLoudness;
    rt.audioEngine = {getLoudness: () => simulatedLoudness};

    // It should report -1 when current step time is null.
    t.strictEqual(sensing.getLoudness(), -1);

    // Stub the current step time.
    rt.currentStepTime = 1000 / 30;

    // The first time it works, it should report the result from the stubbed audio engine.
    t.strictEqual(sensing.getLoudness(), firstLoudness);

    // Update the simulated loudness to a new value.
    simulatedLoudness = secondLoudness;

    // Simulate time passing by advancing the timer forward a little bit.
    // After less than a step, it should still report cached loudness.
    let simulatedTime = Date.now() + (rt.currentStepTime / 2);
    sensing._timer = {time: () => simulatedTime};
    t.strictEqual(sensing.getLoudness(), firstLoudness);

    // Simulate more than a step passing. It should now request the value
    // from the audio engine again.
    simulatedTime += rt.currentStepTime;
    t.strictEqual(sensing.getLoudness(), secondLoudness);

    t.end();
});

test('loud? boolean', t => {
    const rt = new Runtime();
    const sensing = new Sensing(rt);

    // The simplest way to test this is to actually override the getLoudness
    // method, which isLoud uses.
    let simulatedLoudness = 0;
    sensing.getLoudness = () => simulatedLoudness;
    t.false(sensing.isLoud());

    // Check for GREATER than 10, not equal.
    simulatedLoudness = 10;
    t.false(sensing.isLoud());

    simulatedLoudness = 11;
    t.true(sensing.isLoud());

    t.end();
});

test('get attribute of sprite variable', t => {
    const rt = new Runtime();
    const sensing = new Sensing(rt);
    const s = new Sprite(null, rt);
    const target = new RenderedTarget(s, rt);
    const variable = {
        name: 'cars',
        value: 'trucks',
        type: ''
    };
    // Add variable to set the map (it should be empty before this).
    target.variables.anId = variable;
    rt.getSpriteTargetByName = () => target;
    t.equal(sensing.getAttributeOf({PROPERTY: 'cars'}), 'trucks');

    t.end();
});
test('get attribute of variable that does not exist', t => {
    const rt = new Runtime();
    const sensing = new Sensing(rt);
    const s = new Sprite(null, rt);
    const target = new RenderedTarget(s, rt);
    rt.getTargetForStage = () => target;
    t.equal(sensing.getAttributeOf({PROPERTY: 'variableThatDoesNotExist'}), 0);

    t.end();
});

test('username block', t => {
    const rt = new Runtime();
    const sensing = new Sensing(rt);
    const util = new BlockUtility(rt.sequencer);

    t.equal(sensing.getUsername({}, util), '');
    t.end();
});
