'use strict';

const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const libmime = require('libmime');
const mimeFuncs = require('../../lib/mime-funcs');

describe('Mime-Funcs Tests', () => {
    describe('#isPlainText', () => {
        it('should detect plain text', () => {
            assert.strictEqual(mimeFuncs.isPlainText('abc'), true);
            assert.strictEqual(mimeFuncs.isPlainText('abc\x02'), false);
            assert.strictEqual(mimeFuncs.isPlainText('abcõ'), false);
        });
        it('should return true', () => {
            assert.strictEqual(mimeFuncs.isPlainText('az09\t\r\n~!?'), true);
        });

        it('should return false on low bits', () => {
            assert.strictEqual(mimeFuncs.isPlainText('az09\n\x08!?'), false);
        });

        it('should not treat tabs and line breaks as plaintext in a parameter value', () => {
            // a header parameter can not carry these, unlike a header value or a body
            assert.strictEqual(mimeFuncs.isPlainText('az09\t!?', true), false);
            assert.strictEqual(mimeFuncs.isPlainText('az09\r!?', true), false);
            assert.strictEqual(mimeFuncs.isPlainText('az09\n!?', true), false);
            assert.strictEqual(mimeFuncs.isPlainText('az09!?', true), true);
        });

        it('should not treat DEL as plaintext in a parameter value', () => {
            // DEL is neither a token character nor qtext, so it can go neither bare nor quoted.
            // a header value or a body can still carry it, so only the parameter variant rejects it
            assert.strictEqual(mimeFuncs.isPlainText('az09\x7f!?', true), false);
            assert.strictEqual(mimeFuncs.isPlainText('az09\x7f!?'), true);
        });

        it('should return false on high bits', () => {
            assert.strictEqual(mimeFuncs.isPlainText('az09\nõ!?'), false);
        });
    });

    describe('#hasLongerLines', () => {
        it('should detect longer lines', () => {
            assert.strictEqual(mimeFuncs.hasLongerLines('abc\ndef', 5), false);
            assert.strictEqual(mimeFuncs.hasLongerLines('juf\nabcdef\nghi', 5), true);
        });
    });

    describe('#encodeWord', () => {
        it('should encode quoted-printable', () => {
            assert.strictEqual('=?UTF-8?Q?See_on_=C3=B5hin_test?=', mimeFuncs.encodeWord('See on õhin test'));
        });

        it('should encode base64', () => {
            assert.strictEqual('=?UTF-8?B?U2VlIG9uIMO1aGluIHRlc3Q=?=', mimeFuncs.encodeWord('See on õhin test', 'B'));
        });
    });

    describe('#encodeWords', () => {
        it('should encode Ascii range', () => {
            let input1 = 'метель" вьюга',
                input2 = "метель'вьюга",
                input3 = 'Verão você vai adorar!',
                output1 = '=?UTF-8?Q?=D0=BC=D0=B5=D1=82=D0=B5=D0=BB=D1=8C=22_?= =?UTF-8?Q?=D0=B2=D1=8C=D1=8E=D0=B3=D0=B0?=',
                output2 = '=?UTF-8?Q?=D0=BC=D0=B5=D1=82=D0=B5=D0=BB=D1=8C=27?= =?UTF-8?Q?=D0=B2=D1=8C=D1=8E=D0=B3=D0=B0?=',
                output3 = '=?UTF-8?Q?Ver=C3=A3o_voc=C3=AA?= vai adorar!',
                output4 = '=?UTF-8?Q?Ver=C3=A3o_voc=C3=AA_vai_adorar!?=';

            assert.strictEqual(mimeFuncs.encodeWords(input1, 'Q', 52), output1);
            assert.strictEqual(mimeFuncs.encodeWords(input2, 'Q', 52), output2);
            assert.strictEqual(mimeFuncs.encodeWords(input3, 'Q', 52), output3);
            assert.strictEqual(mimeFuncs.encodeWords(input3, 'Q', 52, true), output4);
        });

        it('should split QP on maxLength', () => {
            let inputStr = 'Jõgeva Jõgeva Jõgeva mugeva Jõgeva Jõgeva Jõgeva Jõgeva Jõgeva',
                outputStr =
                    '=?UTF-8?Q?J=C3=B5geva_?= =?UTF-8?Q?J=C3=B5geva_?= =?UTF-8?Q?J=C3=B5geva_?= =?UTF-8?Q?mugeva_J?= =?UTF-8?Q?=C3=B5geva_J?= =?UTF-8?Q?=C3=B5geva_J?= =?UTF-8?Q?=C3=B5geva_J?= =?UTF-8?Q?=C3=B5geva_J?= =?UTF-8?Q?=C3=B5geva?=',
                encoded = mimeFuncs.encodeWords(inputStr, 'Q', 16);

            assert.strictEqual(outputStr, encoded);
            assert.strictEqual(inputStr, libmime.decodeWords(encoded));
        });

        it('should split base64 on maxLength', () => {
            let inputStr = 'õõõõõ õõõõõ õõõõõ mugeva õõõõõ õõõõõ õõõõõ õõõõõ Jõgeva',
                outputStr =
                    '=?UTF-8?B?w7XDtcO1w7XDtSA=?= =?UTF-8?B?w7XDtcO1w7XDtSA=?= =?UTF-8?B?w7XDtcO1w7XDtSBt?= =?UTF-8?B?dWdldmEgw7XDtcO1?= =?UTF-8?B?w7XDtSDDtcO1w7U=?= =?UTF-8?B?w7XDtSDDtcO1w7U=?= =?UTF-8?B?w7XDtSDDtcO1w7U=?= =?UTF-8?B?w7XDtSBKw7VnZXZh?=',
                encoded = mimeFuncs.encodeWords(inputStr, 'B', 30);

            assert.strictEqual(outputStr, encoded);
            assert.strictEqual(inputStr, libmime.decodeWords(encoded));
        });

        it('should not split a non-emoji surrogate pair when chunking base64', () => {
            // U+20000 (CJK Extension B) uses a high surrogate outside the emoji range
            let inputStr = '一' + '\u{20000}'.repeat(7),
                encoded = mimeFuncs.encodeWord(inputStr, 'B', 52);

            assert.strictEqual(inputStr, libmime.decodeWords(encoded));
        });

        it('should not let an unpaired surrogate consume the next character', () => {
            // the lone leading surrogate is lost either way, but it must not swallow the leading
            // half of the valid pair behind it and take that down with it
            let inputStr = '\ud800' + '\u{20000}'.repeat(4),
                encoded = mimeFuncs.encodeWord(inputStr, 'B', 20);

            assert.strictEqual('�' + '\u{20000}'.repeat(4), libmime.decodeWords(encoded));
        });
    });

    describe('#buildHeaderParam', () => {
        it('should return unmodified', () => {
            assert.deepStrictEqual(
                [
                    {
                        key: 'title',
                        value: 'this is just a title'
                    }
                ],
                mimeFuncs.buildHeaderParam('title', 'this is just a title', 500)
            );
        });

        it('should encode and split ascii', () => {
            assert.deepStrictEqual(
                [
                    {
                        key: 'title*0',
                        value: 'this '
                    },
                    {
                        key: 'title*1',
                        value: 'is ju'
                    },
                    {
                        key: 'title*2',
                        value: 'st a '
                    },
                    {
                        key: 'title*3',
                        value: 'title'
                    }
                ],
                mimeFuncs.buildHeaderParam('title', 'this is just a title', 5)
            );
        });

        it('should encode double byte unicode characters', () => {
            assert.deepStrictEqual(
                [
                    {
                        key: 'title*0*',
                        value: "utf-8''Unicode%20title%20%F0%9F%98%8A"
                    }
                ],
                mimeFuncs.buildHeaderParam('title', 'Unicode title 😊', 50)
            );
        });

        it('should percent encode an unpaired surrogate instead of passing it through raw', () => {
            // encodeURIComponent throws on an unpaired surrogate. the fallback used to return the
            // value with only the safe characters stripped out, leaving ; and = raw in a parameter
            // value that is emitted unquoted, so the filename could inject a parameter of its own
            assert.deepStrictEqual(
                [
                    {
                        key: 'filename*0*',
                        value: "utf-8''a%EF%BF%BD%3Bfilename%EF%BF%BD%3Devil.exe"
                    }
                ],
                mimeFuncs.buildHeaderParam('filename', 'a\ud800;filename\ud800=evil.exe', 100)
            );
        });

        it('should not let an unpaired surrogate consume a valid pair behind it', () => {
            // the lone leading surrogate becomes U+FFFD, the astral character after it must survive
            assert.deepStrictEqual(
                [
                    {
                        key: 'filename*0*',
                        value: "utf-8''%EF%BF%BD%F0%A0%80%80"
                    }
                ],
                mimeFuncs.buildHeaderParam('filename', '\ud800\u{20000}', 50)
            );
        });

        it('should star the continuation key of a line restarted for an encoded char', () => {
            // when the encoded char does not fit, the plain line is pushed and a new one starts
            // holding that char. the new line used to keep the old unencoded flag, so it got an
            // unstarred key and a receiver read the percent escapes back as literal text
            assert.deepStrictEqual(
                [
                    {
                        key: 'filename*0*',
                        value: "utf-8''xxxxxxxxxxxx"
                    },
                    {
                        key: 'filename*1',
                        value: 'xxxxxxxxxxxxxx'
                    },
                    {
                        key: 'filename*2*',
                        value: '%C3%BC'
                    }
                ],
                mimeFuncs.buildHeaderParam('filename', 'x'.repeat(26) + 'ü', 20)
            );
        });

        it('should encode and split unicode', () => {
            assert.deepStrictEqual(
                [
                    {
                        key: 'title*0*',
                        value: "utf-8''this%20is%20"
                    },
                    {
                        key: 'title*1',
                        value: 'just a title '
                    },
                    {
                        key: 'title*2*',
                        value: '%C3%B5%C3%A4%C3%B6'
                    },
                    {
                        key: 'title*3*',
                        value: '%C3%BC'
                    }
                ],
                mimeFuncs.buildHeaderParam('title', 'this is just a title õäöü', 20)
            );
        });

        it('should encode and split filename with dashes', () => {
            assert.deepStrictEqual(
                [
                    {
                        key: 'filename*0*',
                        value: "utf-8''%C6%94------%C6%94------%C6%94------%C6%94"
                    },
                    {
                        key: 'filename*1*',
                        value: '------%C6%94------%C6%94------%C6%94------.pdf'
                    }
                ],
                mimeFuncs.buildHeaderParam('filename', 'Ɣ------Ɣ------Ɣ------Ɣ------Ɣ------Ɣ------Ɣ------.pdf', 50)
            );
        });

        it('should encode and decode', () => {
            let input =
                'Lorěm ipsum doloř siť amet, háš peřpetua compřéhenšam at, ei nám modó soleát éxpétěndá! Boňorum vocibůs dignisšim pro ad, ea sensibus efficiendi intellegam ius. Ad nam aperiam delicata voluptaria, vix nobis luptatum ea, ců úsú graeco viďiššě ňusqúam. ';
            let headerLine =
                'content-disposition: attachment; ' +
                mimeFuncs
                    .buildHeaderParam('filename', input, 50)
                    .map(item => item.key + '="' + item.value + '"')
                    .join('; ');
            let parsedHeader = mimeFuncs.parseHeaderValue(headerLine);
            assert.strictEqual(input, libmime.decodeWords(parsedHeader.params.filename));
        });
    });

    describe('#parseHeaderValue', () => {
        it('should handle default value only', () => {
            let str = 'text/plain',
                obj = {
                    value: 'text/plain',
                    params: {}
                };

            assert.deepStrictEqual(mimeFuncs.parseHeaderValue(str), obj);
        });

        it('should handle unquoted params', () => {
            let str = 'text/plain; CHARSET= UTF-8; format=flowed;',
                obj = {
                    value: 'text/plain',
                    params: {
                        charset: 'UTF-8',
                        format: 'flowed'
                    }
                };

            assert.deepStrictEqual(mimeFuncs.parseHeaderValue(str), obj);
        });

        it('should handle quoted params', () => {
            let str = 'text/plain; filename= ";;;\\""; format=flowed;',
                obj = {
                    value: 'text/plain',
                    params: {
                        filename: ';;;"',
                        format: 'flowed'
                    }
                };

            assert.deepStrictEqual(mimeFuncs.parseHeaderValue(str), obj);
        });

        it('should handle multi line values', () => {
            let str =
                    'text/plain; single_encoded*="UTF-8\'\'%C3%95%C3%84%C3%96%C3%9C";\n' +
                    " multi_encoded*0*=UTF-8''%C3%96%C3%9C;\n" +
                    ' multi_encoded*1*=%C3%95%C3%84;\n' +
                    ' no_charset*0=OA;\n' +
                    ' no_charset*1=OU;\n' +
                    " invalid*=utf-8'' _?'=%ab",
                obj = {
                    value: 'text/plain',
                    params: {
                        single_encoded: '=?UTF-8?Q?=C3=95=C3=84=C3=96=C3=9C?=',
                        multi_encoded: '=?UTF-8?Q?=C3=96=C3=9C=C3=95=C3=84?=',
                        no_charset: 'OAOU',
                        invalid: "=?utf-8?Q?_=5f=3f'=3d=ab?="
                    }
                };

            assert.deepStrictEqual(mimeFuncs.parseHeaderValue(str), obj);
        });

        it('should handle params only', () => {
            let str = '; CHARSET= UTF-8; format=flowed;',
                obj = {
                    value: '',
                    params: {
                        charset: 'UTF-8',
                        format: 'flowed'
                    }
                };

            assert.deepStrictEqual(mimeFuncs.parseHeaderValue(str), obj);
        });
    });

    describe('#_buildHeaderValue', () => {
        it('should build header value', () => {
            assert.strictEqual(
                mimeFuncs.buildHeaderValue({
                    value: 'test'
                }),
                'test'
            );
            assert.strictEqual(
                mimeFuncs.buildHeaderValue({
                    value: 'test',
                    params: {
                        a: 'b'
                    }
                }),
                'test; a=b'
            );
            assert.strictEqual(
                mimeFuncs.buildHeaderValue({
                    value: 'test',
                    params: {
                        a: ';'
                    }
                }),
                'test; a=";"'
            );
            assert.strictEqual(
                mimeFuncs.buildHeaderValue({
                    value: 'test',
                    params: {
                        a: ';"'
                    }
                }),
                "test; a*0*=utf-8''%3B%22"
            );
            assert.strictEqual(
                mimeFuncs.buildHeaderValue({
                    value: 'test',
                    params: {
                        a: 'x\ty'
                    }
                }),
                "test; a*0*=utf-8''x%09y"
            );
            assert.strictEqual(
                mimeFuncs.buildHeaderValue({
                    value: 'test',
                    params: {
                        a: 'x\r\ny'
                    }
                }),
                "test; a*0*=utf-8''x%0D%0Ay"
            );
            assert.strictEqual(
                mimeFuncs.buildHeaderValue({
                    value: 'test',
                    params: {
                        a: 'b',
                        c: 'd'
                    }
                }),
                'test; a=b; c=d'
            );
        });

        it('should strip control chars from the value token', () => {
            // the part ahead of the parameters is a token, there is no quoting construct
            // around it that a control char could be escaped into
            assert.strictEqual(
                mimeFuncs.buildHeaderValue({
                    value: 'text/a\x01b\x7fc',
                    params: {
                        charset: 'utf-8'
                    }
                }),
                'text/abc; charset=utf-8'
            );
        });

        it('should strip control chars from a parameter name', () => {
            // the name is a token as well and it is emitted without any quoting around it
            assert.strictEqual(
                mimeFuncs.buildHeaderValue({
                    value: 'text/plain',
                    params: {
                        'ab\x01c\x7fd': '1'
                    }
                }),
                'text/plain; abcd=1'
            );
            // a line break in a name used to end the header and start a new one
            assert.strictEqual(
                mimeFuncs.buildHeaderValue({
                    value: 'text/plain',
                    params: {
                        'a\r\nX-Injected: y': '1'
                    }
                }),
                'text/plain; aX-Injected: y=1'
            );
        });

        it('should handle unicode filename', () => {
            assert.strictEqual(
                mimeFuncs.buildHeaderValue({
                    value: 'test',
                    params: {
                        a: 'b',
                        filename: '😁😂 *\'%()<>@,;:\\"[]?=😃😄zzz😊õäöü😓.pdf'
                    }
                }),
                "test; a=b; filename*0*=utf-8''%F0%9F%98%81%F0%9F%98%82%20%2A%27%25%28%29; filename*1*=%3C%3E%40%2C%3B%3A%5C%22%5B%5D%3F%3D%F0%9F%98%83; filename*2*=%F0%9F%98%84zzz%F0%9F%98%8A%C3%B5%C3%A4%C3%B6; filename*3*=%C3%BC%F0%9F%98%93.pdf"
            );
        });

        it('should handle dashed filename', () => {
            assert.strictEqual(
                mimeFuncs.buildHeaderValue({
                    value: 'test',
                    params: {
                        filename: 'Ɣ------Ɣ------Ɣ------Ɣ------Ɣ------Ɣ------Ɣ------.pdf'
                    }
                }),
                "test; filename*0*=utf-8''%C6%94------%C6%94------%C6%94------%C6%94; filename*1*=------%C6%94------%C6%94------%C6%94------.pdf"
            );
        });

        it('should split emoji filename', () => {
            assert.strictEqual(
                mimeFuncs.buildHeaderValue({
                    value: 'test',
                    params: {
                        a: 'b',
                        filename: 'Jõge-vaŽJõge-vaŽJõge-vaŽ.pdf'
                    }
                }),
                "test; a=b; filename*0*=utf-8''J%C3%B5ge-va%C5%BDJ%C3%B5ge-va%C5%BDJ; filename*1*=%C3%B5ge-va%C5%BD.pdf"
            );
        });

        it('should quote filename with spaces', () => {
            assert.strictEqual(
                mimeFuncs.buildHeaderValue({
                    value: 'test',
                    params: {
                        filename: 'document a.pdf'
                    }
                }),
                'test; filename="document a.pdf"'
            );
        });

        // For exhaustive list of special characters
        // Refer: https://www.w3.org/Protocols/rfc1341/4_Content-Type.html
        it('should quote filename with special characters', () => {
            // The case of browser downloads when we download multiple files with same name.
            assert.strictEqual(
                mimeFuncs.buildHeaderValue({
                    value: 'test',
                    params: {
                        filename: 'receipt(3).pdf'
                    }
                }),
                'test; filename="receipt(3).pdf"'
            );

            // For headers which are comma separated as in case of multiple from members elements.
            assert.strictEqual(
                mimeFuncs.buildHeaderValue({
                    value: 'test',
                    params: {
                        filename: 'jack,jill.pdf'
                    }
                }),
                'test; filename="jack,jill.pdf"'
            );

            // Added support for some more special characters
            let correctString = [
                'space="x y"',
                'small_bracket_open="x(y"',
                'small_bracket_close="x)y"',
                'angle_bracket_open="x<y"',
                'angle_bracket_close="x>y"',
                'at_the_rate="x@y"',
                'semicolon="x;y"',
                'colon="x:y"',
                'back_slash="x\\\\y"',
                'single_quote="x\'y"',
                "double_quotes*0*=utf-8''x%22y",
                'forward_slash="x/y"',
                'big_bracket_open="x[y"',
                'big_bracket_close="x]y"',
                'question_mark="x?y"',
                'comma="x,y"',
                'equals="x=y"',
                'negative_in_mid=x-y',
                'negative_in_start="-x"'
            ].join('; ');

            assert.strictEqual(
                mimeFuncs.buildHeaderValue({
                    value: 'test',
                    params: {
                        space: 'x y',
                        small_bracket_open: 'x(y',
                        small_bracket_close: 'x)y',
                        angle_bracket_open: 'x<y',
                        angle_bracket_close: 'x>y',
                        at_the_rate: 'x@y',
                        semicolon: 'x;y',
                        colon: 'x:y',
                        back_slash: 'x\\y',
                        single_quote: "x'y",
                        double_quotes: 'x"y',
                        forward_slash: 'x/y',
                        big_bracket_open: 'x[y',
                        big_bracket_close: 'x]y',
                        question_mark: 'x?y',
                        comma: 'x,y',
                        equals: 'x=y',
                        negative_in_mid: 'x-y',
                        negative_in_start: '-x'
                    }
                }),
                'test; ' + correctString
            );
        });
    });

    describe('#foldLines', () => {
        it('should Fold long header line', () => {
            let inputStr = 'Subject: Testin command line kirja õkva kakva mõni tõnis kõllas põllas tõllas rõllas jušla kušla tušla musla',
                outputStr =
                    'Subject: Testin command line kirja\r\n' +
                    ' =?UTF-8?Q?=C3=B5kva_kakva_m=C3=B5ni_t=C3=B5nis_k?=\r\n' +
                    ' =?UTF-8?Q?=C3=B5llas_p=C3=B5llas_t=C3=B5llas_r?=\r\n' +
                    ' =?UTF-8?Q?=C3=B5llas_ju=C5=A1la_ku=C5=A1la_tu?= =?UTF-8?Q?=C5=A1la?= musla',
                encodedHeaderLine = mimeFuncs.encodeWords(inputStr, 'Q', 52);

            assert.strictEqual(outputStr, mimeFuncs.foldLines(encodedHeaderLine, 76));
        });

        it('should Fold flowed text', () => {
            let inputStr =
                    'Testin command line kirja õkva kakva mõni tõnis kõllas põllas tõllas rõllas jušla kušla tušla musla Testin command line kirja õkva kakva mõni tõnis kõllas põllas tõllas rõllas jušla kušla tušla musla',
                outputStr =
                    'Testin command line kirja õkva kakva mõni tõnis kõllas põllas tõllas rõllas \r\n' +
                    'jušla kušla tušla musla Testin command line kirja õkva kakva mõni tõnis \r\n' +
                    'kõllas põllas tõllas rõllas jušla kušla tušla musla';

            assert.strictEqual(outputStr, mimeFuncs.foldLines(inputStr, 76, true));
        });

        it('should fold one long line', () => {
            let inputStr =
                    'Subject: =?UTF-8?Q?=CB=86=C2=B8=C3=81=C3=8C=C3=93=C4=B1=C3=8F=CB=87=C3=81=C3=9B^=C2=B8\\=C3=81=C4=B1=CB=86=C3=8C=C3=81=C3=9B=C3=98^\\=CB=9C=C3=9B=CB=9D=E2=84=A2=CB=87=C4=B1=C3=93=C2=B8^\\=CB=9C=EF=AC=81^\\=C2=B7\\=CB=9C=C3=98^=C2=A3=CB=9C#=EF=AC=81^\\=C2=A3=EF=AC=81^\\=C2=A3=EF=AC=81^\\?=',
                outputStr =
                    'Subject:\r\n =?UTF-8?Q?=CB=86=C2=B8=C3=81=C3=8C=C3=93=C4=B1=C3=8F=CB=87=C3=81=C3=9B^=C2=B8\\=C3=81=C4=B1=CB=86=C3=8C=C3=81=C3=9B=C3=98^\\=CB=9C=C3=9B=CB=9D=E2=84=A2=CB=87=C4=B1=C3=93=C2=B8^\\=CB=9C=EF=AC=81^\\=C2=B7\\=CB=9C=C3=98^=C2=A3=CB=9C#=EF=AC=81^\\=C2=A3=EF=AC=81^\\=C2=A3=EF=AC=81^\\?=';

            assert.strictEqual(outputStr, mimeFuncs.foldLines(inputStr, 76));
        });
    });
});
