import { afterEach, describe, expect, it, vi } from "vitest";
import { createStateToken, parseStateToken } from "./slackState";

const SECRET = "test-signing-secret";

afterEach(() => {
  vi.useRealTimers();
});

describe("slackState", () => {
  it("round-trips a payload through createStateToken then parseStateToken", async () => {
    const token = await createStateToken(
      {
        userId: "user-1",
        redirectUri: "https://app.example.com/cb",
      },
      SECRET,
    );

    const parsed = await parseStateToken(token, SECRET);

    expect(parsed).not.toBeNull();
    expect(parsed?.userId).toBe("user-1");
    expect(parsed?.redirectUri).toBe("https://app.example.com/cb");
    expect(parsed?.expiresAt).toBeDefined();
  });

  it("issues a two-part token of the form base64url(payload).base64url(signature)", async () => {
    const token = await createStateToken({ userId: "user-1" }, SECRET);
    const parts = token.split(".");
    expect(parts).toHaveLength(2);
    expect(parts[0].length).toBeGreaterThan(0);
    expect(parts[1].length).toBeGreaterThan(0);
  });

  it("rejects a token whose payload was tampered after signing", async () => {
    const token = await createStateToken({ userId: "user-1" }, SECRET);
    const [, signature] = token.split(".");
    // Forge a payload that grants the install to a different user, keeping the
    // original (now-mismatched) signature.
    const forgedPayload = btoa(
      JSON.stringify({
        userId: "attacker",
        expiresAt: new Date(Date.now() + 60_000).toISOString(),
      }),
    )
      .replace(/\+/g, "-")
      .replace(/\//g, "_")
      .replace(/=+$/, "");
    const forged = `${forgedPayload}.${signature}`;

    expect(await parseStateToken(forged, SECRET)).toBeNull();
  });

  it("rejects a token signed with a different secret", async () => {
    const token = await createStateToken({ userId: "user-1" }, "other-secret");
    expect(await parseStateToken(token, SECRET)).toBeNull();
  });

  it("rejects an unsigned (single-part) token", async () => {
    const unsigned = btoa(
      JSON.stringify({ userId: "user-1", expiresAt: new Date(Date.now() + 60_000).toISOString() }),
    );
    expect(await parseStateToken(unsigned, SECRET)).toBeNull();
  });

  it("rejects an expired token", async () => {
    vi.useFakeTimers();
    vi.setSystemTime(new Date("2026-01-01T00:00:00.000Z"));
    const token = await createStateToken({ userId: "user-1" }, SECRET);

    vi.setSystemTime(new Date("2026-01-01T00:09:59.000Z"));
    expect(await parseStateToken(token, SECRET)).not.toBeNull();

    vi.setSystemTime(new Date("2026-01-01T00:10:01.000Z"));
    expect(await parseStateToken(token, SECRET)).toBeNull();
  });

  it("rejects garbage", async () => {
    expect(await parseStateToken("not-a-token", SECRET)).toBeNull();
    expect(await parseStateToken("", SECRET)).toBeNull();
    expect(await parseStateToken("invalid-base64!@#", SECRET)).toBeNull();
  });
});
