import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { emitNotificationCreated, emitReminderDue } from "../sse/event-bus.js";
import { logger } from "../lib/logger.js";
import { startReminderScheduler, stopReminderScheduler } from "../lib/reminder-scheduler.js";

const { findMany, create, update } = vi.hoisted(() => ({
  findMany: vi.fn(),
  create: vi.fn(),
  update: vi.fn(),
}));

vi.mock("@capakraken/db", () => ({
  prisma: {
    notification: {
      findMany,
      create,
      update,
    },
  },
}));

vi.mock("../sse/event-bus.js", () => ({
  emitReminderDue: vi.fn(),
  emitNotificationCreated: vi.fn(),
}));

vi.mock("../lib/logger.js", () => ({
  logger: {
    error: vi.fn(),
    warn: vi.fn(),
    info: vi.fn(),
    debug: vi.fn(),
  },
}));

describe("reminder scheduler logging", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    vi.useRealTimers();
    findMany.mockResolvedValue([]);
    create.mockResolvedValue({ id: "notification_1" });
    update.mockResolvedValue({});
  });

  afterEach(() => {
    stopReminderScheduler();
  });

  it("logs scheduler lifecycle with structured metadata", async () => {
    startReminderScheduler();

    await vi.waitFor(() => {
      expect(findMany).toHaveBeenCalled();
    });

    expect(logger.info).toHaveBeenCalledWith(
      { pollIntervalMs: 60_000 },
      "Starting reminder scheduler",
    );

    stopReminderScheduler();

    expect(logger.info).toHaveBeenCalledWith("Stopped reminder scheduler");
  });

  it("logs reminder processing failures with reminder context", async () => {
    findMany.mockResolvedValueOnce([
      {
        id: "reminder_1",
        userId: "user_1",
        recurrence: null,
        priority: "HIGH",
        title: "Follow up",
        body: "Body",
        entityId: null,
        entityType: null,
        link: null,
        nextRemindAt: new Date("2026-03-30T08:00:00.000Z"),
      },
    ]);
    update.mockRejectedValueOnce(new Error("db write failed"));

    startReminderScheduler();

    await vi.waitFor(() => {
      expect(logger.error).toHaveBeenCalledWith(
        {
          err: expect.any(Error),
          reminderId: "reminder_1",
          userId: "user_1",
        },
        "Failed to process reminder",
      );
    });

    expect(emitReminderDue).not.toHaveBeenCalled();
    expect(emitNotificationCreated).not.toHaveBeenCalled();
  });

  it("does not start a second reminder poll while the previous batch is still running", async () => {
    vi.useFakeTimers();

    let releaseFindMany: ((value: unknown[]) => void) | null = null;
    findMany.mockImplementationOnce(
      () =>
        new Promise((resolve) => {
          releaseFindMany = resolve;
        }),
    );

    startReminderScheduler();

    expect(findMany).toHaveBeenCalledTimes(1);

    await vi.advanceTimersByTimeAsync(60_000);

    expect(findMany).toHaveBeenCalledTimes(1);
    expect(logger.warn).toHaveBeenCalledWith(
      "Skipping reminder scheduler tick while previous run is still processing",
    );

    releaseFindMany?.([]);
    await Promise.resolve();

    await vi.advanceTimersByTimeAsync(60_000);

    expect(findMany).toHaveBeenCalledTimes(2);
  });

  it("advances overdue recurring reminders to the first occurrence after now", async () => {
    const now = new Date();
    const staleNextRemindAt = new Date(now);
    staleNextRemindAt.setDate(staleNextRemindAt.getDate() - 3);
    staleNextRemindAt.setHours(8, 0, 0, 0);

    const expectedNextRemindAt = new Date(staleNextRemindAt);
    while (expectedNextRemindAt.getTime() <= now.getTime()) {
      expectedNextRemindAt.setDate(expectedNextRemindAt.getDate() + 1);
    }

    const overdueReminder = {
      id: "reminder_recurring_1",
      userId: "user_1",
      recurrence: "daily",
      priority: "MEDIUM",
      title: "Daily standup prep",
      body: "Body",
      entityId: null,
      entityType: null,
      link: null,
      nextRemindAt: staleNextRemindAt,
    };

    findMany.mockResolvedValueOnce([overdueReminder]);

    startReminderScheduler();

    await vi.waitFor(() => {
      expect(create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            userId: "user_1",
            sourceId: "reminder_recurring_1",
            type: "REMINDER_DUE",
          }),
        }),
      );
    });

    expect(update).toHaveBeenCalledWith({
      where: { id: "reminder_recurring_1" },
      data: {
        nextRemindAt: expectedNextRemindAt,
      },
    });
    expect(emitNotificationCreated).toHaveBeenCalledWith("user_1", "notification_1");
    expect(emitReminderDue).toHaveBeenCalledWith("user_1", "notification_1");
  });
});
