import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import type { ConversationHistory } from '../../types/conversation.js';
import type { StarsTransaction, CharacterAnalytics } from '../../types/api.js';

// --- Mock data ---
function makeConversation(overrides: Partial<ConversationHistory> = {}): ConversationHistory {
  return {
    characterId: 'char-1',
    telegramUserId: 100,
    userName: 'TestUser',
    userInfo: { name: 'Test', interests: [], notes: [] },
    messages: [],
    lastActivity: '2024-06-15T12:00:00Z',
    totalMessages: 0,
    totalStarsSpent: 0,
    ...overrides,
  };
}

function makeTransaction(overrides: Partial<StarsTransaction> = {}): StarsTransaction {
  return {
    id: 'tx-1',
    characterId: 'char-1',
    telegramUserId: 100,
    type: 'payment',
    starCount: 50,
    status: 'completed',
    createdAt: '2024-06-15T14:00:00Z',
    ...overrides,
  };
}

// --- Mocks ---
vi.mock('../../utils/index.js', () => ({
  createModuleLogger: () => ({ info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }),
}));

const analyticsStore = new Map<string, CharacterAnalytics>();
const mockAtomicWriteJSON = vi.fn(async (path: string, data: any) => {
  analyticsStore.set(path, data);
});
const mockReadJSON = vi.fn(async (path: string) => {
  return analyticsStore.get(path) || null;
});
const mockEnsureDir = vi.fn();

vi.mock('../data-store/file-ops.js', () => ({
  atomicWriteJSON: (path: string, data: any) => mockAtomicWriteJSON(path, data),
  readJSON: (path: string) => mockReadJSON(path),
  ensureDir: (path: string) => mockEnsureDir(path),
}));

const conversationStore = new Map<string, ConversationHistory[]>();
const transactionStore = new Map<string, StarsTransaction[]>();

const mockDataStore = {
  dataDir: '/tmp/test-data',
  listConversations: vi.fn(async (charId: string) => conversationStore.get(charId) || []),
  getStarsTransactions: vi.fn(async (charId: string) => transactionStore.get(charId) || []),
  listCharacters: vi.fn(async () => [
    { id: 'char-1', status: 'active' },
    { id: 'char-2', status: 'active' },
  ]),
};

vi.mock('node-cron', () => {
  const mockTask = { stop: vi.fn() };
  return {
    default: {
      schedule: vi.fn((_expr: string, cb: () => void) => {
        (mockTask as any)._callback = cb;
        return mockTask;
      }),
    },
  };
});

import { AnalyticsAggregator } from './aggregator.js';
import { AnalyticsEngine } from './index.js';
import cron from 'node-cron';

let aggregator: AnalyticsAggregator;
let engine: AnalyticsEngine;

beforeEach(() => {
  aggregator = new AnalyticsAggregator();
  engine = new AnalyticsEngine(mockDataStore as any);
  analyticsStore.clear();
  conversationStore.clear();
  transactionStore.clear();
  vi.clearAllMocks();
});

// =============================================
// AnalyticsAggregator Tests
// =============================================

describe('AnalyticsAggregator.aggregateConversations', () => {
  it('should return zeros for empty conversations', () => {
    const result = aggregator.aggregateConversations([]);
    expect(result).toEqual({
      totalMessages: 0,
      uniqueUsers: 0,
      averageConversationDuration: 0,
      averageMessagesPerConversation: 0,
    });
  });

  it('should count total messages across conversations', () => {
    const convs = [
      makeConversation({
        telegramUserId: 100,
        messages: [
          { id: '1', role: 'user', content: 'hi', timestamp: '2024-06-15T10:00:00Z' },
          { id: '2', role: 'assistant', content: 'hello', timestamp: '2024-06-15T10:01:00Z' },
        ],
      }),
      makeConversation({
        telegramUserId: 200,
        messages: [
          { id: '3', role: 'user', content: 'hey', timestamp: '2024-06-15T11:00:00Z' },
        ],
      }),
    ];
    const result = aggregator.aggregateConversations(convs);
    expect(result.totalMessages).toBe(3);
    expect(result.uniqueUsers).toBe(2);
    expect(result.averageMessagesPerConversation).toBe(1.5);
  });

  it('should calculate average conversation duration in minutes', () => {
    const convs = [
      makeConversation({
        messages: [
          { id: '1', role: 'user', content: 'hi', timestamp: '2024-06-15T10:00:00Z' },
          { id: '2', role: 'assistant', content: 'hello', timestamp: '2024-06-15T10:30:00Z' },
        ],
      }),
    ];
    const result = aggregator.aggregateConversations(convs);
    expect(result.averageConversationDuration).toBe(30);
  });

  it('should count same user only once for uniqueUsers', () => {
    const convs = [
      makeConversation({ telegramUserId: 100, messages: [{ id: '1', role: 'user', content: 'a', timestamp: '2024-06-15T10:00:00Z' }] }),
      makeConversation({ telegramUserId: 100, messages: [{ id: '2', role: 'user', content: 'b', timestamp: '2024-06-15T11:00:00Z' }] }),
    ];
    const result = aggregator.aggregateConversations(convs);
    expect(result.uniqueUsers).toBe(1);
  });
});

describe('AnalyticsAggregator.aggregateSales', () => {
  it('should return zeros for empty data', () => {
    const result = aggregator.aggregateSales([], []);
    expect(result).toEqual({
      totalStarsEarned: 0,
      totalTransactions: 0,
      conversionRate: 0,
      averageStarsPerUser: 0,
      topSellingHours: [],
    });
  });

  it('should sum stars from completed transactions only', () => {
    const txs = [
      makeTransaction({ starCount: 50, status: 'completed' }),
      makeTransaction({ id: 'tx-2', starCount: 30, status: 'pending' }),
      makeTransaction({ id: 'tx-3', starCount: 20, status: 'completed' }),
    ];
    const result = aggregator.aggregateSales(txs, []);
    expect(result.totalStarsEarned).toBe(70);
    expect(result.totalTransactions).toBe(2);
  });

  it('should calculate conversion rate from pitch metadata', () => {
    const convs = [
      makeConversation({
        messages: [
          { id: '1', role: 'assistant', content: 'pitch', timestamp: '2024-06-15T10:00:00Z', metadata: { starsPitchMade: true } },
          { id: '2', role: 'user', content: 'ok', timestamp: '2024-06-15T10:01:00Z', metadata: { starsAccepted: true } },
          { id: '3', role: 'assistant', content: 'pitch2', timestamp: '2024-06-15T11:00:00Z', metadata: { starsPitchMade: true } },
        ],
      }),
    ];
    const result = aggregator.aggregateSales([], convs);
    // 1 accept / 2 pitches = 0.5
    expect(result.conversionRate).toBe(0.5);
  });

  it('should calculate average stars per paying user', () => {
    const txs = [
      makeTransaction({ telegramUserId: 100, starCount: 50, status: 'completed' }),
      makeTransaction({ id: 'tx-2', telegramUserId: 100, starCount: 30, status: 'completed' }),
      makeTransaction({ id: 'tx-3', telegramUserId: 200, starCount: 20, status: 'completed' }),
    ];
    const result = aggregator.aggregateSales(txs, []);
    // Total 100 stars / 2 unique users = 50
    expect(result.averageStarsPerUser).toBe(50);
  });

  it('should identify top selling hours', () => {
    const txs = [
      makeTransaction({ starCount: 100, createdAt: '2024-06-15T14:00:00Z', status: 'completed' }),
      makeTransaction({ id: 'tx-2', starCount: 50, createdAt: '2024-06-15T14:30:00Z', status: 'completed' }),
      makeTransaction({ id: 'tx-3', starCount: 80, createdAt: '2024-06-15T20:00:00Z', status: 'completed' }),
    ];
    const result = aggregator.aggregateSales(txs, []);
    // Hour 14 = 150 stars, Hour 20 = 80 stars
    expect(result.topSellingHours[0]).toBe(14);
    expect(result.topSellingHours[1]).toBe(20);
  });
});

describe('AnalyticsAggregator.aggregateActivity', () => {
  it('should return all-zero hourly maps for empty data', () => {
    const result = aggregator.aggregateActivity([], []);
    expect(Object.keys(result.hourlyMessageCount).length).toBe(24);
    expect(Object.keys(result.hourlyStarsCount).length).toBe(24);
    for (let h = 0; h < 24; h++) {
      expect(result.hourlyMessageCount[h]).toBe(0);
      expect(result.hourlyStarsCount[h]).toBe(0);
    }
    expect(result.peakHour).toBe(0);
    expect(result.quietHour).toBe(0);
  });

  it('should count messages per hour', () => {
    const convs = [
      makeConversation({
        messages: [
          { id: '1', role: 'user', content: 'a', timestamp: '2024-06-15T10:00:00Z' },
          { id: '2', role: 'user', content: 'b', timestamp: '2024-06-15T10:30:00Z' },
          { id: '3', role: 'user', content: 'c', timestamp: '2024-06-15T14:00:00Z' },
        ],
      }),
    ];
    const result = aggregator.aggregateActivity(convs, []);
    expect(result.hourlyMessageCount[10]).toBe(2);
    expect(result.hourlyMessageCount[14]).toBe(1);
    expect(result.peakHour).toBe(10);
  });

  it('should count stars per hour from completed transactions', () => {
    const txs = [
      makeTransaction({ starCount: 50, createdAt: '2024-06-15T14:00:00Z', status: 'completed' }),
      makeTransaction({ id: 'tx-2', starCount: 30, createdAt: '2024-06-15T14:30:00Z', status: 'completed' }),
      makeTransaction({ id: 'tx-3', starCount: 10, createdAt: '2024-06-15T20:00:00Z', status: 'pending' }),
    ];
    const result = aggregator.aggregateActivity([], txs);
    expect(result.hourlyStarsCount[14]).toBe(80);
    expect(result.hourlyStarsCount[20]).toBe(0); // pending excluded
  });

  it('should identify peak and quiet hours', () => {
    const convs = [
      makeConversation({
        messages: [
          { id: '1', role: 'user', content: 'a', timestamp: '2024-06-15T08:00:00Z' },
          { id: '2', role: 'user', content: 'b', timestamp: '2024-06-15T08:30:00Z' },
          { id: '3', role: 'user', content: 'c', timestamp: '2024-06-15T08:45:00Z' },
          { id: '4', role: 'user', content: 'd', timestamp: '2024-06-15T15:00:00Z' },
        ],
      }),
    ];
    const result = aggregator.aggregateActivity(convs, []);
    expect(result.peakHour).toBe(8);
    // quietHour should be one of the hours with 0 messages
    expect(result.hourlyMessageCount[result.quietHour]).toBe(0);
  });
});

// =============================================
// AnalyticsEngine Tests
// =============================================

describe('AnalyticsEngine.calculateDailyAnalytics', () => {
  it('should calculate and store daily analytics', async () => {
    const date = '2024-06-15';
    conversationStore.set('char-1', [
      makeConversation({
        messages: [
          { id: '1', role: 'user', content: 'hi', timestamp: '2024-06-15T10:00:00Z' },
          { id: '2', role: 'assistant', content: 'hello', timestamp: '2024-06-15T10:05:00Z' },
        ],
      }),
    ]);
    transactionStore.set('char-1', [
      makeTransaction({ starCount: 50, createdAt: '2024-06-15T14:00:00Z' }),
    ]);

    const result = await engine.calculateDailyAnalytics('char-1', date);

    expect(result.characterId).toBe('char-1');
    expect(result.period).toBe('daily');
    expect(result.date).toBe(date);
    expect(result.conversations.totalMessages).toBe(2);
    expect(result.sales.totalStarsEarned).toBe(50);
    expect(mockAtomicWriteJSON).toHaveBeenCalled();
  });

  it('should filter messages to the specified date only', async () => {
    conversationStore.set('char-1', [
      makeConversation({
        messages: [
          { id: '1', role: 'user', content: 'yesterday', timestamp: '2024-06-14T23:00:00Z' },
          { id: '2', role: 'user', content: 'today', timestamp: '2024-06-15T10:00:00Z' },
          { id: '3', role: 'user', content: 'tomorrow', timestamp: '2024-06-16T01:00:00Z' },
        ],
      }),
    ]);

    const result = await engine.calculateDailyAnalytics('char-1', '2024-06-15');
    expect(result.conversations.totalMessages).toBe(1);
  });

  it('should handle empty data gracefully', async () => {
    const result = await engine.calculateDailyAnalytics('char-1', '2024-06-15');
    expect(result.conversations.totalMessages).toBe(0);
    expect(result.sales.totalStarsEarned).toBe(0);
    expect(result.activity.peakHour).toBe(0);
  });
});

describe('AnalyticsEngine.calculateWeeklyAnalytics', () => {
  it('should calculate weekly analytics', async () => {
    conversationStore.set('char-1', [
      makeConversation({
        messages: [
          { id: '1', role: 'user', content: 'hi', timestamp: new Date().toISOString() },
        ],
      }),
    ]);

    const result = await engine.calculateWeeklyAnalytics('char-1');
    expect(result.period).toBe('weekly');
    expect(result.date).toMatch(/^\d{4}-W\d{2}$/);
    expect(mockAtomicWriteJSON).toHaveBeenCalled();
  });
});

describe('AnalyticsEngine.calculateMonthlyAnalytics', () => {
  it('should calculate monthly analytics', async () => {
    conversationStore.set('char-1', [
      makeConversation({
        messages: [
          { id: '1', role: 'user', content: 'hi', timestamp: new Date().toISOString() },
        ],
      }),
    ]);

    const result = await engine.calculateMonthlyAnalytics('char-1');
    expect(result.period).toBe('monthly');
    expect(result.date).toMatch(/^\d{4}-\d{2}$/);
    expect(mockAtomicWriteJSON).toHaveBeenCalled();
  });
});

describe('AnalyticsEngine.getCharacterAnalytics', () => {
  it('should return stored analytics if available', async () => {
    const stored: CharacterAnalytics = {
      characterId: 'char-1',
      period: 'daily',
      date: '2024-06-15',
      conversations: { totalMessages: 42, uniqueUsers: 5, averageConversationDuration: 10, averageMessagesPerConversation: 8.4 },
      sales: { totalStarsEarned: 200, totalTransactions: 4, conversionRate: 0.5, averageStarsPerUser: 50, topSellingHours: [14] },
      activity: { hourlyMessageCount: {}, hourlyStarsCount: {}, peakHour: 14, quietHour: 3 },
    };
    // Pre-store analytics so readJSON returns it
    mockReadJSON.mockResolvedValueOnce(stored);

    const result = await engine.getCharacterAnalytics('char-1', 'daily', '2024-06-15');
    expect(result).toEqual(stored);
  });

  it('should calculate on-demand if not stored', async () => {
    conversationStore.set('char-1', []);
    const result = await engine.getCharacterAnalytics('char-1', 'daily', '2024-06-15');
    expect(result.characterId).toBe('char-1');
    expect(result.period).toBe('daily');
  });
});

describe('AnalyticsEngine.compareCharacters', () => {
  it('should return analytics for multiple characters', async () => {
    conversationStore.set('char-1', []);
    conversationStore.set('char-2', []);

    const results = await engine.compareCharacters(['char-1', 'char-2'], 'daily');
    expect(results).toHaveLength(2);
    expect(results[0].characterId).toBe('char-1');
    expect(results[1].characterId).toBe('char-2');
  });
});

describe('AnalyticsEngine.getSystemOverview', () => {
  it('should aggregate overview across all characters', async () => {
    conversationStore.set('char-1', [
      makeConversation({
        messages: [
          { id: '1', role: 'user', content: 'hi', timestamp: '2024-06-15T10:00:00Z' },
          { id: '2', role: 'assistant', content: 'hello', timestamp: '2024-06-15T10:01:00Z' },
        ],
      }),
    ]);
    conversationStore.set('char-2', [
      makeConversation({
        characterId: 'char-2',
        messages: [
          { id: '3', role: 'user', content: 'hey', timestamp: '2024-06-15T11:00:00Z' },
        ],
      }),
    ]);
    transactionStore.set('char-1', [
      makeTransaction({ starCount: 100, status: 'completed' }),
    ]);
    transactionStore.set('char-2', [
      makeTransaction({ id: 'tx-2', characterId: 'char-2', starCount: 200, status: 'completed' }),
    ]);

    const overview = await engine.getSystemOverview();
    expect(overview.totalCharacters).toBe(2);
    expect(overview.totalMessages).toBe(3);
    expect(overview.totalStars).toBe(300);
    expect(overview.topCharacter).toBe('char-2');
  });

  it('should return undefined topCharacter when no stars earned', async () => {
    conversationStore.set('char-1', []);
    conversationStore.set('char-2', []);
    transactionStore.set('char-1', []);
    transactionStore.set('char-2', []);

    const overview = await engine.getSystemOverview();
    expect(overview.topCharacter).toBeUndefined();
  });
});

describe('AnalyticsEngine scheduling', () => {
  it('should start periodic calculation with cron', () => {
    engine.startPeriodicCalculation('0 0 * * *');
    expect(cron.schedule).toHaveBeenCalledWith('0 0 * * *', expect.any(Function));
  });

  it('should use default cron expression when none provided', () => {
    engine.startPeriodicCalculation();
    expect(cron.schedule).toHaveBeenCalledWith('0 0 * * *', expect.any(Function));
  });

  it('should stop previous cron task when starting new one', () => {
    engine.startPeriodicCalculation();
    const firstTask = vi.mocked(cron.schedule).mock.results[0].value;
    engine.startPeriodicCalculation('0 */6 * * *');
    expect(firstTask.stop).toHaveBeenCalled();
  });

  it('should stop periodic calculation', () => {
    engine.startPeriodicCalculation();
    engine.stopPeriodicCalculation();
    const task = vi.mocked(cron.schedule).mock.results[0].value;
    expect(task.stop).toHaveBeenCalled();
  });

  it('should handle stopPeriodicCalculation when not started', () => {
    // Should not throw
    engine.stopPeriodicCalculation();
  });
});
