import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { createHmac } from "node:crypto";
import { verifySlackSignature, participantForSlackUser } from "@/lib/slack-verify";

/**
 * The signature is one of the two gates on a route that writes splits. Getting
 * it wrong is not a cosmetic bug — it is an unauthenticated write path — so the
 * fail-closed cases are tested first and explicitly.
 */
const SECRET = "test-signing-secret";

function sign(body: string, ts: string, secret = SECRET) {
  return "v0=" + createHmac("sha256", secret).update(`v0:${ts}:${body}`).digest("hex");
}

const now = () => String(Math.floor(Date.now() / 1000));

beforeEach(() => {
  process.env.SLACK_SIGNING_SECRET = SECRET;
});
afterEach(() => {
  vi.useRealTimers();
});

describe("verifySlackSignature", () => {
  it("accepts a correctly signed request", () => {
    const ts = now();
    const body = "payload=%7B%22type%22%3A%22block_actions%22%7D";
    expect(verifySlackSignature(body, ts, sign(body, ts))).toBe(true);
  });

  it("rejects when the signing secret is unset", () => {
    const ts = now();
    const body = "x=1";
    const sig = sign(body, ts);
    delete process.env.SLACK_SIGNING_SECRET;
    // Fails CLOSED. An unset secret waving requests through would turn a
    // misconfigured deploy into an open write endpoint.
    expect(verifySlackSignature(body, ts, sig)).toBe(false);
  });

  it("rejects a tampered body", () => {
    const ts = now();
    const sig = sign("payload=original", ts);
    expect(verifySlackSignature("payload=tampered", ts, sig)).toBe(false);
  });

  it("rejects a signature made with a different secret", () => {
    const ts = now();
    const body = "x=1";
    expect(verifySlackSignature(body, ts, sign(body, ts, "wrong-secret"))).toBe(false);
  });

  it("rejects a replay outside the 5 minute window", () => {
    const old = String(Math.floor(Date.now() / 1000) - 400);
    const body = "x=1";
    expect(verifySlackSignature(body, old, sign(body, old))).toBe(false);
  });

  it("accepts inside the window", () => {
    const recent = String(Math.floor(Date.now() / 1000) - 60);
    const body = "x=1";
    expect(verifySlackSignature(body, recent, sign(body, recent))).toBe(true);
  });

  it("rejects missing headers", () => {
    expect(verifySlackSignature("x=1", null, "v0=abc")).toBe(false);
    expect(verifySlackSignature("x=1", now(), null)).toBe(false);
  });

  it("rejects a signature of the wrong length without throwing", () => {
    // timingSafeEqual throws on length mismatch; a truncated signature must be
    // a plain false, not a 500.
    expect(() => verifySlackSignature("x=1", now(), "v0=short")).not.toThrow();
    expect(verifySlackSignature("x=1", now(), "v0=short")).toBe(false);
  });
});

describe("participantForSlackUser", () => {
  beforeEach(() => {
    process.env.SLACK_USER_MAP = "U111:1, U444:4";
  });

  it("maps known users", () => {
    expect(participantForSlackUser("U111")).toBe(1);
    expect(participantForSlackUser("U444")).toBe(4);
  });

  it("returns null for an unknown user rather than defaulting to the owner", () => {
    // A wrong attribution records the other person's opinion under your name,
    // which is worse than refusing.
    expect(participantForSlackUser("U999")).toBeNull();
  });

  it("returns null when the map is unset", () => {
    delete process.env.SLACK_USER_MAP;
    expect(participantForSlackUser("U111")).toBeNull();
  });
});
