import { describe, it, expect, vi, beforeEach } from "vitest";
import { TRPCError } from "@trpc/server";

const mockResendSend = vi.fn();
const mockMessagingSend = vi.fn();
const mockTwilioCreate = vi.fn();

vi.mock("~/server/db", () => ({
  db: {
    select: vi.fn(),
    insert: vi.fn(),
    update: vi.fn(),
  },
}));

vi.mock("~/server/lib/resend", () => ({
  resend: { emails: { send: mockResendSend } },
}));

vi.mock("~/server/lib/fcm", () => ({
  messaging: { send: mockMessagingSend },
}));

vi.mock("~/server/lib/twilio", () => ({
  twilioClient: { messages: { create: mockTwilioCreate } },
}));

import { db } from "~/server/db";

beforeEach(() => {
  vi.clearAllMocks();
});

describe("sendEmail", () => {
  it("calls Resend with correct parameters", async () => {
    process.env.RESEND_API_KEY = "test-key";
    mockResendSend.mockResolvedValue({
      data: { id: "email-1" },
      error: null,
    });

    const { sendEmail } = await import("./notification.service");
    const result = await sendEmail("test@example.com", "Subject", "<p>Body</p>", "Text body");

    expect(mockResendSend).toHaveBeenCalledWith({
      from: "noreply@shieldai.app",
      to: "test@example.com",
      subject: "Subject",
      html: "<p>Body</p>",
      text: "Text body",
    });
    expect(result).toEqual({ id: "email-1" });
  });

  it("skips sending when Resend API key is not configured", async () => {
    delete process.env.RESEND_API_KEY;

    const { sendEmail } = await import("./notification.service");
    const result = await sendEmail("test@example.com", "Subject", "<p>Body</p>");

    expect(result).toEqual({ id: null });
    expect(mockResendSend).not.toHaveBeenCalled();
  });

  it("throws INTERNAL_SERVER_ERROR when Resend returns an error", async () => {
    process.env.RESEND_API_KEY = "test-key";
    mockResendSend.mockResolvedValue({
      data: null,
      error: { message: "API error" },
    });

    const { sendEmail } = await import("./notification.service");
    await expect(sendEmail("test@example.com", "Subject", "<p>Body</p>")).rejects.toThrow(TRPCError);
    await expect(sendEmail("test@example.com", "Subject", "<p>Body</p>")).rejects.toMatchObject({
      code: "INTERNAL_SERVER_ERROR",
    });
  });
});

describe("sendPush", () => {
  it("sends FCM message to all active devices", async () => {
    const devices = [
      { id: "d1", userId: "u1", token: "token-1", platform: "android", deviceType: "mobile", isActive: true },
      { id: "d2", userId: "u1", token: "token-2", platform: "ios", deviceType: "mobile", isActive: true },
    ];

    (db.select as ReturnType<typeof vi.fn>).mockReturnValue({
      from: vi.fn().mockReturnValue({
        where: vi.fn().mockResolvedValue(devices),
      }),
    });

    mockMessagingSend.mockResolvedValue({});

    const { sendPush } = await import("./notification.service");
    const result = await sendPush("u1", "Title", "Body", { key: "val" });

    expect(result).toEqual({ successCount: 2 });
    expect(mockMessagingSend).toHaveBeenCalledTimes(2);
    expect(mockMessagingSend).toHaveBeenCalledWith({
      token: "token-1",
      notification: { title: "Title", body: "Body" },
      data: { key: "val" },
      android: { priority: "high" },
      apns: { payload: { aps: { alert: { title: "Title", body: "Body" }, sound: "default", badge: 1 } } },
    });
  });

  it("returns 0 success when no active devices", async () => {
    (db.select as ReturnType<typeof vi.fn>).mockReturnValue({
      from: vi.fn().mockReturnValue({
        where: vi.fn().mockResolvedValue([]),
      }),
    });

    const { sendPush } = await import("./notification.service");
    const result = await sendPush("u1", "Title", "Body");

    expect(result).toEqual({ successCount: 0 });
    expect(mockMessagingSend).not.toHaveBeenCalled();
  });

  it("continues sending if one push fails", async () => {
    const devices = [
      { id: "d1", userId: "u1", token: "token-1", platform: "android", deviceType: "mobile", isActive: true },
      { id: "d2", userId: "u1", token: "token-2", platform: "ios", deviceType: "mobile", isActive: true },
    ];

    (db.select as ReturnType<typeof vi.fn>).mockReturnValue({
      from: vi.fn().mockReturnValue({
        where: vi.fn().mockResolvedValue(devices),
      }),
    });

    mockMessagingSend
      .mockRejectedValueOnce(new Error("FCM error"))
      .mockResolvedValueOnce({});

    const { sendPush } = await import("./notification.service");
    const result = await sendPush("u1", "Title", "Body");

    expect(result).toEqual({ successCount: 1 });
  });
});

describe("sendSMS", () => {
  it("calls Twilio with correct parameters", async () => {
    process.env.TWILIO_MESSAGING_SERVICE_SID = "MGxxx";
    mockTwilioCreate.mockResolvedValue({ sid: "SMxxx" });

    const { sendSMS } = await import("./notification.service");
    const result = await sendSMS("+1234567890", "Hello");

    expect(mockTwilioCreate).toHaveBeenCalledWith({
      body: "Hello",
      to: "+1234567890",
      messagingServiceSid: "MGxxx",
    });
    expect(result).toEqual({ sid: "SMxxx" });
  });

  it("throws BAD_REQUEST for non-E.164 phone numbers", async () => {
    const { sendSMS } = await import("./notification.service");
    await expect(sendSMS("1234567890", "Hello")).rejects.toThrow(TRPCError);
    await expect(sendSMS("1234567890", "Hello")).rejects.toMatchObject({
      code: "BAD_REQUEST",
    });

    await expect(sendSMS("+12", "Hello")).rejects.toMatchObject({
      code: "BAD_REQUEST",
    });
  });

  it("accepts valid E.164 phone numbers", async () => {
    mockTwilioCreate.mockResolvedValue({ sid: "SMxxx" });

    const { sendSMS } = await import("./notification.service");
    await expect(sendSMS("+1234567890", "Hello")).resolves.toEqual({ sid: "SMxxx" });
    await expect(sendSMS("+447911123456", "Hello")).resolves.toEqual({ sid: "SMxxx" });
  });
});

describe("registerDevice", () => {
  it("creates a new device token record", async () => {
    (db.select as ReturnType<typeof vi.fn>).mockReturnValue({
      from: vi.fn().mockReturnValue({
        where: vi.fn().mockReturnValue({
          limit: vi.fn().mockResolvedValue([]),
        }),
      }),
    });

    const newDevice = {
      id: "d-new",
      userId: "u1",
      token: "new-token",
      platform: "android",
      deviceType: "mobile",
      isActive: true,
      lastUsedAt: new Date(),
    };

    (db.insert as ReturnType<typeof vi.fn>).mockReturnValue({
      values: vi.fn().mockReturnValue({
        returning: vi.fn().mockResolvedValue([newDevice]),
      }),
    });

    const { registerDevice } = await import("./notification.service");
    const result = await registerDevice("u1", "new-token", "android", "mobile");

    expect(result).toEqual(newDevice);
    expect(db.insert).toHaveBeenCalled();
  });

  it("reactivates an existing token for the same user", async () => {
    const existing = {
      id: "d1",
      userId: "u1",
      token: "existing-token",
      platform: "android",
      deviceType: "mobile",
      isActive: false,
      lastUsedAt: new Date(0),
    };

    (db.select as ReturnType<typeof vi.fn>).mockReturnValue({
      from: vi.fn().mockReturnValue({
        where: vi.fn().mockReturnValue({
          limit: vi.fn().mockResolvedValue([existing]),
        }),
      }),
    });

    const updated = { ...existing, isActive: true, lastUsedAt: expect.any(Date) };

    (db.update as ReturnType<typeof vi.fn>).mockReturnValue({
      set: vi.fn().mockReturnValue({
        where: vi.fn().mockReturnValue({
          returning: vi.fn().mockResolvedValue([updated]),
        }),
      }),
    });

    const { registerDevice } = await import("./notification.service");
    const result = await registerDevice("u1", "existing-token", "android", "mobile");

    expect(result).toEqual(updated);
    expect(db.update).toHaveBeenCalled();
  });

  it("throws CONFLICT when token belongs to another user", async () => {
    const existing = {
      id: "d1",
      userId: "u2",
      token: "other-user-token",
      platform: "android",
      deviceType: "mobile",
      isActive: true,
    };

    (db.select as ReturnType<typeof vi.fn>).mockReturnValue({
      from: vi.fn().mockReturnValue({
        where: vi.fn().mockReturnValue({
          limit: vi.fn().mockResolvedValue([existing]),
        }),
      }),
    });

    const { registerDevice } = await import("./notification.service");
    await expect(registerDevice("u1", "other-user-token", "android", "mobile")).rejects.toThrow(TRPCError);
    await expect(registerDevice("u1", "other-user-token", "android", "mobile")).rejects.toMatchObject({
      code: "CONFLICT",
    });
  });
});

describe("unregisterDevice", () => {
  it("marks a device token as inactive", async () => {
    const existing = {
      id: "d1",
      userId: "u1",
      token: "token-1",
      platform: "android",
      deviceType: "mobile",
      isActive: true,
    };

    (db.select as ReturnType<typeof vi.fn>).mockReturnValue({
      from: vi.fn().mockReturnValue({
        where: vi.fn().mockReturnValue({
          limit: vi.fn().mockResolvedValue([existing]),
        }),
      }),
    });

    const deactivated = { ...existing, isActive: false };

    (db.update as ReturnType<typeof vi.fn>).mockReturnValue({
      set: vi.fn().mockReturnValue({
        where: vi.fn().mockReturnValue({
          returning: vi.fn().mockResolvedValue([deactivated]),
        }),
      }),
    });

    const { unregisterDevice } = await import("./notification.service");
    const result = await unregisterDevice("u1", "token-1");

    expect(result.isActive).toBe(false);
  });

  it("throws NOT_FOUND when token does not exist", async () => {
    (db.select as ReturnType<typeof vi.fn>).mockReturnValue({
      from: vi.fn().mockReturnValue({
        where: vi.fn().mockReturnValue({
          limit: vi.fn().mockResolvedValue([]),
        }),
      }),
    });

    const { unregisterDevice } = await import("./notification.service");
    await expect(unregisterDevice("u1", "nonexistent")).rejects.toThrow(TRPCError);
    await expect(unregisterDevice("u1", "nonexistent")).rejects.toMatchObject({
      code: "NOT_FOUND",
    });
  });
});

describe("listDevices", () => {
  it("returns all devices for a user ordered by creation date", async () => {
    const devices = [
      { id: "d1", userId: "u1", token: "token-1", platform: "android", createdAt: new Date("2024-01-01") },
      { id: "d2", userId: "u1", token: "token-2", platform: "ios", createdAt: new Date("2024-01-02") },
    ];

    (db.select as ReturnType<typeof vi.fn>).mockReturnValue({
      from: vi.fn().mockReturnValue({
        where: vi.fn().mockReturnValue({
          orderBy: vi.fn().mockResolvedValue(devices),
        }),
      }),
    });

    const { listDevices } = await import("./notification.service");
    const result = await listDevices("u1");

    expect(result).toEqual(devices);
    expect(result).toHaveLength(2);
  });
});

describe("getPreferences", () => {
  it("returns existing preferences from DB", async () => {
    const prefs = {
      id: "p1",
      userId: "u1",
      emailEnabled: false,
      pushEnabled: true,
      smsEnabled: false,
    };

    (db.select as ReturnType<typeof vi.fn>).mockReturnValue({
      from: vi.fn().mockReturnValue({
        where: vi.fn().mockReturnValue({
          limit: vi.fn().mockResolvedValue([prefs]),
        }),
      }),
    });

    const { getPreferences } = await import("./notification.service");
    const result = await getPreferences("u1");

    expect(result).toMatchObject({
      emailEnabled: false,
      pushEnabled: true,
      smsEnabled: false,
    });
  });

  it("returns default preferences when no record exists", async () => {
    (db.select as ReturnType<typeof vi.fn>).mockReturnValue({
      from: vi.fn().mockReturnValue({
        where: vi.fn().mockReturnValue({
          limit: vi.fn().mockResolvedValue([]),
        }),
      }),
    });

    const { getPreferences } = await import("./notification.service");
    const result = await getPreferences("u1");

    expect(result).toEqual({
      emailEnabled: true,
      pushEnabled: true,
      smsEnabled: true,
    });
  });
});

describe("updatePreferences", () => {
  it("updates existing preferences", async () => {
    const existing = {
      id: "p1",
      userId: "u1",
      emailEnabled: true,
      pushEnabled: true,
      smsEnabled: true,
    };

    (db.select as ReturnType<typeof vi.fn>).mockReturnValue({
      from: vi.fn().mockReturnValue({
        where: vi.fn().mockReturnValue({
          limit: vi.fn().mockResolvedValue([existing]),
        }),
      }),
    });

    const updated = { ...existing, smsEnabled: false };

    (db.update as ReturnType<typeof vi.fn>).mockReturnValue({
      set: vi.fn().mockReturnValue({
        where: vi.fn().mockReturnValue({
          returning: vi.fn().mockResolvedValue([updated]),
        }),
      }),
    });

    const { updatePreferences } = await import("./notification.service");
    const result = await updatePreferences("u1", { smsEnabled: false });

    expect(result.smsEnabled).toBe(false);
  });

  it("creates new preferences record when none exists", async () => {
    (db.select as ReturnType<typeof vi.fn>).mockReturnValue({
      from: vi.fn().mockReturnValue({
        where: vi.fn().mockReturnValue({
          limit: vi.fn().mockResolvedValue([]),
        }),
      }),
    });

    const created = {
      id: "p-new",
      userId: "u1",
      emailEnabled: false,
      pushEnabled: true,
      smsEnabled: true,
    };

    (db.insert as ReturnType<typeof vi.fn>).mockReturnValue({
      values: vi.fn().mockReturnValue({
        returning: vi.fn().mockResolvedValue([created]),
      }),
    });

    const { updatePreferences } = await import("./notification.service");
    const result = await updatePreferences("u1", { emailEnabled: false });

    expect(result).toEqual(created);
    expect(db.insert).toHaveBeenCalled();
  });
});
