import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { UserbotConfig } from './client.js';

// Mock GramJS TelegramClient
const mockInvoke = vi.fn();
const mockSendMessage = vi.fn();
const mockSendFile = vi.fn();
const mockConnect = vi.fn();
const mockDisconnect = vi.fn();
const mockStart = vi.fn();
const mockAddEventHandler = vi.fn();
const mockSessionSave = vi.fn().mockReturnValue('test-session');
const mockGetEntity = vi.fn();
const mockGetInputEntity = vi.fn();

vi.mock('telegram', () => ({
  TelegramClient: vi.fn().mockImplementation(() => ({
    invoke: mockInvoke,
    sendMessage: mockSendMessage,
    sendFile: mockSendFile,
    connect: mockConnect,
    disconnect: mockDisconnect,
    start: mockStart,
    addEventHandler: mockAddEventHandler,
    session: { save: mockSessionSave },
    getEntity: mockGetEntity,
    getInputEntity: mockGetInputEntity,
  })),
}));

vi.mock('telegram/sessions/index.js', () => ({
  StringSession: vi.fn().mockImplementation((s: string) => ({ _session: s })),
}));

vi.mock('telegram/events/index.js', () => ({
  NewMessage: vi.fn(),
  NewMessageEvent: vi.fn(),
}));

vi.mock('telegram/tl/index.js', () => {
  // Define MockChatInviteExported inside the factory to avoid hoisting issues
  class ChatInviteExported {
    link: string;
    constructor(args: { link: string }) {
      this.link = args.link;
    }
  }

  return {
    Api: {
      channels: {
        CreateChannel: vi.fn().mockImplementation((args: any) => ({ _: 'channels.CreateChannel', ...args })),
      },
      messages: {
        CreateChat: vi.fn().mockImplementation((args: any) => ({ _: 'messages.CreateChat', ...args })),
        ExportChatInvite: vi.fn().mockImplementation((args: any) => ({ _: 'messages.ExportChatInvite', ...args })),
        SetTyping: vi.fn().mockImplementation((args: any) => ({ _: 'messages.SetTyping', ...args })),
      },
      Updates: class {},
      ChatInviteExported,
      SendMessageTypingAction: vi.fn(),
    },
  };
});

vi.mock('telegram/client/uploads.js', () => ({
  CustomFile: vi.fn().mockImplementation((name: string, size: number, _: string, buffer: Buffer) => ({
    name, size, buffer,
  })),
}));

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

vi.mock('../../utils/config.js', () => ({
  config: {
    telegramApiId: 12345,
    telegramApiHash: 'testhash',
    dataDir: '/tmp/test-data',
  },
}));

// Import after mocks are set up
import { UserbotClient } from './client.js';
import { Api } from 'telegram/tl/index.js';

function createClient(overrides?: Partial<UserbotConfig>): UserbotClient {
  return new UserbotClient({
    characterId: 'char-1',
    phoneNumber: '+1234567890',
    telegramApiId: 12345,
    telegramApiHash: 'testhash',
    ...overrides,
  });
}

/** Simulate connecting the client by calling reconnect (sets connected = true) */
async function connectClient(client: UserbotClient): Promise<void> {
  mockConnect.mockResolvedValueOnce(undefined);
  await client.reconnect();
}

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

  describe('createGroup', () => {
    it('throws if not connected', async () => {
      const client = createClient();
      await expect(client.createGroup('Test Group')).rejects.toThrow('Userbot not connected');
    });

    it('creates a normal group via messages.CreateChat', async () => {
      const client = createClient();
      await connectClient(client);

      const mockChat = { id: { valueOf: () => 123456 } };
      mockInvoke
        .mockResolvedValueOnce({ chats: [mockChat] }) // CreateChat
        .mockResolvedValueOnce(Object.assign(Object.create(Api.ChatInviteExported.prototype), { link: 'https://t.me/+abc' }));

      const result = await client.createGroup('Test Group', 'A test group');

      expect(result.chatId).toBe(123456);
      expect(result.inviteLink).toBe('https://t.me/+abc');
      expect(mockInvoke).toHaveBeenCalledTimes(2);
    });

    it('creates a supergroup via channels.CreateChannel with megagroup flag', async () => {
      const client = createClient();
      await connectClient(client);

      const mockChannel = { id: { valueOf: () => 789012 } };
      mockInvoke
        .mockResolvedValueOnce({ chats: [mockChannel] }) // CreateChannel
        .mockResolvedValueOnce(Object.assign(Object.create(Api.ChatInviteExported.prototype), { link: 'https://t.me/+xyz' }));

      const result = await client.createGroup('Super Group', 'A supergroup', true);

      expect(result.chatId).toBe(789012);
      expect(result.inviteLink).toBe('https://t.me/+xyz');
    });

    it('returns chatId without inviteLink if export fails', async () => {
      const client = createClient();
      await connectClient(client);

      const mockChat = { id: { valueOf: () => 111222 } };
      mockInvoke
        .mockResolvedValueOnce({ chats: [mockChat] }) // CreateChat
        .mockRejectedValueOnce(new Error('Export failed')); // ExportChatInvite fails

      const result = await client.createGroup('Group No Link');

      expect(result.chatId).toBe(111222);
      expect(result.inviteLink).toBeUndefined();
    });

    it('throws if no chat returned from API', async () => {
      const client = createClient();
      await connectClient(client);

      mockInvoke.mockResolvedValueOnce({ chats: [] });

      await expect(client.createGroup('Empty Group')).rejects.toThrow('Failed to create group: no chat returned');
    });
  });

  describe('createChannel', () => {
    it('throws if not connected', async () => {
      const client = createClient();
      await expect(client.createChannel('Test Channel')).rejects.toThrow('Userbot not connected');
    });

    it('creates a broadcast channel via channels.CreateChannel', async () => {
      const client = createClient();
      await connectClient(client);

      const mockChannel = { id: { valueOf: () => 555666 } };
      mockInvoke
        .mockResolvedValueOnce({ chats: [mockChannel] }) // CreateChannel
        .mockResolvedValueOnce(Object.assign(Object.create(Api.ChatInviteExported.prototype), { link: 'https://t.me/+chan' }));

      const result = await client.createChannel('Test Channel', 'A test channel');

      expect(result.chatId).toBe(555666);
      expect(result.inviteLink).toBe('https://t.me/+chan');
    });

    it('returns chatId without inviteLink if export fails', async () => {
      const client = createClient();
      await connectClient(client);

      const mockChannel = { id: { valueOf: () => 777888 } };
      mockInvoke
        .mockResolvedValueOnce({ chats: [mockChannel] })
        .mockRejectedValueOnce(new Error('Export failed'));

      const result = await client.createChannel('Channel No Link');

      expect(result.chatId).toBe(777888);
      expect(result.inviteLink).toBeUndefined();
    });

    it('throws if no channel returned from API', async () => {
      const client = createClient();
      await connectClient(client);

      mockInvoke.mockResolvedValueOnce({ chats: [] });

      await expect(client.createChannel('Empty Channel')).rejects.toThrow('Failed to create channel: no chat returned');
    });
  });

  describe('sendMessage', () => {
    it('throws if not connected', async () => {
      const client = createClient();
      await expect(client.sendMessage(123, 'hello')).rejects.toThrow('Userbot not connected');
    });

    it('sends text-only message via GramJS', async () => {
      const client = createClient();
      await connectClient(client);

      const mockEntity = { id: 123 };
      const mockInputPeer = { className: 'InputPeerUser' };
      mockGetEntity.mockResolvedValueOnce(mockEntity);
      mockGetInputEntity.mockResolvedValueOnce(mockInputPeer);
      mockSendMessage.mockResolvedValueOnce(undefined);

      await client.sendMessage(123, 'Hello world');

      expect(mockSendMessage).toHaveBeenCalledWith(mockInputPeer, { message: 'Hello world' });
    });

    it('sends message with image when imageUrl is provided', async () => {
      const client = createClient();
      await connectClient(client);

      const mockEntity = { id: 456 };
      const mockInputPeer = { className: 'InputPeerChannel' };
      mockGetEntity.mockResolvedValueOnce(mockEntity);
      mockGetInputEntity.mockResolvedValueOnce(mockInputPeer);

      const mockBuffer = new ArrayBuffer(8);
      const mockResponse = {
        ok: true,
        arrayBuffer: () => Promise.resolve(mockBuffer),
      };
      vi.stubGlobal('fetch', vi.fn().mockResolvedValueOnce(mockResponse));

      mockSendFile.mockResolvedValueOnce(undefined);

      await client.sendMessage(456, 'Check this out', 'https://example.com/image.jpg');

      expect(mockSendFile).toHaveBeenCalledTimes(1);
      const callArgs = mockSendFile.mock.calls[0];
      expect(callArgs[0]).toEqual(mockInputPeer);
      expect(callArgs[1]).toHaveProperty('caption', 'Check this out');

      vi.unstubAllGlobals();
    });

    it('falls back to text when image fetch fails', async () => {
      const client = createClient();
      await connectClient(client);

      const mockEntity = { id: 789 };
      const mockInputPeer = { className: 'InputPeerUser' };
      mockGetEntity.mockResolvedValueOnce(mockEntity);
      mockGetInputEntity.mockResolvedValueOnce(mockInputPeer);

      vi.stubGlobal('fetch', vi.fn().mockResolvedValueOnce({ ok: false, status: 404 }));
      mockSendMessage.mockResolvedValueOnce(undefined);

      await client.sendMessage(789, 'Fallback text', 'https://example.com/bad.jpg');

      expect(mockSendMessage).toHaveBeenCalledWith(mockInputPeer, { message: 'Fallback text' });

      vi.unstubAllGlobals();
    });
  });
});
