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

const backingStore = new Map<string, ConversationHistory>();
function key(cId: string, uId: number) { return `${cId}:${uId}`; }

vi.mock('../data-store/index.js', () => ({
  dataStore: {
    saveConversationHistory: vi.fn(async (h: ConversationHistory) => {
      backingStore.set(key(h.characterId, h.telegramUserId), structuredClone(h));
    }),
    getConversationHistory: vi.fn(async (cId: string, uId: number) => {
      const v = backingStore.get(key(cId, uId));
      return v ? structuredClone(v) : null;
    }),
    deleteConversation: vi.fn(async (cId: string, uId: number) => {
      return backingStore.delete(key(cId, uId));
    }),
  },
}));

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

import { ConversationManager } from './history.js';
import { dataStore } from '../data-store/index.js';

let manager: ConversationManager;

beforeEach(() => {
  manager = new ConversationManager();
  backingStore.clear();
  vi.mocked(dataStore.saveConversationHistory).mockClear();
  vi.mocked(dataStore.getConversationHistory).mockClear();
  vi.mocked(dataStore.deleteConversation).mockClear();
});

describe('ConversationManager.addMessage', () => {
  it('should create new history when none exists', async () => {
    const result = await manager.addMessage('char-1', 100, 'user', 'hello');
    expect(result.characterId).toBe('char-1');
    expect(result.telegramUserId).toBe(100);
    expect(result.messages).toHaveLength(1);
    expect(result.messages[0].role).toBe('user');
    expect(result.messages[0].content).toBe('hello');
    expect(result.totalMessages).toBe(1);
  });

  it('should append to existing history', async () => {
    await manager.addMessage('char-1', 100, 'user', 'hello');
    const result = await manager.addMessage('char-1', 100, 'assistant', 'hi there');
    expect(result.messages).toHaveLength(2);
    expect(result.messages[1].role).toBe('assistant');
    expect(result.totalMessages).toBe(2);
  });

  it('should assign unique message IDs', async () => {
    await manager.addMessage('char-1', 100, 'user', 'msg1');
    const result = await manager.addMessage('char-1', 100, 'user', 'msg2');
    const ids = result.messages.map(m => m.id);
    expect(new Set(ids).size).toBe(2);
  });

  it('should store metadata when provided', async () => {
    const result = await manager.addMessage('char-1', 100, 'assistant', 'want a pic?', { starsPitchMade: true });
    expect(result.messages[0].metadata?.starsPitchMade).toBe(true);
  });

  it('should not include metadata key when not provided', async () => {
    const result = await manager.addMessage('char-1', 100, 'user', 'hello');
    expect(result.messages[0].metadata).toBeUndefined();
  });

  it('should update lastActivity timestamp', async () => {
    const r1 = await manager.addMessage('char-1', 100, 'user', 'first');
    const t1 = r1.lastActivity;
    await new Promise(r => setTimeout(r, 10));
    const r2 = await manager.addMessage('char-1', 100, 'user', 'second');
    expect(r2.lastActivity >= t1).toBe(true);
  });

  it('should call dataStore.saveConversationHistory', async () => {
    await manager.addMessage('char-1', 100, 'user', 'hello');
    expect(dataStore.saveConversationHistory).toHaveBeenCalledTimes(1);
  });
});

describe('ConversationManager.getHistory', () => {
  it('should return null when no history exists', async () => {
    const result = await manager.getHistory('char-1', 999);
    expect(result).toBeNull();
  });

  it('should return full history', async () => {
    await manager.addMessage('char-1', 100, 'user', 'hello');
    await manager.addMessage('char-1', 100, 'assistant', 'hi');
    const result = await manager.getHistory('char-1', 100);
    expect(result).not.toBeNull();
    expect(result!.messages).toHaveLength(2);
  });
});

describe('ConversationManager.getRecentMessages', () => {
  it('should return empty array when no history exists', async () => {
    const result = await manager.getRecentMessages('char-1', 999);
    expect(result).toEqual([]);
  });

  it('should return all messages within the time window', async () => {
    await manager.addMessage('char-1', 100, 'user', 'recent');
    const result = await manager.getRecentMessages('char-1', 100, 72);
    expect(result).toHaveLength(1);
    expect(result[0].content).toBe('recent');
  });

  it('should filter out old messages', async () => {
    await manager.addMessage('char-1', 100, 'user', 'new message');
    const history = await manager.getHistory('char-1', 100);
    // Insert an old message (4 days ago)
    const oldTimestamp = new Date(Date.now() - 4 * 24 * 60 * 60 * 1000).toISOString();
    history!.messages.unshift({
      id: 'old-msg',
      role: 'user',
      content: 'old message',
      timestamp: oldTimestamp,
    });
    history!.totalMessages = history!.messages.length;
    await dataStore.saveConversationHistory(history!);

    const result = await manager.getRecentMessages('char-1', 100, 72);
    expect(result).toHaveLength(1);
    expect(result[0].content).toBe('new message');
  });

  it('should default to 72 hours', async () => {
    await manager.addMessage('char-1', 100, 'user', 'hello');
    const result = await manager.getRecentMessages('char-1', 100);
    expect(result).toHaveLength(1);
  });
});

describe('ConversationManager.updateUserInfo', () => {
  it('should create history if none exists and set user info', async () => {
    await manager.updateUserInfo('char-1', 100, { name: 'Ali' });
    const history = await manager.getHistory('char-1', 100);
    expect(history).not.toBeNull();
    expect(history!.userInfo.name).toBe('Ali');
  });

  it('should update name on existing history', async () => {
    await manager.addMessage('char-1', 100, 'user', 'hello');
    await manager.updateUserInfo('char-1', 100, { name: 'Ayşe' });
    const history = await manager.getHistory('char-1', 100);
    expect(history!.userInfo.name).toBe('Ayşe');
  });

  it('should merge interests without duplicates', async () => {
    await manager.updateUserInfo('char-1', 100, { interests: ['music', 'art'] });
    await manager.updateUserInfo('char-1', 100, { interests: ['art', 'sports'] });
    const history = await manager.getHistory('char-1', 100);
    expect(history!.userInfo.interests).toEqual(expect.arrayContaining(['music', 'art', 'sports']));
    expect(history!.userInfo.interests).toHaveLength(3);
  });

  it('should merge notes without duplicates', async () => {
    await manager.updateUserInfo('char-1', 100, { notes: ['likes cats'] });
    await manager.updateUserInfo('char-1', 100, { notes: ['likes cats', 'works at tech'] });
    const history = await manager.getHistory('char-1', 100);
    expect(history!.userInfo.notes).toHaveLength(2);
  });
});

describe('ConversationManager.deleteHistory', () => {
  it('should delete existing history and return true', async () => {
    await manager.addMessage('char-1', 100, 'user', 'hello');
    const deleted = await manager.deleteHistory('char-1', 100);
    expect(deleted).toBe(true);
    const result = await manager.getHistory('char-1', 100);
    expect(result).toBeNull();
  });

  it('should return false when no history exists', async () => {
    const deleted = await manager.deleteHistory('char-1', 999);
    expect(deleted).toBe(false);
  });
});

describe('Message ordering', () => {
  it('should keep messages in chronological order', async () => {
    await manager.addMessage('char-1', 100, 'user', 'first');
    await new Promise(r => setTimeout(r, 10));
    await manager.addMessage('char-1', 100, 'assistant', 'second');
    await new Promise(r => setTimeout(r, 10));
    await manager.addMessage('char-1', 100, 'user', 'third');

    const history = await manager.getHistory('char-1', 100);
    const timestamps = history!.messages.map(m => m.timestamp);
    for (let i = 1; i < timestamps.length; i++) {
      expect(timestamps[i] >= timestamps[i - 1]).toBe(true);
    }
  });
});

describe('Photo message saving', () => {
  it('should save a photo message with messageType, imagePath fields', async () => {
    const result = await manager.addMessage('char-1', 100, 'user', '[Fotoğraf]', undefined, {
      messageType: 'photo',
      imagePath: '/static/characters/char-1/incoming-photos/photo_123.jpg',
    });
    expect(result.messages).toHaveLength(1);
    const msg = result.messages[0];
    expect(msg.messageType).toBe('photo');
    expect(msg.imagePath).toBe('/static/characters/char-1/incoming-photos/photo_123.jpg');
    expect(msg.content).toBe('[Fotoğraf]');
    expect(msg.role).toBe('user');
  });

  it('should save a text_photo message with all photo fields', async () => {
    const result = await manager.addMessage('char-1', 100, 'user', 'Check this out!', undefined, {
      messageType: 'text_photo',
      imagePath: '/static/characters/char-1/incoming-photos/photo_456.jpg',
      imageUrl: 'https://example.com/photo.jpg',
    });
    const msg = result.messages[0];
    expect(msg.messageType).toBe('text_photo');
    expect(msg.imagePath).toBe('/static/characters/char-1/incoming-photos/photo_456.jpg');
    expect(msg.imageUrl).toBe('https://example.com/photo.jpg');
    expect(msg.content).toBe('Check this out!');
  });

  it('should not set photo fields when photoInfo is not provided', async () => {
    const result = await manager.addMessage('char-1', 100, 'user', 'just text');
    const msg = result.messages[0];
    expect(msg.messageType).toBeUndefined();
    expect(msg.imagePath).toBeUndefined();
    expect(msg.imageUrl).toBeUndefined();
  });

  it('should preserve photo fields on round-trip read', async () => {
    await manager.addMessage('char-1', 100, 'user', 'photo msg', undefined, {
      messageType: 'photo',
      imagePath: '/static/characters/char-1/incoming-photos/test.jpg',
    });
    const history = await manager.getHistory('char-1', 100);
    expect(history).not.toBeNull();
    const msg = history!.messages[0];
    expect(msg.messageType).toBe('photo');
    expect(msg.imagePath).toBe('/static/characters/char-1/incoming-photos/test.jpg');
    expect(msg.role).toBe('user');
    expect(msg.content).toBe('photo msg');
  });

  it('should save both metadata and photoInfo together', async () => {
    const result = await manager.addMessage('char-1', 100, 'assistant', '[Fotoğraf gönderildi]', { photoSent: true }, {
      messageType: 'photo',
      imagePath: '/static/characters/char-1/generated-images/gen_789.jpg',
    });
    const msg = result.messages[0];
    expect(msg.metadata?.photoSent).toBe(true);
    expect(msg.messageType).toBe('photo');
    expect(msg.imagePath).toBe('/static/characters/char-1/generated-images/gen_789.jpg');
  });
});
