import { beforeEach, describe, expect, it, vi } from "vitest"

const mocks = vi.hoisted(() => ({
  getNotificationPreferences: vi.fn(),
  getUserContactInfo: vi.fn(),
  logNotificationSendBatch: vi.fn(),
  createNotification: vi.fn(),
  inAppSend: vi.fn(),
  smsSend: vi.fn(),
  wechatSend: vi.fn(),
  emailSend: vi.fn(),
}))

vi.mock("./data-access", () => ({
  getUserContactInfo: mocks.getUserContactInfo,
  logNotificationSendBatch: mocks.logNotificationSendBatch,
  createNotification: mocks.createNotification,
}))

vi.mock("./preferences", () => ({
  getNotificationPreferences: mocks.getNotificationPreferences,
}))

vi.mock("./channels/sms-channel", () => ({
  createSmsSender: () => ({
    channel: "sms",
    send: mocks.smsSend,
    sendBatch: vi.fn(),
  }),
}))

vi.mock("./channels/wechat-channel", () => ({
  createWechatSender: () => ({
    channel: "wechat",
    send: mocks.wechatSend,
    sendBatch: vi.fn(),
  }),
}))

vi.mock("./channels/email-channel", () => ({
  createEmailSender: () => ({
    channel: "email",
    send: mocks.emailSend,
    sendBatch: vi.fn(),
  }),
}))

vi.mock("./channels/in-app-channel", () => ({
  createInAppSender: () => ({
    channel: "in_app",
    send: mocks.inAppSend,
    sendBatch: vi.fn(),
  }),
}))

import { sendNotification } from "./dispatcher"
import type { NotificationPayload } from "./types"

describe("sendNotification", () => {
  beforeEach(() => {
    // mockReset (from vitest config) clears implementations before beforeEach.
    // Re-establish channel send implementations so the cached sender registry
    // (module-level singleton in dispatcher.ts) keeps working across tests.
    mocks.inAppSend.mockImplementation(async (payload: NotificationPayload) => {
      const id = await mocks.createNotification({
        userId: payload.userId,
        type: "message",
        title: payload.title,
        content: payload.content,
        link: payload.actionUrl ?? null,
      })
      return {
        channel: "in_app" as const,
        success: true,
        messageId: id,
        sentAt: new Date(),
      }
    })
    mocks.smsSend.mockResolvedValue({
      channel: "sms",
      success: true,
      sentAt: new Date(),
    })
    mocks.wechatSend.mockResolvedValue({
      channel: "wechat",
      success: true,
      sentAt: new Date(),
    })
    mocks.emailSend.mockResolvedValue({
      channel: "email",
      success: true,
      sentAt: new Date(),
    })
  })

  it("should select in_app channel when pushEnabled is true and no contact info", async () => {
    mocks.getNotificationPreferences.mockResolvedValue({
      smsEnabled: false,
      emailEnabled: false,
      pushEnabled: true,
    })
    mocks.getUserContactInfo.mockResolvedValue({ userId: "user-1" })
    mocks.createNotification.mockResolvedValue("notif-1")
    mocks.logNotificationSendBatch.mockResolvedValue(undefined)

    const payload: NotificationPayload = {
      userId: "user-1",
      title: "Test notification",
      content: "Test content",
      type: "info",
    }

    const results = await sendNotification(payload)

    expect(results).toHaveLength(1)
    expect(results[0].channel).toBe("in_app")
    expect(results[0].success).toBe(true)
    expect(mocks.createNotification).toHaveBeenCalledWith(
      expect.objectContaining({
        userId: "user-1",
        title: "Test notification",
      })
    )
  })

  it("should select sms channel when smsEnabled and phone provided", async () => {
    mocks.getNotificationPreferences.mockResolvedValue({
      smsEnabled: true,
      emailEnabled: false,
      pushEnabled: true,
    })
    mocks.getUserContactInfo.mockResolvedValue({ userId: "user-1", phone: "13800138000" })
    mocks.createNotification.mockResolvedValue("notif-1")
    mocks.logNotificationSendBatch.mockResolvedValue(undefined)

    const payload: NotificationPayload = {
      userId: "user-1",
      title: "Test",
      content: "Content",
      type: "info",
    }

    const results = await sendNotification(payload)

    expect(results).toHaveLength(2)
    const channels = results.map((r) => r.channel)
    expect(channels).toContain("in_app")
    expect(channels).toContain("sms")
  })

  it("should select email channel when emailEnabled and email provided", async () => {
    mocks.getNotificationPreferences.mockResolvedValue({
      smsEnabled: false,
      emailEnabled: true,
      pushEnabled: true,
    })
    mocks.getUserContactInfo.mockResolvedValue({ userId: "user-1", email: "test@example.com" })
    mocks.createNotification.mockResolvedValue("notif-1")
    mocks.logNotificationSendBatch.mockResolvedValue(undefined)

    const payload: NotificationPayload = {
      userId: "user-1",
      title: "Test",
      content: "Content",
      type: "info",
    }

    const results = await sendNotification(payload)

    expect(results).toHaveLength(2)
    const channels = results.map((r) => r.channel)
    expect(channels).toContain("in_app")
    expect(channels).toContain("email")
  })

  it("should fallback to in_app when all channels disabled", async () => {
    mocks.getNotificationPreferences.mockResolvedValue({
      smsEnabled: false,
      emailEnabled: false,
      pushEnabled: false,
    })
    mocks.getUserContactInfo.mockResolvedValue({ userId: "user-1" })
    mocks.createNotification.mockResolvedValue("notif-1")
    mocks.logNotificationSendBatch.mockResolvedValue(undefined)

    const payload: NotificationPayload = {
      userId: "user-1",
      title: "Test",
      content: "Content",
      type: "info",
    }

    const results = await sendNotification(payload)

    // pushEnabled false 时，兜底逻辑应至少发 in_app
    expect(results).toHaveLength(1)
    expect(results[0].channel).toBe("in_app")
  })

  it("should select wechat channel when pushEnabled and wechatOpenId provided", async () => {
    mocks.getNotificationPreferences.mockResolvedValue({
      smsEnabled: false,
      emailEnabled: false,
      pushEnabled: true,
    })
    mocks.getUserContactInfo.mockResolvedValue({ userId: "user-1", wechatOpenId: "wx-open-id" })
    mocks.createNotification.mockResolvedValue("notif-1")
    mocks.logNotificationSendBatch.mockResolvedValue(undefined)

    const payload: NotificationPayload = {
      userId: "user-1",
      title: "Test",
      content: "Content",
      type: "info",
    }

    const results = await sendNotification(payload)

    expect(results).toHaveLength(2)
    const channels = results.map((r) => r.channel)
    expect(channels).toContain("in_app")
    expect(channels).toContain("wechat")
  })

  it("should call logNotificationSendBatch with results", async () => {
    mocks.getNotificationPreferences.mockResolvedValue({
      smsEnabled: false,
      emailEnabled: false,
      pushEnabled: true,
    })
    mocks.getUserContactInfo.mockResolvedValue({ userId: "user-1" })
    mocks.createNotification.mockResolvedValue("notif-1")
    mocks.logNotificationSendBatch.mockResolvedValue(undefined)

    const payload: NotificationPayload = {
      userId: "user-1",
      title: "Test",
      content: "Content",
      type: "info",
    }

    await sendNotification(payload)

    expect(mocks.logNotificationSendBatch).toHaveBeenCalledWith(
      expect.arrayContaining([
        expect.objectContaining({ channel: "in_app", success: true }),
      ]),
      { userId: "user-1", title: "Test" }
    )
  })
})
