import { describe, it, expect, vi, beforeEach } from 'vitest';
import { TelegramManager } from './index.js';
import type { TelegramBotInfo } from '../../types/telegram.js';

// --- Mocks ---

const mockGetMe = vi.fn();
const mockSetWebhook = vi.fn();
const mockDeleteWebhook = vi.fn();

vi.mock('./bot-client.js', () => ({
  TelegramBotClient: vi.fn().mockImplementation(() => ({
    getMe: mockGetMe,
    setWebhook: mockSetWebhook,
    deleteWebhook: mockDeleteWebhook,
  })),
}));

vi.mock('./webhook-handler.js', () => ({
  WebhookHandler: vi.fn().mockImplementation(() => ({
    registerToken: vi.fn(),
    unregisterToken: vi.fn(),
  })),
}));

vi.mock('./channel-manager.js', () => ({
  ChannelManager: vi.fn().mockImplementation(() => ({
    stopSchedule: vi.fn(),
    stopAllSchedules: vi.fn(),
  })),
}));

vi.mock('./stars-manager.js', () => ({
  StarsManager: vi.fn().mockImplementation(() => ({})),
}));

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

const BOT_INFO: TelegramBotInfo = {
  id: 111222333,
  is_bot: true,
  first_name: 'TestBot',
  username: 'test_bot',
};

function createMockDataStore() {
  return {
    saveCharacterChannelConfig: vi.fn(),
    getCharacterChannelConfig: vi.fn(),
  } as any;
}

function createMockEncryptedStore() {
  const store = new Map<string, string>();
  return {
    setKey: vi.fn(async (name: string, value: string) => { store.set(name, value); }),
    getKey: vi.fn(async (name: string) => store.get(name) ?? null),
    deleteKey: vi.fn(async (name: string) => { const had = store.has(name); store.delete(name); return had; }),
    listKeys: vi.fn(async () =>
      Array.from(store.entries()).map(([name]) => ({
        name,
        maskedValue: '****',
        createdAt: new Date().toISOString(),
        updatedAt: new Date().toISOString(),
      })),
    ),
    hasKey: vi.fn(async (name: string) => store.has(name)),
    _store: store,
  } as any;
}

describe('TelegramManager', () => {
  let manager: TelegramManager;
  let dataStore: ReturnType<typeof createMockDataStore>;
  let encryptedStore: ReturnType<typeof createMockEncryptedStore>;

  beforeEach(() => {
    vi.clearAllMocks();
    mockGetMe.mockResolvedValue(BOT_INFO);
    mockSetWebhook.mockResolvedValue(true);
    mockDeleteWebhook.mockResolvedValue(true);
    dataStore = createMockDataStore();
    encryptedStore = createMockEncryptedStore();
    manager = new TelegramManager(dataStore, encryptedStore);
  });

  describe('addBot', () => {
    it('validates token via getMe and stores encrypted token', async () => {
      const result = await manager.addBot('char-1', 'token-abc');

      expect(result).toEqual(BOT_INFO);
      expect(mockGetMe).toHaveBeenCalledOnce();
      expect(encryptedStore.setKey).toHaveBeenCalledWith('bot-token:char-1', 'token-abc');
    });

    it('registers token with webhook handler', async () => {
      await manager.addBot('char-1', 'token-abc');

      const handler = manager.getWebhookHandler();
      expect(handler.registerToken).toHaveBeenCalledWith('token-abc');
    });

    it('makes bot available via getBotClient', async () => {
      await manager.addBot('char-1', 'token-abc');

      const client = manager.getBotClient('char-1');
      expect(client).toBeDefined();
    });

    it('throws if getMe fails (invalid token)', async () => {
      mockGetMe.mockRejectedValueOnce(new Error('Unauthorized'));

      await expect(manager.addBot('char-1', 'bad-token')).rejects.toThrow('Unauthorized');
    });
  });

  describe('removeBot', () => {
    it('removes bot, deletes webhook, and cleans up encrypted token', async () => {
      await manager.addBot('char-1', 'token-abc');
      await manager.removeBot('char-1');

      expect(mockDeleteWebhook).toHaveBeenCalled();
      expect(encryptedStore.deleteKey).toHaveBeenCalledWith('bot-token:char-1');
      expect(manager.getBotClient('char-1')).toBeUndefined();
    });

    it('handles removal of non-existent bot gracefully', async () => {
      await expect(manager.removeBot('nonexistent')).resolves.toBeUndefined();
      expect(encryptedStore.deleteKey).toHaveBeenCalledWith('bot-token:nonexistent');
    });

    it('unregisters token from webhook handler', async () => {
      await manager.addBot('char-1', 'token-abc');
      await manager.removeBot('char-1');

      const handler = manager.getWebhookHandler();
      expect(handler.unregisterToken).toHaveBeenCalledWith('token-abc');
    });
  });

  describe('updateBot', () => {
    it('removes old bot and adds new one', async () => {
      await manager.addBot('char-1', 'old-token');

      const newBotInfo = { ...BOT_INFO, username: 'new_bot' };
      mockGetMe.mockResolvedValueOnce(newBotInfo);

      const result = await manager.updateBot('char-1', 'new-token');

      expect(result).toEqual(newBotInfo);
      expect(encryptedStore.deleteKey).toHaveBeenCalledWith('bot-token:char-1');
      expect(encryptedStore.setKey).toHaveBeenCalledWith('bot-token:char-1', 'new-token');
    });
  });

  describe('validateBot', () => {
    it('validates existing bot and updates botInfo', async () => {
      await manager.addBot('char-1', 'token-abc');

      const updatedInfo = { ...BOT_INFO, first_name: 'UpdatedBot' };
      mockGetMe.mockResolvedValueOnce(updatedInfo);

      const result = await manager.validateBot('char-1');

      expect(result).toEqual(updatedInfo);
    });

    it('throws for non-existent bot', async () => {
      await expect(manager.validateBot('nonexistent')).rejects.toThrow('No bot found');
    });
  });

  describe('getBotClient', () => {
    it('returns undefined for unknown character', () => {
      expect(manager.getBotClient('unknown')).toBeUndefined();
    });

    it('returns client for added bot', async () => {
      await manager.addBot('char-1', 'token-abc');
      expect(manager.getBotClient('char-1')).toBeDefined();
    });
  });

  describe('setupWebhook', () => {
    it('sets webhook with correct URL', async () => {
      await manager.addBot('char-1', 'token-abc');
      await manager.setupWebhook('char-1', 'https://example.com');

      expect(mockSetWebhook).toHaveBeenCalledWith(
        'https://example.com/api/v1/webhooks/telegram/token-abc',
      );
    });

    it('throws for non-existent bot', async () => {
      await expect(manager.setupWebhook('nonexistent', 'https://example.com')).rejects.toThrow(
        'No bot found',
      );
    });
  });

  describe('removeWebhook', () => {
    it('deletes webhook for existing bot', async () => {
      await manager.addBot('char-1', 'token-abc');
      await manager.removeWebhook('char-1');

      expect(mockDeleteWebhook).toHaveBeenCalled();
    });

    it('throws for non-existent bot', async () => {
      await expect(manager.removeWebhook('nonexistent')).rejects.toThrow('No bot found');
    });
  });

  describe('getWebhookHandler', () => {
    it('returns the shared WebhookHandler instance', () => {
      const handler = manager.getWebhookHandler();
      expect(handler).toBeDefined();
      expect(manager.getWebhookHandler()).toBe(handler);
    });
  });

  describe('getChannelManager', () => {
    it('returns the shared ChannelManager instance', () => {
      const cm = manager.getChannelManager();
      expect(cm).toBeDefined();
      expect(manager.getChannelManager()).toBe(cm);
    });
  });

  describe('getStarsManager', () => {
    it('returns the shared StarsManager instance', () => {
      const sm = manager.getStarsManager();
      expect(sm).toBeDefined();
      expect(manager.getStarsManager()).toBe(sm);
    });
  });

  describe('start', () => {
    it('loads all stored bot tokens and creates clients', async () => {
      // Pre-populate encrypted store with bot tokens
      encryptedStore._store.set('bot-token:char-1', 'token-1');
      encryptedStore._store.set('bot-token:char-2', 'token-2');
      encryptedStore._store.set('openrouter-key', 'or-key'); // non-bot key

      await manager.start();

      expect(manager.getBotClient('char-1')).toBeDefined();
      expect(manager.getBotClient('char-2')).toBeDefined();
      expect(manager.getActiveBots().size).toBe(2);
    });

    it('continues loading other bots if one fails validation', async () => {
      encryptedStore._store.set('bot-token:char-ok', 'token-ok');
      encryptedStore._store.set('bot-token:char-bad', 'token-bad');

      let callCount = 0;
      mockGetMe.mockImplementation(() => {
        callCount++;
        if (callCount === 1) return Promise.reject(new Error('Invalid token'));
        return Promise.resolve(BOT_INFO);
      });

      await manager.start();

      // At least one bot should be loaded
      expect(manager.getActiveBots().size).toBeGreaterThanOrEqual(1);
    });

    it('handles empty store gracefully', async () => {
      await manager.start();
      expect(manager.getActiveBots().size).toBe(0);
    });
  });

  describe('stop', () => {
    it('cleans up all bots and stops schedules', async () => {
      await manager.addBot('char-1', 'token-1');
      await manager.addBot('char-2', 'token-2');

      await manager.stop();

      expect(manager.getActiveBots().size).toBe(0);
      expect(mockDeleteWebhook).toHaveBeenCalled();
      expect(manager.getChannelManager().stopAllSchedules).toHaveBeenCalled();
    });

    it('handles webhook deletion failure gracefully during stop', async () => {
      await manager.addBot('char-1', 'token-1');
      mockDeleteWebhook.mockRejectedValueOnce(new Error('Network error'));

      await expect(manager.stop()).resolves.toBeUndefined();
      expect(manager.getActiveBots().size).toBe(0);
    });
  });

  describe('getActiveBots', () => {
    it('returns empty map when no bots', () => {
      expect(manager.getActiveBots().size).toBe(0);
    });

    it('returns all active bots with info', async () => {
      await manager.addBot('char-1', 'token-1');
      await manager.addBot('char-2', 'token-2');

      const bots = manager.getActiveBots();
      expect(bots.size).toBe(2);
      expect(bots.get('char-1')?.characterId).toBe('char-1');
      expect(bots.get('char-1')?.botInfo).toEqual(BOT_INFO);
      expect(bots.get('char-2')?.characterId).toBe('char-2');
    });
  });

  describe('getBotStatus', () => {
    it('returns active=false for unknown character', async () => {
      const status = await manager.getBotStatus('unknown');
      expect(status).toEqual({ active: false });
    });

    it('returns active=true with botInfo for added bot', async () => {
      await manager.addBot('char-1', 'token-abc');

      const status = await manager.getBotStatus('char-1');
      expect(status.active).toBe(true);
      expect(status.botInfo).toEqual(BOT_INFO);
    });
  });

  describe('multi-bot management', () => {
    it('manages multiple bots independently', async () => {
      const info1 = { ...BOT_INFO, id: 1, username: 'bot_1' };
      const info2 = { ...BOT_INFO, id: 2, username: 'bot_2' };

      mockGetMe.mockResolvedValueOnce(info1).mockResolvedValueOnce(info2);

      await manager.addBot('char-1', 'token-1');
      await manager.addBot('char-2', 'token-2');

      expect(manager.getActiveBots().size).toBe(2);

      await manager.removeBot('char-1');
      expect(manager.getActiveBots().size).toBe(1);
      expect(manager.getBotClient('char-1')).toBeUndefined();
      expect(manager.getBotClient('char-2')).toBeDefined();
    });
  });
});
