import { describe, expect, test } from "bun:test";
import { PollRunner } from "@/poller/poll-runner.js";
import { FakeMetrics } from "../_helpers/fake-metrics.js";
import { mockLogger } from "../_helpers/mock-logger.js";
import { mock } from "bun:test";
import type { IJobQueue, JobKind } from "@/types/index.js";

function fakeQueue(over: Partial<IJobQueue> = {}): IJobQueue {
  return {
    enqueue: async () => {},
    start: () => {},
    stop: () => {},
    isBusy: () => false,
    getCurrentJobKind: () => null,
    getCurrentJobKey: () => null,
    pendingCount: () => 0,
    getPendingJobKeys: () => [],
    ...over,
  } as IJobQueue;
}

describe("PollRunner", () => {
  test("runs body → emits poll.executed, poll.enqueued, poll.duration", async () => {
    const metrics = new FakeMetrics();
    const logger = mockLogger();
    const runner = new PollRunner({
      poller: "test-poller",
      queue: fakeQueue(),
      metrics,
      logger,
      contendingKinds: [],
      skipLogThrottleMs: 60_000,
    });

    const result = await runner.run(async () => 3);

    expect(result).toBe(3);
    expect(metrics.countOf("poll.executed")).toBe(1);
    expect(metrics.last("poll.executed")?.tags).toEqual({ poller: "test-poller" });
    expect(metrics.last("poll.enqueued")?.value).toBe(3);
    expect(metrics.last("poll.enqueued")?.tags).toEqual({ poller: "test-poller" });
    // duration fires unconditionally — just assert it was emitted
    expect(metrics.countOf("poll.duration")).toBe(1);
    expect(metrics.last("poll.duration")?.tags).toEqual({ poller: "test-poller" });
  });

  test("isBusy() true → body NOT run, returns 0, emits poll.skipped{reason:busy}, logs skip", async () => {
    const metrics = new FakeMetrics();
    const logger = mockLogger();
    const bodyCalled = mock(async () => 0);

    const runner = new PollRunner({
      poller: "test-poller",
      queue: fakeQueue({ isBusy: () => true }),
      metrics,
      logger,
      contendingKinds: [],
      skipLogThrottleMs: 0, // always log
    });

    const result = await runner.run(bodyCalled);

    expect(result).toBe(0);
    expect(bodyCalled).not.toHaveBeenCalled();
    expect(metrics.countOf("poll.skipped")).toBe(1);
    expect(metrics.last("poll.skipped")?.tags).toEqual({ poller: "test-poller", reason: "busy" });
    expect(logger.info).toHaveBeenCalled();
  });

  test("contending kind → body NOT run, returns 0, emits poll.skipped{reason:other_pipeline}", async () => {
    const metrics = new FakeMetrics();
    const logger = mockLogger();
    const contendingKind: JobKind = "jira";
    const bodyCalled = mock(async () => 0);

    const runner = new PollRunner({
      poller: "test-poller",
      queue: fakeQueue({
        isBusy: () => false,
        getCurrentJobKind: () => contendingKind,
      }),
      metrics,
      logger,
      contendingKinds: [contendingKind],
      skipLogThrottleMs: 0,
    });

    const result = await runner.run(bodyCalled);

    expect(result).toBe(0);
    expect(bodyCalled).not.toHaveBeenCalled();
    expect(metrics.countOf("poll.skipped")).toBe(1);
    expect(metrics.last("poll.skipped")?.tags).toEqual({
      poller: "test-poller",
      reason: "other_pipeline",
    });
  });

  test("body throws → returns 0, emits poll.error, logger.error called", async () => {
    const metrics = new FakeMetrics();
    const logger = mockLogger();
    const boom = new Error("exploded");

    const runner = new PollRunner({
      poller: "test-poller",
      queue: fakeQueue(),
      metrics,
      logger,
      contendingKinds: [],
      skipLogThrottleMs: 60_000,
    });

    const result = await runner.run(async () => {
      throw boom;
    });

    expect(result).toBe(0);
    expect(metrics.countOf("poll.error")).toBe(1);
    expect(metrics.last("poll.error")?.tags).toEqual({ poller: "test-poller" });
    expect(logger.error).toHaveBeenCalled();
  });
});
