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

// Mock database
vi.mock('@/lib/db', () => ({
  db: {
    tokenConversation: { findMany: vi.fn() },
    tokenMessage: { findMany: vi.fn() },
    tokenConfig: { findUnique: vi.fn() },
    costEntry: { findMany: 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,
  getCloudSpending,
  getSavingsEstimate,
  formatSpendingContext,
} from '@/lib/token/analytics';
import type { TokenSpending, CloudSpending, SavingsEstimate } from '@/lib/token/analytics';

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

  it('should return zeros when no conversations exist', async () => {
    vi.mocked(db.tokenConversation.findMany).mockResolvedValue([]);
    vi.mocked(db.tokenConfig.findUnique).mockResolvedValue({
      id: 'singleton',
      monthlyBudgetUsd: 50.0,
    } as any);

    const result = await getTokenSpending('user_1');

    expect(result.today).toBe(0);
    expect(result.thisMonth).toBe(0);
    expect(result.allTime).toBe(0);
    expect(result.byModel).toEqual({});
    expect(result.byIntent).toEqual({});
    expect(result.messageCount).toBe(0);
    expect(result.budgetUsedPct).toBe(0);
    expect(result.budgetRemaining).toBe(50.0);
  });

  it('should aggregate costs from assistant messages correctly', async () => {
    vi.mocked(db.tokenConversation.findMany).mockResolvedValue([
      { id: 'conv_1' },
    ] as any);

    const now = new Date();
    vi.mocked(db.tokenMessage.findMany).mockResolvedValue([
      { costUsd: 0.005, model: 'gemini-2.5-flash', messageType: 'chat', createdAt: now },
      { costUsd: 0.010, model: 'gemini-2.5-flash', messageType: 'chat', createdAt: now },
      { costUsd: 0.003, model: 'claude-sonnet-4.6', messageType: 'research', createdAt: now },
    ] as any);

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

    const result = await getTokenSpending('user_1');

    expect(result.allTime).toBe(0.018);
    expect(result.messageCount).toBe(3);
    expect(result.byModel['gemini-2.5-flash']).toBe(0.015);
    expect(result.byModel['claude-sonnet-4.6']).toBe(0.003);
    expect(result.byIntent['chat']).toBe(0.015);
    expect(result.byIntent['research']).toBe(0.003);
  });

  it('should separate today/thisMonth/allTime by date', async () => {
    vi.mocked(db.tokenConversation.findMany).mockResolvedValue([
      { id: 'conv_1' },
    ] as any);

    const now = new Date();
    const yesterday = new Date(now);
    yesterday.setDate(yesterday.getDate() - 1);
    const lastMonth = new Date(now);
    lastMonth.setMonth(lastMonth.getMonth() - 2);

    vi.mocked(db.tokenMessage.findMany).mockResolvedValue([
      { costUsd: 0.01, model: 'model-a', messageType: 'chat', createdAt: now },        // today + thisMonth + allTime
      { costUsd: 0.02, model: 'model-a', messageType: 'chat', createdAt: yesterday },   // thisMonth + allTime (if same month)
      { costUsd: 0.03, model: 'model-a', messageType: 'chat', createdAt: lastMonth },   // allTime only
    ] as any);

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

    const result = await getTokenSpending('user_1');

    expect(result.today).toBe(0.01);
    expect(result.allTime).toBe(0.06);
    // thisMonth includes today + yesterday (if same month) but not lastMonth
    expect(result.thisMonth).toBeGreaterThanOrEqual(0.01);
    expect(result.thisMonth).toBeLessThanOrEqual(0.03);
  });

  it('should group costs by model and intent', async () => {
    vi.mocked(db.tokenConversation.findMany).mockResolvedValue([
      { id: 'conv_1' },
    ] as any);

    const now = new Date();
    vi.mocked(db.tokenMessage.findMany).mockResolvedValue([
      { costUsd: 0.01, model: 'model-a', messageType: 'chat', createdAt: now },
      { costUsd: 0.02, model: 'model-b', messageType: 'research', createdAt: now },
      { costUsd: 0.03, model: 'model-a', messageType: 'research', createdAt: now },
      { costUsd: 0.04, model: null, messageType: null, createdAt: now },
    ] as any);

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

    const result = await getTokenSpending('user_1');

    // By model: model-a=0.04, model-b=0.02, unknown=0.04
    expect(result.byModel['model-a']).toBe(0.04);
    expect(result.byModel['model-b']).toBe(0.02);
    expect(result.byModel['unknown']).toBe(0.04);

    // By intent: chat=0.01+0.04=0.05, research=0.02+0.03=0.05
    expect(result.byIntent['chat']).toBe(0.05);
    expect(result.byIntent['research']).toBe(0.05);
  });

  it('should calculate budget percentage from TokenConfig', async () => {
    vi.mocked(db.tokenConversation.findMany).mockResolvedValue([
      { id: 'conv_1' },
    ] as any);

    vi.mocked(db.tokenMessage.findMany).mockResolvedValue([
      { costUsd: 0.01, model: 'model-a', messageType: 'chat', createdAt: new Date() },
    ] as any);

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

    const result = await getTokenSpending('user_1');

    expect(result.budgetUsedPct).toBe(75);
    expect(result.budgetRemaining).toBe(25);
  });
});

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

  it('should return zeros when no cost entries exist', async () => {
    vi.mocked(db.costEntry.findMany).mockResolvedValue([]);

    const result = await getCloudSpending('user_1');

    expect(result.thisMonth).toBe(0);
    expect(result.lastMonth).toBe(0);
    expect(result.byProvider).toEqual({});
    expect(result.trend).toBe('flat');
  });

  it('should aggregate by provider for current month', async () => {
    const now = new Date();
    const thisMonthPeriod = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;

    vi.mocked(db.costEntry.findMany).mockResolvedValue([
      { amount: 25.0, provider: 'openai', period: thisMonthPeriod },
      { amount: 15.0, provider: 'anthropic', period: thisMonthPeriod },
      { amount: 10.0, provider: 'openai', period: thisMonthPeriod },
    ] as any);

    const result = await getCloudSpending('user_1');

    expect(result.thisMonth).toBe(50);
    expect(result.byProvider['openai']).toBe(35);
    expect(result.byProvider['anthropic']).toBe(15);
  });

  it('should detect upward trend (thisMonth > lastMonth + 5%)', async () => {
    const now = new Date();
    const thisMonthPeriod = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
    const lastMonthDate = new Date(now.getFullYear(), now.getMonth() - 1, 1);
    const lastMonthPeriod = `${lastMonthDate.getFullYear()}-${String(lastMonthDate.getMonth() + 1).padStart(2, '0')}`;

    vi.mocked(db.costEntry.findMany).mockResolvedValue([
      { amount: 120.0, provider: 'openai', period: thisMonthPeriod },
      { amount: 100.0, provider: 'openai', period: lastMonthPeriod },
    ] as any);

    const result = await getCloudSpending('user_1');

    expect(result.trend).toBe('up');
    expect(result.thisMonth).toBe(120);
    expect(result.lastMonth).toBe(100);
  });

  it('should detect downward trend (thisMonth < lastMonth - 5%)', async () => {
    const now = new Date();
    const thisMonthPeriod = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
    const lastMonthDate = new Date(now.getFullYear(), now.getMonth() - 1, 1);
    const lastMonthPeriod = `${lastMonthDate.getFullYear()}-${String(lastMonthDate.getMonth() + 1).padStart(2, '0')}`;

    vi.mocked(db.costEntry.findMany).mockResolvedValue([
      { amount: 80.0, provider: 'openai', period: thisMonthPeriod },
      { amount: 100.0, provider: 'openai', period: lastMonthPeriod },
    ] as any);

    const result = await getCloudSpending('user_1');

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

  it('should detect flat trend (within 5%)', async () => {
    const now = new Date();
    const thisMonthPeriod = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
    const lastMonthDate = new Date(now.getFullYear(), now.getMonth() - 1, 1);
    const lastMonthPeriod = `${lastMonthDate.getFullYear()}-${String(lastMonthDate.getMonth() + 1).padStart(2, '0')}`;

    vi.mocked(db.costEntry.findMany).mockResolvedValue([
      { amount: 102.0, provider: 'openai', period: thisMonthPeriod },
      { amount: 100.0, provider: 'openai', period: lastMonthPeriod },
    ] as any);

    const result = await getCloudSpending('user_1');

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

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

  it('should return zeros when no conversations', async () => {
    vi.mocked(db.tokenConversation.findMany).mockResolvedValue([]);

    const result = await getSavingsEstimate('user_1');

    expect(result.tokenCostUsd).toBe(0);
    expect(result.equivalentCloudCost).toBe(0);
    expect(result.savingsUsd).toBe(0);
    expect(result.savingsPct).toBe(0);
  });

  it('should calculate savings based on average cloud pricing', async () => {
    vi.mocked(db.tokenConversation.findMany).mockResolvedValue([
      { id: 'conv_1' },
    ] as any);

    // average pricing: input=2.25/1M, output=10.0/1M
    vi.mocked(db.tokenMessage.findMany).mockResolvedValue([
      { costUsd: 0.001, inputTokens: 1000, outputTokens: 500 },
      { costUsd: 0.002, inputTokens: 2000, outputTokens: 1000 },
    ] as any);

    const result = await getSavingsEstimate('user_1');

    expect(result.tokenCostUsd).toBe(0.003);
    // Equivalent: (3000/1M)*2.25 + (1500/1M)*10.0 = 0.00675 + 0.015 = 0.02175
    // roundCents uses Math.round(n * 10000) / 10000, fp precision may give 0.0217 or 0.0218
    expect(result.equivalentCloudCost).toBeCloseTo(0.0218, 3);
    expect(result.savingsUsd).toBeGreaterThan(0);
    expect(result.savingsPct).toBeGreaterThan(0);
  });
});

describe('formatSpendingContext', () => {
  it('should include all sections when data is present', () => {
    const tokenSpending: TokenSpending = {
      today: 0.005,
      thisMonth: 0.05,
      allTime: 0.5,
      byModel: { 'gemini-2.5-flash': 0.03, 'claude-sonnet': 0.02 },
      byIntent: { chat: 0.04, research: 0.01 },
      messageCount: 100,
      budgetUsedPct: 25.5,
      budgetRemaining: 37.25,
    };

    const cloudSpending: CloudSpending = {
      thisMonth: 50.0,
      lastMonth: 45.0,
      byProvider: { openai: 30, anthropic: 20 },
      trend: 'up',
    };

    const savings: SavingsEstimate = {
      tokenCostUsd: 0.5,
      equivalentCloudCost: 2.0,
      savingsUsd: 1.5,
      savingsPct: 75.0,
    };

    const result = formatSpendingContext(tokenSpending, cloudSpending, savings);

    // Token section
    expect(result).toContain('Token Assistant Costs');
    expect(result).toContain('$0.0050');
    expect(result).toContain('Messages: 100');
    expect(result).toContain('25.5%');

    // Model section
    expect(result).toContain('Cost by Model');
    expect(result).toContain('gemini-2.5-flash');
    expect(result).toContain('claude-sonnet');

    // Intent section
    expect(result).toContain('Cost by Intent');
    expect(result).toContain('chat');
    expect(result).toContain('research');

    // Cloud section
    expect(result).toContain('Cloud API Spending');
    expect(result).toContain('Trend: up');
    expect(result).toContain('openai');
    expect(result).toContain('anthropic');

    // Savings section
    expect(result).toContain('Savings vs Cloud APIs');
    expect(result).toContain('75.0%');
  });

  it('should omit cloud spending when zero', () => {
    const tokenSpending: TokenSpending = {
      today: 0.01,
      thisMonth: 0.1,
      allTime: 1.0,
      byModel: {},
      byIntent: {},
      messageCount: 10,
      budgetUsedPct: 5,
      budgetRemaining: 45,
    };

    const cloudSpending: CloudSpending = {
      thisMonth: 0,
      lastMonth: 0,
      byProvider: {},
      trend: 'flat',
    };

    const savings: SavingsEstimate = {
      tokenCostUsd: 1.0,
      equivalentCloudCost: 3.0,
      savingsUsd: 2.0,
      savingsPct: 66.67,
    };

    const result = formatSpendingContext(tokenSpending, cloudSpending, savings);

    expect(result).toContain('Token Assistant Costs');
    expect(result).not.toContain('Cloud API Spending');
    expect(result).toContain('Savings vs Cloud APIs');
  });

  it('should omit savings when no cloud equivalent cost', () => {
    const tokenSpending: TokenSpending = {
      today: 0,
      thisMonth: 0,
      allTime: 0,
      byModel: {},
      byIntent: {},
      messageCount: 0,
      budgetUsedPct: 0,
      budgetRemaining: 50,
    };

    const cloudSpending: CloudSpending = {
      thisMonth: 0,
      lastMonth: 0,
      byProvider: {},
      trend: 'flat',
    };

    const savings: SavingsEstimate = {
      tokenCostUsd: 0,
      equivalentCloudCost: 0,
      savingsUsd: 0,
      savingsPct: 0,
    };

    const result = formatSpendingContext(tokenSpending, cloudSpending, savings);

    expect(result).toContain('Token Assistant Costs');
    expect(result).not.toContain('Savings vs Cloud APIs');
  });
});
