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

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

// Mock analytics
vi.mock('@/lib/token/analytics', () => ({
  getTokenSpending: 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 { getTokenSpending } from '@/lib/token/analytics';
import {
  checkAlerts,
  acknowledgeAlert,
  formatAlertContext,
  _resetAcks,
} from '@/lib/token/alerts';
import type { TokenAlert } from '@/lib/token/alerts';

/** Helper: default spending with customizable budgetUsedPct */
function mockSpending(overrides: Partial<{ budgetUsedPct: number; budgetRemaining: number }> = {}) {
  return {
    today: 0,
    thisMonth: 0,
    allTime: 0,
    byModel: {},
    byIntent: {},
    messageCount: 0,
    budgetUsedPct: overrides.budgetUsedPct ?? 0,
    budgetRemaining: overrides.budgetRemaining ?? 50,
  };
}

describe('checkAlerts', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    _resetAcks();
    vi.useFakeTimers();
    vi.setSystemTime(new Date('2026-02-23T12:00:00Z'));
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it('should return no-devices alert when user has no devices', async () => {
    vi.mocked(db.device.findMany).mockResolvedValue([]);
    vi.mocked(db.tokenConfig.findUnique).mockResolvedValue(null);
    vi.mocked(getTokenSpending).mockResolvedValue(mockSpending() as any);

    const alerts = await checkAlerts('user_1');

    expect(alerts).toHaveLength(1);
    expect(alerts[0].id).toBe('fleet-no-devices');
    expect(alerts[0].severity).toBe('info');
    expect(alerts[0].category).toBe('fleet');
    expect(alerts[0].message).toContain('llmhosts up');
  });

  it('should return device-offline alert when lastSeen > 5 min', async () => {
    const tenMinutesAgo = new Date('2026-02-23T11:50:00Z');

    vi.mocked(db.device.findMany).mockResolvedValue([
      {
        id: 'dev_1',
        name: 'home-server',
        status: 'online',
        models: '["llama3.3:70b"]',
        lastSeen: tenMinutesAgo,
      },
    ] as any);
    vi.mocked(db.tokenConfig.findUnique).mockResolvedValue(null);
    vi.mocked(getTokenSpending).mockResolvedValue(mockSpending() as any);

    const alerts = await checkAlerts('user_1');

    const offlineAlert = alerts.find((a) => a.id === 'device-offline-dev_1');
    expect(offlineAlert).toBeDefined();
    expect(offlineAlert!.severity).toBe('critical');
    expect(offlineAlert!.category).toBe('device');
    expect(offlineAlert!.deviceId).toBe('dev_1');
    expect(offlineAlert!.title).toContain('home-server');
  });

  it('should not alert for device already marked offline', async () => {
    const tenMinutesAgo = new Date('2026-02-23T11:50:00Z');

    vi.mocked(db.device.findMany).mockResolvedValue([
      {
        id: 'dev_1',
        name: 'home-server',
        status: 'offline', // already marked offline
        models: '["llama3.3:70b"]',
        lastSeen: tenMinutesAgo,
      },
    ] as any);
    vi.mocked(db.tokenConfig.findUnique).mockResolvedValue(null);
    vi.mocked(getTokenSpending).mockResolvedValue(mockSpending() as any);

    const alerts = await checkAlerts('user_1');

    const offlineAlert = alerts.find((a) => a.id.startsWith('device-offline-'));
    expect(offlineAlert).toBeUndefined();
  });

  it('should return degraded alert for degraded devices', async () => {
    const oneMinuteAgo = new Date('2026-02-23T11:59:00Z');

    vi.mocked(db.device.findMany).mockResolvedValue([
      {
        id: 'dev_2',
        name: 'gpu-box',
        status: 'degraded',
        models: '["qwen3:8b"]',
        lastSeen: oneMinuteAgo,
      },
    ] as any);
    vi.mocked(db.tokenConfig.findUnique).mockResolvedValue(null);
    vi.mocked(getTokenSpending).mockResolvedValue(mockSpending() as any);

    const alerts = await checkAlerts('user_1');

    const degradedAlert = alerts.find((a) => a.id === 'device-degraded-dev_2');
    expect(degradedAlert).toBeDefined();
    expect(degradedAlert!.severity).toBe('warning');
    expect(degradedAlert!.category).toBe('device');
    expect(degradedAlert!.title).toContain('gpu-box');
  });

  it('should return no-models alert when devices have empty models', async () => {
    const oneMinuteAgo = new Date('2026-02-23T11:59:00Z');

    vi.mocked(db.device.findMany).mockResolvedValue([
      {
        id: 'dev_1',
        name: 'empty-box',
        status: 'online',
        models: '[]',
        lastSeen: oneMinuteAgo,
      },
    ] as any);
    vi.mocked(db.tokenConfig.findUnique).mockResolvedValue(null);
    vi.mocked(getTokenSpending).mockResolvedValue(mockSpending() as any);

    const alerts = await checkAlerts('user_1');

    const noModelsAlert = alerts.find((a) => a.id === 'model-none-loaded');
    expect(noModelsAlert).toBeDefined();
    expect(noModelsAlert!.severity).toBe('warning');
    expect(noModelsAlert!.category).toBe('model');
  });

  it('should return budget-warning at >80%', async () => {
    const oneMinuteAgo = new Date('2026-02-23T11:59:00Z');

    vi.mocked(db.device.findMany).mockResolvedValue([
      {
        id: 'dev_1',
        name: 'server',
        status: 'online',
        models: '["llama3.3:70b"]',
        lastSeen: oneMinuteAgo,
      },
    ] as any);
    vi.mocked(db.tokenConfig.findUnique).mockResolvedValue({
      id: 'singleton',
      monthlyBudgetUsd: 50.0,
    } as any);
    vi.mocked(getTokenSpending).mockResolvedValue(
      mockSpending({ budgetUsedPct: 85, budgetRemaining: 7.5 }) as any,
    );

    const alerts = await checkAlerts('user_1');

    const budgetWarning = alerts.find((a) => a.id === 'budget-warning');
    expect(budgetWarning).toBeDefined();
    expect(budgetWarning!.severity).toBe('warning');
    expect(budgetWarning!.category).toBe('budget');
    expect(budgetWarning!.message).toContain('85.0%');
  });

  it('should return budget-critical at >95%', async () => {
    const oneMinuteAgo = new Date('2026-02-23T11:59:00Z');

    vi.mocked(db.device.findMany).mockResolvedValue([
      {
        id: 'dev_1',
        name: 'server',
        status: 'online',
        models: '["llama3.3:70b"]',
        lastSeen: oneMinuteAgo,
      },
    ] as any);
    vi.mocked(db.tokenConfig.findUnique).mockResolvedValue({
      id: 'singleton',
      monthlyBudgetUsd: 50.0,
    } as any);
    vi.mocked(getTokenSpending).mockResolvedValue(
      mockSpending({ budgetUsedPct: 98, budgetRemaining: 1.0 }) as any,
    );

    const alerts = await checkAlerts('user_1');

    const budgetCritical = alerts.find((a) => a.id === 'budget-critical');
    expect(budgetCritical).toBeDefined();
    expect(budgetCritical!.severity).toBe('critical');
    expect(budgetCritical!.message).toContain('98.0%');
  });

  it('should not return budget-warning when budget-critical applies', async () => {
    const oneMinuteAgo = new Date('2026-02-23T11:59:00Z');

    vi.mocked(db.device.findMany).mockResolvedValue([
      {
        id: 'dev_1',
        name: 'server',
        status: 'online',
        models: '["llama3.3:70b"]',
        lastSeen: oneMinuteAgo,
      },
    ] as any);
    vi.mocked(db.tokenConfig.findUnique).mockResolvedValue({
      id: 'singleton',
      monthlyBudgetUsd: 50.0,
    } as any);
    vi.mocked(getTokenSpending).mockResolvedValue(
      mockSpending({ budgetUsedPct: 98, budgetRemaining: 1.0 }) as any,
    );

    const alerts = await checkAlerts('user_1');

    expect(alerts.find((a) => a.id === 'budget-critical')).toBeDefined();
    expect(alerts.find((a) => a.id === 'budget-warning')).toBeUndefined();
  });
});

describe('acknowledgeAlert', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    _resetAcks();
    vi.useFakeTimers();
    vi.setSystemTime(new Date('2026-02-23T12:00:00Z'));
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it('should mark alert as acknowledged', async () => {
    vi.mocked(db.device.findMany).mockResolvedValue([]);
    vi.mocked(db.tokenConfig.findUnique).mockResolvedValue(null);
    vi.mocked(getTokenSpending).mockResolvedValue(mockSpending() as any);

    // Before ack
    let alerts = await checkAlerts('user_1');
    expect(alerts[0].acknowledged).toBe(false);

    // Acknowledge
    acknowledgeAlert('user_1', 'fleet-no-devices');

    // After ack
    alerts = await checkAlerts('user_1');
    expect(alerts[0].acknowledged).toBe(true);
  });

  it('should produce acknowledged=true in checkAlerts results', async () => {
    const tenMinutesAgo = new Date('2026-02-23T11:50:00Z');

    vi.mocked(db.device.findMany).mockResolvedValue([
      {
        id: 'dev_1',
        name: 'home-server',
        status: 'online',
        models: '["llama3.3:70b"]',
        lastSeen: tenMinutesAgo,
      },
    ] as any);
    vi.mocked(db.tokenConfig.findUnique).mockResolvedValue(null);
    vi.mocked(getTokenSpending).mockResolvedValue(mockSpending() as any);

    // Acknowledge device offline alert
    acknowledgeAlert('user_1', 'device-offline-dev_1');

    const alerts = await checkAlerts('user_1');
    const offlineAlert = alerts.find((a) => a.id === 'device-offline-dev_1');
    expect(offlineAlert).toBeDefined();
    expect(offlineAlert!.acknowledged).toBe(true);
  });
});

describe('formatAlertContext', () => {
  it('should return empty string for info-only alerts', () => {
    const alerts: TokenAlert[] = [
      {
        id: 'fleet-no-devices',
        severity: 'info',
        category: 'fleet',
        title: 'No devices connected',
        message: 'Set up your first device.',
        createdAt: new Date(),
        acknowledged: false,
      },
    ];

    const result = formatAlertContext(alerts);

    expect(result).toBe('');
  });

  it('should format warnings and criticals with severity tags', () => {
    const alerts: TokenAlert[] = [
      {
        id: 'budget-warning',
        severity: 'warning',
        category: 'budget',
        title: 'Approaching budget limit',
        message: '85% used.',
        createdAt: new Date(),
        acknowledged: false,
      },
      {
        id: 'device-offline-1',
        severity: 'critical',
        category: 'device',
        title: 'home-server appears offline',
        message: 'Last seen 10m ago.',
        createdAt: new Date(),
        acknowledged: false,
      },
    ];

    const result = formatAlertContext(alerts);

    expect(result).toContain('Active Alerts');
    expect(result).toContain('[WARNING]');
    expect(result).toContain('[CRITICAL]');
    expect(result).toContain('Approaching budget limit');
    expect(result).toContain('home-server appears offline');
    expect(result).toContain('proactively mention');
  });

  it('should sort critical before warning', () => {
    const alerts: TokenAlert[] = [
      {
        id: 'budget-warning',
        severity: 'warning',
        category: 'budget',
        title: 'Warning alert',
        message: 'Warning message.',
        createdAt: new Date(),
        acknowledged: false,
      },
      {
        id: 'device-offline-1',
        severity: 'critical',
        category: 'device',
        title: 'Critical alert',
        message: 'Critical message.',
        createdAt: new Date(),
        acknowledged: false,
      },
    ];

    const result = formatAlertContext(alerts);

    const criticalIndex = result.indexOf('[CRITICAL]');
    const warningIndex = result.indexOf('[WARNING]');
    expect(criticalIndex).toBeLessThan(warningIndex);
  });
});
