import { describe, it, expect, vi, beforeEach } from 'vitest';

// ── Mock grammy before importing adapter ──────────────────────────

const mockUse = vi.fn();
const mockOn = vi.fn();
const mockCommand = vi.fn();
const mockStart = vi.fn();
const mockStop = vi.fn();
const mockSendMessage = vi.fn();

vi.mock('grammy', () => ({
  Bot: vi.fn().mockImplementation(() => ({
    use: mockUse,
    on: mockOn,
    command: mockCommand,
    start: mockStart,
    stop: mockStop,
    api: { sendMessage: mockSendMessage },
  })),
}));

import { TelegramAdapter, type TelegramAdapterConfig } from './adapter.js';
import type { InboundMessage } from '../types.js';

const baseConfig: TelegramAdapterConfig = {
  botToken: 'test-token-123',
  allowedChatIds: [100, 200],
};

describe('TelegramAdapter', () => {
  let adapter: TelegramAdapter;

  beforeEach(() => {
    vi.clearAllMocks();
    adapter = new TelegramAdapter(baseConfig);
  });

  // ── Basic properties ──────────────────────────────────────────

  it('has name "telegram"', () => {
    expect(adapter.name).toBe('telegram');
  });

  it('starts as disconnected', () => {
    expect(adapter.status).toBe('disconnected');
  });

  // ── connect / disconnect ──────────────────────────────────────

  it('connect creates a bot and sets status to connected', async () => {
    await adapter.connect();

    expect(adapter.status).toBe('connected');
    // Bot constructor called with the token
    const { Bot } = await import('grammy');
    expect(Bot).toHaveBeenCalledWith('test-token-123');
  });

  it('connect registers auth middleware, commands, and message handler', async () => {
    await adapter.connect();

    // .use() for auth middleware
    expect(mockUse).toHaveBeenCalledTimes(1);
    // .command() for /start, /reset, /model, /local, /cloud
    expect(mockCommand).toHaveBeenCalledTimes(5);
    expect(mockCommand.mock.calls[0][0]).toBe('start');
    expect(mockCommand.mock.calls[1][0]).toBe('reset');
    expect(mockCommand.mock.calls[2][0]).toBe('model');
    expect(mockCommand.mock.calls[3][0]).toBe('local');
    expect(mockCommand.mock.calls[4][0]).toBe('cloud');
    // .on('message:text', ...) for text handler
    expect(mockOn).toHaveBeenCalledWith('message:text', expect.any(Function));
    // .start() to begin long polling
    expect(mockStart).toHaveBeenCalledTimes(1);
  });

  it('connect registers callback_query handler when hookEngine is provided', async () => {
    const hookEngine = { resolveConfirmation: vi.fn() };
    const adapterWithHooks = new TelegramAdapter({
      ...baseConfig,
      hookEngine: hookEngine as never,
    });

    await adapterWithHooks.connect();

    // Should have .on('callback_query:data', ...) plus .on('message:text', ...)
    expect(mockOn).toHaveBeenCalledWith('callback_query:data', expect.any(Function));
    expect(mockOn).toHaveBeenCalledWith('message:text', expect.any(Function));
  });

  it('disconnect stops the bot and sets status to disconnected', async () => {
    await adapter.connect();
    expect(adapter.status).toBe('connected');

    await adapter.disconnect();
    expect(mockStop).toHaveBeenCalledTimes(1);
    expect(adapter.status).toBe('disconnected');
  });

  it('disconnect is safe to call when not connected', async () => {
    await adapter.disconnect();
    expect(adapter.status).toBe('disconnected');
    expect(mockStop).not.toHaveBeenCalled();
  });

  // ── send ──────────────────────────────────────────────────────

  it('send throws when adapter is not connected', async () => {
    await expect(adapter.send('100', { text: 'hello' })).rejects.toThrow(
      'Telegram adapter not connected',
    );
  });

  it('send delivers a short message in a single API call', async () => {
    await adapter.connect();

    await adapter.send('100', { text: 'Hello there' });

    expect(mockSendMessage).toHaveBeenCalledTimes(1);
    expect(mockSendMessage).toHaveBeenCalledWith(100, 'Hello there', { parse_mode: 'Markdown' });
  });

  it('send chunks a long message that exceeds 4096 chars', async () => {
    await adapter.connect();

    // Create a message that is longer than 4096 chars — two halves joined by a newline
    const half = 'A'.repeat(3000);
    const longMessage = `${half}\n${'B'.repeat(3000)}`;

    await adapter.send('200', { text: longMessage });

    // Should have been split into 2 chunks
    expect(mockSendMessage.mock.calls.length).toBeGreaterThanOrEqual(2);
    // Each call uses numeric chatId and parse_mode
    for (const call of mockSendMessage.mock.calls) {
      expect(call[0]).toBe(200);
      expect(call[2]).toEqual({ parse_mode: 'Markdown' });
    }
  });

  // ── onMessage / inbound handling ──────────────────────────────

  it('onMessage registers a handler that receives text messages', async () => {
    const handler = vi.fn();
    adapter.onMessage(handler);

    await adapter.connect();

    // Get the registered message:text handler from mockOn
    const textHandlerCall = mockOn.mock.calls.find(
      (call) => call[0] === 'message:text',
    );
    expect(textHandlerCall).toBeDefined();

    const textHandler = textHandlerCall![1];

    // Simulate a grammy context object
    const ctx = {
      message: { message_id: 42, text: 'Hello Flynn' },
      chat: { id: 100 },
      from: { first_name: 'Will' },
      replyWithChatAction: vi.fn(),
    };

    await textHandler(ctx);

    expect(ctx.replyWithChatAction).toHaveBeenCalledWith('typing');
    expect(handler).toHaveBeenCalledTimes(1);

    const msg: InboundMessage = handler.mock.calls[0][0];
    expect(msg.channel).toBe('telegram');
    expect(msg.senderId).toBe('100');
    expect(msg.senderName).toBe('Will');
    expect(msg.text).toBe('Hello Flynn');
    expect(msg.id).toBe('42');
  });

  it('text handler does nothing when no message handler is registered', async () => {
    // Don't call onMessage — no handler
    await adapter.connect();

    const textHandlerCall = mockOn.mock.calls.find(
      (call) => call[0] === 'message:text',
    );
    const textHandler = textHandlerCall![1];

    const ctx = {
      message: { message_id: 1, text: 'test' },
      chat: { id: 100 },
      from: { first_name: 'Will' },
      replyWithChatAction: vi.fn(),
    };

    // Should not throw
    await textHandler(ctx);
    expect(ctx.replyWithChatAction).not.toHaveBeenCalled();
  });

  // ── /reset command ────────────────────────────────────────────

  it('/reset command delivers a reset inbound message', async () => {
    const handler = vi.fn();
    adapter.onMessage(handler);

    await adapter.connect();

    // Find the /reset command handler
    const resetCall = mockCommand.mock.calls.find((call) => call[0] === 'reset');
    expect(resetCall).toBeDefined();

    const resetHandler = resetCall![1];

    const ctx = {
      message: { message_id: 99 },
      chat: { id: 100 },
      from: { first_name: 'Will' },
      reply: vi.fn(),
    };

    await resetHandler(ctx);

    expect(ctx.reply).toHaveBeenCalledWith('Conversation reset.');
    expect(handler).toHaveBeenCalledTimes(1);

    const msg: InboundMessage = handler.mock.calls[0][0];
    expect(msg.text).toBe('/reset');
    expect(msg.metadata).toEqual({ isCommand: true, command: 'reset' });
  });

  it('/model command strips @bot suffix in groups', async () => {
    const handler = vi.fn();
    adapter.onMessage(handler);

    await adapter.connect();

    // Find the /model command handler
    const modelCall = mockCommand.mock.calls.find((call) => call[0] === 'model');
    expect(modelCall).toBeDefined();
    const modelHandler = modelCall![1];

    const ctx = {
      message: { message_id: 123, text: '/model@flynn_bot default github/gpt-5-mini' },
      chat: { id: 100 },
      from: { first_name: 'Will' },
    };

    await modelHandler(ctx);

    expect(handler).toHaveBeenCalledTimes(1);
    const msg: InboundMessage = handler.mock.calls[0][0];
    expect(msg.text).toBe('/model default github/gpt-5-mini');
    expect(msg.metadata).toEqual({
      isCommand: true,
      command: 'model',
      commandArgs: 'default github/gpt-5-mini',
    });
  });

  // ── Auth middleware ───────────────────────────────────────────

  it('auth middleware blocks unauthorized chat IDs', async () => {
    await adapter.connect();

    // The first .use() call is the auth middleware
    const authMiddleware = mockUse.mock.calls[0][0];

    const next = vi.fn();
    const ctx = { chat: { id: 999 } }; // Not in allowedChatIds

    await authMiddleware(ctx, next);

    expect(next).not.toHaveBeenCalled();
  });

  it('auth middleware allows authorized chat IDs', async () => {
    await adapter.connect();

    const authMiddleware = mockUse.mock.calls[0][0];

    const next = vi.fn();
    const ctx = { chat: { id: 100 } }; // In allowedChatIds

    await authMiddleware(ctx, next);

    expect(next).toHaveBeenCalledTimes(1);
  });

  // ── Group chat mention gating ──────────────────────────────────

  it('ignores group messages without mention when requireMention is true', async () => {
    const handler = vi.fn();
    adapter.onMessage(handler);

    await adapter.connect();

    // Simulate onStart callback to set botInfo
    const startCall = mockStart.mock.calls[0][0];
    startCall.onStart({ id: 12345, username: 'flynn_bot' });

    const textHandlerCall = mockOn.mock.calls.find(
      (call) => call[0] === 'message:text',
    );
    const textHandler = textHandlerCall![1];

    const ctx = {
      message: { message_id: 42, text: 'Hello everyone', reply_to_message: undefined },
      chat: { id: 100, type: 'group' },
      from: { first_name: 'Will' },
      replyWithChatAction: vi.fn(),
    };

    await textHandler(ctx);

    expect(handler).not.toHaveBeenCalled();
    expect(ctx.replyWithChatAction).not.toHaveBeenCalled();
  });

  it('processes group messages with bot mention when requireMention is true', async () => {
    const handler = vi.fn();
    adapter.onMessage(handler);

    await adapter.connect();

    // Simulate onStart callback to set botInfo
    const startCall = mockStart.mock.calls[0][0];
    startCall.onStart({ id: 12345, username: 'flynn_bot' });

    const textHandlerCall = mockOn.mock.calls.find(
      (call) => call[0] === 'message:text',
    );
    const textHandler = textHandlerCall![1];

    const ctx = {
      message: { message_id: 42, text: '@flynn_bot What is the weather?', reply_to_message: undefined },
      chat: { id: 100, type: 'group' },
      from: { first_name: 'Will' },
      replyWithChatAction: vi.fn(),
    };

    await textHandler(ctx);

    expect(handler).toHaveBeenCalledTimes(1);
    const msg: InboundMessage = handler.mock.calls[0][0];
    expect(msg.text).toBe('What is the weather?');
  });

  it('processes group messages as reply to bot when requireMention is true', async () => {
    const handler = vi.fn();
    adapter.onMessage(handler);

    await adapter.connect();

    // Simulate onStart callback to set botInfo
    const startCall = mockStart.mock.calls[0][0];
    startCall.onStart({ id: 12345, username: 'flynn_bot' });

    const textHandlerCall = mockOn.mock.calls.find(
      (call) => call[0] === 'message:text',
    );
    const textHandler = textHandlerCall![1];

    const ctx = {
      message: {
        message_id: 42,
        text: 'Follow up question',
        reply_to_message: { from: { id: 12345 } },
      },
      chat: { id: 100, type: 'supergroup' },
      from: { first_name: 'Will' },
      replyWithChatAction: vi.fn(),
    };

    await textHandler(ctx);

    expect(handler).toHaveBeenCalledTimes(1);
    const msg: InboundMessage = handler.mock.calls[0][0];
    expect(msg.text).toBe('Follow up question');
  });

  it('processes group messages without mention when requireMention is false', async () => {
    const noMentionAdapter = new TelegramAdapter({
      ...baseConfig,
      requireMention: false,
    });
    const handler = vi.fn();
    noMentionAdapter.onMessage(handler);

    await noMentionAdapter.connect();

    // Simulate onStart callback
    const startCall = mockStart.mock.calls[0][0];
    startCall.onStart({ id: 12345, username: 'flynn_bot' });

    const textHandlerCall = mockOn.mock.calls.find(
      (call) => call[0] === 'message:text',
    );
    const textHandler = textHandlerCall![1];

    const ctx = {
      message: { message_id: 42, text: 'Hello everyone', reply_to_message: undefined },
      chat: { id: 100, type: 'group' },
      from: { first_name: 'Will' },
      replyWithChatAction: vi.fn(),
    };

    await textHandler(ctx);

    expect(handler).toHaveBeenCalledTimes(1);
    const msg: InboundMessage = handler.mock.calls[0][0];
    expect(msg.text).toBe('Hello everyone');
  });

  it('DMs are always processed regardless of requireMention setting', async () => {
    const handler = vi.fn();
    adapter.onMessage(handler);

    await adapter.connect();

    // Simulate onStart callback to set botInfo
    const startCall = mockStart.mock.calls[0][0];
    startCall.onStart({ id: 12345, username: 'flynn_bot' });

    const textHandlerCall = mockOn.mock.calls.find(
      (call) => call[0] === 'message:text',
    );
    const textHandler = textHandlerCall![1];

    const ctx = {
      message: { message_id: 42, text: 'Hello Flynn', reply_to_message: undefined },
      chat: { id: 100, type: 'private' },
      from: { first_name: 'Will' },
      replyWithChatAction: vi.fn(),
    };

    await textHandler(ctx);

    expect(handler).toHaveBeenCalledTimes(1);
    const msg: InboundMessage = handler.mock.calls[0][0];
    expect(msg.text).toBe('Hello Flynn');
  });

  it('strips bot mention from group message text', async () => {
    const handler = vi.fn();
    adapter.onMessage(handler);

    await adapter.connect();

    // Simulate onStart callback to set botInfo
    const startCall = mockStart.mock.calls[0][0];
    startCall.onStart({ id: 12345, username: 'flynn_bot' });

    const textHandlerCall = mockOn.mock.calls.find(
      (call) => call[0] === 'message:text',
    );
    const textHandler = textHandlerCall![1];

    const ctx = {
      message: { message_id: 42, text: '@flynn_bot tell me a joke', reply_to_message: undefined },
      chat: { id: 100, type: 'group' },
      from: { first_name: 'Will' },
      replyWithChatAction: vi.fn(),
    };

    await textHandler(ctx);

    expect(handler).toHaveBeenCalledTimes(1);
    const msg: InboundMessage = handler.mock.calls[0][0];
    expect(msg.text).toBe('tell me a joke');
  });
});
