/**
 * Contract / regression tests for the Tauri terminal adapter.
 *
 * The headline test guards the keystroke-ordering bug: `write` must serialise
 * per handle so bytes reach the backend in call order even when the underlying
 * `invoke`s resolve out of order (Tauri's IPC gives no ordering guarantee for
 * concurrent calls). Without serialisation, fast typing/pasting garbles the CLI
 * input.
 */

import { describe, it, expect, vi, beforeEach } from "vitest";

const invoke = vi.fn();
vi.mock("@tauri-apps/api/core", () => ({
  invoke: (...args: unknown[]) => invoke(...args),
  Channel: class {
    onmessage: ((c: number[]) => void) | null = null;
  },
}));

import { Channel } from "@tauri-apps/api/core";

import { makeTerminalHandle } from "./terminal";

const dec = new TextDecoder();

function handle() {
  return makeTerminalHandle("sess-1", new Channel<number[]>());
}

describe("TauriTerminalGateway write ordering (regression)", () => {
  beforeEach(() => invoke.mockReset());

  it("delivers bytes to the backend in call order despite out-of-order resolution", async () => {
    // Each `write_terminal` invoke resolves after a delay that is the INVERSE of
    // its arrival order, so the *first* call resolves last. If writes were
    // fire-and-forget (no chaining), the backend would observe them reversed.
    const arrivals: string[] = [];
    let order = 0;
    invoke.mockImplementation((cmd: string, payload: { request: { data: number[] } }) => {
      if (cmd !== "write_terminal") return Promise.resolve();
      const callIndex = order++;
      const delay = (4 - callIndex) * 10; // 1st call → longest delay
      return new Promise<void>((resolve) => {
        setTimeout(() => {
          arrivals.push(dec.decode(Uint8Array.from(payload.request.data)));
          resolve();
        }, delay);
      });
    });

    const h = handle();
    const enc = new TextEncoder();
    // Fire five writes back-to-back, as fast typing would.
    const writes = ["a", "b", "c", "d", "e"].map((c) => h.write(enc.encode(c)));
    await Promise.all(writes);

    // Backend stdin order MUST equal the order `write` was called.
    expect(arrivals).toEqual(["a", "b", "c", "d", "e"]);
  });

  it("a rejected write does not block subsequent writes (chain survives errors)", async () => {
    const arrivals: string[] = [];
    let call = 0;
    invoke.mockImplementation(
      (_cmd: string, payload?: { request: { data: number[] } }) => {
        // Only `write_terminal` calls carry a payload; ignore any bare probe.
        if (!payload) return Promise.resolve();
        call++;
        if (call === 1) return Promise.reject(new Error("boom"));
        arrivals.push(dec.decode(Uint8Array.from(payload.request.data)));
        return Promise.resolve();
      },
    );

    const h = handle();
    const enc = new TextEncoder();
    const first = h.write(enc.encode("x")); // rejects
    const second = h.write(enc.encode("y")); // must still run, in order

    // The rejected write surfaces its error to its own caller…
    let firstErr: unknown;
    await first.catch((e) => {
      firstErr = e;
    });
    expect(firstErr).toBeInstanceOf(Error);
    // …but the chain survives, so the next write still reaches the backend.
    await second;
    expect(arrivals).toEqual(["y"]);
  });

  it("write nests sessionId + data array inside the request DTO", async () => {
    invoke.mockResolvedValue(undefined);
    const h = handle();
    await h.write(Uint8Array.from([104, 105])); // "hi"
    expect(invoke).toHaveBeenCalledWith("write_terminal", {
      request: { sessionId: "sess-1", data: [104, 105] },
    });
  });

  it("resize forwards rows/cols inside the request DTO", async () => {
    invoke.mockResolvedValue(undefined);
    const h = handle();
    await h.resize(40, 120);
    expect(invoke).toHaveBeenCalledWith("resize_terminal", {
      request: { sessionId: "sess-1", rows: 40, cols: 120 },
    });
  });
});
