import { describe, test, expect, mock, beforeEach } from "bun:test";
import { JiraTicketReviewPoller } from "@/poller/jira-ticket-review-poller.js";
import {
  markTicketReviewTriggerProcessed,
  resetTicketReviewProcessedStoreForTests,
  ticketReviewSessionStartMsForTests,
} from "@/poller/ticket-review-processed.js";
import type { JiraClientImpl } from "@/jira/client.js";
import { mockLogger } from "../_helpers/mock-logger.js";
import { FakeMetrics } from "../_helpers/fake-metrics.js";
import { METRIC } from "@/metrics/names.js";
import type { IJobQueue } from "@/types/index.js";

function makeQueue(enqueue: () => Promise<void>): IJobQueue {
  return {
    enqueue,
    start: () => {},
    stop: () => {},
    shutdown: async (_graceMs: number) => {},
    isBusy: () => false,
    getCurrentJobKind: () => null,
    pendingCount: () => 0,
    getCurrentJobKey: () => null,
    getPendingJobKeys: () => [],
  };
}

const BOT_AID = "bot-account-111";

function makeIssue(
  issueKey: string,
  comment: { id: string; authorAccountId: string; body: string; created: string },
) {
  return {
    id: `issue-${issueKey}`,
    key: issueKey,
    summary: "Summary",
    comments: [
      {
        id: comment.id,
        author: { accountId: comment.authorAccountId },
        body: comment.body,
        created: comment.created,
      },
    ],
  };
}

describe("JiraTicketReviewPoller", () => {
  beforeEach(() => {
    resetTicketReviewProcessedStoreForTests({ removePersistedFile: true });
  });

  test("start and stop run without throwing", () => {
    const poller = new JiraTicketReviewPoller({
      jiraClient: {
        resolveAccountIdForBotEmail: mock(async () => BOT_AID),
        searchIssuesLite: mock(async () => ({ issues: [] })),
      } as unknown as JiraClientImpl,
      queue: makeQueue(mock(() => Promise.resolve())),
      botEmail: "bot@example.com",
      squadScope: [],
      logger: mockLogger(),
      metrics: new FakeMetrics(),
    });
    poller.start();
    poller.stop();
  });

  test("poll returns 0 when queue is busy", async () => {
    const poller = new JiraTicketReviewPoller({
      jiraClient: {
        resolveAccountIdForBotEmail: mock(async () => BOT_AID),
        searchIssuesLite: mock(async () => ({ issues: [] })),
      } as unknown as JiraClientImpl,
      queue: {
        ...makeQueue(mock(() => Promise.resolve())),
        isBusy: () => true,
      },
      botEmail: "bot@example.com",
      squadScope: [],
      logger: mockLogger(),
      metrics: new FakeMetrics(),
    });
    expect(await poller.poll()).toBe(0);
  });

  test("poll returns 0 when a jira fix job is running", async () => {
    const poller = new JiraTicketReviewPoller({
      jiraClient: {
        resolveAccountIdForBotEmail: mock(async () => BOT_AID),
        searchIssuesLite: mock(async () => ({ issues: [] })),
      } as unknown as JiraClientImpl,
      queue: {
        ...makeQueue(mock(() => Promise.resolve())),
        getCurrentJobKind: () => "jira",
      },
      botEmail: "bot@example.com",
      squadScope: [],
      logger: mockLogger(),
      metrics: new FakeMetrics(),
    });
    expect(await poller.poll()).toBe(0);
  });

  test("does not enqueue when store file missing and comment is before session start", async () => {
    const enqueue = mock(() => Promise.resolve());
    const poller = new JiraTicketReviewPoller({
      jiraClient: {
        resolveAccountIdForBotEmail: mock(async () => BOT_AID),
        searchIssuesLite: mock(async () => ({
          issues: [
            makeIssue("SQ-1", {
              id: "c-old",
              authorAccountId: "human-1",
              body: `Please review-ticket [mention:${BOT_AID}]`,
              created: new Date(ticketReviewSessionStartMsForTests() - 86_400_000).toISOString(),
            }),
          ],
        })),
      } as unknown as JiraClientImpl,
      queue: makeQueue(enqueue),
      botEmail: "bot@example.com",
      squadScope: [],
      logger: mockLogger(),
      metrics: new FakeMetrics(),
    });

    const n = await poller.poll();
    expect(n).toBe(0);
    expect(enqueue).not.toHaveBeenCalled();
  });

  test("enqueues when store file missing but comment is after session start", async () => {
    const enqueue = mock(() => Promise.resolve());
    const poller = new JiraTicketReviewPoller({
      jiraClient: {
        resolveAccountIdForBotEmail: mock(async () => BOT_AID),
        searchIssuesLite: mock(async () => ({
          issues: [
            makeIssue("SQ-2", {
              id: "c-new",
              authorAccountId: "human-1",
              body: `Please review-ticket [mention:${BOT_AID}]`,
              created: new Date(ticketReviewSessionStartMsForTests() + 5_000).toISOString(),
            }),
          ],
        })),
      } as unknown as JiraClientImpl,
      queue: makeQueue(enqueue),
      botEmail: "bot@example.com",
      squadScope: [],
      logger: mockLogger(),
      metrics: new FakeMetrics(),
    });

    const n = await poller.poll();
    expect(n).toBe(1);
    expect(enqueue).toHaveBeenCalledTimes(1);
  });

  test("does not apply session cutoff once persistence file exists", async () => {
    markTicketReviewTriggerProcessed("WARM", "0");
    const enqueue = mock(() => Promise.resolve());
    const poller = new JiraTicketReviewPoller({
      jiraClient: {
        resolveAccountIdForBotEmail: mock(async () => BOT_AID),
        searchIssuesLite: mock(async () => ({
          issues: [
            makeIssue("SQ-3", {
              id: "c-legacy",
              authorAccountId: "human-1",
              body: `Please review-ticket [mention:${BOT_AID}]`,
              created: new Date(
                ticketReviewSessionStartMsForTests() - 10 * 86_400_000,
              ).toISOString(),
            }),
          ],
        })),
      } as unknown as JiraClientImpl,
      queue: makeQueue(enqueue),
      botEmail: "bot@example.com",
      squadScope: [],
      logger: mockLogger(),
      metrics: new FakeMetrics(),
    });

    const n = await poller.poll();
    expect(n).toBe(1);
    expect(enqueue).toHaveBeenCalledTimes(1);
  });

  describe("auth failure handling", () => {
    test("a clean poll emits poll.executed", async () => {
      const metrics = new FakeMetrics();
      const enqueue = mock(() => Promise.resolve());
      const poller = new JiraTicketReviewPoller({
        jiraClient: {
          resolveAccountIdForBotEmail: mock(async () => BOT_AID),
          searchIssuesLite: mock(async () => ({ issues: [] })),
        } as unknown as JiraClientImpl,
        queue: makeQueue(enqueue),
        botEmail: "bot@example.com",
        squadScope: [],
        logger: mockLogger(),
        metrics,
      });

      await poller.poll();

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

    test("a 403 from searchIssuesLite emits poll.auth_failure", async () => {
      const authError = new Error("HTTP 403 Forbidden");
      const metrics = new FakeMetrics();
      const logger = mockLogger();

      const poller = new JiraTicketReviewPoller({
        jiraClient: {
          resolveAccountIdForBotEmail: mock(async () => BOT_AID),
          searchIssuesLite: mock(async () => {
            throw authError;
          }),
        } as unknown as JiraClientImpl,
        queue: makeQueue(mock(() => Promise.resolve())),
        botEmail: "bot@example.com",
        squadScope: [],
        logger,
        metrics,
      });

      await poller.poll();

      expect(metrics.countOf(METRIC.pollAuthFailure)).toBe(1);
      expect(metrics.last(METRIC.pollAuthFailure)?.tags).toMatchObject({
        poller: "jira_ticket_review",
        integration: "jira",
      });
      expect(logger.error).toHaveBeenCalled();
    });
  });
});
