/* global Uint8Array Promise */
const tap = require('tap');
const {AudioContext} = require('web-audio-test-api');

const AudioEngine = require('../src/AudioEngine');


tap.test('SoundPlayer', suite => {

    let audioContext;
    let audioEngine;
    let soundPlayer;

    const help = {
        get engineInputs () {
            return audioEngine.inputNode.toJSON().inputs;
        }
    };

    suite.beforeEach(() => {
        audioContext = new AudioContext();
        audioEngine = new AudioEngine(audioContext);
        // sound will be 0.2 seconds long
        audioContext.DECODE_AUDIO_DATA_RESULT = audioContext.createBuffer(2, 8820, 44100);
        audioContext.DECODE_AUDIO_DATA_FAILED = false;
        const data = new Uint8Array(0);
        return audioEngine.decodeSoundPlayer({data}).then(result => {
            soundPlayer = result;
        });
    });

    suite.afterEach(() => {
        soundPlayer.dispose();
        soundPlayer = null;
        audioEngine = null;
        audioContext.$reset();
        audioContext = null;
    });

    suite.plan(5);

    suite.test('play initializes and creates source node', t => {
        t.plan(3);
        t.equal(soundPlayer.initialized, false, 'not yet initialized');
        soundPlayer.play();
        t.equal(soundPlayer.initialized, true, 'now is initialized');
        t.deepEqual(soundPlayer.outputNode.toJSON(), {
            buffer: audioContext.DECODE_AUDIO_DATA_RESULT.toJSON(),
            inputs: [],
            loop: false,
            loopEnd: 0,
            loopStart: 0,
            name: 'AudioBufferSourceNode',
            playbackRate: {
                inputs: [],
                value: 1
            }
        });

        t.end();
    });

    suite.test('connect', t => {
        t.plan(1);
        soundPlayer.play();
        soundPlayer.connect(audioEngine);
        t.deepEqual(help.engineInputs, [
            soundPlayer.outputNode.toJSON()
        ], 'output node connects to input node');
        t.end();
    });

    suite.test('stop decay', t => {
        t.plan(5);
        soundPlayer.play();
        soundPlayer.connect(audioEngine);
        const outputNode = soundPlayer.outputNode;

        audioContext.$processTo(0);
        soundPlayer.stop();
        t.equal(soundPlayer.outputNode, null, 'nullify outputNode immediately (taken sound is stopping)');
        t.deepEqual(help.engineInputs, [{
            name: 'GainNode',
            gain: {
                value: 1,
                inputs: []
            },
            inputs: [outputNode.toJSON()]
        }], 'output node connects to gain node to input node');

        audioContext.$processTo(audioEngine.DECAY_DURATION / 2);
        t.equal(outputNode.$state, 'PLAYING');

        audioContext.$processTo(audioEngine.DECAY_DURATION + 0.001);
        t.deepEqual(help.engineInputs, [{
            name: 'GainNode',
            gain: {
                value: 0,
                inputs: []
            },
            inputs: [outputNode.toJSON()]
        }], 'output node connects to gain node to input node decayed');

        t.equal(outputNode.$state, 'FINISHED');

        t.end();
    });

    suite.test('play while playing debounces', t => {
        t.plan(7);
        const log = [];
        soundPlayer.connect(audioEngine);
        soundPlayer.play();
        t.equal(soundPlayer.isStarting, true, 'player.isStarting');
        const originalNode = soundPlayer.outputNode;
        // the second play should still "finish" this play
        soundPlayer.finished().then(() => log.push('finished first'));
        soundPlayer.play();
        soundPlayer.finished().then(() => log.push('finished second'));
        soundPlayer.play();
        soundPlayer.finished().then(() => log.push('finished third'));
        soundPlayer.play();
        t.equal(originalNode, soundPlayer.outputNode, 'same output node');
        t.equal(soundPlayer.outputNode.$state, 'PLAYING');
        return Promise.resolve().then(() => {
            t.deepEqual(log, ['finished first', 'finished second', 'finished third'], 'finished in order');

            // fast forward to one ms before decay time
            audioContext.$processTo(audioEngine.DECAY_DURATION - 0.001);
            soundPlayer.play();

            t.equal(originalNode, soundPlayer.outputNode, 'same output node');


            // now at DECAY_DURATION, we should meet a new player as the old one is taken/stopped
            audioContext.$processTo(audioEngine.DECAY_DURATION);

            t.equal(soundPlayer.isStarting, false, 'player.isStarting now false');

            soundPlayer.play();
            t.notEqual(originalNode, soundPlayer.outputNode, 'New output node');

            t.end();
        });

    });

    suite.test('play while playing', t => {
        t.plan(15);
        const log = [];
        soundPlayer.play();
        soundPlayer.finished().then(() => log.push('play 1 finished'));
        soundPlayer.connect(audioEngine);
        const firstPlayNode = soundPlayer.outputNode;

        // go past debounce time and play again
        audioContext.$processTo(audioEngine.DECAY_DURATION);

        return Promise.resolve()
        .then(() => {

            t.equal(soundPlayer.outputNode.$state, 'PLAYING');

            soundPlayer.play();
            soundPlayer.finished().then(() => log.push('play 2 finished'));

            // wait for a micro-task loop to fire our previous events
            return Promise.resolve();
        })
        .then(() => {

            t.equal(log[0], 'play 1 finished');
            t.notEqual(soundPlayer.outputNode, firstPlayNode, 'created new player node');

            t.equal(help.engineInputs.length, 2, 'there should be 2 players connected');
            t.equal(firstPlayNode.$state, 'PLAYING');
            t.equal(soundPlayer.outputNode.$state, 'PLAYING');
            t.equal(help.engineInputs[0].gain.value, 1, 'old sound connectect to gain node with volume 1');

            const {currentTime} = audioContext;
            audioContext.$processTo(currentTime + audioEngine.DECAY_WAIT + 0.001);
            t.notEqual(help.engineInputs[0].gain.value, 1,
            'old sound connected to gain node which will fade');

            audioContext.$processTo(currentTime + audioEngine.DECAY_WAIT + audioEngine.DECAY_DURATION + 0.001);
            t.equal(soundPlayer.outputNode.$state, 'PLAYING');
            t.equal(firstPlayNode.$state, 'FINISHED');

            t.equal(help.engineInputs[0].gain.value, 0, 'faded old sound to 0');

            t.equal(log.length, 1);
            audioContext.$processTo(currentTime + audioEngine.DECAY_WAIT + audioEngine.DECAY_DURATION + 0.3);

            // wait for a micro-task loop to fire our previous events
            return Promise.resolve();
        })
        .then(() => {

            t.equal(log[1], 'play 2 finished');
            t.equal(help.engineInputs.length, 1, 'old sound disconneted itself after done');
            t.equal(log.length, 2);

            t.end();
        });
    });

    suite.end();
});