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

// ── Mock @slack/bolt before importing adapter ──────────────────────

let capturedMessageHandler: ((args: Record<string, unknown>) => Promise<void>) | null = null;
const mockPostMessage = vi.fn();
const mockStart = vi.fn();
const mockStop = vi.fn();

const mockAuthTest = vi.fn().mockResolvedValue({ user_id: 'UBOT123' });
const mockUsersInfo = vi.fn().mockResolvedValue({ user: { real_name: 'Test User', name: 'testuser' } });

/** Create a fresh mock Bolt App instance. */
function createMockApp() {
  capturedMessageHandler = null;
  return {
    message: vi.fn((handler: (args: Record<string, unknown>) => Promise<void>) => {
      capturedMessageHandler = handler;
    }),
    start: mockStart,
    stop: mockStop,
    client: {
      chat: {
        postMessage: mockPostMessage,
      },
      auth: {
        test: mockAuthTest,
      },
      users: {
        info: mockUsersInfo,
      },
    },
  };
}

let mockApp = createMockApp();

vi.mock('@slack/bolt', () => ({
  App: vi.fn().mockImplementation(() => mockApp),
}));

import { SlackAdapter, type SlackAdapterConfig } from './adapter.js';
import type { InboundMessage } from '../types.js';

const baseConfig: SlackAdapterConfig = {
  botToken: 'xoxb-test-token',
  appToken: 'xapp-test-token',
  signingSecret: 'test-signing-secret',
};

/** Helper: simulate a Slack message event through the captured handler. */
async function simulateMessage(message: Record<string, unknown>) {
  if (!capturedMessageHandler) {throw new Error('No message handler captured — call connect() first');}
  await capturedMessageHandler({ message });
}

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

  beforeEach(async () => {
    vi.clearAllMocks();
    mockApp = createMockApp();
    // Re-wire the App mock to return the fresh mockApp
    const { App } = vi.mocked(await import('@slack/bolt'));
    (App as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => mockApp);
    adapter = new SlackAdapter(baseConfig);
  });

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

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

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

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

  it('connect creates Bolt App with Socket Mode and sets connected status', async () => {
    await adapter.connect();

    expect(adapter.status).toBe('connected');
    const { App } = await import('@slack/bolt');
    expect(App).toHaveBeenCalledWith({
      token: 'xoxb-test-token',
      appToken: 'xapp-test-token',
      signingSecret: 'test-signing-secret',
      socketMode: true,
    });
    expect(mockStart).toHaveBeenCalledTimes(1);
  });

  it('connect registers message handler via app.message()', async () => {
    await adapter.connect();

    expect(mockApp.message).toHaveBeenCalledTimes(1);
    expect(capturedMessageHandler).toBeTypeOf('function');
  });

  it('disconnect stops app and sets 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 when not connected', async () => {
    await adapter.disconnect();
    expect(adapter.status).toBe('disconnected');
    // No app to stop — should not throw
  });

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

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

  it('send delivers a short message with correct channel and thread_ts', async () => {
    await adapter.connect();

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

    expect(mockPostMessage).toHaveBeenCalledTimes(1);
    expect(mockPostMessage).toHaveBeenCalledWith({
      channel: 'C123',
      text: 'Hello there',
      thread_ts: '1234.5678',
    });
  });

  it('send chunks long messages (>4000 chars)', async () => {
    await adapter.connect();

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

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

    // Should have been split into at least 2 chunks
    expect(mockPostMessage.mock.calls.length).toBeGreaterThanOrEqual(2);
    // All calls should target the same channel and thread
    for (const call of mockPostMessage.mock.calls) {
      expect(call[0].channel).toBe('C123');
      expect(call[0].thread_ts).toBe('1234.5678');
    }
  });

  it('send throws on invalid peer ID format (no colon)', async () => {
    await adapter.connect();

    await expect(adapter.send('invalid-no-colon', { text: 'hello' })).rejects.toThrow(
      'Invalid peer ID format: invalid-no-colon',
    );
  });

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

  it('inbound message triggers handler with correct peerId (channelId:threadTs)', async () => {
    const handler = vi.fn();
    adapter.onMessage(handler);

    await adapter.connect();

    await simulateMessage({
      ts: '1234.5678',
      thread_ts: '1111.0000',
      channel: 'C123',
      user: 'U456',
      text: 'Hello Flynn',
    });

    expect(handler).toHaveBeenCalledTimes(1);
    const msg: InboundMessage = handler.mock.calls[0][0];
    expect(msg.channel).toBe('slack');
    expect(msg.senderId).toBe('C123:1111.0000');
    expect(msg.senderName).toBe('Test User');
    expect(msg.text).toBe('Hello Flynn');
    expect(msg.id).toBe('1234.5678');
  });

  it('inbound message uses thread_ts for peer ID when in thread', async () => {
    const handler = vi.fn();
    adapter.onMessage(handler);

    await adapter.connect();

    await simulateMessage({
      ts: '2222.3333',
      thread_ts: '1111.0000',
      channel: 'C123',
      user: 'U456',
      text: 'Threaded reply',
    });

    const msg: InboundMessage = handler.mock.calls[0][0];
    expect(msg.senderId).toBe('C123:1111.0000');
  });

  it('inbound message falls back to ts when no thread_ts', async () => {
    const handler = vi.fn();
    adapter.onMessage(handler);

    await adapter.connect();

    await simulateMessage({
      ts: '2222.3333',
      channel: 'C123',
      user: 'U456',
      text: 'Top-level message',
    });

    const msg: InboundMessage = handler.mock.calls[0][0];
    expect(msg.senderId).toBe('C123:2222.3333');
  });

  it('ignores bot messages (bot_id present)', async () => {
    const handler = vi.fn();
    adapter.onMessage(handler);

    await adapter.connect();

    await simulateMessage({
      ts: '1234.5678',
      channel: 'C123',
      user: 'U456',
      text: 'Bot message',
      bot_id: 'B789',
    });

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

  it('ignores bot messages (subtype === "bot_message")', async () => {
    const handler = vi.fn();
    adapter.onMessage(handler);

    await adapter.connect();

    await simulateMessage({
      ts: '1234.5678',
      channel: 'C123',
      user: 'U456',
      text: 'Bot message',
      subtype: 'bot_message',
    });

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

  it('ignores messages in disallowed channels', async () => {
    const restrictedAdapter = new SlackAdapter({
      ...baseConfig,
      allowedChannelIds: ['C-allowed'],
    });
    const handler = vi.fn();
    restrictedAdapter.onMessage(handler);

    await restrictedAdapter.connect();

    await simulateMessage({
      ts: '1234.5678',
      channel: 'C-not-allowed',
      user: 'U456',
      text: 'Hello',
    });

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

  it('strips bot mentions (<@U\\w+> pattern)', async () => {
    const handler = vi.fn();
    adapter.onMessage(handler);

    await adapter.connect();

    await simulateMessage({
      ts: '1234.5678',
      channel: 'C123',
      user: 'U456',
      text: '<@UBOT123> What is the weather?',
    });

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

  it('!reset command delivers reset metadata', async () => {
    const handler = vi.fn();
    adapter.onMessage(handler);

    await adapter.connect();

    await simulateMessage({
      ts: '1234.5678',
      channel: 'C123',
      user: 'U456',
      text: '!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('"reset" text (without !) delivers reset metadata', async () => {
    const handler = vi.fn();
    adapter.onMessage(handler);

    await adapter.connect();

    await simulateMessage({
      ts: '1234.5678',
      channel: 'C123',
      user: 'U456',
      text: '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('does nothing when no message handler is registered', async () => {
    // Don't call onMessage — no handler registered
    await adapter.connect();

    // Should not throw
    await simulateMessage({
      ts: '1234.5678',
      channel: 'C123',
      user: 'U456',
      text: 'Hello',
    });
  });

  it('messages with no text property are handled (empty string)', async () => {
    const handler = vi.fn();
    adapter.onMessage(handler);

    await adapter.connect();

    await simulateMessage({
      ts: '1234.5678',
      channel: 'C123',
      user: 'U456',
      // no text property
    });

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

  it('allowed channel messages pass through when allowedChannelIds configured', async () => {
    const restrictedAdapter = new SlackAdapter({
      ...baseConfig,
      allowedChannelIds: ['C-allowed'],
    });
    const handler = vi.fn();
    restrictedAdapter.onMessage(handler);

    await restrictedAdapter.connect();

    await simulateMessage({
      ts: '1234.5678',
      channel: 'C-allowed',
      user: 'U456',
      text: 'Hello',
    });

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

  // ── Mention gating ─────────────────────────────────────────────

  it('ignores messages without mention when requireMention is true', async () => {
    const mentionAdapter = new SlackAdapter({
      ...baseConfig,
      requireMention: true,
    });
    const handler = vi.fn();
    mentionAdapter.onMessage(handler);

    await mentionAdapter.connect();

    await simulateMessage({
      ts: '1234.5678',
      channel: 'C123',
      user: 'U456',
      text: 'Hello everyone',
    });

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

  it('processes messages with bot mention when requireMention is true', async () => {
    const mentionAdapter = new SlackAdapter({
      ...baseConfig,
      requireMention: true,
    });
    const handler = vi.fn();
    mentionAdapter.onMessage(handler);

    await mentionAdapter.connect();

    await simulateMessage({
      ts: '1234.5678',
      channel: 'C123',
      user: 'U456',
      text: '<@UBOT123> What is the weather?',
    });

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

  it('processes all messages when requireMention is false (default)', async () => {
    const handler = vi.fn();
    adapter.onMessage(handler);

    await adapter.connect();

    await simulateMessage({
      ts: '1234.5678',
      channel: 'C123',
      user: 'U456',
      text: 'Hello without mention',
    });

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