#!/usr/bin/env node

"use strict";

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

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

global.Zmodem = require('./lib/zmodem');

var zdle = new Zmodem.ZDLE( { escape_ctrl_chars: true } );

tape('trim_leading_garbage', function(t) {
    var header = Zmodem.Header.build('ZACK');

    var header_octets = new Map( [
        [ "hex", header.to_hex(), ],
        [ "b16", header.to_binary16(zdle), ],
        [ "b32", header.to_binary32(zdle), ],
    ] );

    var leading_garbage = [
        "",
        " ",
        "\n\n",
        "\r\n\r\n",
        "*",
        "**",
        "*\x18",
        "*\x18D",
        "**\x18",
    ];

    leading_garbage.forEach( (garbage) => {
        let garbage_json = JSON.stringify(garbage);
        let garbage_octets = testhelp.string_to_octets( garbage );

        for ( let [label, hdr_octets] of header_octets ) {
            var input = garbage_octets.slice(0).concat( hdr_octets );
            var trimmed = Zmodem.Header.trim_leading_garbage(input);

            t.deepEquals(trimmed, garbage_octets, `${garbage_json} + ${label}: garbage trimmed`);
            t.deepEquals(input, hdr_octets, `… leaving the header`);
        }
    } );

    //----------------------------------------------------------------------

    //input, number of bytes trimmed
    var partial_trims = [
        [ "*", 0 ],
        [ "**", 0 ],
        [ "***", 1 ],
        [ "*\x18**", 2 ],
        [ "*\x18*\x18", 2 ],
        [ "*\x18*\x18**", 4 ],
        [ "*\x18*\x18*\x18", 4 ],
    ];

    partial_trims.forEach( (cur) => {
        let [ input, trimmed_count ] = cur;

        let input_json = JSON.stringify(input);

        let input_octets = testhelp.string_to_octets(input);

        let garbage = Zmodem.Header.trim_leading_garbage(input_octets.slice(0));

        t.deepEquals(
            garbage,
            input_octets.slice(0, trimmed_count),
            `${input_json}: trim first ${trimmed_count} byte(s)`
        );
    } );

    t.end();
});

//Test that we parse a trailing 0x8a, since we ourselves follow the
//documentation and put a plain LF (0x0a).
tape('parse_hex', function(t) {
    var octets = testhelp.string_to_octets( "**\x18B0901020304a57f\x0d\x8a" );

    var parsed = Zmodem.Header.parse( octets );

    t.is( parsed[1], 16, 'CRC size' );

    t.is(
        parsed[0].NAME,
        'ZRPOS',
        'parsed NAME'
    );

    t.is(
        parsed[0].TYPENUM,
        9,
        'parsed TYPENUM'
    );

    t.is(
        parsed[0].get_offset(),
        0x04030201,             //it’s little-endian
        'parsed offset'
    );

    t.end();
} );

tape('round-trip, empty headers', function(t) {
    ["ZRQINIT", "ZSKIP", "ZABORT", "ZFIN", "ZFERR"].forEach( (n) => {
        var orig = Zmodem.Header.build(n);

        var hex = orig.to_hex();
        var b16 = orig.to_binary16(zdle);
        var b32 = orig.to_binary32(zdle);

        var rounds = new Map( [
            [ "to_hex", hex ],
            [ "to_binary16", b16 ],
            [ "to_binary32", b32 ],
        ] );

        for ( const [ enc, h ] of rounds ) {
            let [ parsed, crclen ] = Zmodem.Header.parse(h);

            t.is( parsed.NAME, orig.NAME, `${n}, ${enc}: NAME` );
            t.is( parsed.TYPENUM, orig.TYPENUM, `${n}, ${enc}: TYPENUM` );

            //Here’s where we test the CRC length in the response.
            t.is(
                crclen,
                /32/.test(enc) ? 32 : 16,
                `${n}, ${enc}: CRC length`,
            );
        }
    } );

    t.end();
} );

tape('round-trip, offset headers', function(t) {
    ["ZRPOS", "ZDATA", "ZEOF"].forEach( (n) => {
        var orig = Zmodem.Header.build(n, 12345);

        var hex = orig.to_hex();
        var b16 = orig.to_binary16(zdle);
        var b32 = orig.to_binary32(zdle);

        var rounds = new Map( [
            [ "to_hex", hex ],
            [ "to_binary16", b16 ],
            [ "to_binary32", b32 ],
        ] );

        for ( const [ enc, h ] of rounds ) {
            //Here’s where we test that parse() leaves in trailing bytes.
            let extra = [99, 99, 99];
            let bytes_with_extra = h.slice().concat(extra);

            let parsed = Zmodem.Header.parse(bytes_with_extra)[0];

            t.is( parsed.NAME, orig.NAME, `${n}, ${enc}: NAME` );
            t.is( parsed.TYPENUM, orig.TYPENUM, `${n}, ${enc}: TYPENUM` );
            t.is( parsed.get_offset(), orig.get_offset(), `${n}, ${enc}: get_offset()` );

            let expected = extra.slice(0);
            if (enc === "to_hex") {
                expected.splice( 0, 0, Zmodem.ZMLIB.XON );
            }

            t.deepEquals(
                bytes_with_extra,
                expected,
                `${enc}: parse() leaves in trailing bytes`,
            );
        }
    } );

    t.end();
} );

tape('round-trip, ZSINIT', function(t) {
    var opts = [
        [],
        ["ESCCTL"],
    ];

    opts.forEach( (args) => {
        var orig = Zmodem.Header.build("ZSINIT", args);

        var hex = orig.to_hex();
        var b16 = orig.to_binary16(zdle);
        var b32 = orig.to_binary32(zdle);

        var rounds = new Map( [
            [ "to_hex", hex ],
            [ "to_binary16", b16 ],
            [ "to_binary32", b32 ],
        ] );

        var args_str = JSON.stringify(args);

        for ( const [ enc, h ] of rounds ) {
            let parsed = Zmodem.Header.parse(h)[0];

            t.is( parsed.NAME, orig.NAME, `opts ${args_str}: ${enc}: NAME` );
            t.is( parsed.TYPENUM, orig.TYPENUM, `opts ${args_str}: ${enc}: TYPENUM` );

            t.is( parsed.escape_ctrl_chars(), orig.escape_ctrl_chars(), `opts ${args_str}: ${enc}: escape_ctrl_chars()` );
            t.is( parsed.escape_8th_bit(), orig.escape_8th_bit(), `opts ${args_str}: ${enc}: escape_8th_bit()` );
        }
    } );

    t.end();
} );

tape('round-trip, ZRINIT', function(t) {
    var opts = [];

    [ [], ["CANFDX"] ].forEach( (canfdx) => {
        [ [], ["CANOVIO"] ].forEach( (canovio) => {
            [ [], ["CANBRK"] ].forEach( (canbrk) => {
                [ [], ["CANFC32"] ].forEach( (canfc32) => {
                    [ [], ["ESCCTL"] ].forEach( (escctl) => {
                        opts.push( [
                            ...canfdx,
                            ...canovio,
                            ...canbrk,
                            ...canfc32,
                            ...escctl,
                        ] );
                    } );
                } );
            } );
        } );
    } );

    opts.forEach( (args) => {
        var orig = Zmodem.Header.build("ZRINIT", args);

        var hex = orig.to_hex();
        var b16 = orig.to_binary16(zdle);
        var b32 = orig.to_binary32(zdle);

        var rounds = new Map( [
            [ "to_hex", hex ],
            [ "to_binary16", b16 ],
            [ "to_binary32", b32 ],
        ] );

        var args_str = JSON.stringify(args);

        for ( const [ enc, h ] of rounds ) {
            let parsed = Zmodem.Header.parse(h)[0];

            t.is( parsed.NAME, orig.NAME, `opts ${args_str}: ${enc}: NAME` );
            t.is( parsed.TYPENUM, orig.TYPENUM, `opts ${args_str}: ${enc}: TYPENUM` );

            t.is( parsed.can_full_duplex(), orig.can_full_duplex(), `opts ${args_str}: ${enc}: can_full_duplex()` );
            t.is( parsed.can_overlap_io(), orig.can_overlap_io(), `opts ${args_str}: ${enc}: can_overlap_io()` );
            t.is( parsed.can_break(), orig.can_break(), `opts ${args_str}: ${enc}: can_break()` );
            t.is( parsed.can_fcs_32(), orig.can_fcs_32(), `opts ${args_str}: ${enc}: can_fcs_32()` );
            t.is( parsed.escape_ctrl_chars(), orig.escape_ctrl_chars(), `opts ${args_str}: ${enc}: escape_ctrl_chars()` );
            t.is( parsed.escape_8th_bit(), orig.escape_8th_bit(), `opts ${args_str}: ${enc}: escape_8th_bit()` );
        }
    } );

    t.end();
} );

tape('hex_final_XON', function(t) {
    var hex_ZFIN = Zmodem.Header.build("ZFIN").to_hex();

    t.notEquals(
        hex_ZFIN.slice(-1)[0],
        Zmodem.ZMLIB.XON,
        'ZFIN hex does NOT end with XON',
    );

    var hex_ZACK = Zmodem.Header.build("ZACK").to_hex();

    t.notEquals(
        hex_ZACK.slice(-1)[0],
        Zmodem.ZMLIB.XON,
        'ZACK hex does NOT end with XON',
    );

    var headers = [
        "ZRQINIT",
        Zmodem.Header.build("ZRINIT", []),
        Zmodem.Header.build("ZSINIT", []),
        "ZRPOS",
        "ZABORT",
        "ZFERR",
    ];

    //These are the only headers we expect to send as hex … right?
    headers.forEach( hdr => {
        if (typeof hdr === "string") hdr = Zmodem.Header.build(hdr);

        t.is(
            hdr.to_hex().slice(-1)[0],
            Zmodem.ZMLIB.XON,
            `${hdr.NAME} hex ends with XON`
        );
    } );

    t.end();
} );
