import { createKyselyMock, type KyselyMock } from "@tailor-platform/sdk/vitest";
import { describe, expect, it, vi } from "vitest";
import type { Namespace } from "../generated/kysely-tailordb";
import type { DestinationAdapter } from "../lib/dispatchAdapters";
import { deliverDestinationLog } from "./deliverDestination";

type Mock = KyselyMock<Namespace[keyof Namespace]>;

const NOW = new Date("2024-06-15T12:00:00.000Z");

const pendingLog = {
  id: "log-1",
  eventType: "PO_ISSUED",
  sourceType: "PurchaseOrder",
  sourceId: "po-1",
  channelId: "SLACK",
  externalChannelRef: "C0123456789",
  subject: "PO PO-001 issued",
  body: "Purchase order PO-001 has been issued.",
  status: "PENDING",
};

function stage(mock: Mock, row: Record<string, unknown> | null = pendingLog) {
  mock.setQueryResolver((query) => {
    if (query.sql.startsWith("select") && query.sql.includes('from "DestinationDeliveryLog"')) {
      return row ? [row] : [];
    }
    return [];
  });
}

function makeAdapter(send?: DestinationAdapter["send"]): DestinationAdapter {
  return {
    send:
      send ?? vi.fn(() => Promise.resolve({ ok: true as const, externalMessageId: "slack-ts-1" })),
  };
}

describe("deliverDestinationLog", () => {
  it("posts a PENDING log outside any transaction, records SENT, and passes the log id as the provider idempotency key", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    stage(mock);
    const adapter = makeAdapter();

    const outcome = await deliverDestinationLog(mock.db, "log-1", adapter, NOW);

    expect(outcome.status).toBe("SENT");
    expect(outcome.externalMessageId).toBe("slack-ts-1");
    expect(adapter.send).toHaveBeenCalledWith(
      expect.objectContaining({
        channelId: "SLACK",
        idempotencyKey: "log-1",
        externalChannelRef: "C0123456789",
      }),
    );
    const update = mock.updates[0]?.updateValues() as {
      status?: string;
      providerMessageId?: string;
    };
    expect(update.status).toBe("SENT");
    expect(update.providerMessageId).toBe("slack-ts-1");
  });

  it("records FAILED with the normalized failure reason on an adapter error", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    stage(mock);
    const adapter = makeAdapter(() =>
      Promise.resolve({
        ok: false as const,
        errorClass: "SlackApiError",
        errorDetail: "channel_not_found",
      }),
    );

    const outcome = await deliverDestinationLog(mock.db, "log-1", adapter, NOW);

    expect(outcome.status).toBe("FAILED");
    expect(outcome.reason).toBe("SlackApiError");
    const update = mock.updates[0]?.updateValues() as { status?: string; failureReason?: string };
    expect(update.status).toBe("FAILED");
    expect(update.failureReason).toBe("channel_not_found");
  });

  it("contains a throwing adapter as a FAILED record", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    stage(mock);
    const adapter = makeAdapter(() => {
      throw new Error("boom");
    });

    const outcome = await deliverDestinationLog(mock.db, "log-1", adapter, NOW);

    expect(outcome.status).toBe("FAILED");
    expect(outcome.reason).toBe("DestinationAdapterThrew");
  });

  it("is a no-op when the log is no longer PENDING", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    stage(mock, { ...pendingLog, status: "SENT" });
    const adapter = makeAdapter();

    const outcome = await deliverDestinationLog(mock.db, "log-1", adapter, NOW);

    expect(outcome.status).toBe("SKIPPED");
    expect(adapter.send).not.toHaveBeenCalled();
    expect(mock.updates).toHaveLength(0);
  });
});
