#!/usr/bin/env node

"use strict";

const tape = require('blue-tape');

const SZ_PATH = require('which').sync('sz', {nothrow: true});

if (!SZ_PATH) {
    tape.only('SKIP: no “sz” in PATH!', (t) => {
        t.end();
    });
}

const spawn = require('child_process').spawn;

var helper = require('./lib/testhelp');

Object.assign(
    global,
    {
        Zmodem: require('./lib/zmodem'),
    }
);

var FILE1 = helper.make_temp_file(10 * 1024 * 1024);    //10 MiB

function _test_steps(t, sz_args, steps) {
    return helper.exec_lrzsz_steps( t, SZ_PATH, sz_args, steps );
}

tape('abort() after ZRQINIT', (t) => {
    return _test_steps( t, [FILE1], [
        (zsession, child) => {
            zsession.abort();
            return true;
        },
    ] ).then( (inputs) => {
        //console.log("inputs", inputs);

        var str = String.fromCharCode.apply( String, inputs[ inputs.length - 1 ]);
        t.ok(
            str.match(/\x18\x18\x18\x18\x18/),
            'abort() right after receipt of ZRQINIT',
        );
    } );
});

tape('abort() after ZFILE', (t) => {
    return _test_steps( t, [FILE1], [
        (zsession) => {
            zsession.start();
            return true;
        },
        (zsession) => {
            zsession.abort();
            return true;
        },
    ] ).then( (inputs) => {
        //console.log("inputs", inputs);

        var str = String.fromCharCode.apply( String, inputs[ inputs.length - 1 ]);
        t.ok(
            str.match(/\x18\x18\x18\x18\x18/),
            'abort() right after receipt of ZFILE',
        );
    } );
});

//NB: This test is not unlikely to flap since it depends
//on sz reading the abort sequence prior to finishing its read
//of the file.
tape('abort() during download', { timeout: 30000 }, (t) => {
    var child_pms = _test_steps( t, [FILE1], [
        (zsession) => {
            zsession.on("offer", (offer) => offer.accept() );
            zsession.start();
            return true;
        },
        (zsession) => {
            zsession.abort();
            return true;
        },
    ] );

    return child_pms.then( (inputs) => {
        t.notEquals( inputs, undefined, 'abort() during download ends the transmission' );

        t.ok(
            inputs.every( function(bytes) {
                var str = String.fromCharCode.apply( String, bytes );
                return !/THE_END/.test(str);
            } ),
            "the end of the file was not sent",
        );
    } );
});

//This only works because we use CRC32 to receive. CRC16 in lsz has a
//buffer overflow bug, fixed here:
//
//  https://github.com/gooselinux/lrzsz/blob/master/lrzsz-0.12.20.patch
//
tape('skip() during download', { timeout: 30000 }, (t) => {
    var filenames = [FILE1, helper.make_temp_file(12345678)];
    //filenames = ["-vvvvvvvvvvvvv", FILE1, _make_temp_file()];

    var started, second_offer;

    return _test_steps( t, filenames, [
        (zsession) => {
            if (!started) {
                function offer_taker(offer) {
                    offer.accept();
                    offer.skip();
                    zsession.off("offer", offer_taker);
                    zsession.on("offer", (offer2) => {
                        second_offer = offer2;
                        offer2.skip();
                    });
                }
                zsession.on("offer", offer_taker);
                zsession.start();
                started = true;
            }
            //return true;
        },
    ] ).then( (inputs) => {
        var never_end = inputs.every( function(bytes) {
            var str = String.fromCharCode.apply( String, bytes );
            return !/THE_END/.test(str);
        } );

        // This is race-prone.
        //t.ok( never_end, "the end of a file is never sent" );

        t.ok( !!second_offer, "we got a 2nd offer after the first" );
    } );
});

tape('skip() - immediately - at end of download', { timeout: 30000 }, (t) => {
    var filenames = [helper.make_temp_file(123)];

    var started;

    return _test_steps( t, filenames, [
        (zsession) => {
            if (!started) {
                function offer_taker(offer) {
                    offer.accept();
                    offer.skip();
                }
                zsession.on("offer", offer_taker);
                zsession.start();

                started = true;
            }
        },
    ] );
});

// Verify a skip() that happens after a transfer is complete.
// There are no assertions here.
tape('skip() - after a parse - at end of download', { timeout: 30000 }, (t) => {
    var filenames = [helper.make_temp_file(123)];

    var the_offer, started, skipped, completed;

    return _test_steps( t, filenames, [
        (zsession) => {
            if (!started) {
                function offer_taker(offer) {
                    the_offer = offer;
                    var promise = the_offer.accept();
                    promise.then( () => {
                        completed = 1;
                    } );
                }
                zsession.on("offer", offer_taker);
                zsession.start();
                started = true;
            }

            return the_offer;
        },
        () => {
            if (!skipped && !completed) {
                the_offer.skip();
                skipped = true;
            }
        },
    ] );
});

var happy_filenames = [
    helper.make_temp_file(5),
    helper.make_temp_file(3),
    helper.make_temp_file(1),
    helper.make_empty_temp_file(),
];

tape('happy-path: single batch', { timeout: 30000 }, (t) => {
    var started, the_offer;

    var args = happy_filenames;

    var buffers = [];

    var child_pms = _test_steps( t, args, [
        (zsession) => {
            if (!started) {
                function offer_taker(offer) {
                    the_offer = offer;
                    the_offer.accept( { on_input: "spool_array" } ).then( (byte_lists) => {
                        var flat = [].concat.apply([], byte_lists);
                        var str = String.fromCharCode.apply( String, flat );
                        buffers.push(str);
                    } );
                }
                zsession.on("offer", offer_taker);
                zsession.start();
                started = true;
            }

            return false;
        },
    ] );

    return child_pms.then( (inputs) => {
        t.equals( buffers[0], "xxxxx=THE_END", '5-byte transfer plus end' );
        t.equals( buffers[1], "xxx=THE_END", '3-byte transfer plus end' );
        t.equals( buffers[2], "x=THE_END", '1-byte transfer plus end' );
        t.equals( buffers[3], "", 'empty transfer plus end' );
    } );
});

tape('happy-path: individual transfers', { timeout: 30000 }, (t) => {
    var promises = happy_filenames.map( (fn) => {
        var str;

        var started;

        var child_pms = _test_steps( t, [fn], [
            (zsession) => {
                if (!started) {
                    function offer_taker(offer) {
                        offer.accept( { on_input: "spool_array" } ).then( (byte_lists) => {
                            var flat = [].concat.apply([], byte_lists);
                            str = String.fromCharCode.apply( String, flat );
                        } );
                    }
                    zsession.on("offer", offer_taker);
                    zsession.start();
                    started = true;
                }

                return false;
            },
        ] );

        return child_pms.then( () => str );
    } );

    return Promise.all(promises).then( (strs) => {
        t.equals( strs[0], "xxxxx=THE_END", '5-byte transfer plus end' );
        t.equals( strs[1], "xxx=THE_END", '3-byte transfer plus end' );
        t.equals( strs[2], "x=THE_END", '1-byte transfer plus end' );
        t.equals( strs[3], "", 'empty transfer plus end' );
    } );
});

//This doesn’t work because we automatically send ZFIN once we receive it,
//which prompts the child to finish up.
tape.skip("abort() after ZEOF", (t) => {
    var received;

    return _test_steps( t, [FILE1], [
        (zsession) => {
            zsession.on("offer", (offer) => {
                offer.accept().then( () => { received = true } );
            } );
            zsession.start();
            return true;
        },
        (zsession) => {
            if (received) {
                zsession.abort();
                return true;
            }
        },
    ] ).then( (inputs) => {
        var str = String.fromCharCode.apply( String, inputs[ inputs.length - 1 ]);
        t.is( str, "OO", "successful close despite abort" );
    } );
});