/*
 * 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_ULTRA; 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);
  });

  test("oversized size hint returns exactly the real payload", () => {
    // Regression: the full hint-sized buffer used to be returned, with an
    // uninitialized (allocUnsafe) tail past the real decompressed size.
    const data = Buffer.from("oversized hint test".repeat(50));
    const compressed = zxc.compress(data);
    const decompressed = zxc.decompress(compressed, {
      size: data.length + 4096,
    });

    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);
    expect(zxc.LEVEL_ULTRA).toBe(7);
  });
});

// =============================================================================
// 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();
  });
});
