import { describe, test, expect } from "bun:test";
import { get } from "svelte/store";
import { parseServerMsgV2 } from "./protocol-v2";
import { StreamingClientV2, WINDOW_LIMIT } from "./streaming-v2";

// Mock WebSocket: this suite only needs the socket the client writes its requests to.
class MockWebSocket {
  public static readonly OPEN = 1;
  public static instances: MockWebSocket[] = [];
  public sent: string[] = [];
  public readyState: number = MockWebSocket.OPEN;
  public url: string;
  public onopen: (() => void) | null = null;
  public onmessage: ((event: { data: string }) => void) | null = null;
  public onerror: (() => void) | null = null;
  public onclose: (() => void) | null = null;
  constructor(url: string) {
    this.url = url;
    MockWebSocket.instances.push(this);
  }
  send(data: string) {
    this.sent.push(data);
  }
  close() {}
}

(globalThis as any).WebSocket = MockWebSocket;

/**
 * The same frames the daemon's own suite pins (`klbr-ipc/src/v2.rs`, `window_frames`), replayed through
 * the parser this console uses. Two implementations of one contract drift in silence otherwise: a renamed
 * field here is an empty session there, with nothing red on either side.
 */
describe("window frames shared with the daemon", () => {
  const frames = {
    started: '{"type":"sync_started","session_id":"default","from_seq":26,"to_seq":31,"earliest_seq":1,"fold_seq":25}',
    completedNoPage: '{"type":"sync_completed","session_id":"default","latest_seq":31}',
    completedWithFloor: '{"type":"sync_completed","session_id":"default","latest_seq":31,"earliest_seq":1}',
    event: '{"type":"event","record":{"seq":26,"session_id":"default","lane":"input","payload":{"source":"operator","text":"turn 361"},"created_ms":1010}}',
  };

  test("the daemon's range, floor and fold are read as the console reads them", () => {
    const started = parseServerMsgV2(frames.started);
    expect(started.type).toBe("sync_started");
    if (started.type !== "sync_started") throw new Error("unreachable");
    expect(started.from_seq).toBe(26);
    expect(started.to_seq).toBe(31);
    expect(started.earliest_seq).toBe(1);
    expect(started.fold_seq).toBe(25);

    const bare = parseServerMsgV2(frames.completedNoPage);
    if (bare.type !== "sync_completed") throw new Error("unreachable");
    expect(bare.earliest_seq).toBeUndefined();
    const floored = parseServerMsgV2(frames.completedWithFloor);
    if (floored.type !== "sync_completed") throw new Error("unreachable");
    expect(floored.earliest_seq).toBe(1);
  });

  test("a client fed those frames holds a window it can page and knows where it starts", () => {
    const sessionId = "daemon-frames-" + Math.random().toString(36).slice(2, 7);
    const client = new StreamingClientV2("ws://test", sessionId);
    client.handleServerMsg({ type: "sync_started", session_id: sessionId, from_seq: 26, to_seq: 31, earliest_seq: 1, fold_seq: 25 });
    client.handleServerMsg(parseServerMsgV2(frames.event));
    client.handleServerMsg(parseServerMsgV2(frames.completedWithFloor));

    const conn = get(client.connectionStore);
    // The floor is under the window, so there is history to fetch; the fold is above nothing the console
    // holds, so the window it has does start at the session's fold.
    expect(conn.hasEarlier).toBe(true);
    expect(conn.foldSeq).toBe(25);
    // The replay's range ends at the newest record, so the live cursor does too: everything the daemon
    // announced is what the console was sent.
    expect(conn.lastSeenSeq).toBe(31);
    expect(get(client.timelineStore).map((item) => item.seq)).toEqual([26]);
  });

  test("the console's own requests are the shapes the daemon parses", () => {
    const sessionId = "daemon-requests-" + Math.random().toString(36).slice(2, 7);
    const client = new StreamingClientV2("ws://test", sessionId, "daemon-requests");
    client.connect();
    const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1];
    socket.readyState = MockWebSocket.OPEN;
    socket.onopen?.();
    expect(JSON.parse(socket.sent[0] ?? "{}")).toEqual({ type: "subscribe", session_id: sessionId, after_seq: 0, limit: WINDOW_LIMIT });
  });
});
