import { describe, expect, it, vi, beforeEach } from "vitest";
import type { FastifyBaseLogger } from "fastify";
import type { CardActionEvent, FeishuBindingLifecycleEvent, FeishuRuntime } from "../../src/feishu/client.js";
import { startFeishuListenerWithClient } from "../../src/feishu/client.js";

const larkMock = vi.hoisted(() => {
  const starts: Array<{ readonly eventDispatcher?: unknown }> = [];
  class MockEventDispatcher {
    handlers: Record<string, (data: unknown) => Promise<void>> = {};

    register(handlers: Record<string, (data: unknown) => Promise<void>>): this {
      this.handlers = handlers;
      return this;
    }
  }
  class MockWSClient {
    constructor(private readonly params: { readonly onReady?: () => void }) {}
    start(params: { readonly eventDispatcher?: unknown }): void {
      starts.push(params);
      this.params.onReady?.();
    }
  }
  return { starts, MockEventDispatcher, MockWSClient };
});

vi.mock("@larksuiteoapi/node-sdk", () => ({
  AppType: { SelfBuild: "SelfBuild" },
  Domain: { Feishu: "Feishu" },
  LoggerLevel: { info: "info" },
  Client: vi.fn(),
  EventDispatcher: larkMock.MockEventDispatcher,
  WSClient: larkMock.MockWSClient,
}));

describe("Feishu card action callbacks", () => {
  beforeEach(() => {
    larkMock.starts.length = 0;
  });

  it("acks card action callbacks before the business handler settles", async () => {
    const action = deferred<void>();
    const onCardAction = vi.fn(async () => action.promise);
    const logger = silentLogger();

    await startFeishuListenerWithClient(
      { appId: "app-id", appSecret: "app-secret", botOpenId: "bot-open-id" },
      fakeClient(),
      logger,
      async () => {},
      onCardAction,
    );

    const handler = cardActionHandler();
    const handlerDone = handler(makeCardActionEvent()).then(() => "done" as const);
    const earlyResult = await Promise.race([handlerDone, delay(25).then(() => "timeout" as const)]);

    action.resolve();
    await handlerDone;

    expect(earlyResult).toBe("done");
    expect(onCardAction).toHaveBeenCalledTimes(1);
  });

  it("logs background card action failures after acking", async () => {
    const logger = silentLogger();
    const err = new Error("handler failed");

    await startFeishuListenerWithClient(
      { appId: "app-id", appSecret: "app-secret", botOpenId: "bot-open-id" },
      fakeClient(),
      logger,
      async () => {},
      async () => {
        throw err;
      },
    );

    await cardActionHandler()(makeCardActionEvent());
    await vi.waitFor(() => {
      expect(logger.error).toHaveBeenCalledWith({ err }, "feishu card action handler threw");
    });
  });

  it("dispatches chat dissolution and bot-removal lifecycle events", async () => {
    const onLifecycle = vi.fn(async (_event: FeishuBindingLifecycleEvent) => {});
    await startFeishuListenerWithClient(
      { appId: "app-id", appSecret: "app-secret", botOpenId: "bot-open-id" },
      fakeClient(),
      silentLogger(),
      async () => {},
      undefined,
      undefined,
      onLifecycle,
    );

    await lifecycleHandler("im.chat.disbanded_v1")({
      event_id: "event-dissolved",
      chat_id: "chat-dissolved",
    });
    await lifecycleHandler("im.chat.member.bot.deleted_v1")({
      header: { event_id: "event-bot-removed" },
      chat_id: "chat-bot-removed",
    });

    expect(onLifecycle).toHaveBeenNthCalledWith(1, {
      eventId: "event-dissolved",
      chatId: "chat-dissolved",
      reason: "chat_dissolved",
    });
    expect(onLifecycle).toHaveBeenNthCalledWith(2, {
      eventId: "event-bot-removed",
      chatId: "chat-bot-removed",
      reason: "bot_removed",
    });
  });

  it("propagates lifecycle archival failures to the WS dispatcher", async () => {
    const failure = new Error("archive failed");
    await startFeishuListenerWithClient(
      { appId: "app-id", appSecret: "app-secret", botOpenId: "bot-open-id" },
      fakeClient(),
      silentLogger(),
      async () => {},
      undefined,
      undefined,
      async () => { throw failure; },
    );

    await expect(lifecycleHandler("im.chat.disbanded_v1")({
      event_id: "event-failure",
      chat_id: "chat-failure",
    })).rejects.toThrow(failure);
  });
});

function cardActionHandler(): (data: unknown) => Promise<void> {
  const start = larkMock.starts[0];
  if (start === undefined) throw new Error("WSClient.start was not called");
  const dispatcher = start.eventDispatcher as { readonly handlers?: Record<string, (data: unknown) => Promise<void>> };
  const handler = dispatcher.handlers?.["card.action.trigger"];
  if (handler === undefined) throw new Error("card.action.trigger handler was not registered");
  return handler;
}

function lifecycleHandler(eventType: string): (data: unknown) => Promise<void> {
  const start = larkMock.starts[0];
  if (start === undefined) throw new Error("WSClient.start was not called");
  const dispatcher = start.eventDispatcher as { readonly handlers?: Record<string, (data: unknown) => Promise<void>> };
  const handler = dispatcher.handlers?.[eventType];
  if (handler === undefined) throw new Error(`${eventType} handler was not registered`);
  return handler;
}

function makeCardActionEvent(): CardActionEvent {
  return {
    operator: { open_id: "ou_user" },
    action: { value: { interrupt_run: "run-1" }, tag: "button" },
    context: { open_chat_id: "chat-1", open_message_id: "message-1" },
  };
}

function fakeClient(): FeishuRuntime["client"] {
  return {} as FeishuRuntime["client"];
}

function silentLogger(): FastifyBaseLogger {
  return {
    info: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
    debug: vi.fn(),
    fatal: vi.fn(),
    child() { return this; },
    level: "silent",
  } as unknown as FastifyBaseLogger;
}

function delay(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

interface Deferred<T> {
  readonly promise: Promise<T>;
  readonly resolve: (value: T) => void;
}

function deferred<T>(): Deferred<T> {
  let resolve!: (value: T) => void;
  const promise = new Promise<T>((res) => {
    resolve = res;
  });
  return { promise, resolve };
}
