/*eslint no-unused-expressions: 0, prefer-arrow-callback: 0 */

'use strict';

const { parser } = require('../lib/handler/imap-handler');

let asyncWrapper = (test, handler) => {
    handler(test)
        .then(() => test.done())
        .catch(err => {
            test.ifError(err);
            test.done();
        });
};

// Error path tests
//
// NOTE: All error inputs use a non-status command (FETCH) rather than OK/NO/BAD/BYE.
// Status commands consume their entire remainder as human-readable text and never
// invoke the TokenParser, so errors like E9/E10/E13 cannot be triggered through
// the public parser() API when the command is a status response.
//
// Error-path tests use asyncWrapper with an internal try/catch so that err.code
// can be asserted. The handler re-throws when no error was caught so that a missing
// rejection is still surfaced as a test failure.

/**
 * E9: Unclosed quoted string. The node's isClosed flag remains false when
 * getAttributes() walks the tree, triggering ParserError9.
 */
module.exports['Token Parser: E9: unclosed quoted string throws ParserError9'] = test =>
    asyncWrapper(test, async test => {
        let err;
        try {
            await parser('* FETCH "unterminated');
        } catch (e) {
            err = e;
        }
        if (!err) throw new Error('Expected parser to throw but it did not');
        test.ok(err, 'expected an error to be thrown');
        test.equal(err.code, 'ParserError9');
    });

/**
 * E10: Unexpected list terminator. A ')' is encountered when the current node
 * is not a LIST node, triggering ParserError10.
 */
module.exports['Token Parser: E10: unexpected ) throws ParserError10'] = test =>
    asyncWrapper(test, async test => {
        let err;
        try {
            await parser('* FETCH )');
        } catch (e) {
            err = e;
        }
        if (!err) throw new Error('Expected parser to throw but it did not');
        test.ok(err, 'expected an error to be thrown');
        test.equal(err.code, 'ParserError10');
    });

/**
 * E13: Unexpected control character. A character below 0x80 that is not in
 * ATOM-CHAR and not one of the explicitly allowed exceptions (\, %) triggers
 * ParserError13 in the STATE_NORMAL default branch.
 * \x01 is a control character excluded from ATOM-CHAR.
 */
module.exports['Token Parser: E13: control character in attribute position throws ParserError13'] = test =>
    asyncWrapper(test, async test => {
        let err;
        try {
            await parser('* FETCH \x01rest');
        } catch (e) {
            err = e;
        }
        if (!err) throw new Error('Expected parser to throw but it did not');
        test.ok(err, 'expected an error to be thrown');
        test.equal(err.code, 'ParserError13');
    });

/**
 * E18: Backslash escape at end of quoted string input. When a backslash is
 * encountered in STATE_STRING and there is no following character (i >= len),
 * ParserError18 is thrown.
 * The JS string '* FETCH "test\\' represents the IMAP input: * FETCH "test\
 */
module.exports['Token Parser: E18: escape at end of quoted string throws ParserError18'] = test =>
    asyncWrapper(test, async test => {
        let err;
        try {
            await parser('* FETCH "test\\');
        } catch (e) {
            err = e;
        }
        if (!err) throw new Error('Expected parser to throw but it did not');
        test.ok(err, 'expected an error to be thrown');
        test.equal(err.code, 'ParserError18');
    });

/**
 * E23: Empty literal braces. When '}' is encountered in STATE_LITERAL before
 * any digit has been stored in literalLength, ParserError23 is thrown because
 * the check is: if (!('literalLength' in this.currentNode)) throw E23.
 */
module.exports['Token Parser: E23: empty literal braces throws ParserError23'] = test =>
    asyncWrapper(test, async test => {
        let err;
        try {
            await parser('* FETCH {}', { literals: [] });
        } catch (e) {
            err = e;
        }
        if (!err) throw new Error('Expected parser to throw but it did not');
        test.ok(err, 'expected an error to be thrown');
        test.equal(err.code, 'ParserError23');
    });

/**
 * E25: Non-digit character inside literal size braces. Any character inside
 * {…} that is not a digit (and not '}' or '+' with literalPlus) triggers
 * ParserError25.
 */
module.exports['Token Parser: E25: non-digit in literal size throws ParserError25'] = test =>
    asyncWrapper(test, async test => {
        let err;
        try {
            await parser('* FETCH {abc}', { literals: [] });
        } catch (e) {
            err = e;
        }
        if (!err) throw new Error('Expected parser to throw but it did not');
        test.ok(err, 'expected an error to be thrown');
        test.equal(err.code, 'ParserError25');
    });

/**
 * E26: Leading zero in literal size. After '0' is stored as literalLength,
 * any following digit triggers ParserError26 because a leading zero is invalid.
 */
module.exports['Token Parser: E26: leading zero in literal size throws ParserError26'] = test =>
    asyncWrapper(test, async test => {
        let err;
        try {
            await parser('* FETCH {01}', { literals: [Buffer.from('x')] });
        } catch (e) {
            err = e;
        }
        if (!err) throw new Error('Expected parser to throw but it did not');
        test.ok(err, 'expected an error to be thrown');
        test.equal(err.code, 'ParserError26');
    });

/**
 * MAX_IMAP_NESTING_REACHED: Nesting depth exceeds MAX_NODE_DEPTH (25).
 * createNode() throws when node.depth > 25. The root TREE node starts at
 * depth 0, so depth 1 is the first LIST ('('), and 26 open parens reaches
 * depth 26 which exceeds the limit.
 */
module.exports['Token Parser: MAX_IMAP_NESTING_REACHED: deep nesting throws MAX_IMAP_NESTING_REACHED'] = test =>
    asyncWrapper(test, async test => {
        let err;
        try {
            const depth = 26;
            const input = '* FETCH ' + '('.repeat(depth) + 'x' + ')'.repeat(depth);
            await parser(input);
        } catch (e) {
            err = e;
        }
        if (!err) throw new Error('Expected parser to throw but it did not');
        test.ok(err, 'expected an error to be thrown');
        test.equal(err.code, 'MAX_IMAP_NESTING_REACHED');
    });

// Happy-path tests

/**
 * NIL atom is returned as null in the attributes array. The ATOM node whose
 * value is "NIL" (case-insensitive) is converted to null in getAttributes().
 */
module.exports['Token Parser: NIL atom is parsed as null'] = test =>
    asyncWrapper(test, async test => {
        const result = await parser('* FETCH NIL');
        const attrs = result.attributes;
        test.ok(Array.isArray(attrs), 'attributes should be an array');
        test.equal(attrs.length, 1, 'should have exactly one attribute');
        test.strictEqual(attrs[0], null, 'NIL should be parsed as null');
    });

/**
 * Nested lists produce nested arrays in the attributes output.
 * Input ((a b)) produces: [ [ [ {a}, {b} ] ] ] — the attributes array contains
 * one outer list which contains one inner list with two atom elements.
 */
module.exports['Token Parser: nested lists produce nested arrays'] = test =>
    asyncWrapper(test, async test => {
        const result = await parser('* FETCH ((a b))');
        const attrs = result.attributes;
        test.ok(Array.isArray(attrs), 'attributes should be an array');
        const outer = attrs[0];
        test.ok(Array.isArray(outer), 'outer list should be an array');
        const inner = outer[0];
        test.ok(Array.isArray(inner), 'inner list should be an array');
        test.equal(inner.length, 2, 'inner list should have 2 elements');
        test.equal(inner[0].value, 'a', 'first inner element value should be a');
        test.equal(inner[1].value, 'b', 'second inner element value should be b');
    });

/**
 * Quoted string with escaped double-quote characters. The backslash-escape
 * mechanism converts \" to " in the parsed value. The JS literal
 * '* FETCH "hello \\"world\\""' represents: * FETCH "hello \"world\""
 */
module.exports['Token Parser: quoted string with escaped quotes is parsed correctly'] = test =>
    asyncWrapper(test, async test => {
        const result = await parser('* FETCH "hello \\"world\\""');
        const attrs = result.attributes;
        test.ok(Array.isArray(attrs), 'attributes should be an array');
        test.equal(attrs.length, 1, 'should have exactly one attribute');
        test.equal(attrs[0].type, 'STRING', 'type should be STRING');
        test.equal(attrs[0].value, 'hello "world"', 'escaped quotes should be unescaped');
    });

/**
 * Empty quoted string results in a STRING attribute with an empty value.
 */
module.exports['Token Parser: empty quoted string produces STRING with empty value'] = test =>
    asyncWrapper(test, async test => {
        const result = await parser('* FETCH ""');
        const attrs = result.attributes;
        test.ok(Array.isArray(attrs), 'attributes should be an array');
        test.equal(attrs.length, 1, 'should have exactly one attribute');
        test.equal(attrs[0].type, 'STRING', 'type should be STRING');
        test.equal(attrs[0].value, '', 'value should be empty string');
    });

/**
 * Literal data provided via the literals option is parsed into a LITERAL attribute.
 * When options.literals is set and '}' is encountered with a valid size, the next
 * Buffer in the literals array is used as the literal value directly.
 * The literal size in braces must be followed by \r\n or \n per IMAP protocol.
 */
module.exports['Token Parser: literal with pre-parsed data produces LITERAL attribute'] = test =>
    asyncWrapper(test, async test => {
        const data = Buffer.from('hello');
        const result = await parser('* FETCH {5}\r\n', { literals: [data] });
        const attrs = result.attributes;
        test.ok(Array.isArray(attrs), 'attributes should be an array');
        test.equal(attrs.length, 1, 'should have exactly one attribute');
        test.equal(attrs[0].type, 'LITERAL', 'type should be LITERAL');
        test.ok(Buffer.isBuffer(attrs[0].value), 'literal value should be a Buffer');
        test.equal(attrs[0].value.toString(), 'hello', 'literal value should match provided buffer');
    });

/**
 * A plain ATOM token produces an ATOM attribute with the correct string value.
 */
module.exports['Token Parser: atom value produces ATOM attribute'] = test =>
    asyncWrapper(test, async test => {
        const result = await parser('* FETCH INBOX');
        const attrs = result.attributes;
        test.ok(Array.isArray(attrs), 'attributes should be an array');
        test.equal(attrs.length, 1, 'should have exactly one attribute');
        test.equal(attrs[0].type, 'ATOM', 'type should be ATOM');
        test.equal(attrs[0].value, 'INBOX', 'value should be INBOX');
    });

/**
 * E29: Range separator ':' after a character that is not a digit or '*'.
 * Input "1,:5" starts as ATOM "1", ',' triggers SEQUENCE reclassification and
 * appends ',' (value becomes "1,"). Then ':' fires E29 because last char ','
 * is not a digit or '*'.
 */
module.exports['Token Parser: E29: range separator after non-digit/non-star throws ParserError29'] = test =>
    asyncWrapper(test, async test => {
        let err;
        try {
            await parser('* FETCH 1,:5');
        } catch (e) {
            err = e;
        }
        if (!err) throw new Error('Expected parser to throw but it did not');
        test.ok(err, 'expected an error to be thrown');
        test.equal(err.code, 'ParserError29');
    });

/**
 * E30: Wildcard '*' when last char is not ',' or ':'.
 * Input "1:2*" enters SEQUENCE after "1:" and appends "2" (value "1:2").
 * Then '*' fires E30 because last char '2' is not ',' or ':'.
 */
module.exports['Token Parser: E30: wildcard after digit throws ParserError30'] = test =>
    asyncWrapper(test, async test => {
        let err;
        try {
            await parser('* FETCH 1:2*');
        } catch (e) {
            err = e;
        }
        if (!err) throw new Error('Expected parser to throw but it did not');
        test.ok(err, 'expected an error to be thrown');
        test.equal(err.code, 'ParserError30');
    });

/**
 * E31: Separator ',' after a character that is not a digit or '*'.
 * Input "1:,5" enters SEQUENCE after "1" and appends ':' (value "1:").
 * Then ',' fires E31 because last char ':' is not a digit or '*'.
 */
module.exports['Token Parser: E31: comma after colon throws ParserError31'] = test =>
    asyncWrapper(test, async test => {
        let err;
        try {
            await parser('* FETCH 1:,5');
        } catch (e) {
            err = e;
        }
        if (!err) throw new Error('Expected parser to throw but it did not');
        test.ok(err, 'expected an error to be thrown');
        test.equal(err.code, 'ParserError31');
    });

/**
 * E32: Separator ',' after bare '*' (not in a range).
 * Input "*,5" starts SEQUENCE with value "*". Then ',' passes E31
 * (last char '*' satisfies the check) but fires E32 because last char
 * is '*' and the char before it (at(-2)) is not ':'.
 */
module.exports['Token Parser: E32: comma after bare star throws ParserError32'] = test =>
    asyncWrapper(test, async test => {
        let err;
        try {
            await parser('* FETCH *,5');
        } catch (e) {
            err = e;
        }
        if (!err) throw new Error('Expected parser to throw but it did not');
        test.ok(err, 'expected an error to be thrown');
        test.equal(err.code, 'ParserError32');
    });

/**
 * E33: Non-digit, non-special character in sequence position.
 * Input "1:a" enters SEQUENCE after "1" and appends ':' (value "1:").
 * Then 'a' is not a digit, not ':', not '*', not ',' so E33 fires.
 */
module.exports['Token Parser: E33: non-digit non-special char in sequence throws ParserError33'] = test =>
    asyncWrapper(test, async test => {
        let err;
        try {
            await parser('* FETCH 1:a');
        } catch (e) {
            err = e;
        }
        if (!err) throw new Error('Expected parser to throw but it did not');
        test.ok(err, 'expected an error to be thrown');
        test.equal(err.code, 'ParserError33');
    });

/**
 * E34: Digit immediately after '*'.
 * Input "*1" starts SEQUENCE with value "*". Then '1' is a digit but
 * last char is '*', so E34 fires (digits cannot follow '*').
 */
module.exports['Token Parser: E34: digit after star throws ParserError34'] = test =>
    asyncWrapper(test, async test => {
        let err;
        try {
            await parser('* FETCH *1');
        } catch (e) {
            err = e;
        }
        if (!err) throw new Error('Expected parser to throw but it did not');
        test.ok(err, 'expected an error to be thrown');
        test.equal(err.code, 'ParserError34');
    });

module.exports['Token Parser: REFERRAL response code is parsed as an IMAP URL atom'] = test =>
    asyncWrapper(test, async test => {
        let r = await parser('* OK [REFERRAL imap://user@host/INBOX] please use another server');
        let section = r.attributes[0].section;
        test.equal(section[0].value, 'REFERRAL');
        test.equal(section[1].value, 'imap://user@host/INBOX');
    });

module.exports['Token Parser: malformed REFERRAL with no closing bracket consumes the rest'] = test =>
    asyncWrapper(test, async test => {
        // Missing ']' previously produced a negative-index substring (garbage). The whole
        // remaining string should be captured as the URL instead, without throwing.
        let r = await parser('* OK [REFERRAL imap://user@host/INBOX');
        let section = r.attributes[0].section;
        test.equal(section[0].value, 'REFERRAL');
        test.equal(section[1].value, 'imap://user@host/INBOX');
    });

// ---------------------------------------------------------------------------
// Additional error-path and edge-case coverage (E11, E12, E16, E17, E19-E22,
// E24, E27) plus the bare-"*" sequence and "~"-as-atom normalization paths.
// All use a non-status command (FETCH) so the TokenParser is actually invoked.
// ---------------------------------------------------------------------------

const expectParserError = (input, code) => test =>
    asyncWrapper(test, async test => {
        let err;
        try {
            await parser(input);
        } catch (e) {
            err = e;
        }
        if (!err) throw new Error('Expected parser to throw but it did not');
        test.equal(err.code, code);
    });

module.exports['Token Parser: E11: unexpected ] throws ParserError11'] = expectParserError('* 1 FETCH (])', 'ParserError11');
module.exports['Token Parser: E12: ~ not followed by { throws ParserError12'] = expectParserError('* 1 FETCH ~%', 'ParserError12');
module.exports['Token Parser: E16: \\* followed by char throws ParserError17'] = expectParserError('* 1 FETCH \\*x', 'ParserError17');
module.exports['Token Parser: E19: partial ending with . throws ParserError19'] = expectParserError('* 1 FETCH BODY[]<0.>', 'ParserError19');
module.exports['Token Parser: E20: partial leading . throws ParserError20'] = expectParserError('* 1 FETCH BODY[]<.>', 'ParserError20');
module.exports['Token Parser: E21: non-digit in partial throws ParserError21'] = expectParserError('* 1 FETCH BODY[]<0.x>', 'ParserError21');
module.exports['Token Parser: E22: invalid leading-zero partial throws ParserError22'] = expectParserError('* 1 FETCH BODY[]<00>', 'ParserError22');
module.exports['Token Parser: E24: literal prefix not followed by CRLF throws ParserError24'] = expectParserError('* 1 FETCH {3}xyz', 'ParserError24');
module.exports['Token Parser: E27: whitespace mid-sequence throws ParserError27'] = expectParserError('* 1 FETCH 1: 2', 'ParserError27');

module.exports['Token Parser: bare * is normalized to an ATOM'] = test =>
    asyncWrapper(test, async test => {
        let r = await parser('* 1 FETCH *');
        let last = r.attributes[r.attributes.length - 1];
        test.equal(last.type, 'ATOM');
        test.equal(last.value, '*');
    });

module.exports['Token Parser: ~ followed by atom chars becomes an ATOM'] = test =>
    asyncWrapper(test, async test => {
        let r = await parser('* 1 FETCH ~abc');
        let last = r.attributes[r.attributes.length - 1];
        test.equal(last.type, 'ATOM');
        test.equal(last.value, '~abc');
    });

module.exports['Token Parser: E16: invalid char in atom throws ParserError16'] = expectParserError('* 1 FETCH 3* 4', 'ParserError16');

module.exports['Token Parser: literal+ (non-synchronizing) marker is accepted'] = test =>
    asyncWrapper(test, async test => {
        // {3+} is a LITERAL+ prefix; with literalPlus enabled and no pre-parsed
        // literals, the parser reads the literal bytes inline.
        let r = await parser('* 1 FETCH {3+}\r\nabc', { literalPlus: true });
        let lit = r.attributes[r.attributes.length - 1];
        test.equal(lit.type, 'LITERAL');
        test.equal(lit.value.toString(), 'abc');
    });

module.exports['Token Parser: literal prefix terminated by bare LF is accepted'] = test =>
    asyncWrapper(test, async test => {
        let r = await parser('* 1 FETCH {3}\nabc');
        let lit = r.attributes[r.attributes.length - 1];
        test.equal(lit.type, 'LITERAL');
        test.equal(lit.value.toString(), 'abc');
    });
