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

// ── Hoist fetch mock before imports ───────────────────────────────────────────

const { mockFetch } = vi.hoisted(() => ({
  mockFetch: vi.fn(),
}));

vi.stubGlobal('fetch', mockFetch);

// ── Tests: slack.ts ───────────────────────────────────────────────────────────

describe('sendSlackAlert', () => {
  const WEBHOOK = 'https://hooks.slack.com/services/T000/B000/xxxx';

  const payload = {
    requestId: 'req-123',
    name: 'Alice',
    email: 'alice@acme.com',
    company: 'Acme Corp',
    role: 'CTO',
    use_case: 'Automate social posts',
    timeline: 'ASAP',
    systems: 'Salesforce',
    requirements: 'SSO required',
    source: 'squaremcp.com',
    ipAddress: '1.2.3.4',
  };

  beforeEach(() => {
    vi.resetModules();
    mockFetch.mockReset();
    delete process.env['SLACK_PILOT_WEBHOOK_URL'];
  });

  it('returns false and skips fetch when env var is not set', async () => {
    const { sendSlackAlert } = await import('./slack.js');
    const result = await sendSlackAlert(payload);
    expect(result).toBe(false);
    expect(mockFetch).not.toHaveBeenCalled();
  });

  it('returns false and skips fetch when env var is empty string', async () => {
    process.env['SLACK_PILOT_WEBHOOK_URL'] = '';
    const { sendSlackAlert } = await import('./slack.js');
    const result = await sendSlackAlert(payload);
    expect(result).toBe(false);
    expect(mockFetch).not.toHaveBeenCalled();
  });

  it('posts JSON to the webhook URL and returns true on 200', async () => {
    process.env['SLACK_PILOT_WEBHOOK_URL'] = WEBHOOK;
    mockFetch.mockResolvedValueOnce({ ok: true, status: 200, text: async () => 'ok' });

    const { sendSlackAlert } = await import('./slack.js');
    const result = await sendSlackAlert(payload);

    expect(result).toBe(true);
    expect(mockFetch).toHaveBeenCalledOnce();
    const [url, opts] = mockFetch.mock.calls[0] as [string, RequestInit];
    expect(url).toBe(WEBHOOK);
    expect(opts.method).toBe('POST');
    expect(opts.headers).toMatchObject({ 'Content-Type': 'application/json' });
  });

  it('sends a valid Block Kit payload containing all key fields', async () => {
    process.env['SLACK_PILOT_WEBHOOK_URL'] = WEBHOOK;
    mockFetch.mockResolvedValueOnce({ ok: true, status: 200, text: async () => 'ok' });

    const { sendSlackAlert } = await import('./slack.js');
    await sendSlackAlert(payload);

    const body = JSON.parse(mockFetch.mock.calls[0][1].body as string);
    const raw = JSON.stringify(body);
    expect(raw).toContain('Alice');
    expect(raw).toContain('alice@acme.com');
    expect(raw).toContain('Acme Corp');
    expect(raw).toContain('req-123');
    expect(raw).toContain('1.2.3.4');
    expect(raw).toContain('Automate social posts');
    // Must use Block Kit (blocks array)
    expect(body.blocks).toBeInstanceOf(Array);
    expect(body.blocks.length).toBeGreaterThan(0);
  });

  it('returns false and logs error when HTTP response is not ok', async () => {
    process.env['SLACK_PILOT_WEBHOOK_URL'] = WEBHOOK;
    mockFetch.mockResolvedValueOnce({
      ok: false,
      status: 400,
      text: async () => 'invalid_payload',
    });
    const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});

    const { sendSlackAlert } = await import('./slack.js');
    const result = await sendSlackAlert(payload);

    expect(result).toBe(false);
    expect(consoleSpy).toHaveBeenCalledWith(
      expect.stringContaining('[notification:slack]')
    );
    consoleSpy.mockRestore();
  });

  it('returns false and does not throw when fetch rejects (network error)', async () => {
    process.env['SLACK_PILOT_WEBHOOK_URL'] = WEBHOOK;
    mockFetch.mockRejectedValueOnce(new Error('ECONNREFUSED'));
    const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});

    const { sendSlackAlert } = await import('./slack.js');
    const result = await sendSlackAlert(payload);

    expect(result).toBe(false);
    consoleSpy.mockRestore();
  });

  it('falls back to _None specified_ when systems/requirements are empty', async () => {
    process.env['SLACK_PILOT_WEBHOOK_URL'] = WEBHOOK;
    mockFetch.mockResolvedValueOnce({ ok: true, status: 200, text: async () => 'ok' });

    const { sendSlackAlert } = await import('./slack.js');
    await sendSlackAlert({ ...payload, systems: '', requirements: '' });

    const body = JSON.parse(mockFetch.mock.calls[0][1].body as string);
    const raw = JSON.stringify(body);
    expect(raw).toContain('_None specified_');
  });
});

// ── Tests: index.ts (notifyNewPilotRequest) ────────────────────────────────────

describe('notifyNewPilotRequest', () => {
  const makeReq = (overrides?: Partial<{ origin?: string; host?: string; ip?: string }>) => ({
    get: (h: string) => {
      if (h === 'origin') return overrides?.origin ?? 'squaremcp.com';
      if (h === 'host') return overrides?.host ?? 'squaremcp.com';
      return undefined;
    },
    ip: overrides?.ip ?? '9.9.9.9',
  });

  const realBody = {
    name: 'Bob',
    email: 'bob@realco.com',
    company: 'RealCo',
    role: 'VP Eng',
    use_case: 'Publish content',
    timeline: 'Q3',
    systems: 'HubSpot',
    requirements: '',
  };

  beforeEach(() => {
    vi.resetModules();
    mockFetch.mockReset();
    delete process.env['SLACK_PILOT_WEBHOOK_URL'];
  });

  it('calls sendSlackAlert for real submissions when webhook is configured', async () => {
    process.env['SLACK_PILOT_WEBHOOK_URL'] = 'https://hooks.slack.com/test';
    mockFetch.mockResolvedValue({ ok: true, status: 200, text: async () => 'ok' });

    const { notifyNewPilotRequest } = await import('./index.js');
    await notifyNewPilotRequest(realBody, 'req-abc', makeReq());
    // give the fire-and-forget microtask time to run
    await new Promise((r) => setTimeout(r, 10));

    expect(mockFetch).toHaveBeenCalledOnce();
  });

  it('does not call sendSlackAlert when webhook is not configured', async () => {
    const { notifyNewPilotRequest } = await import('./index.js');
    await notifyNewPilotRequest(realBody, 'req-abc', makeReq());
    await new Promise((r) => setTimeout(r, 10));

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

  describe('test submission filtering', () => {
    beforeEach(() => {
      process.env['SLACK_PILOT_WEBHOOK_URL'] = 'https://hooks.slack.com/test';
      mockFetch.mockResolvedValue({ ok: true, status: 200, text: async () => 'ok' });
    });

    const cases: Array<[string, Partial<typeof realBody>]> = [
      ['@example.com email', { email: 'test@example.com' }],
      ['name contains E2E', { name: 'E2E Runner' }],
      ['name contains Smoke Test', { name: 'Smoke Test Bot' }],
      ['name contains QA Test', { name: 'QA Test User' }],
      ['name contains Browser Test', { name: 'Browser Test' }],
      ['company is QA Check', { company: 'QA Check' }],
      ['company is QA Corp', { company: 'QA Corp' }],
      ['company is SquareMCP E2E', { company: 'SquareMCP E2E' }],
      ['company is SquareMCP Test', { company: 'SquareMCP Test' }],
      ['company is SquareMCP Browser', { company: 'SquareMCP Browser' }],
    ];

    it.each(cases)('skips alert for: %s', async (_label, override) => {
      const { notifyNewPilotRequest } = await import('./index.js');
      await notifyNewPilotRequest({ ...realBody, ...override }, 'req-skip', makeReq());
      await new Promise((r) => setTimeout(r, 10));
      expect(mockFetch).not.toHaveBeenCalled();
    });
  });

  it('uses req.socket.remoteAddress when req.ip is absent', async () => {
    process.env['SLACK_PILOT_WEBHOOK_URL'] = 'https://hooks.slack.com/test';
    mockFetch.mockResolvedValue({ ok: true, status: 200, text: async () => 'ok' });

    const reqNoIp = {
      get: (_h: string) => 'squaremcp.com',
      ip: undefined,
      socket: { remoteAddress: '5.5.5.5' },
    };

    const { notifyNewPilotRequest } = await import('./index.js');
    await notifyNewPilotRequest(realBody, 'req-ip', reqNoIp);
    await new Promise((r) => setTimeout(r, 10));

    const body = JSON.parse(mockFetch.mock.calls[0][1].body as string);
    expect(JSON.stringify(body)).toContain('5.5.5.5');
  });

  it('uses origin header as source when available', async () => {
    process.env['SLACK_PILOT_WEBHOOK_URL'] = 'https://hooks.slack.com/test';
    mockFetch.mockResolvedValue({ ok: true, status: 200, text: async () => 'ok' });

    const { notifyNewPilotRequest } = await import('./index.js');
    await notifyNewPilotRequest(
      realBody,
      'req-origin',
      makeReq({ origin: 'https://squaremcp.com' })
    );
    await new Promise((r) => setTimeout(r, 10));

    const body = JSON.parse(mockFetch.mock.calls[0][1].body as string);
    expect(JSON.stringify(body)).toContain('https://squaremcp.com');
  });

  it('falls back to host header when origin is absent', async () => {
    process.env['SLACK_PILOT_WEBHOOK_URL'] = 'https://hooks.slack.com/test';
    mockFetch.mockResolvedValue({ ok: true, status: 200, text: async () => 'ok' });

    const reqNoOrigin = {
      get: (h: string) => (h === 'host' ? 'squaremcp.com' : undefined),
      ip: '1.1.1.1',
    };

    const { notifyNewPilotRequest } = await import('./index.js');
    await notifyNewPilotRequest(realBody, 'req-host', reqNoOrigin);
    await new Promise((r) => setTimeout(r, 10));

    const body = JSON.parse(mockFetch.mock.calls[0][1].body as string);
    expect(JSON.stringify(body)).toContain('squaremcp.com');
  });

  it('does not propagate when sendSlackAlert rejects', async () => {
    process.env['SLACK_PILOT_WEBHOOK_URL'] = 'https://hooks.slack.com/test';
    mockFetch.mockRejectedValue(new Error('network failure'));
    const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});

    const { notifyNewPilotRequest } = await import('./index.js');
    // Should resolve without throwing even if alert fails
    await expect(notifyNewPilotRequest(realBody, 'req-fail', makeReq())).resolves.toBeUndefined();
    await new Promise((r) => setTimeout(r, 10));

    consoleSpy.mockRestore();
  });
});
