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

const { mockQuery, mockRPush, mockExpire } = vi.hoisted(() => ({
  mockQuery: vi.fn(),
  mockRPush: vi.fn(),
  mockExpire: vi.fn(),
}));

vi.mock('../db.js', () => ({ getPool: vi.fn(() => ({ query: mockQuery })) }));
vi.mock('../redis.js', () => ({
  default: { rPush: mockRPush, expire: mockExpire },
}));

global.fetch = vi.fn();

import { deliverWebhook, isValidWebhookUrl } from './delivery.js';

// ── URL validation ──────────────────────────────────────────────

describe('isValidWebhookUrl', () => {
  it('accepts https:// with public hostname', () => {
    expect(isValidWebhookUrl('https://my-server.example.com/hook')).toBe(true);
  });

  it('rejects ftp:// and other non-http schemes', () => {
    expect(isValidWebhookUrl('ftp://example.com/hook')).toBe(false);
  });

  it('rejects 127.x loopback', () => {
    expect(isValidWebhookUrl('http://127.0.0.1/hook')).toBe(false);
  });

  it('rejects 10.x private range', () => {
    expect(isValidWebhookUrl('https://10.0.0.1/hook')).toBe(false);
  });

  it('rejects 192.168.x private range', () => {
    expect(isValidWebhookUrl('https://192.168.1.1/hook')).toBe(false);
  });

  it('rejects 172.16-31.x private range', () => {
    expect(isValidWebhookUrl('https://172.16.0.1/hook')).toBe(false);
    expect(isValidWebhookUrl('https://172.31.255.255/hook')).toBe(false);
  });

  it('rejects localhost hostname', () => {
    expect(isValidWebhookUrl('http://localhost/hook')).toBe(false);
  });

  it('rejects .local domains', () => {
    expect(isValidWebhookUrl('http://myserver.local/hook')).toBe(false);
  });

  it('rejects invalid URLs', () => {
    expect(isValidWebhookUrl('not-a-url')).toBe(false);
  });
});

// ── deliverWebhook ──────────────────────────────────────────────

describe('deliverWebhook', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    vi.useFakeTimers();
    mockRPush.mockResolvedValue(1);
    mockExpire.mockResolvedValue(1);
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it('does nothing when customer has no webhook_url', async () => {
    mockQuery.mockResolvedValue([[{ webhook_url: null, webhook_secret: null }]]);
    await deliverWebhook('cust-1', 'whatsapp', 'inbound_message', { from: '+1234' });
    expect(fetch).not.toHaveBeenCalled();
  });

  it('does nothing when customer not found', async () => {
    mockQuery.mockResolvedValue([[]]);
    await deliverWebhook('cust-1', 'whatsapp', 'inbound_message', { from: '+1234' });
    expect(fetch).not.toHaveBeenCalled();
  });

  it('POSTs to webhook_url with correct headers on success', async () => {
    mockQuery.mockResolvedValue([[{
      webhook_url: 'https://example.com/hook',
      webhook_secret: 'secret123',
    }]]);
    (fetch as ReturnType<typeof vi.fn>).mockResolvedValue({ ok: true });

    await deliverWebhook('cust-1', 'whatsapp', 'inbound_message', { from: '+1234', text: 'hi' });

    expect(fetch).toHaveBeenCalledTimes(1);
    const [url, opts] = (fetch as ReturnType<typeof vi.fn>).mock.calls[0] as [string, RequestInit];
    expect(url).toBe('https://example.com/hook');
    expect((opts.headers as Record<string, string>)['Content-Type']).toBe('application/json');
    expect((opts.headers as Record<string, string>)['X-SquareMCP-Signature']).toMatch(/^sha256=[0-9a-f]{64}$/);
  });

  it('sends correct HMAC signature', async () => {
    const secret = 'mysecret';
    mockQuery.mockResolvedValue([[{ webhook_url: 'https://example.com/hook', webhook_secret: secret }]]);
    (fetch as ReturnType<typeof vi.fn>).mockResolvedValue({ ok: true });

    await deliverWebhook('cust-1', 'whatsapp', 'inbound_message', { from: '+1' });

    const [, opts] = (fetch as ReturnType<typeof vi.fn>).mock.calls[0] as [string, RequestInit];
    const sig = (opts.headers as Record<string, string>)['X-SquareMCP-Signature'];
    const body = opts.body as string;

    // Verify the signature independently
    const { createHmac } = await import('crypto');
    const expected = `sha256=${createHmac('sha256', secret).update(body).digest('hex')}`;
    expect(sig).toBe(expected);
  });

  it('retries on failure and pushes to DLQ after all attempts', async () => {
    mockQuery.mockResolvedValue([[{ webhook_url: 'https://example.com/hook', webhook_secret: 'sec' }]]);
    (fetch as ReturnType<typeof vi.fn>).mockResolvedValue({ ok: false, status: 500 });

    const deliverPromise = deliverWebhook('cust-1', 'whatsapp', 'inbound_message', { from: '+1' });

    // Advance timers through all retry delays: 1s, 4s, 16s
    await vi.advanceTimersByTimeAsync(1000);
    await vi.advanceTimersByTimeAsync(4000);
    await vi.advanceTimersByTimeAsync(16000);
    await deliverPromise;

    expect(fetch).toHaveBeenCalledTimes(4); // 1 initial + 3 retries
    expect(mockRPush).toHaveBeenCalledWith(
      'webhook:dlq:cust-1',
      expect.stringContaining('"customerId":"cust-1"')
    );
    expect(mockExpire).toHaveBeenCalledWith('webhook:dlq:cust-1', 604800);
  });

  it('does not push to DLQ on first-attempt success', async () => {
    mockQuery.mockResolvedValue([[{ webhook_url: 'https://example.com/hook', webhook_secret: 'sec' }]]);
    (fetch as ReturnType<typeof vi.fn>).mockResolvedValue({ ok: true });

    await deliverWebhook('cust-1', 'whatsapp', 'inbound_message', { from: '+1' });

    expect(mockRPush).not.toHaveBeenCalled();
    expect(mockExpire).not.toHaveBeenCalled();
  });

  it('succeeds on second attempt (one initial failure)', async () => {
    mockQuery.mockResolvedValue([[{ webhook_url: 'https://example.com/hook', webhook_secret: 'sec' }]]);
    (fetch as ReturnType<typeof vi.fn>)
      .mockResolvedValueOnce({ ok: false, status: 503 })
      .mockResolvedValueOnce({ ok: true });

    const deliverPromise = deliverWebhook('cust-1', 'whatsapp', 'inbound_message', { from: '+1' });
    await vi.advanceTimersByTimeAsync(1000);
    await deliverPromise;

    expect(fetch).toHaveBeenCalledTimes(2);
    expect(mockRPush).not.toHaveBeenCalled();
  });
});
