import { describe, expect, it } from "vitest";
import { encode, FrameDecoder } from "./framing.js";

describe("framing", () => {
  it("encode/decode round-trips", () => {
    const msg = JSON.stringify({ jsonrpc: "2.0", method: "test", params: { a: 1 } });
    const encoded = encode(msg);
    const decoder = new FrameDecoder();
    const messages = decoder.decode(encoded);
    expect(messages).toHaveLength(1);
    expect(messages[0]).toBe(msg);
  });

  it("decoder reassembles a frame split across two chunks", () => {
    const msg = JSON.stringify({ jsonrpc: "2.0", method: "test" });
    const encoded = encode(msg);
    const mid = Math.floor(encoded.length / 2);
    const chunk1 = encoded.slice(0, mid);
    const chunk2 = encoded.slice(mid);

    const decoder = new FrameDecoder();
    const result1 = decoder.decode(chunk1);
    expect(result1).toHaveLength(0);

    const result2 = decoder.decode(chunk2);
    expect(result2).toHaveLength(1);
    expect(result2[0]).toBe(msg);
  });

  it("decoder yields two messages from one chunk", () => {
    const msg1 = JSON.stringify({ jsonrpc: "2.0", method: "a" });
    const msg2 = JSON.stringify({ jsonrpc: "2.0", method: "b" });
    const encoded1 = encode(msg1);
    const encoded2 = encode(msg2);

    const combined = new Uint8Array(encoded1.length + encoded2.length);
    combined.set(encoded1);
    combined.set(encoded2, encoded1.length);

    const decoder = new FrameDecoder();
    const messages = decoder.decode(combined);
    expect(messages).toHaveLength(2);
    expect(messages[0]).toBe(msg1);
    expect(messages[1]).toBe(msg2);
  });
});

describe("multi-byte UTF-8", () => {
  it("round-trips a message with multi-byte characters", () => {
    const msg = JSON.stringify({
      jsonrpc: "2.0",
      method: "textDocument/publishDiagnostics",
      params: { message: "Type '漢字' is not assignable to type 'number'. 🚫" },
    });
    const encoded = encode(msg);
    const decoder = new FrameDecoder();
    const messages = decoder.decode(encoded);
    expect(messages).toHaveLength(1);
    expect(messages[0]).toBe(msg);
  });

  it("reassembles a multi-byte message split at a character boundary", () => {
    // A message whose JSON body contains 3-byte UTF-8 characters (漢字).
    // We split the encoded frame so the boundary falls INSIDE a multi-byte
    // sequence — the old string-based decoder would corrupt this.
    const msg = JSON.stringify({
      jsonrpc: "2.0",
      method: "test",
      params: { text: "漢字テスト" },
    });
    const encoded = encode(msg);

    // Find a split point inside the body (skip the ASCII header).
    const headerEnd = encoded.indexOf(0x0d, 0); // first \r
    const bodyStart = headerEnd + 4; // skip \r\n\r\n
    // Split in the middle of the body — likely inside a multi-byte char.
    const splitPoint = bodyStart + Math.floor((encoded.length - bodyStart) / 2);
    const chunk1 = encoded.slice(0, splitPoint);
    const chunk2 = encoded.slice(splitPoint);

    const decoder = new FrameDecoder();
    expect(decoder.decode(chunk1)).toHaveLength(0); // incomplete
    const result = decoder.decode(chunk2);
    expect(result).toHaveLength(1);
    expect(result[0]).toBe(msg);
  });

  it("handles Content-Length in bytes (not characters)", () => {
    // Content-Length counts bytes. For multi-byte content, byte length
    // > character length. The decoder must slice by bytes, not chars.
    const unicode = "🎉分段測試";
    const msg = JSON.stringify({ jsonrpc: "2.0", method: "test", params: { text: unicode } });
    const encoded = encode(msg);

    // Verify the Content-Length header matches the byte length of the body.
    const headerStr = new TextDecoder().decode(encoded.slice(0, encoded.indexOf(0x0d)));
    const contentLengthMatch = /Content-Length:\s*(\d+)/i.exec(headerStr);
    expect(contentLengthMatch).not.toBeNull();
    const declaredLength = Number.parseInt(contentLengthMatch?.[1], 10);
    const bodyBytes = new TextEncoder().encode(msg);
    expect(declaredLength).toBe(bodyBytes.length);

    const decoder = new FrameDecoder();
    const messages = decoder.decode(encoded);
    expect(messages).toHaveLength(1);
    expect(messages[0]).toBe(msg);
  });

  it("reassembles two multi-byte messages from one chunk", () => {
    const msg1 = JSON.stringify({ jsonrpc: "2.0", method: "a", params: { t: "日本語" } });
    const msg2 = JSON.stringify({ jsonrpc: "2.0", method: "b", params: { t: "한국어" } });
    const encoded1 = encode(msg1);
    const encoded2 = encode(msg2);

    const combined = new Uint8Array(encoded1.length + encoded2.length);
    combined.set(encoded1);
    combined.set(encoded2, encoded1.length);

    const decoder = new FrameDecoder();
    const messages = decoder.decode(combined);
    expect(messages).toHaveLength(2);
    expect(messages[0]).toBe(msg1);
    expect(messages[1]).toBe(msg2);
  });

  it("reassembles a multi-byte message split across three chunks", () => {
    const msg = JSON.stringify({
      jsonrpc: "2.0",
      method: "test",
      params: { text: "𝕳𝖊𝖑𝖑𝖔, 世界! Привет! 🌍" },
    });
    const encoded = encode(msg);

    const third = Math.floor(encoded.length / 3);
    const decoder = new FrameDecoder();
    expect(decoder.decode(encoded.slice(0, third))).toHaveLength(0);
    expect(decoder.decode(encoded.slice(third, third * 2))).toHaveLength(0);
    const result = decoder.decode(encoded.slice(third * 2));
    expect(result).toHaveLength(1);
    expect(result[0]).toBe(msg);
  });
});
