/**
 * L3 — behavioural tests for {@link MockTerminalGateway}: it greets on open,
 * echoes writes back through `onData` (translating CR→CRLF like a cooked
 * terminal), behaves on resize/close, and mints distinct session ids.
 */
import { describe, it, expect, vi } from "vitest";

import { MockTerminalGateway } from "./index";

/** Flush queued microtasks (the greeting is delivered via `queueMicrotask`). */
async function flushMicrotasks() {
  await Promise.resolve();
  await Promise.resolve();
}

function decode(chunks: Uint8Array[]): string {
  const dec = new TextDecoder();
  return chunks.map((c) => dec.decode(c)).join("");
}

describe("MockTerminalGateway", () => {
  it("greets on open with the cwd via onData", async () => {
    const gw = new MockTerminalGateway();
    const received: Uint8Array[] = [];
    await gw.openTerminal({ cwd: "/work/dir", rows: 24, cols: 80 }, (b) =>
      received.push(b),
    );

    await flushMicrotasks();
    expect(decode(received)).toContain("/work/dir");
  });

  it("returns a handle with a sessionId and write/resize/close methods", async () => {
    const gw = new MockTerminalGateway();
    const handle = await gw.openTerminal(
      { cwd: "/c", rows: 24, cols: 80 },
      () => {},
    );
    expect(typeof handle.sessionId).toBe("string");
    expect(typeof handle.write).toBe("function");
    expect(typeof handle.resize).toBe("function");
    expect(typeof handle.close).toBe("function");
  });

  it("echoes written bytes back through onData", async () => {
    const gw = new MockTerminalGateway();
    const received: Uint8Array[] = [];
    const handle = await gw.openTerminal(
      { cwd: "/c", rows: 24, cols: 80 },
      (b) => received.push(b),
    );
    await flushMicrotasks();
    received.length = 0; // drop the greeting

    await handle.write(new TextEncoder().encode("hi"));
    expect(decode(received)).toBe("hi");
  });

  it("translates a CR keystroke to CRLF on echo", async () => {
    const gw = new MockTerminalGateway();
    const received: Uint8Array[] = [];
    const handle = await gw.openTerminal(
      { cwd: "/c", rows: 24, cols: 80 },
      (b) => received.push(b),
    );
    await flushMicrotasks();
    received.length = 0;

    // 0x0d == CR
    await handle.write(Uint8Array.from([0x61, 0x0d])); // 'a' + CR
    const out = received.flatMap((c) => Array.from(c));
    expect(out).toEqual([0x61, 0x0d, 0x0a]); // 'a', CR, LF
  });

  it("stops echoing once closed", async () => {
    const gw = new MockTerminalGateway();
    const received: Uint8Array[] = [];
    const handle = await gw.openTerminal(
      { cwd: "/c", rows: 24, cols: 80 },
      (b) => received.push(b),
    );
    await flushMicrotasks();
    received.length = 0;

    await handle.close();
    await handle.write(new TextEncoder().encode("ignored"));
    expect(received).toHaveLength(0);
  });

  it("resize resolves without throwing", async () => {
    const gw = new MockTerminalGateway();
    const handle = await gw.openTerminal(
      { cwd: "/c", rows: 24, cols: 80 },
      () => {},
    );
    await expect(handle.resize(40, 120)).resolves.toBeUndefined();
  });

  it("mints distinct session ids for two opens", async () => {
    const gw = new MockTerminalGateway();
    const a = await gw.openTerminal({ cwd: "/c", rows: 24, cols: 80 }, () => {});
    const b = await gw.openTerminal({ cwd: "/c", rows: 24, cols: 80 }, () => {});
    expect(a.sessionId).not.toEqual(b.sessionId);
  });

  it("does not echo before any write (only the greeting)", async () => {
    const gw = new MockTerminalGateway();
    const onData = vi.fn();
    await gw.openTerminal({ cwd: "/c", rows: 24, cols: 80 }, onData);
    await flushMicrotasks();
    // Exactly one delivery so far: the greeting.
    expect(onData).toHaveBeenCalledTimes(1);
  });

  it("detach stops delivery to the old view but keeps the session alive", async () => {
    const gw = new MockTerminalGateway();
    const first: Uint8Array[] = [];
    const handle = await gw.openTerminal(
      { cwd: "/c", rows: 24, cols: 80 },
      (b) => first.push(b),
    );
    await flushMicrotasks();
    first.length = 0;

    handle.detach();
    // Output produced after detach must NOT reach the detached view.
    await handle.write(new TextEncoder().encode("after-detach"));
    expect(first).toHaveLength(0);

    // But the session is still alive: reattach succeeds.
    await expect(
      gw.reattach(handle.sessionId, () => {}),
    ).resolves.toBeDefined();
  });

  it("reattach replays scrollback and resumes live output", async () => {
    const gw = new MockTerminalGateway();
    const handle = await gw.openTerminal(
      { cwd: "/work", rows: 24, cols: 80 },
      () => {},
    );
    await flushMicrotasks();
    await handle.write(new TextEncoder().encode("typed"));
    handle.detach();

    const fresh: Uint8Array[] = [];
    const { handle: h2, scrollback } = await gw.reattach(
      handle.sessionId,
      (b) => fresh.push(b),
    );
    // Scrollback carries the prior greeting + echoed input.
    const sb = decode([scrollback]);
    expect(sb).toContain("/work");
    expect(sb).toContain("typed");
    // New output now flows to the re-attached view.
    await h2.write(new TextEncoder().encode("more"));
    expect(decode(fresh)).toBe("more");
  });

  it("reattach to a closed session rejects (PTY is gone)", async () => {
    const gw = new MockTerminalGateway();
    const handle = await gw.openTerminal(
      { cwd: "/c", rows: 24, cols: 80 },
      () => {},
    );
    await handle.close();
    await expect(gw.reattach(handle.sessionId, () => {})).rejects.toMatchObject({
      code: "NOT_FOUND",
    });
  });

  it("closeTerminal(sessionId) kills the PTY so a later reattach fails (Bug #3)", async () => {
    const gw = new MockTerminalGateway();
    const handle = await gw.openTerminal(
      { cwd: "/c", rows: 24, cols: 80 },
      () => {},
    );
    await gw.closeTerminal(handle.sessionId);
    await expect(gw.reattach(handle.sessionId, () => {})).rejects.toMatchObject({
      code: "NOT_FOUND",
    });
  });

  it("closeTerminal on an unknown session is a no-op (best-effort)", async () => {
    const gw = new MockTerminalGateway();
    await expect(gw.closeTerminal("does-not-exist")).resolves.toBeUndefined();
  });
});
