import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import type { ChannelConfig, TelegramBotClient } from './channel-manager.js';

// --- Mock data store ---
const channelStore = new Map<string, ChannelConfig>();

vi.mock('../data-store/index.js', () => ({
  dataStore: {
    saveCharacterChannelConfig: vi.fn(async (id: string, config: ChannelConfig) => {
      channelStore.set(id, structuredClone(config));
    }),
    getCharacterChannelConfig: vi.fn(async (id: string) => {
      const v = channelStore.get(id);
      return v ? structuredClone(v) : null;
    }),
  },
}));

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

import { ChannelManager } from './channel-manager.js';

function createMockBotClient(overrides?: Partial<TelegramBotClient>): TelegramBotClient {
  return {
    createChannel: vi.fn(async (title: string) => ({
      chatId: Math.floor(Math.random() * -1000000),
      inviteLink: `https://t.me/+${title.replace(/\s/g, '')}`,
    })),
    sendMessage: vi.fn(async () => {}),
    sendPhoto: vi.fn(async () => {}),
    sendPaidMedia: vi.fn(async () => {}),
    getChatMemberCount: vi.fn(async () => 42),
    ...overrides,
  };
}

function createMockDataStore() {
  return {
    saveCharacterChannelConfig: vi.fn(async (id: string, config: ChannelConfig) => {
      channelStore.set(id, structuredClone(config));
    }),
    getCharacterChannelConfig: vi.fn(async (id: string) => {
      const v = channelStore.get(id);
      return v ? structuredClone(v) : null;
    }),
  } as any;
}

let manager: ChannelManager;
let mockBot: TelegramBotClient;
let mockStore: ReturnType<typeof createMockDataStore>;

beforeEach(() => {
  channelStore.clear();
  vi.clearAllMocks();
  mockBot = createMockBotClient();
  mockStore = createMockDataStore();
  manager = new ChannelManager(mockStore, (id) => (id === 'char-1' ? mockBot : undefined));
});

afterEach(() => {
  manager.stopAllSchedules();
});

describe('ChannelManager.createChannels', () => {
  it('should create normal and VIP channels', async () => {
    const config = await manager.createChannels('char-1', mockBot, 'Test Normal', 'Test VIP');

    expect(config.characterId).toBe('char-1');
    expect(config.normalChannel).toBeDefined();
    expect(config.normalChannel!.title).toBe('Test Normal');
    expect(config.normalChannel!.memberCount).toBe(0);
    expect(config.vipChannel).toBeDefined();
    expect(config.vipChannel!.title).toBe('Test VIP');
    expect(config.vipChannel!.starPrice).toBe(50);
    expect(config.createdAt).toBeTruthy();
    expect(config.updatedAt).toBeTruthy();
  });

  it('should call createChannel on bot client twice', async () => {
    await manager.createChannels('char-1', mockBot, 'Normal', 'VIP');
    expect(mockBot.createChannel).toHaveBeenCalledTimes(2);
    expect(mockBot.createChannel).toHaveBeenCalledWith('Normal', 'Public promotional content');
    expect(mockBot.createChannel).toHaveBeenCalledWith('VIP', 'VIP exclusive content — Stars access');
  });

  it('should persist config to data store', async () => {
    await manager.createChannels('char-1', mockBot, 'Normal', 'VIP');
    expect(mockStore.saveCharacterChannelConfig).toHaveBeenCalledWith('char-1', expect.objectContaining({ characterId: 'char-1' }));
  });

  it('should store invite links from bot client', async () => {
    const bot = createMockBotClient({
      createChannel: vi.fn(async (title: string) => ({
        chatId: -100,
        inviteLink: `https://t.me/+invite_${title}`,
      })),
    });

    const config = await manager.createChannels('char-1', bot, 'N', 'V');
    expect(config.normalChannel!.inviteLink).toBe('https://t.me/+invite_N');
    expect(config.vipChannel!.inviteLink).toBe('https://t.me/+invite_V');
  });
});

describe('ChannelManager.getChannelConfig', () => {
  it('should return null for unknown character', async () => {
    const result = await manager.getChannelConfig('unknown');
    expect(result).toBeNull();
  });

  it('should return config after creation', async () => {
    await manager.createChannels('char-1', mockBot, 'N', 'V');
    const config = await manager.getChannelConfig('char-1');
    expect(config).not.toBeNull();
    expect(config!.characterId).toBe('char-1');
  });

  it('should load from store if not cached', async () => {
    const stored: ChannelConfig = {
      characterId: 'char-2',
      normalChannel: { chatId: -200, title: 'Stored Normal', memberCount: 10 },
      vipChannel: { chatId: -201, title: 'Stored VIP', memberCount: 5, starPrice: 100 },
      createdAt: '2024-01-01T00:00:00Z',
      updatedAt: '2024-01-01T00:00:00Z',
    };
    channelStore.set('char-2', stored);

    const manager2 = new ChannelManager(mockStore, () => undefined);
    const config = await manager2.getChannelConfig('char-2');
    expect(config).not.toBeNull();
    expect(config!.normalChannel!.title).toBe('Stored Normal');
  });
});

describe('ChannelManager.updateChannelConfig', () => {
  beforeEach(async () => {
    await manager.createChannels('char-1', mockBot, 'Normal', 'VIP');
  });

  it('should update publishSchedule', async () => {
    const original = await manager.getChannelConfig('char-1');
    const updated = await manager.updateChannelConfig('char-1', { publishSchedule: '0 9 * * *' });
    expect(updated.publishSchedule).toBe('0 9 * * *');
    expect(updated.updatedAt).toBeTruthy();
    expect(updated.createdAt).toBe(original!.createdAt);
  });

  it('should merge normalChannel updates', async () => {
    const original = await manager.getChannelConfig('char-1');
    const updated = await manager.updateChannelConfig('char-1', {
      normalChannel: { chatId: original!.normalChannel!.chatId, title: 'Updated Normal', memberCount: 100 },
    });
    expect(updated.normalChannel!.title).toBe('Updated Normal');
    expect(updated.normalChannel!.memberCount).toBe(100);
  });

  it('should merge vipChannel updates', async () => {
    const updated = await manager.updateChannelConfig('char-1', {
      vipChannel: { chatId: -999, title: 'Premium VIP', memberCount: 50, starPrice: 200 },
    });
    expect(updated.vipChannel!.starPrice).toBe(200);
    expect(updated.vipChannel!.title).toBe('Premium VIP');
  });

  it('should not allow overriding characterId or createdAt', async () => {
    const original = await manager.getChannelConfig('char-1');
    const updated = await manager.updateChannelConfig('char-1', {
      characterId: 'hacked',
      createdAt: '1999-01-01T00:00:00Z',
    } as Partial<ChannelConfig>);
    expect(updated.characterId).toBe('char-1');
    expect(updated.createdAt).toBe(original!.createdAt);
  });

  it('should throw for non-existent character', async () => {
    await expect(manager.updateChannelConfig('unknown', {})).rejects.toThrow('Channel config not found');
  });

  it('should persist updated config', async () => {
    await manager.updateChannelConfig('char-1', { publishSchedule: '*/5 * * * *' });
    expect(mockStore.saveCharacterChannelConfig).toHaveBeenCalled();
  });
});

describe('ChannelManager.publishToNormal', () => {
  beforeEach(async () => {
    await manager.createChannels('char-1', mockBot, 'Normal', 'VIP');
  });

  it('should send text message to normal channel', async () => {
    await manager.publishToNormal('char-1', { text: 'Hello world!' });
    expect(mockBot.sendMessage).toHaveBeenCalledWith(
      expect.any(Number),
      'Hello world!',
    );
  });

  it('should send photo to normal channel', async () => {
    await manager.publishToNormal('char-1', { photoUrl: 'https://example.com/photo.jpg', caption: 'Check this out' });
    expect(mockBot.sendPhoto).toHaveBeenCalledWith(
      expect.any(Number),
      'https://example.com/photo.jpg',
      'Check this out',
    );
  });

  it('should prefer photo over text when both provided', async () => {
    await manager.publishToNormal('char-1', { text: 'fallback', photoUrl: 'https://example.com/photo.jpg' });
    expect(mockBot.sendPhoto).toHaveBeenCalled();
    expect(mockBot.sendMessage).not.toHaveBeenCalled();
  });

  it('should throw if normal channel not configured', async () => {
    await expect(manager.publishToNormal('unknown', { text: 'hi' })).rejects.toThrow('Normal channel not configured');
  });

  it('should throw if bot client not available', async () => {
    // Create channels with a direct bot, then try to publish with a manager that can't find the bot
    const noBot = new ChannelManager(mockStore, () => undefined);
    await manager.createChannels('char-1', mockBot, 'N', 'V');
    await expect(noBot.publishToNormal('char-1', { text: 'hi' })).rejects.toThrow('Bot client not available');
  });
});

describe('ChannelManager.publishToVIP', () => {
  beforeEach(async () => {
    await manager.createChannels('char-1', mockBot, 'Normal', 'VIP');
  });

  it('should send paid media to VIP channel', async () => {
    await manager.publishToVIP('char-1', 'https://example.com/vip.jpg', 100, 'Exclusive!');
    expect(mockBot.sendPaidMedia).toHaveBeenCalledWith({
      chat_id: expect.any(Number),
      star_count: 100,
      media: [{ type: 'photo', media: 'https://example.com/vip.jpg' }],
      caption: 'Exclusive!',
    });
  });

  it('should use default starPrice when starCount is 0', async () => {
    await manager.publishToVIP('char-1', 'https://example.com/vip.jpg', 0);
    expect(mockBot.sendPaidMedia).toHaveBeenCalledWith(
      expect.objectContaining({ star_count: 50 }),
    );
  });

  it('should throw if VIP channel not configured', async () => {
    await expect(manager.publishToVIP('unknown', 'url', 10)).rejects.toThrow('VIP channel not configured');
  });

  it('should throw if bot client not available', async () => {
    const noBot = new ChannelManager(mockStore, () => undefined);
    await manager.createChannels('char-1', mockBot, 'N', 'V');
    await expect(noBot.publishToVIP('char-1', 'url', 10)).rejects.toThrow('Bot client not available');
  });
});

describe('ChannelManager.getStats', () => {
  beforeEach(async () => {
    await manager.createChannels('char-1', mockBot, 'Normal', 'VIP');
  });

  it('should return member counts from Telegram API', async () => {
    const stats = await manager.getStats('char-1');
    expect(stats).not.toBeNull();
    expect(stats!.normalMembers).toBe(42);
    expect(stats!.vipMembers).toBe(42);
  });

  it('should call getChatMemberCount for both channels', async () => {
    await manager.getStats('char-1');
    expect(mockBot.getChatMemberCount).toHaveBeenCalledTimes(2);
  });

  it('should return null for unknown character', async () => {
    const stats = await manager.getStats('unknown');
    expect(stats).toBeNull();
  });

  it('should return null if bot client not available', async () => {
    const noBot = new ChannelManager(mockStore, () => undefined);
    await manager.createChannels('char-1', mockBot, 'N', 'V');
    const stats = await noBot.getStats('char-1');
    expect(stats).toBeNull();
  });

  it('should persist updated member counts', async () => {
    await manager.getStats('char-1');
    expect(mockStore.saveCharacterChannelConfig).toHaveBeenCalled();
  });

  it('should handle API errors gracefully', async () => {
    const failBot = createMockBotClient({
      getChatMemberCount: vi.fn(async () => { throw new Error('API error'); }),
    });
    const failManager = new ChannelManager(mockStore, () => failBot);
    await failManager.createChannels('char-1', failBot, 'N', 'V');

    const stats = await failManager.getStats('char-1');
    expect(stats).not.toBeNull();
    expect(stats!.normalMembers).toBe(0);
    expect(stats!.vipMembers).toBe(0);
  });
});

describe('ChannelManager scheduling', () => {
  it('should start a cron schedule', async () => {
    await manager.createChannels('char-1', mockBot, 'N', 'V');
    expect(() => manager.startSchedule('char-1', '0 9 * * *')).not.toThrow();
  });

  it('should throw for invalid cron expression', () => {
    expect(() => manager.startSchedule('char-1', 'not-a-cron')).toThrow('Invalid cron expression');
  });

  it('should stop a schedule', async () => {
    await manager.createChannels('char-1', mockBot, 'N', 'V');
    manager.startSchedule('char-1', '0 9 * * *');
    expect(() => manager.stopSchedule('char-1')).not.toThrow();
  });

  it('should stop all schedules', async () => {
    await manager.createChannels('char-1', mockBot, 'N', 'V');
    manager.startSchedule('char-1', '0 9 * * *');
    expect(() => manager.stopAllSchedules()).not.toThrow();
  });

  it('should replace existing schedule on restart', async () => {
    await manager.createChannels('char-1', mockBot, 'N', 'V');
    manager.startSchedule('char-1', '0 9 * * *');
    manager.startSchedule('char-1', '0 18 * * *');
    // No error means old was stopped and new was started
    manager.stopAllSchedules();
  });

  it('should start schedule when publishSchedule is updated', async () => {
    await manager.createChannels('char-1', mockBot, 'N', 'V');
    await manager.updateChannelConfig('char-1', { publishSchedule: '*/30 * * * *' });
    // Schedule should be running — stop it to verify no error
    manager.stopAllSchedules();
  });
});
