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

import { GatewayClient } from "@/lib/gateway/GatewayClient";

type MockClientOptions = {
  token?: unknown;
  onHello?: (hello: unknown) => void;
  onClose?: (info: { code: number; reason: string }) => void;
};

type MockInstance = {
  opts: MockClientOptions;
  stopped: boolean;
};

let instances: MockInstance[] = [];

vi.mock("@/lib/gateway/openclaw/GatewayBrowserClient", () => {
  class GatewayBrowserClient {
    connected = false;
    private index: number;

    constructor(opts: MockClientOptions) {
      this.index = instances.length;
      instances.push({ opts, stopped: false });
    }

    start() {
      this.connected = true;
    }

    stop() {
      this.connected = false;
      instances[this.index]!.stopped = true;
    }

    request() {
      return Promise.resolve({});
    }
  }

  return { GatewayBrowserClient };
});

afterEach(() => {
  instances = [];
});

describe("GatewayClient reconnect recovery", () => {
  it("allows a fresh connect after unexpected close", async () => {
    const client = new GatewayClient();
    const statuses: string[] = [];
    client.onStatus((status) => statuses.push(status));

    const firstConnect = client.connect({
      gatewayUrl: "ws://example.invalid",
      token: "old-token",
    });
    const first = instances[0];
    if (!first) throw new Error("Expected first GatewayBrowserClient instance");

    const onHelloFirst = first.opts.onHello;
    const onCloseFirst = first.opts.onClose;
    if (!onHelloFirst || !onCloseFirst) {
      throw new Error("Expected first instance callbacks");
    }

    onHelloFirst({});
    await expect(firstConnect).resolves.toBeUndefined();

    onCloseFirst({ code: 1012, reason: "upstream closed" });

    expect(first.stopped).toBe(true);
    expect(statuses.at(-1)).toBe("disconnected");

    const secondConnect = client.connect({
      gatewayUrl: "ws://example.invalid",
      token: "new-token",
    });
    const second = instances[1];
    if (!second) throw new Error("Expected second GatewayBrowserClient instance");

    expect(second.opts.token).toBe("new-token");

    const onHelloSecond = second.opts.onHello;
    if (!onHelloSecond) {
      throw new Error("Expected second instance onHello callback");
    }

    onHelloSecond({});
    await expect(secondConnect).resolves.toBeUndefined();

    expect(statuses.at(-1)).toBe("connected");
  });

  it("ignores stale onClose callbacks from old instances", async () => {
    const client = new GatewayClient();
    const statuses: string[] = [];
    client.onStatus((status) => statuses.push(status));

    const firstConnect = client.connect({ gatewayUrl: "ws://example.invalid" });
    const first = instances[0];
    if (!first) throw new Error("Expected first GatewayBrowserClient instance");

    const onHelloFirst = first.opts.onHello;
    const onCloseFirst = first.opts.onClose;
    if (!onHelloFirst || !onCloseFirst) {
      throw new Error("Expected first instance callbacks");
    }

    onHelloFirst({});
    await firstConnect;

    onCloseFirst({ code: 1012, reason: "upstream closed" });

    const secondConnect = client.connect({ gatewayUrl: "ws://example.invalid" });
    const second = instances[1];
    if (!second) throw new Error("Expected second GatewayBrowserClient instance");

    const onHelloSecond = second.opts.onHello;
    if (!onHelloSecond) {
      throw new Error("Expected second instance onHello callback");
    }

    onHelloSecond({});
    await secondConnect;

    const statusCountBeforeStaleClose = statuses.length;
    onCloseFirst({ code: 1012, reason: "late stale close" });

    expect(statuses.length).toBe(statusCountBeforeStaleClose);
    expect(statuses.at(-1)).toBe("connected");
  });
});
