import os from "node:os";
import path from "node:path";
import { afterAll, beforeAll, describe, expect, test } from "vitest";
import {
  BACKEND_GATEWAY_CLIENT,
  connectReq,
  CONTROL_UI_CLIENT,
  ConnectErrorDetailCodes,
  getFreePort,
  openWs,
  originForPort,
  rpcReq,
  restoreGatewayToken,
  startGatewayServer,
  testState,
} from "./server.auth.shared.js";

function expectAuthErrorDetails(params: {
  details: unknown;
  expectedCode: string;
  canRetryWithDeviceToken?: boolean;
  recommendedNextStep?: string;
}) {
  const details = params.details as
    | {
        code?: string;
        canRetryWithDeviceToken?: boolean;
        recommendedNextStep?: string;
      }
    | undefined;
  expect(details?.code).toBe(params.expectedCode);
  if (params.canRetryWithDeviceToken !== undefined) {
    expect(details?.canRetryWithDeviceToken).toBe(params.canRetryWithDeviceToken);
  }
  if (params.recommendedNextStep !== undefined) {
    expect(details?.recommendedNextStep).toBe(params.recommendedNextStep);
  }
}

async function expectSharedOperatorScopesCleared(
  port: number,
  auth: { token?: string; password?: string },
) {
  const ws = await openWs(port);
  try {
    const res = await connectReq(ws, {
      ...auth,
      scopes: ["operator.admin"],
      device: null,
    });
    expect(res.ok).toBe(true);

    const adminRes = await rpcReq(ws, "set-heartbeats", { enabled: false });
    expect(adminRes.ok).toBe(false);
    expect(adminRes.error?.message ?? "").toContain("missing scope");
  } finally {
    ws.close();
  }
}

describe("gateway auth compatibility baseline", () => {
  describe("token mode", () => {
    let server: Awaited<ReturnType<typeof startGatewayServer>>;
    let port = 0;
    let prevToken: string | undefined;

    beforeAll(async () => {
      prevToken = process.env.OPENCLAW_GATEWAY_TOKEN;
      testState.gatewayAuth = { mode: "token", token: "secret" };
      process.env.OPENCLAW_GATEWAY_TOKEN = "secret";
      port = await getFreePort();
      server = await startGatewayServer(port);
    });

    afterAll(async () => {
      await server.close();
      restoreGatewayToken(prevToken);
    });

    test("keeps valid shared-token connect behavior unchanged", async () => {
      const ws = await openWs(port);
      try {
        const res = await connectReq(ws, { token: "secret" });
        expect(res.ok).toBe(true);
      } finally {
        ws.close();
      }
    });

    test("clears requested scopes for shared-token operator connects without device identity", async () => {
      await expectSharedOperatorScopesCleared(port, { token: "secret" });
    });

    test("returns stable token-missing details for control ui without token", async () => {
      const ws = await openWs(port, { origin: originForPort(port) });
      try {
        const res = await connectReq(ws, {
          skipDefaultAuth: true,
          client: { ...CONTROL_UI_CLIENT },
        });
        expect(res.ok).toBe(false);
        expect(res.error?.message ?? "").toContain("Control UI settings");
        expectAuthErrorDetails({
          details: res.error?.details,
          expectedCode: ConnectErrorDetailCodes.AUTH_TOKEN_MISSING,
          canRetryWithDeviceToken: false,
          recommendedNextStep: "update_auth_configuration",
        });
      } finally {
        ws.close();
      }
    });

    test("provides one-time retry hint for shared token mismatches", async () => {
      const ws = await openWs(port);
      try {
        const res = await connectReq(ws, { token: "wrong" });
        expect(res.ok).toBe(false);
        expect(res.error?.message ?? "").toContain("gateway token mismatch");
        expectAuthErrorDetails({
          details: res.error?.details,
          expectedCode: ConnectErrorDetailCodes.AUTH_TOKEN_MISMATCH,
          canRetryWithDeviceToken: true,
          recommendedNextStep: "retry_with_device_token",
        });
      } finally {
        ws.close();
      }
    });

    test("keeps explicit device token mismatch semantics stable", async () => {
      const ws = await openWs(port);
      try {
        const res = await connectReq(ws, {
          skipDefaultAuth: true,
          deviceToken: "not-a-valid-device-token",
        });
        expect(res.ok).toBe(false);
        expect(res.error?.message ?? "").toContain("device token mismatch");
        expectAuthErrorDetails({
          details: res.error?.details,
          expectedCode: ConnectErrorDetailCodes.AUTH_DEVICE_TOKEN_MISMATCH,
          canRetryWithDeviceToken: false,
          recommendedNextStep: "update_auth_credentials",
        });
      } finally {
        ws.close();
      }
    });

    test("keeps local backend device-token reconnects out of pairing", async () => {
      const identityPath = path.join(
        os.tmpdir(),
        `openclaw-backend-device-${process.pid}-${port}.json`,
      );
      const { loadOrCreateDeviceIdentity, publicKeyRawBase64UrlFromPem } =
        await import("../infra/device-identity.js");
      const { approveDevicePairing, requestDevicePairing, rotateDeviceToken } =
        await import("../infra/device-pairing.js");

      const identity = loadOrCreateDeviceIdentity(identityPath);
      const pending = await requestDevicePairing({
        deviceId: identity.deviceId,
        publicKey: publicKeyRawBase64UrlFromPem(identity.publicKeyPem),
        clientId: BACKEND_GATEWAY_CLIENT.id,
        clientMode: BACKEND_GATEWAY_CLIENT.mode,
        role: "operator",
        scopes: ["operator.admin"],
      });
      await approveDevicePairing(pending.request.requestId);

      const rotated = await rotateDeviceToken({
        deviceId: identity.deviceId,
        role: "operator",
        scopes: ["operator.admin"],
      });
      expect(rotated.ok).toBe(true);
      const rotatedToken = rotated.ok ? rotated.entry.token : "";
      expect(rotatedToken).toBeTruthy();

      const ws = await openWs(port);
      try {
        const res = await connectReq(ws, {
          skipDefaultAuth: true,
          client: { ...BACKEND_GATEWAY_CLIENT },
          deviceIdentityPath: identityPath,
          deviceToken: rotatedToken,
          scopes: ["operator.admin"],
        });
        expect(res.ok).toBe(true);
        expect((res.payload as { type?: string } | undefined)?.type).toBe("hello-ok");
      } finally {
        ws.close();
      }
    });
  });

  describe("password mode", () => {
    let server: Awaited<ReturnType<typeof startGatewayServer>>;
    let port = 0;
    let prevToken: string | undefined;

    beforeAll(async () => {
      prevToken = process.env.OPENCLAW_GATEWAY_TOKEN;
      testState.gatewayAuth = { mode: "password", password: "secret" };
      delete process.env.OPENCLAW_GATEWAY_TOKEN;
      port = await getFreePort();
      server = await startGatewayServer(port);
    });

    afterAll(async () => {
      await server.close();
      restoreGatewayToken(prevToken);
    });

    test("keeps valid shared-password connect behavior unchanged", async () => {
      const ws = await openWs(port);
      try {
        const res = await connectReq(ws, { password: "secret" });
        expect(res.ok).toBe(true);
      } finally {
        ws.close();
      }
    });

    test("returns stable password mismatch details", async () => {
      const ws = await openWs(port);
      try {
        const res = await connectReq(ws, { password: "wrong" });
        expect(res.ok).toBe(false);
        expectAuthErrorDetails({
          details: res.error?.details,
          expectedCode: ConnectErrorDetailCodes.AUTH_PASSWORD_MISMATCH,
          canRetryWithDeviceToken: false,
          recommendedNextStep: "update_auth_credentials",
        });
      } finally {
        ws.close();
      }
    });

    test("clears requested scopes for shared-password operator connects without device identity", async () => {
      await expectSharedOperatorScopesCleared(port, { password: "secret" });
    });
  });

  describe("none mode", () => {
    let server: Awaited<ReturnType<typeof startGatewayServer>>;
    let port = 0;
    let prevToken: string | undefined;

    beforeAll(async () => {
      prevToken = process.env.OPENCLAW_GATEWAY_TOKEN;
      testState.gatewayAuth = { mode: "none" };
      delete process.env.OPENCLAW_GATEWAY_TOKEN;
      port = await getFreePort();
      server = await startGatewayServer(port);
    });

    afterAll(async () => {
      await server.close();
      restoreGatewayToken(prevToken);
    });

    test("keeps auth-none loopback behavior unchanged", async () => {
      const ws = await openWs(port);
      try {
        const res = await connectReq(ws, { skipDefaultAuth: true });
        expect(res.ok).toBe(true);
      } finally {
        ws.close();
      }
    });
  });
});
