import { describe, it, expect, vi, beforeEach } from 'vitest';

// Mock email sending
vi.mock('@/lib/email/send', () => ({
  sendDeviceOfflineEmail: vi.fn().mockResolvedValue({ success: true }),
  sendBudgetAlertEmail: vi.fn().mockResolvedValue({ success: true }),
  sendMonthlySummaryEmail: vi.fn().mockResolvedValue({ success: true }),
}));

// Mock database
vi.mock('@/lib/db', () => ({
  db: {
    device: { findUnique: vi.fn() },
    tokenConfig: { findUnique: vi.fn() },
  },
}));

// Mock analytics
vi.mock('@/lib/token/analytics', () => ({
  getTokenSpending: vi.fn(),
  getSavingsEstimate: vi.fn(),
}));

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

import { db } from '@/lib/db';
import { sendDeviceOfflineEmail, sendBudgetAlertEmail, sendMonthlySummaryEmail } from '@/lib/email/send';
import { getTokenSpending, getSavingsEstimate } from '@/lib/token/analytics';
import {
  processAlertNotifications,
  sendMonthlySummaryForUser,
  _resetCooldowns,
} from '@/lib/token/notifications';
import type { TokenAlert } from '@/lib/token/alerts';

/** Helper to build a TokenAlert */
function makeAlert(overrides: Partial<TokenAlert>): TokenAlert {
  return {
    id: 'test-alert',
    severity: 'warning',
    category: 'device',
    title: 'Test alert',
    message: 'Test message',
    createdAt: new Date('2026-02-23T12:00:00Z'),
    acknowledged: false,
    ...overrides,
  };
}

describe('processAlertNotifications', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    _resetCooldowns();
  });

  it('should do nothing for info-only alerts', async () => {
    const alerts: TokenAlert[] = [
      makeAlert({ id: 'fleet-no-devices', severity: 'info', category: 'fleet' }),
    ];

    await processAlertNotifications('user_1', 'user@test.com', 'Alice', alerts);

    expect(sendDeviceOfflineEmail).not.toHaveBeenCalled();
    expect(sendBudgetAlertEmail).not.toHaveBeenCalled();
  });

  it('should send device offline email for device-offline alert', async () => {
    const alerts: TokenAlert[] = [
      makeAlert({
        id: 'device-offline-dev_1',
        severity: 'critical',
        category: 'device',
        title: 'home-server appears offline',
        deviceId: 'dev_1',
      }),
    ];

    vi.mocked(db.device.findUnique).mockResolvedValue({
      name: 'home-server',
      lastSeen: new Date('2026-02-23T11:50:00Z'),
    } as any);

    await processAlertNotifications('user_1', 'user@test.com', 'Alice', alerts);

    expect(sendDeviceOfflineEmail).toHaveBeenCalledWith(
      'user@test.com',
      'Alice',
      'home-server',
      new Date('2026-02-23T11:50:00Z'),
    );
  });

  it('should respect cooldown — second call within 1h does not send', async () => {
    const alerts: TokenAlert[] = [
      makeAlert({
        id: 'device-offline-dev_1',
        severity: 'critical',
        category: 'device',
        title: 'home-server appears offline',
        deviceId: 'dev_1',
      }),
    ];

    vi.mocked(db.device.findUnique).mockResolvedValue({
      name: 'home-server',
      lastSeen: new Date('2026-02-23T11:50:00Z'),
    } as any);

    // First call — sends
    await processAlertNotifications('user_1', 'user@test.com', 'Alice', alerts);
    expect(sendDeviceOfflineEmail).toHaveBeenCalledTimes(1);

    // Second call — in cooldown, should not send
    await processAlertNotifications('user_1', 'user@test.com', 'Alice', alerts);
    expect(sendDeviceOfflineEmail).toHaveBeenCalledTimes(1);
  });

  it('should send budget alert for budget-critical', async () => {
    const alerts: TokenAlert[] = [
      makeAlert({
        id: 'budget-critical',
        severity: 'critical',
        category: 'budget',
        title: 'Budget nearly exhausted',
      }),
    ];

    vi.mocked(db.tokenConfig.findUnique).mockResolvedValue({
      id: 'singleton',
      monthlyBudgetUsd: 50.0,
      totalSpentUsd: 48.0,
    } as any);

    await processAlertNotifications('user_1', 'user@test.com', 'Alice', alerts);

    expect(sendBudgetAlertEmail).toHaveBeenCalledWith(
      'user@test.com',
      'Alice',
      48.0,
      50.0,
      96,
    );
  });

  it('should send budget alert for budget-warning', async () => {
    const alerts: TokenAlert[] = [
      makeAlert({
        id: 'budget-warning',
        severity: 'warning',
        category: 'budget',
        title: 'Approaching budget limit',
      }),
    ];

    vi.mocked(db.tokenConfig.findUnique).mockResolvedValue({
      id: 'singleton',
      monthlyBudgetUsd: 100.0,
      totalSpentUsd: 85.0,
    } as any);

    await processAlertNotifications('user_1', 'user@test.com', 'Alice', alerts);

    expect(sendBudgetAlertEmail).toHaveBeenCalledWith(
      'user@test.com',
      'Alice',
      85.0,
      100.0,
      85,
    );
  });

  it('should swallow errors without throwing', async () => {
    const alerts: TokenAlert[] = [
      makeAlert({
        id: 'device-offline-dev_1',
        severity: 'critical',
        category: 'device',
        title: 'home-server appears offline',
        deviceId: 'dev_1',
      }),
    ];

    vi.mocked(db.device.findUnique).mockRejectedValue(new Error('DB down'));

    // Should not throw
    await expect(
      processAlertNotifications('user_1', 'user@test.com', 'Alice', alerts),
    ).resolves.toBeUndefined();
  });
});

describe('sendMonthlySummaryForUser', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    _resetCooldowns();
  });

  it('should send summary with correct data', async () => {
    vi.mocked(getTokenSpending).mockResolvedValue({
      today: 0,
      thisMonth: 5.0,
      allTime: 15.0,
      byModel: { 'gemini-2.5-flash': 10.0, 'claude-sonnet': 5.0 },
      byIntent: { chat: 12.0, research: 3.0 },
      messageCount: 200,
      budgetUsedPct: 30,
      budgetRemaining: 35,
    } as any);

    vi.mocked(getSavingsEstimate).mockResolvedValue({
      tokenCostUsd: 15.0,
      equivalentCloudCost: 50.0,
      savingsUsd: 35.0,
      savingsPct: 70.0,
    } as any);

    await sendMonthlySummaryForUser('user_1', 'user@test.com', 'Alice');

    expect(sendMonthlySummaryEmail).toHaveBeenCalledTimes(1);
    expect(sendMonthlySummaryEmail).toHaveBeenCalledWith(
      'user@test.com',
      expect.objectContaining({
        name: 'Alice',
        totalMessages: 200,
        totalCost: 15.0,
        topModel: 'gemini-2.5-flash',
        topModelCost: 10.0,
        savingsUsd: 35.0,
        savingsPct: 70.0,
      }),
    );
  });

  it('should skip when no messages exist', async () => {
    vi.mocked(getTokenSpending).mockResolvedValue({
      today: 0,
      thisMonth: 0,
      allTime: 0,
      byModel: {},
      byIntent: {},
      messageCount: 0,
      budgetUsedPct: 0,
      budgetRemaining: 50,
    } as any);

    vi.mocked(getSavingsEstimate).mockResolvedValue({
      tokenCostUsd: 0,
      equivalentCloudCost: 0,
      savingsUsd: 0,
      savingsPct: 0,
    } as any);

    await sendMonthlySummaryForUser('user_1', 'user@test.com', 'Alice');

    expect(sendMonthlySummaryEmail).not.toHaveBeenCalled();
  });

  it('should swallow errors without throwing', async () => {
    vi.mocked(getTokenSpending).mockRejectedValue(new Error('DB failure'));

    await expect(
      sendMonthlySummaryForUser('user_1', 'user@test.com', 'Alice'),
    ).resolves.toBeUndefined();
  });
});
