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

// ── Mock whatsapp-web.js before importing adapter ───────────────
// We need to intercept the Client constructor and its event handlers.

let capturedEventHandlers: Record<string, ((...args: unknown[]) => void)[]> = {};
const mockSendMessage = vi.fn();
const mockInitialize = vi.fn();
const mockDestroy = vi.fn();

interface MockClientInfo {
  pushname?: string;
  wid?: { _serialized: string };
}

/** Create a fresh mock Client instance. */
function createMockClient() {
  capturedEventHandlers = {};
  return {
    on: vi.fn((event: string, handler: (...args: unknown[]) => void) => {
      if (!capturedEventHandlers[event]) {
        capturedEventHandlers[event] = [];
      }
      capturedEventHandlers[event].push(handler);
    }),
    initialize: mockInitialize,
    destroy: mockDestroy,
    sendMessage: mockSendMessage,
    info: { pushname: 'Flynn', wid: { _serialized: '5511000000000@c.us' } } as MockClientInfo,
  };
}

let mockClient = createMockClient();

vi.mock('whatsapp-web.js', () => {
  const Client = vi.fn().mockImplementation(() => mockClient);
  const LocalAuth = vi.fn().mockImplementation((opts: Record<string, unknown>) => ({
    type: 'local',
    ...opts,
  }));
  return {
    default: { Client, LocalAuth, MessageMedia: vi.fn() },
    Client,
    LocalAuth,
    MessageMedia: vi.fn(),
  };
});

import { WhatsAppAdapter, type WhatsAppAdapterConfig } from './adapter.js';
import type { InboundMessage } from '../types.js';

const baseConfig: WhatsAppAdapterConfig = {};

/** Helper: simulate a WhatsApp event through the captured handler. */
function simulateEvent(event: string, ...args: unknown[]) {
  const handlers = capturedEventHandlers[event];
  if (!handlers || handlers.length === 0) {
    throw new Error(`No handler captured for event "${event}" — call connect() first`);
  }
  for (const handler of handlers) {
    handler(...args);
  }
}

/** Create a mock WhatsApp message object. */
function createMockMessage(overrides: Record<string, unknown> = {}) {
  return {
    id: { id: 'MSG001', fromMe: false, ...(overrides._id as Record<string, unknown> ?? {}) },
    from: '5511999999999@c.us',
    body: 'Hello Flynn',
    timestamp: 1700000000,
    fromMe: false,
    author: undefined,
    ...overrides,
  };
}

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

  beforeEach(async () => {
    vi.clearAllMocks();
    mockClient = createMockClient();
    // Re-wire the Client mock to return the fresh mockClient
    const { Client } = vi.mocked(await import('whatsapp-web.js'));
    (Client as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => mockClient);
    adapter = new WhatsAppAdapter(baseConfig);
  });

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

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

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

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

  it('connect creates Client and sets connected status after ready event', async () => {
    const connectPromise = adapter.connect();

    // Simulate the ready event
    simulateEvent('ready');
    await connectPromise;

    expect(adapter.status).toBe('connected');
    expect(mockInitialize).toHaveBeenCalledTimes(1);
  });

  it('connect registers message and ready event handlers', async () => {
    const connectPromise = adapter.connect();
    simulateEvent('ready');
    await connectPromise;

    // Should have registered at least 'ready' and 'message' handlers
    expect(capturedEventHandlers['ready']).toBeDefined();
    expect(capturedEventHandlers['message']).toBeDefined();
  });

  it('connect sets error status on auth_failure', async () => {
    const connectPromise = adapter.connect();

    // Simulate auth failure
    simulateEvent('auth_failure', 'Invalid session');

    await expect(connectPromise).rejects.toThrow();
    expect(adapter.status).toBe('error');
  });

  it('disconnect destroys client and sets disconnected', async () => {
    const connectPromise = adapter.connect();
    simulateEvent('ready');
    await connectPromise;

    expect(adapter.status).toBe('connected');

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

  it('disconnect is safe when not connected', async () => {
    await adapter.disconnect();
    expect(adapter.status).toBe('disconnected');
    // No client to destroy — should not throw
  });

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

  it('send throws when not connected', async () => {
    await expect(
      adapter.send('5511999999999@c.us', { text: 'hello' }),
    ).rejects.toThrow('WhatsApp adapter not connected');
  });

  it('send delivers a short message to the correct peer', async () => {
    const connectPromise = adapter.connect();
    simulateEvent('ready');
    await connectPromise;

    await adapter.send('5511999999999@c.us', { text: 'Hello there' });

    expect(mockSendMessage).toHaveBeenCalledTimes(1);
    expect(mockSendMessage).toHaveBeenCalledWith('5511999999999@c.us', 'Hello there');
  });

  it('send chunks long messages (>4096 chars)', async () => {
    const connectPromise = adapter.connect();
    simulateEvent('ready');
    await connectPromise;

    // Create a message 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('5511999999999@c.us', { text: longMessage });

    // Should have been split into at least 2 chunks
    expect(mockSendMessage.mock.calls.length).toBeGreaterThanOrEqual(2);
    // All calls should target the same peer
    for (const call of mockSendMessage.mock.calls) {
      expect(call[0]).toBe('5511999999999@c.us');
    }
  });

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

  it('inbound message triggers handler with correct peerId (phone@c.us)', async () => {
    const handler = vi.fn();
    adapter.onMessage(handler);

    const connectPromise = adapter.connect();
    simulateEvent('ready');
    await connectPromise;

    simulateEvent('message', createMockMessage({
      from: '5511999999999@c.us',
      body: 'Hello Flynn',
    }));

    expect(handler).toHaveBeenCalledTimes(1);
    const msg: InboundMessage = handler.mock.calls[0][0];
    expect(msg.channel).toBe('whatsapp');
    expect(msg.senderId).toBe('5511999999999@c.us');
    expect(msg.text).toBe('Hello Flynn');
    expect(msg.id).toBe('MSG001');
  });

  it('ignores messages from the bot itself (fromMe === true)', async () => {
    const handler = vi.fn();
    adapter.onMessage(handler);

    const connectPromise = adapter.connect();
    simulateEvent('ready');
    await connectPromise;

    simulateEvent('message', createMockMessage({
      fromMe: true,
    }));

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

  it('ignores messages from numbers not in allowed_numbers list', async () => {
    const restrictedAdapter = new WhatsAppAdapter({
      allowedNumbers: ['5511888888888'],
    });
    const handler = vi.fn();
    restrictedAdapter.onMessage(handler);

    const connectPromise = restrictedAdapter.connect();
    simulateEvent('ready');
    await connectPromise;

    simulateEvent('message', createMockMessage({
      from: '5511999999999@c.us',
      body: 'Hello',
    }));

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

  it('allows messages when allowed_numbers is empty (no restriction)', async () => {
    const handler = vi.fn();
    adapter.onMessage(handler);

    const connectPromise = adapter.connect();
    simulateEvent('ready');
    await connectPromise;

    simulateEvent('message', createMockMessage({
      from: '5511999999999@c.us',
      body: 'Hello',
    }));

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

  it('allows messages from numbers in the allowed list', async () => {
    const restrictedAdapter = new WhatsAppAdapter({
      allowedNumbers: ['5511999999999'],
    });
    const handler = vi.fn();
    restrictedAdapter.onMessage(handler);

    const connectPromise = restrictedAdapter.connect();
    simulateEvent('ready');
    await connectPromise;

    simulateEvent('message', createMockMessage({
      from: '5511999999999@c.us',
      body: 'Hello',
    }));

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

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

    const connectPromise = adapter.connect();
    simulateEvent('ready');
    await connectPromise;

    simulateEvent('message', createMockMessage({
      body: '!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);

    const connectPromise = adapter.connect();
    simulateEvent('ready');
    await connectPromise;

    simulateEvent('message', createMockMessage({
      body: '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
    const connectPromise = adapter.connect();
    simulateEvent('ready');
    await connectPromise;

    // Should not throw
    simulateEvent('message', createMockMessage());
  });

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

    const connectPromise = adapter.connect();
    simulateEvent('ready');
    await connectPromise;

    simulateEvent('message', createMockMessage({
      body: '',
    }));

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

  it('extracts sender name from message contact when available', async () => {
    const handler = vi.fn();
    adapter.onMessage(handler);

    const connectPromise = adapter.connect();
    simulateEvent('ready');
    await connectPromise;

    simulateEvent('message', createMockMessage({
      from: '5511999999999@c.us',
      body: 'Hello',
      _data: { notifyName: 'John' },
    }));

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

  it('strips @c.us suffix when checking allowed numbers', async () => {
    const restrictedAdapter = new WhatsAppAdapter({
      allowedNumbers: ['5511999999999'],
    });
    const handler = vi.fn();
    restrictedAdapter.onMessage(handler);

    const connectPromise = restrictedAdapter.connect();
    simulateEvent('ready');
    await connectPromise;

    // Message comes with @c.us suffix
    simulateEvent('message', createMockMessage({
      from: '5511999999999@c.us',
      body: 'Hello',
    }));

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

  it('ignores group messages when no allowedGroupIds configured', async () => {
    const handler = vi.fn();
    adapter.onMessage(handler);

    const connectPromise = adapter.connect();
    simulateEvent('ready');
    await connectPromise;

    simulateEvent('message', createMockMessage({
      from: '120363025555555555@g.us',
      body: 'Group message',
    }));

    // WhatsApp adapter should ignore group messages when allowedGroupIds is empty
    expect(handler).not.toHaveBeenCalled();
  });

  // ── Group chat support ────────────────────────────────────────

  it('processes group messages from allowed group IDs with mention', async () => {
    const groupAdapter = new WhatsAppAdapter({
      allowedGroupIds: ['120363025555555555'],
      requireMention: true,
    });
    const handler = vi.fn();
    groupAdapter.onMessage(handler);

    const connectPromise = groupAdapter.connect();
    simulateEvent('ready');
    await connectPromise;

    simulateEvent('message', createMockMessage({
      from: '120363025555555555@g.us',
      body: '@5511000000000 Hello bot',
    }));

    expect(handler).toHaveBeenCalledTimes(1);
    const msg: InboundMessage = handler.mock.calls[0][0];
    expect(msg.text).toBe('Hello bot');
    expect(msg.senderId).toBe('120363025555555555@g.us');
  });

  it('ignores group messages from disallowed group IDs', async () => {
    const groupAdapter = new WhatsAppAdapter({
      allowedGroupIds: ['120363025555555555'],
    });
    const handler = vi.fn();
    groupAdapter.onMessage(handler);

    const connectPromise = groupAdapter.connect();
    simulateEvent('ready');
    await connectPromise;

    simulateEvent('message', createMockMessage({
      from: '999999999999999999@g.us',
      body: '@5511000000000 Hello bot',
    }));

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

  it('ignores group messages without mention when requireMention is true', async () => {
    const groupAdapter = new WhatsAppAdapter({
      allowedGroupIds: ['120363025555555555'],
      requireMention: true,
    });
    const handler = vi.fn();
    groupAdapter.onMessage(handler);

    const connectPromise = groupAdapter.connect();
    simulateEvent('ready');
    await connectPromise;

    simulateEvent('message', createMockMessage({
      from: '120363025555555555@g.us',
      body: 'Hello everyone',
    }));

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

  it('processes group messages without mention when requireMention is false', async () => {
    const groupAdapter = new WhatsAppAdapter({
      allowedGroupIds: ['120363025555555555'],
      requireMention: false,
    });
    const handler = vi.fn();
    groupAdapter.onMessage(handler);

    const connectPromise = groupAdapter.connect();
    simulateEvent('ready');
    await connectPromise;

    simulateEvent('message', createMockMessage({
      from: '120363025555555555@g.us',
      body: 'Hello everyone',
    }));

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

  it('strips bot mention from group message text', async () => {
    const groupAdapter = new WhatsAppAdapter({
      allowedGroupIds: ['120363025555555555'],
      requireMention: true,
    });
    const handler = vi.fn();
    groupAdapter.onMessage(handler);

    const connectPromise = groupAdapter.connect();
    simulateEvent('ready');
    await connectPromise;

    simulateEvent('message', createMockMessage({
      from: '120363025555555555@g.us',
      body: '@5511000000000 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('DM messages are always processed regardless of requireMention', async () => {
    const groupAdapter = new WhatsAppAdapter({
      requireMention: true,
    });
    const handler = vi.fn();
    groupAdapter.onMessage(handler);

    const connectPromise = groupAdapter.connect();
    simulateEvent('ready');
    await connectPromise;

    simulateEvent('message', createMockMessage({
      from: '5511999999999@c.us',
      body: 'Hello Flynn',
    }));

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

  it('group messages detected via mentionedIds array', async () => {
    const groupAdapter = new WhatsAppAdapter({
      allowedGroupIds: ['120363025555555555'],
      requireMention: true,
    });
    const handler = vi.fn();
    groupAdapter.onMessage(handler);

    const connectPromise = groupAdapter.connect();
    simulateEvent('ready');
    await connectPromise;

    simulateEvent('message', createMockMessage({
      from: '120363025555555555@g.us',
      body: 'Hello bot',
      mentionedIds: ['5511000000000@c.us'],
    }));

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

  it('passes data_dir to LocalAuth strategy', async () => {
    const adapterWithDir = new WhatsAppAdapter({
      dataDir: '/tmp/whatsapp-session',
    });

    const connectPromise = adapterWithDir.connect();
    simulateEvent('ready');
    await connectPromise;

    const { LocalAuth } = await import('whatsapp-web.js');
    expect(LocalAuth).toHaveBeenCalledWith(
      expect.objectContaining({ dataPath: '/tmp/whatsapp-session' }),
    );
  });

  it('connect sets error status when initialize() rejects', async () => {
    mockInitialize.mockRejectedValueOnce(new Error('Browser launch failed'));

    await expect(adapter.connect()).rejects.toThrow('Browser launch failed');
    expect(adapter.status).toBe('error');
  });

  it('disconnect cleans up even when destroy() throws', async () => {
    const connectPromise = adapter.connect();
    simulateEvent('ready');
    await connectPromise;

    mockDestroy.mockRejectedValueOnce(new Error('Cleanup failed'));

    // Should not throw — wrapped in try/finally
    await adapter.disconnect();
    expect(adapter.status).toBe('disconnected');
  });
});
