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 { DuplicateChannelIdError, MissingRequiredFieldError } from "../lib/errors.generated";
import { baseInAppChannel, baseEmailChannel } from "../testing/fixtures";
import { run, type ChannelCapabilities } from "./createNotificationChannel";

const ctx: CommandContext = { actorId: "admin-1", permissions: [] };

const inAppCapabilities: ChannelCapabilities = {
  supportsHtmlBody: false,
  supportsAttachments: false,
  supportsRichActions: true,
};

const emailCapabilities: ChannelCapabilities = {
  supportsHtmlBody: true,
  supportsAttachments: true,
  supportsRichActions: false,
};

describe("createNotificationChannel", () => {
  it("creates a NotificationChannel row with channelId IN_APP, the supplied displayName, capabilities, and enabled=true by default", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    // 1. check existing (none), 2. insert channel
    mock.enqueueResults([], [{ ...baseInAppChannel }]);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          channelId: "IN_APP",
          displayName: "In-app inbox",
          capabilities: inAppCapabilities,
        },
        ctx,
      ),
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.channel.channelId).toBe("IN_APP");
      expect(result.value.channel.enabled).toBe(true);
    }
    expect(mock.inserts[0].insertValues()).toEqual(
      expect.objectContaining({ channelId: "IN_APP", enabled: true }),
    );
  });

  it("creates a NotificationChannel row with enabled=false when explicitly supplied", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([], [{ ...baseEmailChannel, enabled: false }]);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          channelId: "EMAIL",
          displayName: "Email",
          capabilities: emailCapabilities,
          enabled: false,
        },
        ctx,
      ),
    );

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.value.channel.enabled).toBe(false);
    }
    expect(mock.inserts[0].insertValues()).toEqual(expect.objectContaining({ enabled: false }));
  });

  it("returns DUPLICATE_CHANNEL_ID when a NotificationChannel for the same channelId already exists", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([{ id: baseInAppChannel.id }]);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          channelId: "IN_APP",
          displayName: "In-app inbox",
          capabilities: inAppCapabilities,
        },
        ctx,
      ),
    );

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

  it("returns MISSING_REQUIRED_FIELD when displayName is blank", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          channelId: "IN_APP",
          displayName: "   ",
          capabilities: inAppCapabilities,
        },
        ctx,
      ),
    );

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

  it("returns MISSING_REQUIRED_FIELD when capabilities is omitted", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          channelId: "IN_APP",
          displayName: "In-app inbox",
        } as unknown as Parameters<typeof run>[1],
        ctx,
      ),
    );

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

  it("accepts reserved channelIds (SMS, PUSH, SLACK, TEAMS) and persists the registry row even though no adapter is wired", async () => {
    const reserved: ChannelCapabilities = {
      supportsHtmlBody: false,
      supportsAttachments: false,
      supportsRichActions: false,
    };

    for (const channelId of ["SMS", "PUSH", "SLACK", "TEAMS"] as const) {
      const mock = createKyselyMock<Namespace[keyof Namespace]>();
      mock.enqueueResults(
        [],
        [
          {
            ...baseInAppChannel,
            channelId,
            displayName: channelId,
          },
        ],
      );

      const result = await mock.withTx((trx) =>
        run(
          trx,
          {
            channelId,
            displayName: channelId,
            capabilities: reserved,
          },
          ctx,
        ),
      );

      expect(result.ok).toBe(true);
      if (result.ok) {
        expect(result.value.channel.channelId).toBe(channelId);
      }
    }
  });

  it("derives kind=PERSONAL for IN_APP/EMAIL/SMS/PUSH and kind=DESTINATION for SLACK/TEAMS at insert time without any caller-supplied kind input", async () => {
    const caps: ChannelCapabilities = {
      supportsHtmlBody: false,
      supportsAttachments: false,
      supportsRichActions: false,
    };

    const cases = [
      { channelId: "IN_APP", kind: "PERSONAL" },
      { channelId: "EMAIL", kind: "PERSONAL" },
      { channelId: "SMS", kind: "PERSONAL" },
      { channelId: "PUSH", kind: "PERSONAL" },
      { channelId: "SLACK", kind: "DESTINATION" },
      { channelId: "TEAMS", kind: "DESTINATION" },
    ] as const;

    for (const { channelId, kind } of cases) {
      const mock = createKyselyMock<Namespace[keyof Namespace]>();
      mock.enqueueResults([], [{ ...baseInAppChannel, channelId, kind }]);

      const result = await mock.withTx((trx) =>
        run(trx, { channelId, displayName: channelId, capabilities: caps }, ctx),
      );

      expect(result.ok).toBe(true);
      expect(mock.inserts[0].insertValues()).toEqual(expect.objectContaining({ channelId, kind }));
    }
  });

  it("persists the capabilities object verbatim so template rendering can branch on supportsHtmlBody, supportsAttachments, and supportsRichActions", async () => {
    const mock = createKyselyMock<Namespace[keyof Namespace]>();
    mock.enqueueResults([], [{ ...baseEmailChannel }]);

    const result = await mock.withTx((trx) =>
      run(
        trx,
        {
          channelId: "EMAIL",
          displayName: "Email",
          capabilities: emailCapabilities,
        },
        ctx,
      ),
    );

    expect(result.ok).toBe(true);
    expect(mock.inserts[0].insertValues()).toEqual(
      expect.objectContaining({ capabilities: emailCapabilities }),
    );
  });
});
