import { describe, it, expect, vi, beforeEach } from 'vitest';
import { execFile } from 'child_process';
import type { ChildProcess } from 'child_process';

import { SignalAdapter } from './adapter.js';
import type { InboundMessage } from '../types.js';

vi.mock('child_process', () => ({
  execFile: vi.fn(),
}));

const mockExecFile = vi.mocked(execFile);
type ExecFileCallback = NonNullable<Parameters<typeof execFile>[3]>;

function mockChildProcess(): ChildProcess {
  return {} as ChildProcess;
}

function mockExecFileOnce(impl: (callback: ExecFileCallback) => void): void {
  mockExecFile.mockImplementationOnce((_cmd, _args, _opts, callback) => {
    if (typeof callback === 'function') {
      impl(callback as ExecFileCallback);
    }
    return mockChildProcess();
  });
}

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

  it('has name signal and starts disconnected', () => {
    const adapter = new SignalAdapter({ account: '+15551234567' });
    expect(adapter.name).toBe('signal');
    expect(adapter.status).toBe('disconnected');
  });

  it('connect checks signal-cli availability', async () => {
    const adapter = new SignalAdapter({
      account: '+15551234567',
      pollIntervalMs: 60000,
    });
    mockExecFileOnce((callback) => callback(null, 'signal-cli 0.13.2', ''));
    mockExecFileOnce((callback) => callback(null, '', ''));

    await adapter.connect();
    await adapter.disconnect();

    expect(mockExecFile).toHaveBeenCalled();
    const versionCall = mockExecFile.mock.calls[0];
    expect(versionCall[0]).toBe('signal-cli');
    expect(versionCall[1]).toEqual(['--version']);
  });

  it('send uses -g for group peers', async () => {
    const adapter = new SignalAdapter({ account: '+15551234567' });
    mockExecFileOnce((callback) => callback(null, 'signal-cli 0.13.2', ''));
    mockExecFileOnce((callback) => callback(null, '', ''));
    mockExecFileOnce((callback) => callback(null, '', ''));

    await adapter.connect();
    await adapter.send('group:abcd1234', { text: 'Hello group' });
    await adapter.disconnect();

    const sendCall = mockExecFile.mock.calls.find((call) => Array.isArray(call[1]) && call[1].includes('send'));
    expect(sendCall).toBeDefined();
    expect(sendCall?.[1]).toEqual(['-u', '+15551234567', 'send', '-m', 'Hello group', '-g', 'abcd1234']);
  });

  it('send uploads binary attachments via --attachment', async () => {
    const adapter = new SignalAdapter({ account: '+15551234567' });
    mockExecFileOnce((callback) => callback(null, 'signal-cli 0.13.2', ''));
    mockExecFileOnce((callback) => callback(null, '', ''));
    mockExecFileOnce((callback) => callback(null, '', ''));

    await adapter.connect();
    await adapter.send('+15550001111', {
      text: '',
      attachments: [{ mimeType: 'image/png', data: 'aGVsbG8=', filename: 'image.png' }],
    });
    await adapter.disconnect();

    const attachmentCall = mockExecFile.mock.calls.find((call) => {
      const args = call[1];
      return Array.isArray(args) && args.includes('--attachment');
    });
    expect(attachmentCall).toBeDefined();
    const args = attachmentCall?.[1] as string[];
    expect(args[0]).toBe('-u');
    expect(args[1]).toBe('+15551234567');
    expect(args[2]).toBe('send');
    expect(args[3]).toBe('--attachment');
    expect(typeof args[4]).toBe('string');
    expect(args[5]).toBe('+15550001111');
  });

  it('parses DM receive payload and forwards inbound message', async () => {
    const adapter = new SignalAdapter({
      account: '+15551234567',
      allowedNumbers: ['+15550001111'],
    });
    const messages: InboundMessage[] = [];
    adapter.onMessage((msg) => {
      messages.push(msg);
    });

    const run = adapter as unknown as {
      processReceiveOutput: (output: string) => Promise<void>;
    };
    await run.processReceiveOutput(
      JSON.stringify({
        envelope: {
          source: '+15550001111',
          sourceName: 'Alice',
          timestamp: 1700000000000,
          dataMessage: { message: 'hello from signal' },
        },
      }),
    );

    expect(messages).toHaveLength(1);
    expect(messages[0].channel).toBe('signal');
    expect(messages[0].senderId).toBe('+15550001111');
    expect(messages[0].text).toBe('hello from signal');
  });

  it('requires mention in groups and strips leading mention token', async () => {
    const adapter = new SignalAdapter({
      account: '+15551234567',
      allowedGroupIds: ['grp1'],
      mentionName: 'flynn',
      requireMention: true,
    });
    const messages: InboundMessage[] = [];
    adapter.onMessage((msg) => {
      messages.push(msg);
    });

    const run = adapter as unknown as {
      processReceiveOutput: (output: string) => Promise<void>;
    };

    await run.processReceiveOutput(
      JSON.stringify({
        envelope: {
          source: '+15550001111',
          timestamp: 1700000000000,
          dataMessage: {
            message: '@flynn check status',
            groupInfo: { groupId: 'grp1' },
          },
        },
      }),
    );

    expect(messages).toHaveLength(1);
    expect(messages[0].senderId).toBe('group:grp1');
    expect(messages[0].text).toBe('check status');
  });
});
