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

import { encode, decode } from '../dist/index.js';
import { getEncoder } from '../dist/encoder.js';
import { toArray } from './.utils.ts';

// @ts-expect-error Access private property
const poolInit = getEncoder().pool as Uint8Array<ArrayBuffer>;

test('encode function', () => {
    expect(() =>
        encode(function () {
            //noop
        }),
    ).toThrow();
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode bigint', () => {
    expect(toArray(encode(1n))).toEqual(toArray(encode(1)));
    expect(toArray(encode(0x1234_5678_90ab_cdefn))).toEqual(
        toArray('L', 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef),
    );
    expect(() => encode(0x8234_5678_90ab_cdefn)).toThrow(/BigInt value out of range:/);
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode symbol', () => {
    expect(() => encode(Symbol('sym'))).toThrow();
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode undefined', () => {
    expect(toArray(encode(undefined))).toEqual(toArray('N'));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode null', () => {
    expect(toArray(encode(null))).toEqual(toArray('Z'));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode true', () => {
    expect(toArray(encode(true))).toEqual(toArray('T'));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode false', () => {
    expect(toArray(encode(false))).toEqual(toArray('F'));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode int8', () => {
    expect(toArray(encode(-1))).toEqual(toArray('i', 255));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode uint8', () => {
    expect(toArray(encode(200))).toEqual(toArray('U', 200));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode int16', () => {
    expect(toArray(encode(0x1234))).toEqual(toArray('I', 0x12, 0x34));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode int32', () => {
    expect(toArray(encode(0x1234_5678))).toEqual(toArray('l', 0x12, 0x34, 0x56, 0x78));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode float32', () => {
    expect(toArray(encode(1.003_906_25))).toEqual(toArray('d', 0x3f, 0x80, 0x80, 0x00));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode float32 (too large integer)', () => {
    expect(toArray(encode(2_147_483_648))).toEqual(toArray('d', 0x4f, 0x00, 0x00, 0x00));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode float64', () => {
    expect(toArray(encode(100_000.003_906_25))).toEqual(toArray('D', 0x40, 0xf8, 0x6a, 0x00, 0x10, 0x00, 0x00, 0x00));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode char', () => {
    expect(toArray(encode('a'))).toEqual(toArray('C', 'a'));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode char 127', () => {
    expect(toArray(encode('\u007F'))).toEqual(toArray('C', '\u007F'));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode char 257', () => {
    expect(toArray(encode('\u0123'))).toEqual(toArray('S', 'i', 2, 196, 163));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode char emoji', () => {
    expect(toArray(encode('💖'))).toEqual(toArray('S', 'i', 4, 240, 159, 146, 150));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode string', () => {
    expect(toArray(encode('ubjson'))).toEqual(toArray('S', 'i', 6, ...'ubjson'));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode string (len = 60)', () => {
    // 不生成 u8
    const str = 'ubjson'.repeat(10);
    expect(toArray(encode(str))).toEqual(toArray('S', 'i', str.length, ...str));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode string (len = 120)', () => {
    // 不生成 u8
    const str = 'ubjson'.repeat(20);
    expect(toArray(encode(str))).toEqual(toArray('S', 'i', str.length, ...str));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode string (len = 180)', () => {
    // 不生成 u8
    const str = 'ubjson'.repeat(30);
    expect(toArray(encode(str))).toEqual(toArray('S', 'I', 0, str.length, ...str));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode string (no encodeInto)', () => {
    // eslint-disable-next-line @typescript-eslint/unbound-method
    const { encodeInto } = TextEncoder.prototype;
    // @ts-expect-error 移除 encodeInto 以测试兼容性
    TextEncoder.prototype.encodeInto = undefined;
    expect(toArray(encode('ubjson'))).toEqual(toArray('S', 'i', 6, ...'ubjson'));

    TextEncoder.prototype.encodeInto = encodeInto;
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode array', () => {
    expect(toArray(encode([1, 2, 3, -1]))).toEqual(toArray('[', 'U', 1, 'U', 2, 'U', 3, 'i', 255, ']'));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode array (empty)', () => {
    expect(toArray(encode([]))).toEqual(toArray('[', ']'));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode array (undefined)', () => {
    expect(toArray(encode([undefined]))).toEqual(toArray('[', 'Z', ']'));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode array (function)', () => {
    expect(toArray(encode([() => 1]))).toEqual(toArray('[', 'Z', ']'));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode array (spares)', () => {
    const array = Array.from({ length: 3 });
    array[1] = true;
    expect(toArray(encode(array))).toEqual(toArray('[', 'Z', 'T', 'Z', ']'));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode array (mixed)', () => {
    expect(toArray(encode([1, 'a', true]))).toEqual(toArray('[', 'U', 1, 'C', 'a', 'T', ']'));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode array (int8)', () => {
    expect(toArray(encode([-1, 2, 3]))).toEqual(toArray('[', 'i', 255, 'U', 2, 'U', 3, ']'));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode array (int16)', () => {
    expect(toArray(encode([255, -1]))).toEqual(toArray('[', 'U', 0xff, 'i', 0xff, ']'));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode array (only null values)', () => {
    expect(toArray(encode([null, null, null]))).toEqual(toArray('[', 'Z', 'Z', 'Z', ']'));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode N-D array', () => {
    expect(
        decode(
            encode([
                [1, 2, 3],
                [4, 5, 6],
            ]),
        ),
    ).toEqual([
        [1, 2, 3],
        [4, 5, 6],
    ]);
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode array of objects', () => {
    expect(
        decode(
            encode([
                { a: 1, b: 2, c: 3 },
                { d: 4, e: 5, f: 6 },
            ]),
        ),
    ).toEqual([
        { a: 1, b: 2, c: 3 },
        { d: 4, e: 5, f: 6 },
    ]);
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode array of objects of arrays', () => {
    expect(
        decode(
            encode([
                { a: [1, 2], b: [3, 4] },
                { c: [5, 6], d: [7, 8] },
            ]),
        ),
    ).toEqual([
        { a: [1, 2], b: [3, 4] },
        { c: [5, 6], d: [7, 8] },
    ]);
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode array (int8 typed array)', () => {
    expect(toArray(encode(Int8Array.from([18, -2])))).toEqual(toArray('[', '$', 'i', '#', 'i', 2, 0x12, 0xfe));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode array (uint8 typed array)', () => {
    expect(toArray(encode(Uint8Array.from([18, 254])))).toEqual(toArray('[', '$', 'U', '#', 'i', 2, 0x12, 0xfe));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode array (buffer array)', () => {
    expect(toArray(encode(Buffer.from([18, -2])))).toEqual(toArray('[', '$', 'U', '#', 'i', 2, 0x12, 0xfe));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode array (int16 typed array)', () => {
    expect(toArray(encode(Int16Array.from([4660, -292])))).toEqual(
        toArray('[', '$', 'I', '#', 'i', 2, 0x12, 0x34, 0xfe, 0xdc),
    );
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode array (int32 typed array)', () => {
    expect(toArray(encode(Int32Array.from([305_419_896, -19_088_744])))).toEqual(
        toArray('[', '$', 'l', '#', 'i', 2, 0x12, 0x34, 0x56, 0x78, 0xfe, 0xdc, 0xba, 0x98),
    );
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode array (float32 typed array)', () => {
    expect(toArray(encode(Float32Array.from([0.25, 0.125])))).toEqual(
        toArray('[', '$', 'd', '#', 'i', 2, 0x3e, 0x80, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00),
    );
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode array (float64 typed array)', () => {
    expect(toArray(encode(Float64Array.from([0.25, 0.125])))).toEqual(
        toArray(
            '[',
            '$',
            'D',
            '#',
            'i',
            2,
            0x3f,
            0xd0,
            0x00,
            0x00,
            0x00,
            0x00,
            0x00,
            0x00,
            0x3f,
            0xc0,
            0x00,
            0x00,
            0x00,
            0x00,
            0x00,
            0x00,
        ),
    );
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode array (uint8clamped typed array)', () => {
    expect(() => encode(new Uint8ClampedArray())).toThrow();
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode array (uint16 typed array)', () => {
    expect(() => encode(new Uint16Array())).toThrow();
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode array (uint32 typed array)', () => {
    expect(() => encode(new Uint32Array())).toThrow();
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode array (uint64 typed array)', () => {
    expect(() => encode(new BigUint64Array())).toThrow();
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode array (int64 typed array)', () => {
    expect(toArray(encode(new BigInt64Array([1n, 0x1234_5678_1234_5678n])))).toEqual(
        toArray(
            '[',
            '$',
            'L',
            '#',
            'i',
            2,
            0x00,
            0x00,
            0x00,
            0x00,
            0x00,
            0x00,
            0x00,
            0x01,
            0x12,
            0x34,
            0x56,
            0x78,
            0x12,
            0x34,
            0x56,
            0x78,
        ),
    );
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode object', () => {
    expect(toArray(encode({ a: 1, b: 2, c: 3 }))).toEqual(
        toArray('{', 'i', 1, 'a', 'U', 1, 'i', 1, 'b', 'U', 2, 'i', 1, 'c', 'U', 3, '}'),
    );
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode object (keep order)', () => {
    expect(toArray(encode({ b: 2, a: 1, c: 3 }, { sortObjectKeys: true }))).toEqual(
        toArray('{', 'i', 1, 'a', 'U', 1, 'i', 1, 'b', 'U', 2, 'i', 1, 'c', 'U', 3, '}'),
    );
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode object (empty)', () => {
    expect(toArray(encode({}))).toEqual(toArray('{', '}'));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode object (empty key)', () => {
    expect(toArray(encode({ '': '' }))).toEqual(toArray('{', 'i', 0, 'S', 'i', 0, '}'));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode object (mixed)', () => {
    expect(toArray(encode({ a: 1, b: 'a', c: true }))).toEqual(
        toArray('{', 'i', 1, 'a', 'U', 1, 'i', 1, 'b', 'C', 'a', 'i', 1, 'c', 'T', '}'),
    );
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode object (only null values)', () => {
    expect(toArray(encode({ a: null, b: null, c: null }))).toEqual(
        toArray('{', 'i', 1, 'a', 'Z', 'i', 1, 'b', 'Z', 'i', 1, 'c', 'Z', '}'),
    );
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode object (skip prototype)', () => {
    const obj = Object.create({ a: 2, x: 'xx' }) as Record<string, unknown>;
    obj['a'] = 1;
    obj['b'] = 'a';
    obj['c'] = true;
    expect(toArray(encode(obj))).toEqual(
        toArray('{', 'i', 1, 'a', 'U', 1, 'i', 1, 'b', 'C', 'a', 'i', 1, 'c', 'T', '}'),
    );
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode object (skip symbol)', () => {
    const obj = { [Symbol()]: true, a: 1 };
    expect(toArray(encode(obj))).toEqual(toArray('{', 'i', 1, 'a', 'U', 1, '}'));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode object (skip non-enumerable)', () => {
    const obj = {};
    Object.defineProperty(obj, 'a', { value: 1, configurable: true, writable: true });
    expect(toArray(encode(obj))).toEqual(toArray('{', '}'));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode object (include getter)', () => {
    const obj = {};
    Object.defineProperty(obj, 'a', { get: () => 1, enumerable: true });
    expect(toArray(encode(obj))).toEqual(toArray('{', 'i', 1, 'a', 'U', 1, '}'));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode object (skip undefined)', () => {
    const obj = { a: undefined };
    expect(toArray(encode(obj))).toEqual(toArray('{', '}'));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode object (skip function)', () => {
    const obj = { a: 1, b: () => 1 };
    expect(toArray(encode(obj))).toEqual(toArray('{', 'i', 1, 'a', 'U', 1, '}'));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode object (include null)', () => {
    const obj = { a: null };
    expect(toArray(encode(obj))).toEqual(toArray('{', 'i', 1, 'a', 'Z', '}'));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode huge typed array (16K)', () => {
    const obj = new Uint8Array(16 * 1024);
    expect(toArray(encode(obj).slice(0, 8))).toEqual(toArray('[', '$', 'U', '#', 'I', 0x40, 0x00, 0));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode huge typed array (~128M)', () => {
    const obj = new Uint8Array(128 * 1024 * 1024 - 10);
    obj[0] = 0x58;
    expect(toArray(encode(obj).slice(0, 10))).toEqual(toArray('[', '$', 'U', '#', 'l', 0x7, 0xff, 0xff, 0xf6, 0x58));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode huge typed array (128M)', () => {
    const obj = new Uint8Array(128 * 1024 * 1024);
    obj[0] = 0x58;
    expect(toArray(encode(obj).slice(0, 10))).toEqual(toArray('[', '$', 'U', '#', 'l', 0x8, 0x00, 0x00, 0x00, 0x58));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode huge typed array (1G) [error]', () => {
    const obj = new Uint8Array(1 * 1024 * 1024 * 1024);
    expect(() => encode(obj)).toThrow(/Buffer has exceed max size/);
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode huge data (~128M)', () => {
    const obj = [new Uint8Array(128 * 1024 * 1024 - 20)];
    obj[0][0] = 0x12;
    obj[0][1] = 0x34;
    expect(toArray(encode(obj).slice(0, 12))).toEqual(
        toArray('[', '[', '$', 'U', '#', 'l', 0x7, 0xff, 0xff, 0xec, 0x12, 0x34),
    );
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode huge data (128M)', () => {
    const obj = [new Uint8Array(128 * 1024 * 1024)];
    obj[0][0] = 0x12;
    obj[0][1] = 0x34;
    expect(toArray(encode(obj).slice(0, 12))).toEqual(
        toArray('[', '[', '$', 'U', '#', 'l', 0x8, 0x00, 0x00, 0x00, 0x12, 0x34),
    );
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode huge data (256M)', () => {
    const obj = [new Uint8Array(128 * 1024 * 1024 - 9), new Uint8Array(128 * 1024 * 1024 - 9)];
    obj[0][0] = 0x12;
    obj[0][1] = 0x34;
    obj[1][0] = 0x56;
    obj[1][1] = 0x78;
    const encoded = encode(obj);
    expect(encoded.length).toBe(128 * 1024 * 1024 * 2 + 2);
    expect(toArray(encoded.slice(0, 12))).toEqual(
        toArray('[', '[', '$', 'U', '#', 'l', 0x7, 0xff, 0xff, 0xf7, 0x12, 0x34),
    );
    expect(toArray(encoded.slice(128 * 1024 * 1024 + 1, 128 * 1024 * 1024 + 1 + 11))).toEqual(
        toArray('[', '$', 'U', '#', 'l', 0x7, 0xff, 0xff, 0xf7, 0x56, 0x78),
    );
    expect(toArray(encoded.slice(-1))).toEqual(toArray(']'));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode custom toJSON', () => {
    const obj = {
        toJSON: () => ({ a: 1 }),
    };
    expect(toArray(encode(obj))).toEqual(toArray('{', 'i', 1, 'a', 'U', 1, '}'));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});

test('encode Date', () => {
    const obj = new Date(0);
    expect(toArray(encode(obj))).toEqual(toArray('S', 'i', 24, ...'1970-01-01T00:00:00.000Z'));
    // @ts-expect-error Access private property
    expect(getEncoder().pool).toBe(poolInit);
});
