import { describe, expect, it, vi } from "vitest";
import type { PrismaClient } from "@prisma/client";
import { InMemoryModelRegistry } from "../../src/agent/models.js";
import type { RunRequest, RunResult } from "../../src/agent/runner.js";
import type { FeishuRuntime, MessageReceiveEvent } from "../../src/feishu/client.js";
import { addReaction, removeReaction } from "../../src/feishu/client.js";
import { makeTriggerHandler } from "../../src/feishu/trigger.js";
import type { PermissionAuthorizer } from "../../src/permission.js";
import type { RuntimeSettings } from "../../src/settings/runtime.js";

describe("Feishu reactions", () => {
  it("addReaction returns reaction_id on success", async () => {
    const request = vi.fn(async () => ({ data: { reaction_id: "reaction-1" } }));
    const rt = mockRuntime({ request });

    await expect(addReaction(rt, "message-1", "Typing")).resolves.toBe("reaction-1");
    expect(request).toHaveBeenCalledWith({
      method: "POST",
      url: "/open-apis/im/v1/messages/message-1/reactions",
      data: { reaction_type: { emoji_type: "Typing" } },
    });
  });

  it("addReaction returns null on API failure", async () => {
    const rt = mockRuntime({
      request: vi.fn(async () => {
        throw new Error("api failed");
      }),
    });

    await expect(addReaction(rt, "message-1", "Typing")).resolves.toBeNull();
  });

  it("removeReaction returns true on success", async () => {
    const request = vi.fn(async () => ({}));
    const rt = mockRuntime({ request });

    await expect(removeReaction(rt, "message-1", "reaction-1")).resolves.toBe(true);
    expect(request).toHaveBeenCalledWith({
      method: "DELETE",
      url: "/open-apis/im/v1/messages/message-1/reactions/reaction-1",
    });
  });

  it("removeReaction returns false on API failure", async () => {
    const rt = mockRuntime({
      request: vi.fn(async () => {
        throw new Error("api failed");
      }),
    });

    await expect(removeReaction(rt, "message-1", "reaction-1")).resolves.toBe(false);
  });

  it("adds Typing on start and removes it on success", async () => {
    const run = deferred<RunResult>();
    const rt = mockRuntime();
    const runAgent = vi.fn((req: RunRequest) => {
      req.onStream?.({ type: "text-delta", text: "done" });
      return run.promise;
    });

    await triggerWithRunAgent(runAgent, rt);

    expect(rt.reactionRequests).toEqual([
      { kind: "add", messageId: "message-1", emoji: "Typing", reactionId: "reaction-1" },
    ]);

    run.resolve(completedRunResult("done"));
    await vi.waitFor(() => {
      expect(rt.reactionRequests).toEqual([
        { kind: "add", messageId: "message-1", emoji: "Typing", reactionId: "reaction-1" },
        { kind: "remove", messageId: "message-1", reactionId: "reaction-1" },
      ]);
    });
  });

  it("replaces Typing with CrossMark on failure", async () => {
    const run = deferred<RunResult>();
    const rt = mockRuntime();
    const runAgent = vi.fn(() => run.promise);

    await triggerWithRunAgent(runAgent, rt);

    run.reject(new Error("agent failed"));
    await vi.waitFor(() => {
      expect(rt.reactionRequests).toEqual([
        { kind: "add", messageId: "message-1", emoji: "Typing", reactionId: "reaction-1" },
        { kind: "remove", messageId: "message-1", reactionId: "reaction-1" },
        { kind: "add", messageId: "message-1", emoji: "CrossMark", reactionId: "reaction-2" },
      ]);
    });
  });
});

async function triggerWithRunAgent(
  runAgent: (req: RunRequest) => Promise<RunResult>,
  rt: MockRuntime,
): Promise<void> {
  const trigger = makeTriggerHandler({
    prisma: mockPrisma(),
    settings: mockSettings(),
    logger: rt.logger,
    authorizer: allowAllAuthorizer(),
    projectWorkspaceRoot: "/tmp",
    publicBaseUrl: "https://educraft.example.test",
    siloOrganizationId: "org_test_default",
    runAgent,
    messageBatcherOptions: { maxMessages: 1 },
  });

  await trigger(makeEvent(), rt);
}

function completedRunResult(text: string): RunResult {
  return {
    status: "completed",
    text,
    usage: { inputTokens: 1, outputTokens: 1 },
    numTurns: 1,
  };
}

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

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

const bot = { key: "@_user_1", id: { open_id: "ou_bot" }, name: "Bot" };

function makeEvent(): MessageReceiveEvent {
  return {
    message: {
      message_id: "message-1",
      chat_id: "chat-1",
      chat_type: "group",
      message_type: "text",
      content: JSON.stringify({ text: "@_user_1 do work" }),
      mentions: [bot],
    },
    sender: { sender_id: { open_id: "ou_user" }, sender_type: "user" },
  };
}

type ReactionRequest =
  | { readonly kind: "add"; readonly messageId: string; readonly emoji: string; readonly reactionId: string }
  | { readonly kind: "remove"; readonly messageId: string; readonly reactionId: string };

interface MockRuntime extends FeishuRuntime {
  readonly reactionRequests: ReactionRequest[];
}

function mockRuntime(options: { readonly request?: (payload: unknown) => Promise<unknown> } = {}): MockRuntime {
  const reactionRequests: ReactionRequest[] = [];
  let nextReactionId = 1;
  const request =
    options.request ??
    vi.fn(async (payload: unknown) => {
      const requestPayload = payload as { method?: string; url?: string; data?: { reaction_type?: { emoji_type?: string } } };
      const addMatch = requestPayload.url?.match(/\/messages\/([^/]+)\/reactions$/);
      if (requestPayload.method === "POST" && addMatch?.[1] !== undefined) {
        const reactionId = `reaction-${nextReactionId}`;
        nextReactionId += 1;
        reactionRequests.push({
          kind: "add",
          messageId: addMatch[1],
          emoji: requestPayload.data?.reaction_type?.emoji_type ?? "",
          reactionId,
        });
        return { data: { reaction_id: reactionId } };
      }

      const removeMatch = requestPayload.url?.match(/\/messages\/([^/]+)\/reactions\/([^/]+)$/);
      if (requestPayload.method === "DELETE" && removeMatch?.[1] !== undefined && removeMatch[2] !== undefined) {
        reactionRequests.push({ kind: "remove", messageId: removeMatch[1], reactionId: removeMatch[2] });
      }
      return {};
    });

  return {
    client: {
      request,
      im: {
        v1: {
          message: {
            create: vi.fn(async () => ({ data: { message_id: "reply-message-1" } })),
            patch: vi.fn(async () => ({})),
          },
        },
      },
    } as unknown as FeishuRuntime["client"],
    logger: silentLogger(),
    reactionRequests,
  };
}

function mockSettings(): RuntimeSettings {
  const registry = new InMemoryModelRegistry(
    [{ id: "mock-model", label: "Mock", toolCapable: true }],
    [{ id: "draft", label: "Draft", defaultModel: "mock-model" }],
  );
  return {
    async provider(providerId) {
      return {
        id: providerId,
        async openAgentLease() {
          return {
            sdkEnv: {},
            sensitiveValues: [],
            async close() {},
          };
        },
      };
    },
    async modelRegistry() {
      return registry;
    },
    async runPolicy() {
      return { maxTurns: 1, maxConcurrentRuns: 1, maxRunSeconds: 300 };
    },
  };
}

function allowAllAuthorizer(): PermissionAuthorizer {
  return {
    async can(req) {
      return {
        allowed: true,
        reason: "test allow",
        action: req.action,
        resource: req.resource,
        actor: req.actor,
        organizationId: "org_test_default",
        actorUserId: "user-1",
        principals: [{ type: "USER", id: "ou_user" }],
        requiredRole: "EDIT",
        effectiveRole: "EDIT",
      };
    },
  };
}

function mockPrisma(): PrismaClient {
  let lock: { projectId: string; runId: string } | null = null;
  const session = { id: "session-1", metadata: {} };

  const client = {
    organization: {
      findFirst: vi.fn(async () => ({ id: "org_test_default", slug: "test-default" })),
    },
    feishuUserIdentity: {
      findFirst: vi.fn(async () => ({
        user: {
          organizationMemberships: [{
            role: "OWNER",
            organization: { id: "org_test_default", name: "Test Organization" },
          }],
        },
      })),
    },
    feishuEventReceipt: {
      findUnique: vi.fn(async () => null),
      create: vi.fn(async () => ({ id: "receipt-1" })),
    },
    projectGroupBinding: {
      findFirst: vi.fn(async () => ({ projectId: "project-1", selectedRole: { roleId: "draft" } })),
    },
    project: {
      findUnique: vi.fn(async () => ({ workspaceDir: "/tmp/cph-project" })),
    },
    agentSession: {
      findFirst: vi.fn(async () => null),
      create: vi.fn(async () => session),
      update: vi.fn(async () => session),
    },
    agentRun: {
      count: vi.fn(async () => 0),
      create: vi.fn(async () => ({ id: "run-1" })),
      findUnique: vi.fn(async () => null),
      update: vi.fn(async () => ({ id: "run-1" })),
    },
    usageFact: {
      create: vi.fn(async () => ({ id: "fact-1" })),
    },
    projectAgentLock: {
      findUnique: vi.fn(async () => (lock === null ? null : { runId: lock.runId })),
      create: vi.fn(async (args: { data: { projectId: string; runId: string } }) => {
        lock = { projectId: args.data.projectId, runId: args.data.runId };
        return lock;
      }),
      deleteMany: vi.fn(async () => {
        lock = null;
        return { count: 1 };
      }),
    },
    auditEntry: {
      create: vi.fn(async () => ({ id: "audit-1" })),
    },
  } as unknown as PrismaClient;
  Object.assign(client, {
    $transaction: vi.fn(async (callback: (tx: PrismaClient) => Promise<unknown>) => callback(client)),
    $queryRaw: vi.fn(async () => [{ id: "org_test_default", status: "ACTIVE" }]),
    $executeRaw: vi.fn(async () => 1),
  });
  return client;
}

function silentLogger(): FeishuRuntime["logger"] {
  return {
    info() {},
    warn() {},
    error() {},
    debug() {},
    fatal() {},
    child() { return this; },
    level: "silent",
  } as unknown as FeishuRuntime["logger"];
}
