/**
 * Tests from https://bitbucket.org/shelacek/ubjson
 */
import { buffer } from 'node:stream/consumers';
import { encode, encoder } from '../../dist/stream/index.js';
import { decode, encode as encodeRef } from '../../dist/index.js';
import { toArray } from '../.utils.ts';
import { Transform } from 'node:stream';

/**
 * 包装为 promise
 */
async function encodeAsync(value: unknown): Promise<Buffer<ArrayBuffer>> {
    return await buffer(encode(value));
}

test('encode function', async () => {
    await expect(async () =>
        encodeAsync(function () {
            // noop
        }),
    ).rejects.toThrow();
});

test('encode bigint', async () => {
    expect(toArray(await encodeAsync(1n))).toEqual(toArray('U', 1));
    expect(toArray(await encodeAsync(0x1234_5678_8765_4321n))).toEqual(
        toArray('L', 0x12, 0x34, 0x56, 0x78, 0x87, 0x65, 0x43, 0x21),
    );
    await expect(encodeAsync(0x8234_5678_90ab_cdefn)).rejects.toThrow(/BigInt value out of range:/);
});

test('encode symbol', async () => {
    await expect(async () => encodeAsync(Symbol('sym'))).rejects.toThrow();
});

test('encode undefined', async () => {
    expect(toArray(await encodeAsync(undefined))).toEqual(toArray('N'));
});

test('encode null', async () => {
    expect(toArray(await encodeAsync(null))).toEqual(toArray('Z'));
});

test('encode true', async () => {
    expect(toArray(await encodeAsync(true))).toEqual(toArray('T'));
});

test('encode false', async () => {
    expect(toArray(await encodeAsync(false))).toEqual(toArray('F'));
});

test('encode int8', async () => {
    expect(toArray(await encodeAsync(-1))).toEqual(toArray('i', 255));
});

test('encode uint8', async () => {
    expect(toArray(await encodeAsync(200))).toEqual(toArray('U', 200));
});

test('encode int16', async () => {
    expect(toArray(await encodeAsync(0x1234))).toEqual(toArray('I', 0x12, 0x34));
});

test('encode int32', async () => {
    expect(toArray(await encodeAsync(0x1234_5678))).toEqual(toArray('l', 0x12, 0x34, 0x56, 0x78));
});

test('encode float32', async () => {
    expect(toArray(await encodeAsync(1.003_906_25))).toEqual(toArray('d', 0x3f, 0x80, 0x80, 0x00));
});

test('encode float32 (too large integer)', async () => {
    expect(toArray(await encodeAsync(2_147_483_648))).toEqual(toArray('d', 0x4f, 0x00, 0x00, 0x00));
});

test('encode float64', async () => {
    expect(toArray(await encodeAsync(100_000.003_906_25))).toEqual(
        toArray('D', 0x40, 0xf8, 0x6a, 0x00, 0x10, 0x00, 0x00, 0x00),
    );
});

test('encode char', async () => {
    expect(toArray(await encodeAsync('a'))).toEqual(toArray('C', 'a'));
});

test('encode char 127', async () => {
    expect(toArray(await encodeAsync('\u007F'))).toEqual(toArray('C', '\u007F'));
});

test('encode char 257', async () => {
    expect(toArray(await encodeAsync('\u0123'))).toEqual(toArray('S', 'i', 2, 196, 163));
});

test('encode char emoji', async () => {
    expect(toArray(await encodeAsync('💖'))).toEqual(toArray('S', 'i', 4, 240, 159, 146, 150));
});

test('encode string', async () => {
    expect(toArray(await encodeAsync('ubjson'))).toEqual(toArray('S', 'i', 6, 'u', 'b', 'j', 's', 'o', 'n'));
});

test('encode huge string', async () => {
    const str = 'ubjson💖💖💖'.repeat(10000);
    expect(toArray(await encodeAsync(str))).toEqual(toArray(encodeRef(str)));
});

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

    TextEncoder.prototype.encodeInto = encodeInto;
});

test('encode array', async () => {
    expect(toArray(await encodeAsync([1, 2, 3, -1]))).toEqual(toArray('[', 'U', 1, 'U', 2, 'U', 3, 'i', 255, ']'));
});

test('encode array (empty)', async () => {
    expect(toArray(await encodeAsync([]))).toEqual(toArray('[', ']'));
});

test('encode array (undefined)', async () => {
    expect(toArray(await encodeAsync([undefined]))).toEqual(toArray('[', 'Z', ']'));
});

test('encode array (spares)', async () => {
    const array = Array.from({ length: 3 });
    array[1] = true;
    expect(toArray(await encodeAsync(array))).toEqual(toArray('[', 'Z', 'T', 'Z', ']'));
});

test('encode array (mixed)', async () => {
    expect(toArray(await encodeAsync([1, 'a', true]))).toEqual(toArray('[', 'U', 1, 'C', 'a', 'T', ']'));
});

test('encode array (int8)', async () => {
    expect(toArray(await encodeAsync([-1, 2, 3]))).toEqual(toArray('[', 'i', 255, 'U', 2, 'U', 3, ']'));
});

test('encode array (int16)', async () => {
    expect(toArray(await encodeAsync([255, -1]))).toEqual(toArray('[', 'U', 0xff, 'i', 0xff, ']'));
});

test('encode array (only null values)', async () => {
    expect(toArray(await encodeAsync([null, null, null]))).toEqual(toArray('[', 'Z', 'Z', 'Z', ']'));
});

test('encode N-D array', async () => {
    expect(
        decode(
            await encodeAsync([
                [1, 2, 3],
                [4, 5, 6],
            ]),
        ),
    ).toEqual([
        [1, 2, 3],
        [4, 5, 6],
    ]);
});

test('encode array of objects', async () => {
    expect(
        decode(
            await encodeAsync([
                { 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 },
    ]);
});

test('encode array of objects of arrays', async () => {
    expect(
        decode(
            await encodeAsync([
                { 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] },
    ]);
});

test('encode array (int8 typed array)', async () => {
    expect(toArray(await encodeAsync(Int8Array.from([18, -2])))).toEqual(
        toArray('[', '$', 'i', '#', 'i', 2, 0x12, 0xfe),
    );
});

test('encode array (uint8 typed array)', async () => {
    expect(toArray(await encodeAsync(Uint8Array.from([18, 254])))).toEqual(
        toArray('[', '$', 'U', '#', 'i', 2, 0x12, 0xfe),
    );
});

test('encode array (int16 typed array)', async () => {
    expect(toArray(await encodeAsync(Int16Array.from([4660, -292])))).toEqual(
        toArray('[', '$', 'I', '#', 'i', 2, 0x12, 0x34, 0xfe, 0xdc),
    );
});

test('encode array (int32 typed array)', async () => {
    expect(toArray(await encodeAsync(Int32Array.from([305_419_896, -19_088_744])))).toEqual(
        toArray('[', '$', 'l', '#', 'i', 2, 0x12, 0x34, 0x56, 0x78, 0xfe, 0xdc, 0xba, 0x98),
    );
});

test('encode array (float32 typed array)', async () => {
    expect(toArray(await encodeAsync(Float32Array.from([0.25, 0.125])))).toEqual(
        toArray('[', '$', 'd', '#', 'i', 2, 0x3e, 0x80, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00),
    );
});

test('encode array (float64 typed array)', async () => {
    expect(toArray(await encodeAsync(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,
        ),
    );
});

test('encode array (uint8clamped typed array)', async () => {
    await expect(async () => encodeAsync(new Uint8ClampedArray())).rejects.toThrow();
});

test('encode array (uint16 typed array)', async () => {
    await expect(async () => encodeAsync(new Uint16Array())).rejects.toThrow();
});

test('encode array (uint32 typed array)', async () => {
    await expect(async () => encodeAsync(new Uint32Array())).rejects.toThrow();
});

test('encode array (uint64 typed array)', async () => {
    await expect(async () => encodeAsync(new BigUint64Array())).rejects.toThrow();
});

test('encode array (int64 typed array)', async () => {
    expect(toArray(await encodeAsync(new BigInt64Array()))).toEqual(toArray('[', '$', 'L', '#', 'i', 0));
});

test('encode object', async () => {
    expect(toArray(await encodeAsync({ a: 1, b: 2, c: 3 }))).toEqual(
        toArray('{', 'i', 1, 'a', 'U', 1, 'i', 1, 'b', 'U', 2, 'i', 1, 'c', 'U', 3, '}'),
    );
});

test('encode object (empty)', async () => {
    expect(toArray(await encodeAsync({}))).toEqual(toArray('{', '}'));
});

test('encode object (empty key)', async () => {
    expect(toArray(await encodeAsync({ '': '' }))).toEqual(toArray('{', 'i', 0, 'S', 'i', 0, '}'));
});

test('encode object (mixed)', async () => {
    expect(toArray(await encodeAsync({ a: 1, b: 'a', c: true }))).toEqual(
        toArray('{', 'i', 1, 'a', 'U', 1, 'i', 1, 'b', 'C', 'a', 'i', 1, 'c', 'T', '}'),
    );
});

test('encode object (only null values)', async () => {
    expect(toArray(await encodeAsync({ a: null, b: null, c: null }))).toEqual(
        toArray('{', 'i', 1, 'a', 'Z', 'i', 1, 'b', 'Z', 'i', 1, 'c', 'Z', '}'),
    );
});

test('encode object (skip prototype)', async () => {
    const obj = Object.create({ a: 2, x: 'xx' }) as Record<string, unknown>;
    obj['a'] = 1;
    obj['b'] = 'a';
    obj['c'] = true;
    expect(toArray(await encodeAsync(obj))).toEqual(
        toArray('{', 'i', 1, 'a', 'U', 1, 'i', 1, 'b', 'C', 'a', 'i', 1, 'c', 'T', '}'),
    );
});

test('encode object (skip symbol)', async () => {
    const obj = { [Symbol()]: true, a: 1 };
    expect(toArray(await encodeAsync(obj))).toEqual(toArray('{', 'i', 1, 'a', 'U', 1, '}'));
});

test('encode object (skip non-enumerable)', async () => {
    const obj = {};
    Object.defineProperty(obj, 'a', { value: 1, configurable: true, writable: true });
    expect(toArray(await encodeAsync(obj))).toEqual(toArray('{', '}'));
});

test('encode object (include getter)', async () => {
    const obj = {};
    Object.defineProperty(obj, 'a', { get: () => 1, enumerable: true });
    expect(toArray(await encodeAsync(obj))).toEqual(toArray('{', 'i', 1, 'a', 'U', 1, '}'));
});

test('encode object (skip undefined)', async () => {
    const obj = { a: undefined };
    expect(toArray(await encodeAsync(obj))).toEqual(toArray('{', '}'));
});

test('encode object (skip function)', async () => {
    const obj = {
        a: () => {
            // noop
        },
    };
    expect(toArray(await encodeAsync(obj))).toEqual(toArray('{', '}'));
});

test('encode object (include null)', async () => {
    const obj = { a: null };
    expect(toArray(await encodeAsync(obj))).toEqual(toArray('{', 'i', 1, 'a', 'Z', '}'));
});

test('encode huge typed array (16K)', async () => {
    const obj = new Uint8Array(16 * 1024);
    expect(toArray((await encodeAsync(obj)).slice(0, 8))).toEqual(toArray('[', '$', 'U', '#', 'I', 0x40, 0x00, 0));
});

test('encode huge typed array (~128M)', async () => {
    const obj = new Uint8Array(128 * 1024 * 1024 - 10);
    expect(toArray((await encodeAsync(obj)).slice(0, 10))).toEqual(
        toArray('[', '$', 'U', '#', 'l', 0x7, 0xff, 0xff, 0xf6, 0),
    );
});

test('encode huge data (~128M)', async () => {
    const obj = [new Uint8Array(128 * 1024 * 1024 - 20)];
    expect(toArray((await encodeAsync(obj)).subarray(0, 11))).toEqual(
        toArray('[', '[', '$', 'U', '#', 'l', 0x7, 0xff, 0xff, 0xec, 0),
    );
});

test('encode stream', async () => {
    const stream = Transform.fromWeb(encoder() as never, { objectMode: true });
    stream.write(undefined);
    stream.write(true);
    stream.write(false);
    stream.end();
    const result = await buffer(stream);
    expect(toArray(result)).toEqual(toArray('N', 'T', 'F'));
});

test('encode in parallel', async () => {
    const obj = { a: 1, b: 'str' };
    const result = await Promise.all([encodeAsync(obj), encodeAsync(obj)]);
    expect(toArray(result[0])).toEqual(toArray('{', 'i', 1, 'a', 'U', 1, 'i', 1, 'b', 'S', 'i', 3, ...'str', '}'));
    expect(toArray(result[1])).toEqual(toArray('{', 'i', 1, 'a', 'U', 1, 'i', 1, 'b', 'S', 'i', 3, ...'str', '}'));
});
