/*
 * ZXC - High-performance lossless compression
 *
 * Copyright (c) 2025-2026 Bertrand Lebonnois and contributors.
 * SPDX-License-Identifier: BSD-3-Clause
 */

'use strict';

const zxc = require('../lib/index');

// =============================================================================
// Roundtrip tests
// =============================================================================

describe('compress/decompress roundtrip', () => {
    const testCases = [
        { name: 'normal data',  data: Buffer.from('hello world'.repeat(10)) },
        { name: 'single byte',  data: Buffer.from('a') },
        { name: 'empty buffer', data: Buffer.alloc(0) },
        { name: 'large 10 MB',  data: Buffer.alloc(10_000_000, 0x42) },
    ];

    for (const { name, data } of testCases) {
        test(`roundtrip: ${name}`, () => {
            for (let level = zxc.LEVEL_FASTEST; level <= zxc.LEVEL_DENSITY; level++) {
                const compressed = zxc.compress(data, { level });
                const size = zxc.getDecompressedSize(compressed);
                const decompressed = zxc.decompress(compressed, { size });

                expect(decompressed.length).toBe(data.length);
                expect(Buffer.compare(decompressed, data)).toBe(0);
            }
        });
    }

    test('roundtrip with auto-size detection', () => {
        const data = Buffer.from('auto size detection test'.repeat(100));
        const compressed = zxc.compress(data);
        const decompressed = zxc.decompress(compressed);

        expect(decompressed.length).toBe(data.length);
        expect(Buffer.compare(decompressed, data)).toBe(0);
    });
});

// =============================================================================
// compressBound
// =============================================================================

describe('compressBound', () => {
    test('returns a value >= input size', () => {
        const bound = zxc.compressBound(1024);
        expect(bound).toBeGreaterThanOrEqual(1024);
    });

    test('returns 0-based bound for size 0', () => {
        const bound = zxc.compressBound(0);
        expect(bound).toBeGreaterThanOrEqual(0);
    });
});

// =============================================================================
// Corruption detection
// =============================================================================

describe('corruption detection', () => {
    test('detects corrupted data with checksum', () => {
        const data = Buffer.from('hello world'.repeat(10));
        const compressed = zxc.compress(data, { checksum: true });

        // Corrupt last byte
        const corrupted = Buffer.from(compressed);
        corrupted[corrupted.length - 1] ^= 0x01;

        expect(() => {
            zxc.decompress(corrupted, { size: data.length, checksum: true });
        }).toThrow();
    });

    test('throws on truncated compressed input', () => {
        expect(() => {
            zxc.decompress(Buffer.from([0x01, 0x02, 0x03]), { size: 100 });
        }).toThrow();
    });
});

// =============================================================================
// Invalid input types
// =============================================================================

describe('invalid input handling', () => {
    test('compress rejects non-Buffer', () => {
        expect(() => zxc.compress('string')).toThrow(TypeError);
        expect(() => zxc.compress(123)).toThrow(TypeError);
        expect(() => zxc.compress(null)).toThrow(TypeError);
    });

    test('decompress rejects non-Buffer', () => {
        expect(() => zxc.decompress('string')).toThrow(TypeError);
        expect(() => zxc.decompress(123)).toThrow(TypeError);
    });

    test('getDecompressedSize rejects non-Buffer', () => {
        expect(() => zxc.getDecompressedSize('string')).toThrow(TypeError);
    });

    test('compressBound rejects non-number', () => {
        expect(() => zxc.compressBound('abc')).toThrow(TypeError);
    });
});

// =============================================================================
// Constants
// =============================================================================

describe('constants', () => {
    test('compression levels are defined', () => {
        expect(zxc.LEVEL_FASTEST).toBe(1);
        expect(zxc.LEVEL_FAST).toBe(2);
        expect(zxc.LEVEL_DEFAULT).toBe(3);
        expect(zxc.LEVEL_BALANCED).toBe(4);
        expect(zxc.LEVEL_COMPACT).toBe(5);
        expect(zxc.LEVEL_DENSITY).toBe(6);
    });
});

// =============================================================================
// Error Handling
// =============================================================================

describe('error handling', () => {
    test('error constants are defined', () => {
        expect(zxc.ERROR_MEMORY).toBe(-1);
        expect(zxc.ERROR_DST_TOO_SMALL).toBe(-2);
        expect(zxc.ERROR_SRC_TOO_SMALL).toBe(-3);
        expect(zxc.ERROR_BAD_MAGIC).toBe(-4);
        expect(zxc.ERROR_BAD_VERSION).toBe(-5);
        expect(zxc.ERROR_BAD_HEADER).toBe(-6);
        expect(zxc.ERROR_BAD_CHECKSUM).toBe(-7);
        expect(zxc.ERROR_CORRUPT_DATA).toBe(-8);
        expect(zxc.ERROR_BAD_OFFSET).toBe(-9);
        expect(zxc.ERROR_OVERFLOW).toBe(-10);
        expect(zxc.ERROR_IO).toBe(-11);
        expect(zxc.ERROR_NULL_INPUT).toBe(-12);
        expect(zxc.ERROR_BAD_BLOCK_TYPE).toBe(-13);
        expect(zxc.ERROR_BAD_BLOCK_SIZE).toBe(-14);
    });

    test('errorName works', () => {
        expect(zxc.errorName(zxc.ERROR_DST_TOO_SMALL)).toBe('ZXC_ERROR_DST_TOO_SMALL');
        expect(zxc.errorName(-999)).toBe('ZXC_UNKNOWN_ERROR');
    });

    test('thrown errors have code property', () => {
        try {
            zxc.decompress(Buffer.from([0x01, 0x02, 0x03]), { size: 100 });
            throw new Error('Should have thrown');
        } catch (err) {
            expect(err.code).toBe(zxc.ERROR_NULL_INPUT);
            expect(err.message).toBe('ZXC_ERROR_NULL_INPUT');
        }
    });

    test('detects bad magic with correct code', () => {
        try {
            // Provide enough bytes (16 for header) but fake magic
            const fakeHeader = Buffer.alloc(32);
            fakeHeader.write('FAKE', 0, 'utf8');
            zxc.decompress(fakeHeader, { size: 100 });
            throw new Error('Should have thrown');
        } catch (err) {
            expect(err.code).toBe(zxc.ERROR_BAD_HEADER);
        }
    });
});

// =============================================================================
// Push Streaming API (zxc.CStream / zxc.DStream)
// =============================================================================

function pstreamRoundtrip(data, { level, checksum } = {}) {
    const cs = new zxc.CStream({ level, checksum });
    const compressedChunks = [];
    // Feed in 17-byte slices to exercise the buffering/state machine.
    const step = Math.max(1, Math.min(17, data.length));
    for (let i = 0; i < data.length; i += step) {
        compressedChunks.push(cs.compress(data.subarray(i, i + step)));
    }
    compressedChunks.push(cs.end());
    cs.close();
    const compressed = Buffer.concat(compressedChunks);

    const ds = new zxc.DStream({ checksum });
    const outChunks = [];
    const dstep = Math.max(1, Math.min(31, compressed.length));
    for (let i = 0; i < compressed.length; i += dstep) {
        outChunks.push(ds.decompress(compressed.subarray(i, i + dstep)));
    }
    expect(ds.finished()).toBe(true);
    ds.close();
    return Buffer.concat(outChunks);
}

describe('pstream roundtrip', () => {
    test('small payload', () => {
        const data = Buffer.from('Hello pstream! Round-trip through the Node.js push API.');
        expect(pstreamRoundtrip(data).equals(data)).toBe(true);
    });

    test('with checksum', () => {
        const data = Buffer.alloc(32 * 1024);
        for (let i = 0; i < data.length; i++) data[i] = i % 251;
        expect(pstreamRoundtrip(data, { checksum: true }).equals(data)).toBe(true);
    });

    test('multi-block (>512 KB)', () => {
        const data = Buffer.alloc(1536 * 1024);
        for (let i = 0; i < data.length; i++) data[i] = (i * 7) % 256;
        expect(pstreamRoundtrip(data).equals(data)).toBe(true);
    });
});

describe('pstream lifecycle', () => {
    test('size hints non-zero', () => {
        const cs = new zxc.CStream();
        expect(cs.inSize()).toBeGreaterThan(0);
        expect(cs.outSize()).toBeGreaterThan(0);
        cs.close();

        const ds = new zxc.DStream();
        expect(ds.inSize()).toBeGreaterThan(0);
        expect(ds.outSize()).toBeGreaterThan(0);
        expect(ds.finished()).toBe(false);
        ds.close();
    });

    test('use after close throws', () => {
        const cs = new zxc.CStream();
        cs.close();
        expect(() => cs.compress(Buffer.from('x'))).toThrow(/closed/);

        const ds = new zxc.DStream();
        ds.close();
        expect(() => ds.decompress(Buffer.from('x'))).toThrow(/closed/);
    });

    test('truncated stream not finished', () => {
        const cs = new zxc.CStream();
        const compressed = Buffer.concat([cs.compress(Buffer.from('hello '.repeat(1000))), cs.end()]);
        cs.close();

        const ds = new zxc.DStream();
        ds.decompress(compressed.subarray(0, compressed.length - 5));
        expect(ds.finished()).toBe(false);
        ds.close();
    });
});
