import { describe, expect, it, vi } from "vitest";
import { createSlackDestinationAdapter } from "./slackDestinationAdapter";

const ACTIVE_CONN = { status: "ACTIVE", teamId: "T123" };

function makeAdapter(overrides: Partial<Parameters<typeof createSlackDestinationAdapter>[0]> = {}) {
  return createSlackDestinationAdapter({
    loadConnection: vi.fn(() => Promise.resolve(ACTIVE_CONN)),
    secretManager: {
      getSecret: vi.fn(() => Promise.resolve("xoxb-token")),
    },
    postChatMessage: vi.fn(() =>
      Promise.resolve({ ok: true, ts: "1700000000.123456", channel: "C1" }),
    ),
    ...overrides,
  });
}

const sendArgs = {
  channelId: "SLACK",
  idempotencyKey: "destination-log-1",
  externalChannelRef: "C1",
  subject: "PO PO-001 issued",
  body: "Purchase order PO-001 has been issued.",
};

describe("createSlackDestinationAdapter", () => {
  it("posts through an ACTIVE workspace and returns SENT with the provider ts", async () => {
    const post = vi.fn(() =>
      Promise.resolve({ ok: true as const, ts: "1700000000.123456", channel: "C1" }),
    );
    const adapter = makeAdapter({ postChatMessage: post });

    const outcome = await adapter.send(sendArgs);

    expect(outcome.ok).toBe(true);
    if (outcome.ok) {
      expect(outcome.externalMessageId).toBe("1700000000.123456");
    }
    expect(post).toHaveBeenCalledWith("xoxb-token", "C1", "PO PO-001 issued");
  });

  it("fails fast with ChannelMismatch for a non-SLACK channelId, without resolving the connection or calling the Slack client", async () => {
    const load = vi.fn(() => Promise.resolve(ACTIVE_CONN));
    const post = vi.fn();
    const adapter = makeAdapter({ loadConnection: load, postChatMessage: post });

    const outcome = await adapter.send({ ...sendArgs, channelId: "TEAMS" });

    expect(outcome.ok).toBe(false);
    if (!outcome.ok) {
      expect(outcome.errorClass).toBe("ChannelMismatch");
      expect(outcome.errorDetail).toBe("TEAMS");
    }
    expect(load).not.toHaveBeenCalled();
    expect(post).not.toHaveBeenCalled();
  });

  it("fails fast with workspace_not_connected when no connection exists, without calling the Slack client", async () => {
    const post = vi.fn();
    const adapter = makeAdapter({
      loadConnection: vi.fn(() => Promise.resolve(null)),
      postChatMessage: post,
    });

    const outcome = await adapter.send(sendArgs);

    expect(outcome.ok).toBe(false);
    if (!outcome.ok) {
      expect(outcome.errorClass).toBe("WorkspaceNotConnected");
      expect(outcome.errorDetail).toBe("workspace_not_connected");
    }
    expect(post).not.toHaveBeenCalled();
  });

  it("fails fast with workspace_not_connected when the connection is REVOKED", async () => {
    const post = vi.fn();
    const adapter = makeAdapter({
      loadConnection: vi.fn(() => Promise.resolve({ status: "REVOKED", teamId: "T123" })),
      postChatMessage: post,
    });

    const outcome = await adapter.send(sendArgs);

    expect(outcome.ok).toBe(false);
    expect(post).not.toHaveBeenCalled();
  });

  it("returns FAILED SlackApiError when the Slack API responds not-ok", async () => {
    const adapter = makeAdapter({
      postChatMessage: vi.fn(() => Promise.resolve({ ok: false, error: "internal_error" })),
    });

    const outcome = await adapter.send(sendArgs);

    expect(outcome.ok).toBe(false);
    if (!outcome.ok) {
      expect(outcome.errorClass).toBe("SlackApiError");
      expect(outcome.errorDetail).toBe("internal_error");
    }
  });

  it.each(["not_in_channel", "channel_not_found"])(
    "classifies %s as SlackChannelUnreachable, distinct from the generic SlackApiError",
    async (error) => {
      const adapter = makeAdapter({
        postChatMessage: vi.fn(() => Promise.resolve({ ok: false, error })),
      });

      const outcome = await adapter.send(sendArgs);

      expect(outcome.ok).toBe(false);
      if (!outcome.ok) {
        expect(outcome.errorClass).toBe("SlackChannelUnreachable");
        expect(outcome.errorDetail).toBe(error);
      }
    },
  );

  it("redacts the provider response to { ok, ts, channel, error } only — never the posted text", async () => {
    const adapter = makeAdapter({
      postChatMessage: vi.fn(() =>
        Promise.resolve({ ok: true as const, ts: "111.222", channel: "C1" }),
      ),
    });

    const outcome = await adapter.send(sendArgs);

    expect(outcome.providerResponse).toBeDefined();
    const parsed = JSON.parse(outcome.providerResponse as string) as Record<string, unknown>;
    expect(new Set(Object.keys(parsed))).toEqual(new Set(["channel", "error", "ok", "ts"]));
    expect(JSON.stringify(parsed)).not.toContain("PO PO-001 issued");
  });

  it("returns FAILED when posting throws, with a redacted provider response", async () => {
    const adapter = makeAdapter({
      postChatMessage: vi.fn(() => Promise.reject(new Error("network down"))),
    });

    const outcome = await adapter.send(sendArgs);

    expect(outcome.ok).toBe(false);
    if (!outcome.ok) {
      expect(outcome.errorClass).toBe("SlackPostThrew");
      expect(outcome.providerResponse).toContain("network down");
    }
  });

  it("classifies an HTTP 429 as SlackRateLimited and carries the Retry-After in the failure detail", async () => {
    const adapter = makeAdapter({
      postChatMessage: vi.fn(() =>
        Promise.resolve({ ok: false, error: "ratelimited", retryAfterSeconds: 30 }),
      ),
    });

    const outcome = await adapter.send(sendArgs);

    expect(outcome.ok).toBe(false);
    if (!outcome.ok) {
      expect(outcome.errorClass).toBe("SlackRateLimited");
      expect(outcome.errorDetail).toBe("ratelimited retry_after=30");
    }
  });

  it("classifies token_revoked / invalid_auth / account_inactive as SlackTokenRevoked", async () => {
    for (const error of ["token_revoked", "invalid_auth", "account_inactive"]) {
      const adapter = makeAdapter({
        postChatMessage: vi.fn(() => Promise.resolve({ ok: false, error })),
      });

      const outcome = await adapter.send(sendArgs);

      expect(outcome.ok).toBe(false);
      if (!outcome.ok) {
        expect(outcome.errorClass).toBe("SlackTokenRevoked");
        expect(outcome.errorDetail).toBe(error);
      }
    }
  });

  it("invokes markConnectionRevoked (best-effort) when the token is revoked", async () => {
    const markConnectionRevoked = vi.fn(() => Promise.resolve());
    const adapter = makeAdapter({
      postChatMessage: vi.fn(() => Promise.resolve({ ok: false, error: "token_revoked" })),
      markConnectionRevoked,
    });

    const outcome = await adapter.send(sendArgs);

    expect(outcome.ok).toBe(false);
    expect(markConnectionRevoked).toHaveBeenCalledWith("token_revoked");
  });

  it("still returns the classified failure when markConnectionRevoked throws", async () => {
    const adapter = makeAdapter({
      postChatMessage: vi.fn(() => Promise.resolve({ ok: false, error: "invalid_auth" })),
      markConnectionRevoked: vi.fn(() => Promise.reject(new Error("db down"))),
    });

    const outcome = await adapter.send(sendArgs);

    expect(outcome.ok).toBe(false);
    if (!outcome.ok) {
      expect(outcome.errorClass).toBe("SlackTokenRevoked");
    }
  });

  it("does not auto-revoke for non-auth failures like channel_not_found", async () => {
    const markConnectionRevoked = vi.fn(() => Promise.resolve());
    const adapter = makeAdapter({
      postChatMessage: vi.fn(() => Promise.resolve({ ok: false, error: "channel_not_found" })),
      markConnectionRevoked,
    });
    // channel_not_found is a non-auth failure: classified SlackChannelUnreachable
    // and must NOT trigger token revocation.

    const outcome = await adapter.send(sendArgs);

    expect(outcome.ok).toBe(false);
    if (!outcome.ok) {
      expect(outcome.errorClass).toBe("SlackChannelUnreachable");
    }
    expect(markConnectionRevoked).not.toHaveBeenCalled();
  });

  it("classifies an AbortSignal timeout as SlackTimeout", async () => {
    const timeoutError = new DOMException("The operation timed out.", "TimeoutError");
    const adapter = makeAdapter({
      postChatMessage: vi.fn(() => Promise.reject(timeoutError)),
    });

    const outcome = await adapter.send(sendArgs);

    expect(outcome.ok).toBe(false);
    if (!outcome.ok) {
      expect(outcome.errorClass).toBe("SlackTimeout");
      expect(outcome.errorDetail).toBe("timeout");
    }
  });
});
