import type { CommandContext } from "@tailor-platform/erp-kit/core";
import { createKyselyMock } from "@tailor-platform/sdk/vitest";
import { describe, expect, it } from "vitest";
import type { Namespace } from "../generated/kysely-tailordb";
import { InvalidEventError, SignatureInvalidError } from "../lib/errors.generated";
import { run } from "./handleSlackAppUninstalled";

const ctx: CommandContext = { actorId: "system", permissions: [] };

const SIGNING_SECRET = "slack-signing-secret";

const baseConnection = {
  id: "swc-1",
  teamId: "T123",
  teamName: "Acme",
  botUserId: "U999",
  status: "ACTIVE" as const,
  installedAt: new Date("2026-01-01T00:00:00.000Z"),
  lastValidatedAt: null,
  revokedAt: null,
  createdAt: new Date("2026-01-01T00:00:00.000Z"),
  updatedAt: null,
};

async function signSlackRequest(
  rawBody: string,
  timestamp: string,
  secret: string,
): Promise<string> {
  const key = await crypto.subtle.importKey(
    "raw",
    new TextEncoder().encode(secret),
    { name: "HMAC", hash: "SHA-256" },
    false,
    ["sign"],
  );
  const mac = new Uint8Array(
    await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(`v0:${timestamp}:${rawBody}`)),
  );
  let hex = "";
  for (const byte of mac) hex += byte.toString(16).padStart(2, "0");
  return `v0=${hex}`;
}

async function makeInput(
  overrides: { teamId?: string; rawBody?: string; timestamp?: string; secret?: string } & Partial<
    Parameters<typeof run>[1]
  > = {},
) {
  const rawBody =
    overrides.rawBody ??
    JSON.stringify({
      team_id: overrides.teamId ?? "T123",
      event: { type: "app_uninstalled" },
      type: "event_callback",
    });
  const timestamp = overrides.timestamp ?? String(Math.floor(Date.now() / 1000));
  const signature =
    overrides.signature ??
    (await signSlackRequest(rawBody, timestamp, overrides.secret ?? SIGNING_SECRET));
  return {
    rawBody,
    timestamp,
    signature,
    signingSecret: overrides.signingSecret ?? SIGNING_SECRET,
  };
}

describe("handleSlackAppUninstalled", () => {
  it("marks the matching SlackWorkspaceIntegration REVOKED and stamps revokedAt", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    // select by teamId (found), update returningAll
    mock.enqueueResults(
      [{ ...baseConnection }],
      [{ ...baseConnection, status: "REVOKED", revokedAt: new Date() }],
    );

    const result = await mock.withTx(async (trx) => run(trx, await makeInput(), ctx));

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.revoked).toBe(true);
    }
    expect(mock.updates.length).toBe(1);
    const patch = mock.updates[0].updateValues() as { status?: string; revokedAt?: unknown };
    expect(patch.status).toBe("REVOKED");
    expect(patch.revokedAt).toBeInstanceOf(Date);
  });

  it("returns a no-op success when no workspace matches the teamId", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([]);

    const result = await mock.withTx(async (trx) =>
      run(trx, await makeInput({ teamId: "T-unknown" }), ctx),
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.revoked).toBe(false);
    }
    expect(mock.updates.length).toBe(0);
  });

  it("rejects an event body without a team_id with INVALID_EVENT", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    const result = await mock.withTx(async (trx) =>
      run(trx, await makeInput({ rawBody: JSON.stringify({ event: {} }) }), ctx),
    );

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(InvalidEventError);
    }
    expect(mock.updates.length).toBe(0);
  });

  it("rejects an incorrect signature with SIGNATURE_INVALID", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    const result = await mock.withTx(async (trx) =>
      run(trx, await makeInput({ secret: "wrong-secret" }), ctx),
    );

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(SignatureInvalidError);
    }
    expect(mock.updates.length).toBe(0);
  });

  it("rejects a timestamp older than the 5 minute tolerance with SIGNATURE_INVALID (replay protection)", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    const stale = String(Math.floor(Date.now() / 1000) - 60 * 6);
    // The signature itself is valid for the stale timestamp.
    const result = await mock.withTx(async (trx) =>
      run(trx, await makeInput({ timestamp: stale }), ctx),
    );

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(SignatureInvalidError);
    }
  });

  it("rejects a rawBody that was tampered with after signing with SIGNATURE_INVALID", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    const input = await makeInput();
    const tampered = {
      ...input,
      rawBody: input.rawBody.replace("T123", "T999"),
    };

    const result = await mock.withTx((trx) => run(trx, tampered, ctx));

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(SignatureInvalidError);
    }
    expect(mock.updates.length).toBe(0);
  });

  it("rejects missing signature material with SIGNATURE_INVALID", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    const input = await makeInput();

    const result = await mock.withTx((trx) => run(trx, { ...input, signature: "" }, ctx));

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBeInstanceOf(SignatureInvalidError);
    }
  });
});
