/**
 * Unit tests for RelayClient — tests pure logic without real WebSocket connections.
 */
import { describe, it, expect } from "bun:test";

// ── TCP probe URL parsing (reimplemented from relay-client.ts) ──────────────

function parseTcpTarget(wsUrl: string): { hostname: string; port: number } | null {
  try {
    const httpUrl = wsUrl.replace(/^ws(s)?:\/\//, "http$1://");
    const parsed = new URL(httpUrl);
    const hostname = parsed.hostname;
    const port = parsed.port ? parseInt(parsed.port, 10) : 9750;
    return { hostname, port };
  } catch {
    return null;
  }
}

// ── Tests ───────────────────────────────────────────────────────────────────

describe("RelayClient", () => {
  // ── TCP probe URL parsing ───────────────────────────────────────────────

  describe("TCP probe URL parsing", () => {
    it("parses ws:// with explicit port", () => {
      const result = parseTcpTarget("ws://192.168.1.10:9750");
      expect(result).toEqual({ hostname: "192.168.1.10", port: 9750 });
    });

    it("parses wss:// with explicit port", () => {
      const result = parseTcpTarget("wss://relay.example.com:8443");
      expect(result).toEqual({ hostname: "relay.example.com", port: 8443 });
    });

    it("defaults to port 9750 when no port specified", () => {
      const result = parseTcpTarget("ws://relay.local");
      expect(result).toEqual({ hostname: "relay.local", port: 9750 });
    });

    it("parses IPv6 address with brackets", () => {
      const result = parseTcpTarget("ws://[::1]:9750");
      expect(result).not.toBeNull();
      expect(result!.hostname).toBe("[::1]"); // Bun's URL() keeps brackets for IPv6
      expect(result!.port).toBe(9750);
    });

    it("parses IPv6 with port", () => {
      const result = parseTcpTarget("ws://[2001:db8::1]:4567");
      expect(result).not.toBeNull();
      expect(result!.hostname).toBe("[2001:db8::1]");
      expect(result!.port).toBe(4567);
    });

    it("returns null for invalid URL", () => {
      const result = parseTcpTarget("not a url at all");
      expect(result).toBeNull();
    });

    it("handles ws:// path correctly", () => {
      const result = parseTcpTarget("ws://relay.example.com:9750/connect");
      expect(result).toEqual({ hostname: "relay.example.com", port: 9750 });
    });
  });

  // ── Machine tracking ──────────────────────────────────────────────────

  describe("machine tracking", () => {
    it("machineId is a UUID string", () => {
      const id = crypto.randomUUID();
      expect(typeof id).toBe("string");
      expect(id).toMatch(
        /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
      );
    });

    it("disconnect removes from map", () => {
      const machines = new Map<string, { status: string; cancelled: boolean }>();
      machines.set("m1", { status: "connected", cancelled: false });

      // Simulate disconnect
      const m = machines.get("m1")!;
      m.cancelled = true;
      m.status = "disconnected";
      machines.delete("m1");

      expect(machines.has("m1")).toBe(false);
    });

    it("removeMachine cleans up completely", () => {
      const machines = new Map<string, { status: string; ws: null }>();
      machines.set("m1", { status: "connected", ws: null });
      machines.set("m2", { status: "connecting", ws: null });

      // removeMachine = disconnect + delete
      machines.delete("m1");
      expect(machines.size).toBe(1);
      expect(machines.has("m1")).toBe(false);
      expect(machines.has("m2")).toBe(true);
    });

    it("listMachines returns all tracked machines", () => {
      const machines = new Map<
        string,
        { machineId: string; label: string; url: string; status: string; latencyMs: number | null }
      >();
      machines.set("m1", {
        machineId: "m1",
        label: "Dev Server",
        url: "ws://dev:9750",
        status: "connected",
        latencyMs: 12,
      });
      machines.set("m2", {
        machineId: "m2",
        label: "Staging",
        url: "ws://staging:9750",
        status: "disconnected",
        latencyMs: null,
      });

      const list = Array.from(machines.values());
      expect(list).toHaveLength(2);
      expect(list[0].label).toBe("Dev Server");
      expect(list[0].latencyMs).toBe(12);
      expect(list[1].latencyMs).toBeNull();
    });
  });

  // ── Status and event callbacks ────────────────────────────────────────

  describe("callback management", () => {
    it("stores and invokes event listeners", () => {
      const listeners: Array<(id: string, ev: unknown) => void> = [];
      const received: unknown[] = [];

      listeners.push((id, ev) => received.push({ id, ev }));
      listeners.push((id, ev) => received.push({ id, ev }));

      const event = { type: "pty_created", sessionId: "s1" };
      for (const cb of listeners) {
        cb("m1", event);
      }
      expect(received).toHaveLength(2);
    });

    it("survives callback errors", () => {
      const listeners: Array<(id: string) => void> = [];
      const results: string[] = [];

      listeners.push(() => {
        throw new Error("boom");
      });
      listeners.push((id) => results.push(id));

      for (const cb of listeners) {
        try {
          cb("m1");
        } catch {
          // swallow like RelayClient does
        }
      }
      expect(results).toEqual(["m1"]);
    });
  });

  // ── Reconnection backoff ──────────────────────────────────────────────

  describe("exponential backoff", () => {
    it("doubles delay up to maxDelay", () => {
      let delay = 1000;
      const maxDelay = 30000;
      const delays: number[] = [];

      for (let i = 0; i < 10; i++) {
        delays.push(delay);
        delay = Math.min(delay * 2, maxDelay);
      }

      expect(delays[0]).toBe(1000);
      expect(delays[1]).toBe(2000);
      expect(delays[4]).toBe(16000);
      expect(delays[5]).toBe(30000); // capped
      expect(delays[9]).toBe(30000); // stays capped
    });
  });
});
