import { describe, test, expect, mock } from "bun:test";
import { SlackMentionPoller } from "@/poller/slack-mention-poller.js";
import { METRIC } from "@/metrics/names.js";
import { SLACK_MENTION } from "@/core/constants.js";
import { createFakeQueue } from "../_helpers/fake-queue.js";
import { FakeMetrics } from "../_helpers/fake-metrics.js";
import { mockLogger } from "../_helpers/mock-logger.js";

// ── shape helpers ────────────────────────────────────────────────────────────

type SlackMessage = {
  ts: string;
  text?: string;
  user?: string;
  bot_id?: string;
  reply_count?: number;
};

function makeChannel(id = "C001", name = "general") {
  return { id, name };
}

function buildDeps(
  overrides: {
    channels?: Array<{ id: string; name: string }>;
    messages?: SlackMessage[];
    threadReplies?: SlackMessage[];
    reactions?: { claimed: boolean; done: boolean; failed: boolean };
    reactionsError?: Error;
    repliedAfter?: boolean;
    replyCheckError?: Error;
    listError?: Error;
    fetchError?: Error;
  } = {},
) {
  const channels = overrides.channels ?? [makeChannel()];
  const messages = overrides.messages ?? [];
  const threadReplies = overrides.threadReplies ?? [];

  const slackNotifier = {
    listBotChannels: mock(async () => {
      if (overrides.listError) throw overrides.listError;
      return channels;
    }),
    fetchChannelHistory: mock(async (_channelId: string) => {
      if (overrides.fetchError) throw overrides.fetchError;
      return messages;
    }),
    fetchThreadReplies: mock(async (_channelId: string, _ts: string) => threadReplies),
    getBotReactions: mock(async (_channelId: string, _messageTs: string, _botUserId: string) => {
      if (overrides.reactionsError) throw overrides.reactionsError;
      return overrides.reactions ?? { claimed: false, done: false, failed: false };
    }),
    hasBotRepliedAfter: mock(async (_channelId: string, _threadTs: string, _afterTs: string) => {
      if (overrides.replyCheckError) throw overrides.replyCheckError;
      return overrides.repliedAfter ?? false;
    }),
    addReaction: mock(async (_channelId: string, _messageTs: string, _emoji: string) => {}),
  };

  const queue = createFakeQueue();
  const logger = mockLogger();
  const metrics = new FakeMetrics();

  return { slackNotifier, queue, logger, metrics };
}

// ── tests ────────────────────────────────────────────────────────────────────

describe("SlackMentionPoller — channel refresh", () => {
  test("fetches channels on first poll", async () => {
    const { slackNotifier, queue, logger, metrics } = buildDeps({ messages: [] });
    const poller = new SlackMentionPoller({
      slackNotifier: slackNotifier as never,
      queue,
      slackBotUserId: "U001",
      logger,
      metrics,
    });

    await (poller as unknown as { poll(): Promise<void> }).poll();

    expect(slackNotifier.listBotChannels).toHaveBeenCalledTimes(1);
  });

  test("does not re-fetch channels within CHANNEL_REFRESH_MS", async () => {
    const { slackNotifier, queue, logger, metrics } = buildDeps({ messages: [] });
    const poller = new SlackMentionPoller({
      slackNotifier: slackNotifier as never,
      queue,
      slackBotUserId: "U001",
      logger,
      metrics,
    });

    const pollMethod = (poller as unknown as { poll(): Promise<void> }).poll.bind(poller);
    await pollMethod();
    await pollMethod();

    // Second poll should use cache, not re-fetch
    expect(slackNotifier.listBotChannels).toHaveBeenCalledTimes(1);
  });

  test("re-fetches channels when cache is stale (beyond CHANNEL_REFRESH_MS)", async () => {
    const { slackNotifier, queue, logger, metrics } = buildDeps({ messages: [] });
    const poller = new SlackMentionPoller({
      slackNotifier: slackNotifier as never,
      queue,
      slackBotUserId: "U001",
      logger,
      metrics,
    });

    // Manually set old fetch timestamp to force refresh
    (poller as unknown as { channelsFetchedAt: number }).channelsFetchedAt =
      Date.now() - SLACK_MENTION.CHANNEL_REFRESH_MS - 1000;
    (poller as unknown as { cachedChannels: unknown[] }).cachedChannels = [makeChannel()];

    const pollMethod = (poller as unknown as { poll(): Promise<void> }).poll.bind(poller);
    await pollMethod();

    expect(slackNotifier.listBotChannels).toHaveBeenCalledTimes(1);
  });
});

describe("SlackMentionPoller — mention detection", () => {
  test("enqueues a top-level mention from a human user", async () => {
    const { slackNotifier, queue, logger, metrics } = buildDeps({
      messages: [
        {
          ts: "111.000",
          text: "<@U001> help me please",
          user: "UHUMAN",
        },
      ],
    });

    const poller = new SlackMentionPoller({
      slackNotifier: slackNotifier as never,
      queue,
      slackBotUserId: "U001",
      logger,
      metrics,
    });

    await (poller as unknown as { poll(): Promise<void> }).poll();

    expect(queue.enqueued).toHaveLength(1);
    expect(queue.enqueued[0]).toMatchObject({
      jobKind: "slack_mention",
      channelId: "C001",
      userId: "UHUMAN",
    });
  });

  test("does not enqueue bot messages (bot_id set)", async () => {
    const { slackNotifier, queue, logger, metrics } = buildDeps({
      messages: [
        {
          ts: "111.000",
          text: "<@U001> help me",
          user: "UHUMAN",
          bot_id: "B001", // this is a bot message
        },
      ],
    });

    const poller = new SlackMentionPoller({
      slackNotifier: slackNotifier as never,
      queue,
      slackBotUserId: "U001",
      logger,
      metrics,
    });

    await (poller as unknown as { poll(): Promise<void> }).poll();

    expect(queue.enqueued).toHaveLength(0);
  });

  test("does not enqueue when mention text is empty after removing the tag", async () => {
    const { slackNotifier, queue, logger, metrics } = buildDeps({
      messages: [
        {
          ts: "111.000",
          text: "<@U001>", // only mention, no actual content
          user: "UHUMAN",
        },
      ],
    });

    const poller = new SlackMentionPoller({
      slackNotifier: slackNotifier as never,
      queue,
      slackBotUserId: "U001",
      logger,
      metrics,
    });

    await (poller as unknown as { poll(): Promise<void> }).poll();

    expect(queue.enqueued).toHaveLength(0);
  });

  test("skips a mention already marked done (white_check_mark)", async () => {
    const { slackNotifier, queue, logger, metrics } = buildDeps({
      messages: [
        {
          ts: "111.000",
          text: "<@U001> please help",
          user: "UHUMAN",
        },
      ],
      reactions: { claimed: true, done: true, failed: false },
    });

    const poller = new SlackMentionPoller({
      slackNotifier: slackNotifier as never,
      queue,
      slackBotUserId: "U001",
      logger,
      metrics,
    });

    await (poller as unknown as { poll(): Promise<void> }).poll();

    expect(queue.enqueued).toHaveLength(0);
    expect(slackNotifier.addReaction).not.toHaveBeenCalled();
  });

  test("skips a mention already marked failed (x)", async () => {
    const { slackNotifier, queue, logger, metrics } = buildDeps({
      messages: [
        {
          ts: "111.000",
          text: "<@U001> please help",
          user: "UHUMAN",
        },
      ],
      reactions: { claimed: true, done: false, failed: true },
    });

    const poller = new SlackMentionPoller({
      slackNotifier: slackNotifier as never,
      queue,
      slackBotUserId: "U001",
      logger,
      metrics,
    });

    await (poller as unknown as { poll(): Promise<void> }).poll();

    expect(queue.enqueued).toHaveLength(0);
  });

  test("enqueues an unmarked mention and adds eyes", async () => {
    const { slackNotifier, queue, logger, metrics } = buildDeps({
      messages: [
        {
          ts: "111.000",
          text: "<@U001> please help",
          user: "UHUMAN",
        },
      ],
      reactions: { claimed: false, done: false, failed: false },
    });

    const poller = new SlackMentionPoller({
      slackNotifier: slackNotifier as never,
      queue,
      slackBotUserId: "U001",
      logger,
      metrics,
    });

    await (poller as unknown as { poll(): Promise<void> }).poll();

    expect(queue.enqueued).toHaveLength(1);
    expect(slackNotifier.addReaction).toHaveBeenCalledWith(
      "C001",
      "111.000",
      SLACK_MENTION.REACTION_CLAIMED,
    );
  });

  test("re-enqueues an interrupted mention (only eyes) without re-adding eyes", async () => {
    const { slackNotifier, queue, logger, metrics } = buildDeps({
      messages: [
        {
          ts: "111.000",
          text: "<@U001> please help",
          user: "UHUMAN",
        },
      ],
      reactions: { claimed: true, done: false, failed: false },
    });

    const poller = new SlackMentionPoller({
      slackNotifier: slackNotifier as never,
      queue,
      slackBotUserId: "U001",
      logger,
      metrics,
    });

    await (poller as unknown as { poll(): Promise<void> }).poll();

    expect(queue.enqueued).toHaveLength(1);
    expect(slackNotifier.addReaction).not.toHaveBeenCalled();
  });

  test("does NOT re-enqueue when the bot already replied even if the ✅ reaction is missing", async () => {
    // C2 regression: reply succeeded but the advisory ✅ write failed, so only 👀
    // is present. The authoritative reply-existence check must prevent a duplicate reply.
    const { slackNotifier, queue, logger, metrics } = buildDeps({
      messages: [
        {
          ts: "111.000",
          text: "<@U001> please help",
          user: "UHUMAN",
        },
      ],
      reactions: { claimed: true, done: false, failed: false },
      repliedAfter: true,
    });

    const poller = new SlackMentionPoller({
      slackNotifier: slackNotifier as never,
      queue,
      slackBotUserId: "U001",
      logger,
      metrics,
    });

    await (poller as unknown as { poll(): Promise<void> }).poll();

    expect(queue.enqueued).toHaveLength(0);
  });

  test("skips a mention safely when getBotReactions throws (does not crash the poll)", async () => {
    const { slackNotifier, queue, logger, metrics } = buildDeps({
      messages: [
        {
          ts: "111.000",
          text: "<@U001> please help",
          user: "UHUMAN",
        },
      ],
      reactionsError: new Error("reactions API down"),
    });

    const poller = new SlackMentionPoller({
      slackNotifier: slackNotifier as never,
      queue,
      slackBotUserId: "U001",
      logger,
      metrics,
    });

    await (poller as unknown as { poll(): Promise<void> }).poll();

    expect(queue.enqueued).toHaveLength(0);
    // poll itself completed cleanly (no consecutive failure recorded)
    expect((poller as unknown as { consecutiveFailures: number }).consecutiveFailures).toBe(0);
  });

  test("enqueues thread reply with mention", async () => {
    const { slackNotifier, queue, logger, metrics } = buildDeps({
      messages: [
        {
          ts: "100.000",
          text: "parent message",
          user: "UHUMAN",
          reply_count: 1,
        },
      ],
      threadReplies: [
        // Skip the parent
        { ts: "100.000", text: "parent message", user: "UHUMAN" },
        // Actual reply with mention
        { ts: "200.000", text: "<@U001> could you review this?", user: "UHUMAN2" },
      ],
    });

    const poller = new SlackMentionPoller({
      slackNotifier: slackNotifier as never,
      queue,
      slackBotUserId: "U001",
      logger,
      metrics,
    });

    await (poller as unknown as { poll(): Promise<void> }).poll();

    expect(queue.enqueued).toHaveLength(1);
    expect(queue.enqueued[0]).toMatchObject({
      jobKind: "slack_mention",
      threadTs: "100.000",
      messageTs: "200.000",
    });
  });

  test("does not enqueue thread replies without the mention tag", async () => {
    const { slackNotifier, queue, logger, metrics } = buildDeps({
      messages: [
        {
          ts: "100.000",
          text: "parent message",
          user: "UHUMAN",
          reply_count: 1,
        },
      ],
      threadReplies: [
        { ts: "100.000", text: "parent message", user: "UHUMAN" },
        { ts: "200.000", text: "just a comment without mention", user: "UHUMAN2" },
      ],
    });

    const poller = new SlackMentionPoller({
      slackNotifier: slackNotifier as never,
      queue,
      slackBotUserId: "U001",
      logger,
      metrics,
    });

    await (poller as unknown as { poll(): Promise<void> }).poll();

    expect(queue.enqueued).toHaveLength(0);
  });
});

describe("SlackMentionPoller — error handling", () => {
  test("increments consecutiveFailures on error", async () => {
    const { slackNotifier, queue, logger, metrics } = buildDeps({
      listError: new Error("Slack API down"),
    });

    const poller = new SlackMentionPoller({
      slackNotifier: slackNotifier as never,
      queue,
      slackBotUserId: "U001",
      logger,
      metrics,
    });

    await (poller as unknown as { poll(): Promise<void> }).poll();

    expect((poller as unknown as { consecutiveFailures: number }).consecutiveFailures).toBe(1);
    expect(logger.warn).toHaveBeenCalled();
  });

  test("resets consecutiveFailures after a successful poll", async () => {
    const { slackNotifier, queue, logger, metrics } = buildDeps({ messages: [] });
    const poller = new SlackMentionPoller({
      slackNotifier: slackNotifier as never,
      queue,
      slackBotUserId: "U001",
      logger,
      metrics,
    });

    // Simulate a previous failure
    (poller as unknown as { consecutiveFailures: number }).consecutiveFailures = 2;

    await (poller as unknown as { poll(): Promise<void> }).poll();

    expect((poller as unknown as { consecutiveFailures: number }).consecutiveFailures).toBe(0);
  });
});

describe("SlackMentionPoller — lifecycle", () => {
  test("start() is idempotent — double start does not throw", () => {
    const { slackNotifier, queue, logger, metrics } = buildDeps({ messages: [] });
    const poller = new SlackMentionPoller({
      slackNotifier: slackNotifier as never,
      queue,
      slackBotUserId: "U001",
      logger,
      metrics,
    });

    expect(() => {
      poller.start();
      poller.start();
    }).not.toThrow();
    poller.stop();
  });

  test("stop() clears the timer", () => {
    const { slackNotifier, queue, logger, metrics } = buildDeps({ messages: [] });
    const poller = new SlackMentionPoller({
      slackNotifier: slackNotifier as never,
      queue,
      slackBotUserId: "U001",
      logger,
      metrics,
    });

    poller.start();
    expect(() => poller.stop()).not.toThrow();
  });

  test("stop() on un-started poller does not throw", () => {
    const { slackNotifier, queue, logger, metrics } = buildDeps({ messages: [] });
    const poller = new SlackMentionPoller({
      slackNotifier: slackNotifier as never,
      queue,
      slackBotUserId: "U001",
      logger,
      metrics,
    });

    expect(() => poller.stop()).not.toThrow();
  });
});

describe("SlackMentionPoller — metrics", () => {
  test("emits poll.executed on a clean poll (empty channel list)", async () => {
    const { slackNotifier, queue, logger, metrics } = buildDeps({ channels: [], messages: [] });
    const poller = new SlackMentionPoller({
      slackNotifier: slackNotifier as never,
      queue,
      slackBotUserId: "U001",
      logger,
      metrics,
    });

    await (poller as unknown as { poll(): Promise<void> }).poll();

    expect(metrics.countOf(METRIC.pollExecuted)).toBe(1);
    expect(metrics.last(METRIC.pollExecuted)?.tags).toMatchObject({ poller: "slack_mention" });
  });

  test("emits poll.error and poll.auth_failure on account_inactive; escalates to error after N failures", async () => {
    const authError = new Error("account_inactive");
    const { slackNotifier, queue, logger, metrics } = buildDeps({ listError: authError });
    const poller = new SlackMentionPoller({
      slackNotifier: slackNotifier as never,
      queue,
      slackBotUserId: "U001",
      logger,
      metrics,
    });

    const pollMethod = (poller as unknown as { poll(): Promise<void> }).poll.bind(poller);

    // Three consecutive failures to reach FAILURE_ESCALATION_THRESHOLD (3)
    await pollMethod();
    await pollMethod();
    await pollMethod();

    // poll.error should have been incremented 3 times
    expect(metrics.countOf(METRIC.pollError)).toBe(3);

    // poll.auth_failure should have been incremented 3 times (account_inactive matches)
    expect(metrics.countOf(METRIC.pollAuthFailure)).toBeGreaterThanOrEqual(1);
    expect(metrics.last(METRIC.pollAuthFailure)?.tags).toMatchObject({
      poller: "slack_mention",
      integration: "slack",
    });

    // After 3 failures consecutiveFailures === 3 >= threshold(3) → logger.error
    expect(logger.error).toHaveBeenCalled();
  });
});
