import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { WebhookHandler, _verifyHmac, _renderTemplate } from './webhooks.js';
import type { WebhookConfig } from '../config/schema.js';
import type { InboundMessage } from '../channels/types.js';
import type { IncomingMessage, ServerResponse } from 'http';
import { createHmac } from 'crypto';
import { EventEmitter } from 'events';

function makeWebhook(overrides?: Partial<WebhookConfig>): WebhookConfig {
  return {
    name: 'test-hook',
    message: '{{body}}',
    output: { channel: 'telegram', peer: '123' },
    enabled: true,
    ...overrides,
  };
}

/** Create a mock IncomingMessage that emits the given body. */
function mockRequest(body: string, headers: Record<string, string> = {}): IncomingMessage {
  const emitter = new EventEmitter();
  (emitter as any).headers = headers;
  // Simulate data arriving next tick
  process.nextTick(() => {
    emitter.emit('data', Buffer.from(body));
    emitter.emit('end');
  });
  return emitter as unknown as IncomingMessage;
}

/** Create a mock ServerResponse that captures writeHead and end calls. */
function mockResponse(): ServerResponse & { statusCode_: number; body_: string; headers_: Record<string, string> } {
  const res: any = {
    statusCode_: 0,
    body_: '',
    headers_: {},
    writeHead(code: number, headers?: Record<string, string>) {
      res.statusCode_ = code;
      if (headers) {res.headers_ = headers;}
      return res;
    },
    end(body?: string) {
      res.body_ = body ?? '';
      return res;
    },
  };
  return res;
}

describe('WebhookHandler', () => {
  let handler: WebhookHandler;
  let mockChannelRegistry: { get: ReturnType<typeof vi.fn> };

  beforeEach(() => {
    mockChannelRegistry = {
      get: vi.fn(),
    };
  });

  afterEach(async () => {
    if (handler) {
      await handler.disconnect();
    }
  });

  it('implements ChannelAdapter interface', () => {
    handler = new WebhookHandler([], mockChannelRegistry as any);
    expect(handler.name).toBe('webhook');
    expect(handler.status).toBe('disconnected');
  });

  it('status changes to connected after connect()', async () => {
    handler = new WebhookHandler([], mockChannelRegistry as any);
    await handler.connect();
    expect(handler.status).toBe('connected');
  });

  it('status changes to disconnected after disconnect()', async () => {
    handler = new WebhookHandler([], mockChannelRegistry as any);
    await handler.connect();
    await handler.disconnect();
    expect(handler.status).toBe('disconnected');
  });

  it('lists registered webhook names', () => {
    const webhooks = [
      makeWebhook({ name: 'hook-a' }),
      makeWebhook({ name: 'hook-b', enabled: false }),
    ];
    handler = new WebhookHandler(webhooks, mockChannelRegistry as any);

    const names = handler.getWebhookNames();
    expect(names).toEqual(['hook-a', 'hook-b']);
  });

  it('handleRequest produces correct InboundMessage', async () => {
    const webhooks = [makeWebhook()];
    handler = new WebhookHandler(webhooks, mockChannelRegistry as any);

    const messages: InboundMessage[] = [];
    handler.onMessage((msg: InboundMessage) => messages.push(msg));
    await handler.connect();

    const req = mockRequest('hello world');
    const res = mockResponse();

    const result = await handler.handleRequest('test-hook', req, res);

    expect(result).toBe(true);
    expect(res.statusCode_).toBe(202);
    expect(messages).toHaveLength(1);
    expect(messages[0].channel).toBe('webhook');
    expect(messages[0].senderId).toBe('test-hook');
    expect(messages[0].text).toBe('hello world');
  });

  it('returns false for unknown webhook', async () => {
    handler = new WebhookHandler([], mockChannelRegistry as any);
    await handler.connect();

    const req = mockRequest('test');
    const res = mockResponse();

    const result = await handler.handleRequest('nonexistent', req, res);

    expect(result).toBe(false);
    expect(res.statusCode_).toBe(404);
  });

  it('returns false for disabled webhook', async () => {
    const webhooks = [makeWebhook({ enabled: false })];
    handler = new WebhookHandler(webhooks, mockChannelRegistry as any);
    await handler.connect();

    const req = mockRequest('test');
    const res = mockResponse();

    const result = await handler.handleRequest('test-hook', req, res);

    expect(result).toBe(false);
    expect(res.statusCode_).toBe(404);
  });

  it('verifies valid HMAC signature', async () => {
    const secret = 'my-secret-key';
    const webhooks = [makeWebhook({ secret })];
    handler = new WebhookHandler(webhooks, mockChannelRegistry as any);

    const messages: InboundMessage[] = [];
    handler.onMessage((msg: InboundMessage) => messages.push(msg));
    await handler.connect();

    const body = '{"event":"push"}';
    const signature = 'sha256=' + createHmac('sha256', secret).update(body).digest('hex');

    const req = mockRequest(body, { 'x-webhook-signature': signature });
    const res = mockResponse();

    const result = await handler.handleRequest('test-hook', req, res);

    expect(result).toBe(true);
    expect(res.statusCode_).toBe(202);
    expect(messages).toHaveLength(1);
  });

  it('rejects invalid HMAC signature', async () => {
    const secret = 'my-secret-key';
    const webhooks = [makeWebhook({ secret })];
    handler = new WebhookHandler(webhooks, mockChannelRegistry as any);

    const messages: InboundMessage[] = [];
    handler.onMessage((msg: InboundMessage) => messages.push(msg));
    await handler.connect();

    const req = mockRequest('{"event":"push"}', { 'x-webhook-signature': 'sha256=invalid' });
    const res = mockResponse();

    const result = await handler.handleRequest('test-hook', req, res);

    expect(result).toBe(false);
    expect(res.statusCode_).toBe(401);
    expect(messages).toHaveLength(0);
  });

  it('rejects missing HMAC signature when secret is configured', async () => {
    const secret = 'my-secret-key';
    const webhooks = [makeWebhook({ secret })];
    handler = new WebhookHandler(webhooks, mockChannelRegistry as any);

    const messages: InboundMessage[] = [];
    handler.onMessage((msg: InboundMessage) => messages.push(msg));
    await handler.connect();

    const req = mockRequest('{"event":"push"}');
    const res = mockResponse();

    const result = await handler.handleRequest('test-hook', req, res);

    expect(result).toBe(false);
    expect(res.statusCode_).toBe(401);
    expect(messages).toHaveLength(0);
  });

  it('forwards response to output channel on send()', async () => {
    const mockOutputAdapter = {
      send: vi.fn().mockResolvedValue(undefined),
    };
    mockChannelRegistry.get.mockReturnValue(mockOutputAdapter);

    const webhooks = [makeWebhook()];
    handler = new WebhookHandler(webhooks, mockChannelRegistry as any);
    await handler.connect();

    await handler.send('test-hook', { text: 'Agent response' });

    expect(mockChannelRegistry.get).toHaveBeenCalledWith('telegram');
    expect(mockOutputAdapter.send).toHaveBeenCalledWith('123', { text: 'Agent response' });
  });

  it('logs warning when output channel not found', async () => {
    mockChannelRegistry.get.mockReturnValue(undefined);
    const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});

    const webhooks = [makeWebhook()];
    handler = new WebhookHandler(webhooks, mockChannelRegistry as any);
    await handler.connect();

    await handler.send('test-hook', { text: 'Agent response' });

    expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Output channel'));
    warnSpy.mockRestore();
  });

  it('logs warning when webhook name not found in send()', async () => {
    const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});

    const webhooks = [makeWebhook()];
    handler = new WebhookHandler(webhooks, mockChannelRegistry as any);
    await handler.connect();

    await handler.send('nonexistent-hook', { text: 'response' });

    expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('No webhook'));
    warnSpy.mockRestore();
  });
});

describe('renderTemplate', () => {
  it('replaces {{body}} with raw body', () => {
    const result = _renderTemplate('Received: {{body}}', 'hello');
    expect(result).toBe('Received: hello');
  });

  it('replaces {{json.field}} with JSON field value', () => {
    const result = _renderTemplate('Event: {{json.action}}', '{"action":"push","repo":"test"}');
    expect(result).toBe('Event: push');
  });

  it('replaces multiple {{json.field}} placeholders', () => {
    const result = _renderTemplate(
      '{{json.action}} on {{json.repo}}',
      '{"action":"push","repo":"my-repo"}',
    );
    expect(result).toBe('push on my-repo');
  });

  it('returns empty string for missing JSON fields', () => {
    const result = _renderTemplate('Value: {{json.missing}}', '{"action":"push"}');
    expect(result).toBe('Value: ');
  });

  it('returns empty string for invalid JSON body with json placeholder', () => {
    const result = _renderTemplate('Value: {{json.field}}', 'not-json');
    expect(result).toBe('Value: ');
  });

  it('stringifies non-string JSON values', () => {
    const result = _renderTemplate('Count: {{json.count}}', '{"count":42}');
    expect(result).toBe('Count: 42');
  });

  it('handles template with both {{body}} and {{json.field}}', () => {
    const body = '{"action":"deploy"}';
    const result = _renderTemplate('Action: {{json.action}}, Raw: {{body}}', body);
    expect(result).toBe('Action: deploy, Raw: {"action":"deploy"}');
  });
});

describe('verifyHmac', () => {
  it('returns true for valid signature with sha256= prefix', () => {
    const secret = 'test-secret';
    const body = 'test-body';
    const sig = 'sha256=' + createHmac('sha256', secret).update(body).digest('hex');
    expect(_verifyHmac(body, secret, sig)).toBe(true);
  });

  it('returns true for valid signature without prefix', () => {
    const secret = 'test-secret';
    const body = 'test-body';
    const sig = createHmac('sha256', secret).update(body).digest('hex');
    expect(_verifyHmac(body, secret, sig)).toBe(true);
  });

  it('returns false for invalid signature', () => {
    expect(_verifyHmac('body', 'secret', 'sha256=deadbeef')).toBe(false);
  });
});
