/**
 * Tests from https://bitbucket.org/shelacek/ubjson
 */

import { decode, UnexpectedEof } from '../dist/index.js';
import { toBuffer } from './.utils.ts';

test('decode unsupported type', () => {
    expect(() => decode(toBuffer('!'))).toThrow();
});

test('decode undefined data', () => {
    // @ts-expect-error 不传参数
    expect(() => decode(undefined)).toThrow();
});

test('decode undefined', () => {
    expect(decode(toBuffer('N'))).toBeUndefined();
});

test('decode undefined (multiple noop)', () => {
    expect(decode(toBuffer('N', 'N', 'N'))).toBeUndefined();
});

test('decode undefined (empty buffer)', () => {
    expect(decode(toBuffer())).toBeUndefined();
});

test('decode null', () => {
    expect(decode(toBuffer('Z'))).toBeNull();
});

test('decode true', () => {
    expect(decode(toBuffer('T'))).toBe(true);
});

test('decode false', () => {
    expect(decode(toBuffer('F'))).toBe(false);
});

test('decode int8', () => {
    expect(decode(toBuffer('i', 100))).toBe(100);
});

test('decode uint8', () => {
    expect(decode(toBuffer('U', 200))).toBe(200);
});

test('decode int16', () => {
    expect(decode(toBuffer('I', 0x12, 0x34))).toBe(0x1234);
});

test('decode int32', () => {
    expect(decode(toBuffer('l', 0x12, 0x34, 0x56, 0x78))).toBe(0x1234_5678);
});

test('decode int64', () => {
    expect(decode(toBuffer('L', 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0))).toBe(0x1234_5678_9abc_def0n);
    expect(decode(toBuffer('L', 0x00, 0x04, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0))).toBe(0x04_5678_9abc_def0);
});

test('decode float32', () => {
    expect(decode(toBuffer('d', 0x3f, 0x80, 0x80, 0x00))).toBe(1.003_906_25);
});

test('decode float64', () => {
    expect(decode(toBuffer('D', 0x40, 0xf8, 0x6a, 0x00, 0x10, 0x00, 0x00, 0x00))).toBe(100_000.003_906_25);
});

test('decode int8 [unexpected eof]', () => {
    expect(() => decode(toBuffer('i'))).toThrow(UnexpectedEof);
});

test('decode uint8 [unexpected eof]', () => {
    expect(() => decode(toBuffer('U'))).toThrow(UnexpectedEof);
});

test('decode int16 [unexpected eof]', () => {
    expect(() => decode(toBuffer('I', 0x12))).toThrow(UnexpectedEof);
});

test('decode int32 [unexpected eof]', () => {
    expect(() => decode(toBuffer('l', 0x12, 0x34, 0x56))).toThrow(UnexpectedEof);
});

test('decode int64 [unexpected eof]', () => {
    expect(() => decode(toBuffer('L', 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde))).toThrow(UnexpectedEof);
});

test('decode float32 [unexpected eof]', () => {
    expect(() => decode(toBuffer('d', 0x3f, 0x80, 0x80))).toThrow(UnexpectedEof);
});

test('decode float64 [unexpected eof]', () => {
    expect(() => decode(toBuffer('D', 0x40, 0xf8, 0x6a, 0x00, 0x10, 0x00, 0x00))).toThrow(UnexpectedEof);
});

test('decode high-precision number [error]', () => {
    expect(() => decode(toBuffer('H', 'i', 3, '1', '.', '1'))).toThrow();
});

test('decode high-precision number [unexpected eof]', () => {
    expect(() => decode(toBuffer('H', 'i', 3, '1', '.'))).toThrow(UnexpectedEof);
});

test('decode char', () => {
    expect(decode(toBuffer('C', 'a'))).toBe('a');
});

test('decode char [unexpected eof]', () => {
    expect(() => decode(toBuffer('C'))).toThrow(UnexpectedEof);
});

test('decode string', () => {
    expect(decode(toBuffer('S', 'i', 6, 'u', 'b', 'j', 's', 'o', 'n'))).toBe('ubjson');
});

test('decode string (u8 len)', () => {
    expect(decode(toBuffer('S', 'U', 6, 'u', 'b', 'j', 's', 'o', 'n'))).toBe('ubjson');
});

test('decode empty string', () => {
    expect(decode(toBuffer('S', 'i', 0))).toBe('');
});

test('decode string (bad size) [error]', () => {
    expect(() => decode(toBuffer('S', 'i', 0xff, 'x'))).toThrow(/Invalid length/);
});

test('decode short string (unexpected eof) [error]', () => {
    expect(() => decode(toBuffer('S', 'i', 2, 'x'))).toThrow(UnexpectedEof);
});

test('decode long string (unexpected eof) [error]', () => {
    expect(() => decode(toBuffer('S', 'i', 20, 'x'))).toThrow(UnexpectedEof);
});

test('decode huge string (unexpected eof) [error]', () => {
    expect(() => decode(toBuffer('S', 'U', 200, 'x'))).toThrow(UnexpectedEof);
});

test('decode ascii string', () => {
    const header = toBuffer('S', 'I', 0x3f, 0xff);
    // eslint-disable-next-line unicorn/prefer-code-point
    const payload = new Uint8Array(0x3fff + header.byteLength).fill('a'.charCodeAt(0));
    payload.set(header);
    expect(decode(payload)).toBe('a'.repeat(0x3fff));
});

test('decode ascii string [huge]', () => {
    const header = toBuffer('S', 'I', 0x7f, 0xff);
    // eslint-disable-next-line unicorn/prefer-code-point
    const payload = new Uint8Array(0x7fff + header.byteLength).fill('a'.charCodeAt(0));
    payload.set(header);
    expect(decode(payload)).toBe('a'.repeat(0x7fff));
});

test('decode huge string', () => {
    expect(decode(toBuffer('S', 'l', 0x00, 0x00, 0x00, 6, 'u', 'b', 'j', 's', 'o', 'n'))).toBe('ubjson');
});

test('decode huge string [int64 length]', () => {
    expect(decode(toBuffer('S', 'L', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 'x'))).toBe('x');
    expect(() => decode(toBuffer('S', 'L', 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 'x'))).toThrow(
        /Invalid length/,
    );
});

test('decode array', () => {
    expect(decode(toBuffer('[', 'i', 1, 'i', 2, 'i', 3, ']'))).toEqual([1, 2, 3]);
});

test('decode array (with no-op)', () => {
    expect(decode(toBuffer('[', 'i', 1, 'N', 'i', 2, 'i', 3, 'N', ']'))).toEqual([1, 2, 3]);
});

test('decode array (empty)', () => {
    expect(decode(toBuffer('[', ']'))).toEqual([]);
});

test('decode array (empty, optimized)', () => {
    expect(decode(toBuffer('[', '#', 'i', 0))).toEqual([]);
});

test('decode array (empty, strongly typed, optimized)', () => {
    expect(decode(toBuffer('[', '$', 'i', '#', 'i', 0))).toEqual(new Int8Array(0));
});

test('decode array (mixed, optimized)', () => {
    expect(decode(toBuffer('[', '#', 'i', 3, 'i', 1, 'C', 'a', 'T'))).toEqual([1, 'a', true]);
});

test('decode array (strongly typed, optimized)', () => {
    expect(decode(toBuffer('[', '$', 'i', '#', 'i', 3, 1, 2, 3))).toEqual(new Int8Array([1, 2, 3]));
});

test('decode array (strongly typed, empty, optimized)', () => {
    expect(decode(toBuffer('[', '$', 'i', '#', 'i', 0))).toEqual(new Int8Array([]));
});

test('decode N-D array (strongly typed, optimized)', () => {
    expect(
        decode(toBuffer('[', '$', '[', '#', 'i', 2, '$', 'i', '#', 'i', 3, 1, 2, 3, '$', 'i', '#', 'i', 3, 4, 5, 6)),
    ).toEqual([new Int8Array([1, 2, 3]), new Int8Array([4, 5, 6])]);
});

test('decode array of objects (optimized)', () => {
    expect(
        decode(
            toBuffer(
                '[',
                '$',
                '{',
                '#',
                'i',
                2,
                '$',
                'i',
                '#',
                'i',
                3,
                'i',
                1,
                'a',
                1,
                'i',
                1,
                'b',
                2,
                'i',
                1,
                'c',
                3,
                '$',
                'i',
                '#',
                'i',
                3,
                'i',
                1,
                'd',
                4,
                'i',
                1,
                'e',
                5,
                'i',
                1,
                'f',
                6,
            ),
        ),
    ).toEqual([
        { a: 1, b: 2, c: 3 },
        { d: 4, e: 5, f: 6 },
    ]);
});

test('decode array of objects of arrays (optimized)', () => {
    expect(
        decode(
            toBuffer(
                '[',
                '$',
                '{',
                '#',
                'i',
                2,
                '$',
                '[',
                '#',
                'i',
                2,
                'i',
                1,
                'a',
                '$',
                'i',
                '#',
                'i',
                2,
                1,
                2,
                'i',
                1,
                'b',
                '$',
                'i',
                '#',
                'i',
                2,
                3,
                4,
                '$',
                '[',
                '#',
                'i',
                2,
                'i',
                1,
                'c',
                '$',
                'i',
                '#',
                'i',
                2,
                5,
                6,
                'i',
                1,
                'd',
                '$',
                'i',
                '#',
                'i',
                2,
                7,
                8,
            ),
        ),
    ).toEqual([
        { a: new Int8Array([1, 2]), b: new Int8Array([3, 4]) },
        { c: new Int8Array([5, 6]), d: new Int8Array([7, 8]) },
    ]);
});

test('decode array (strongly typed i8, unexpected eof, optimized)', () => {
    expect(() => decode(toBuffer('[', '$', 'i', '#', 'i', 3, 1, 2))).toThrow(UnexpectedEof);
});

test('decode array (strongly typed u8, unexpected eof, optimized)', () => {
    expect(() => decode(toBuffer('[', '$', 'U', '#', 'i', 3, 1, 2))).toThrow(UnexpectedEof);
});

test('decode array (strongly typed i16, unexpected eof, optimized)', () => {
    expect(() => decode(toBuffer('[', '$', 'I', '#', 'i', 3, 1, 2))).toThrow(UnexpectedEof);
});

test('decode array (strongly typed i32, unexpected eof, optimized)', () => {
    expect(() => decode(toBuffer('[', '$', 'l', '#', 'i', 3, 1, 2))).toThrow(UnexpectedEof);
});

test('decode array (strongly typed i64, unexpected eof, optimized)', () => {
    expect(() => decode(toBuffer('[', '$', 'L', '#', 'i', 3, 1, 2))).toThrow(UnexpectedEof);
});

test('decode array (strongly typed, invalid length value, optimized)', () => {
    expect(() => decode(toBuffer('[', '$', 'i', '#', 'i', -1))).toThrow();
});

test('decode array (strongly typed, invalid length type, optimized)', () => {
    expect(() => decode(toBuffer('[', '$', 'i', '#', 'C', '0'))).toThrow();
});

test('decode array (strongly typed, malformed, optimized)', () => {
    expect(() => decode(toBuffer('[', '$', 'i', 1, 2, 3, ']'))).toThrow();
});

test('decode array (only null values, optimized)', () => {
    expect(decode(toBuffer('[', '$', 'Z', '#', 'i', 3))).toEqual([null, null, null]);
});

test('decode array (only true values, optimized)', () => {
    expect(decode(toBuffer('[', '$', 'T', '#', 'i', 3))).toEqual([true, true, true]);
});

test('decode array (only false values, optimized)', () => {
    expect(decode(toBuffer('[', '$', 'F', '#', 'i', 3))).toEqual([false, false, false]);
});

test('decode array (int8, strongly typed, optimized) [use typed array]', () => {
    const actual = decode(toBuffer('[', '$', 'i', '#', 'i', 2, 0x12, 0xfe));
    expect(actual).toBeInstanceOf(Int8Array);
    expect(actual).toEqual(Int8Array.from([18, -2]));
});

test('decode array (uint8, strongly typed, optimized) [use typed array]', () => {
    const actual = decode(toBuffer('[', '$', 'U', '#', 'i', 2, 0x12, 0xfe));
    expect(actual).toBeInstanceOf(Uint8Array);
    expect(actual).toEqual(Uint8Array.from([18, 254]));
});

test('decode array (int16, strongly typed, optimized) [use typed array]', () => {
    const actual = decode(toBuffer('[', '$', 'I', '#', 'i', 2, 0x12, 0x34, 0xfe, 0xdc));
    expect(actual).toBeInstanceOf(Int16Array);
    expect(actual).toEqual(Int16Array.from([4660, -292]));
});

test('decode array (int32, strongly typed, optimized) [use typed array]', () => {
    const actual = decode(toBuffer('[', '$', 'l', '#', 'i', 2, 0x12, 0x34, 0x56, 0x78, 0xfe, 0xdc, 0xba, 0x98));
    expect(actual).toBeInstanceOf(Int32Array);
    expect(actual).toEqual(Int32Array.from([305_419_896, -19_088_744]));
});

test('decode array (int64, strongly typed, optimized) [use typed array]', () => {
    expect(decode(toBuffer('[', '$', 'L', '#', 'i', 1, 0x12, 0x34, 0x56, 0x78, 0xfe, 0xdc, 0xba, 0x98))).toEqual(
        new BigInt64Array([0x1234_5678_fedc_ba98n]),
    );
});

test('decode array (int64, strongly typed, optimized, empty) [use typed array]', () => {
    expect(decode(toBuffer('[', '$', 'L', '#', 'i', 0))).toEqual(new BigInt64Array([]));
});

test('decode array (float32, strongly typed, optimized) [use typed array]', () => {
    const actual = decode(toBuffer('[', '$', 'd', '#', 'i', 2, 0x3e, 0x80, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00));
    expect(actual).toBeInstanceOf(Float32Array);
    expect(actual).toEqual(Float32Array.from([0.25, 0.125]));
});

test('decode array (float64, strongly typed, optimized) [use typed array]', () => {
    const actual = decode(
        toBuffer(
            '[',
            '$',
            'D',
            '#',
            'i',
            2,
            0x3f,
            0xd0,
            0x00,
            0x00,
            0x00,
            0x00,
            0x00,
            0x00,
            0x3f,
            0xc0,
            0x00,
            0x00,
            0x00,
            0x00,
            0x00,
            0x00,
        ),
    );
    expect(actual).toBeInstanceOf(Float64Array);
    expect(actual).toEqual(Float64Array.from([0.25, 0.125]));
});

test('decode object', () => {
    expect(decode(toBuffer('{', 'i', 1, 'a', 'i', 1, 'i', 1, 'b', 'i', 2, 'i', 1, 'c', 'i', 3, '}'))).toEqual({
        a: 1,
        b: 2,
        c: 3,
    });
});

test('decode object (with no-op)', () => {
    expect(
        decode(
            toBuffer('N', '{', 'N', 'i', 1, 'a', 'i', 1, 'i', 1, 'b', 'N', 'i', 2, 'i', 1, 'c', 'i', 3, 'N', '}', 'N'),
        ),
    ).toEqual({ a: 1, b: 2, c: 3 });
});

test('decode array (empty, optimized)', () => {
    expect(decode(toBuffer('{', '#', 'i', 0))).toEqual({});
});

test('decode object (mixed, optimized)', () => {
    expect(decode(toBuffer('{', '#', 'i', 3, 'i', 1, 'a', 'i', 1, 'i', 1, 'b', 'C', 'a', 'i', 1, 'c', 'T'))).toEqual({
        a: 1,
        b: 'a',
        c: true,
    });
});

test('decode object (strongly typed, optimized)', () => {
    expect(decode(toBuffer('{', '$', 'i', '#', 'i', 3, 'i', 1, 'a', 1, 'i', 1, 'b', 2, 'i', 1, 'c', 3))).toEqual({
        a: 1,
        b: 2,
        c: 3,
    });
});

test('decode object (only null values, optimized)', () => {
    expect(decode(toBuffer('{', '$', 'Z', '#', 'i', 3, 'i', 1, 'a', 'i', 1, 'b', 'i', 1, 'c'))).toEqual({
        a: null,
        b: null,
        c: null,
    });
});

test('decode object (empty key)', () => {
    expect(decode(toBuffer('{', 'i', 0, 'T', '}'))).toEqual({
        '': true,
    });
});

test('decode object (empty key, optimized)', () => {
    expect(decode(toBuffer('{', '$', 'Z', '#', 'i', 3, 'i', 0, 'i', 1, 'a', 'i', 1, 'b'))).toEqual({
        '': null,
        a: null,
        b: null,
    });
});

test('decode (eof at marker)', () => {
    expect(() => decode(toBuffer('i'))).toThrow(UnexpectedEof);
    expect(() => decode(toBuffer('{'))).toThrow(UnexpectedEof);
    expect(() => decode(toBuffer('{', 'i', 1, 'a'))).toThrow(UnexpectedEof);
});

test('decode (eof at key)', () => {
    expect(() => decode(toBuffer('{', 'i', 2, 'a'))).toThrow(UnexpectedEof);
});

describe('proto poisoning attack', () => {
    it('should remove __proto__ key', () => {
        const obj = decode(
            toBuffer('{', 'i', 9, '__proto__', '{', 'i', 1, 'a', 'S', 'i', 3, 'abc', '}', '}'),
        ) as Record<string, unknown>;
        expect(Object.hasOwn(obj, '__proto__')).toBe(false);
        expect(obj['__proto__']).toBe(Object.prototype);
    });
    it('should allow __proto__ key', () => {
        const obj = decode(toBuffer('{', 'i', 9, '__proto__', '{', 'i', 1, 'a', 'S', 'i', 3, 'abc', '}', '}'), {
            protoAction: 'allow',
        }) as Record<string, unknown>;
        expect(Object.hasOwn(obj, '__proto__')).toBe(true);
        expect(obj['__proto__']).toEqual({ a: 'abc' });
    });
    it('should throw on __proto__ key', () => {
        expect(() =>
            decode(toBuffer('{', 'i', 9, '__proto__', '{', 'i', 1, 'a', 'S', 'i', 3, 'abc', '}', '}'), {
                protoAction: 'error',
            }),
        ).toThrow(`Unexpected "__proto__"`);
    });
});

describe('constructor poisoning attack', () => {
    it('should remove constructor key', () => {
        const obj = decode(toBuffer('{', 'i', 11, 'constructor', '{', 'i', 1, 'a', 'S', 'i', 3, 'abc', '}', '}'), {
            constructorAction: 'remove',
        }) as Record<string, unknown>;
        expect(Object.hasOwn(obj, 'constructor')).toBe(false);
        expect(obj.constructor).toBe(Object);
    });
    it('should allow constructor key', () => {
        const obj = decode(
            toBuffer('{', 'i', 11, 'constructor', '{', 'i', 1, 'a', 'S', 'i', 3, 'abc', '}', '}'),
        ) as Record<string, unknown>;
        expect(Object.hasOwn(obj, 'constructor')).toBe(true);
        expect(obj.constructor).toEqual({ a: 'abc' });
    });
    it('should throw on constructor key', () => {
        expect(() =>
            decode(toBuffer('{', 'i', 11, 'constructor', '{', 'i', 1, 'a', 'S', 'i', 3, 'abc', '}', '}'), {
                constructorAction: 'error',
            }),
        ).toThrow(`Unexpected "constructor"`);
    });
});
