// Tests for src/triggers.js — event-driven trigger engine
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const os = require('os');

// Mock os.homedir for webhook log path
const mockHome = path.join(os.tmpdir(), `pre-test-trig-home-${process.pid}`);
jest.spyOn(os, 'homedir').mockReturnValue(mockHome);

jest.mock('../src/constants');
const { PRE_DIR } = require('../src/constants');
const triggers = require('../src/triggers');

const triggersFile = path.join(PRE_DIR, 'triggers.json');
const webhookLogFile = path.join(mockHome, '.pre', 'logs', 'webhooks.jsonl');

describe('triggers', () => {
  beforeEach(() => {
    fs.writeFileSync(triggersFile, '[]');
    try { fs.unlinkSync(webhookLogFile); } catch {}
  });

  beforeAll(() => {
    fs.mkdirSync(path.join(mockHome, '.pre'), { recursive: true });
  });

  afterAll(() => {
    triggers.shutdown();
    fs.rmSync(mockHome, { recursive: true, force: true });
  });

  // ── dispatcher ──

  test('returns error for unknown action', () => {
    expect(triggers.trigger({ action: 'bogus' })).toMatch(/error/i);
  });

  // ── add ──

  describe('add trigger', () => {
    test('creates a file_watch trigger', () => {
      const result = triggers.trigger({
        action: 'add', type: 'file_watch', name: 'Watcher',
        prompt: 'Process {path}', path: PRE_DIR,
      });
      expect(result).toContain('Trigger created');
      expect(triggers.loadTriggers()).toHaveLength(1);
    });

    test('creates a webhook trigger with auto-generated secret', () => {
      const result = triggers.trigger({
        action: 'add', type: 'webhook', name: 'CI', prompt: '{payload}',
      });
      expect(result).toContain('webhook');
      expect(result).toContain('Secret:');
      expect(result).toContain('won\'t be shown again');
      const t = triggers.loadTriggers()[0];
      expect(t.config.secret).toHaveLength(64); // 32 bytes hex
      expect(t.config.auth_mode).toBe('bearer');
    });

    test('creates a webhook trigger with custom secret', () => {
      triggers.trigger({
        action: 'add', type: 'webhook', name: 'CI', prompt: '{payload}', secret: 'custom-s3cret',
      });
      expect(triggers.loadTriggers()[0].config.secret).toBe('custom-s3cret');
    });

    test('creates a webhook trigger with HMAC mode', () => {
      triggers.trigger({
        action: 'add', type: 'webhook', name: 'GH', prompt: '{payload}', auth_mode: 'hmac',
      });
      const t = triggers.loadTriggers()[0];
      expect(t.config.auth_mode).toBe('hmac');
    });

    test('creates a webhook trigger with IP allowlist', () => {
      triggers.trigger({
        action: 'add', type: 'webhook', name: 'IP', prompt: '{payload}',
        ip_allowlist: ['127.0.0.1', '10.0.0.5'],
      });
      expect(triggers.loadTriggers()[0].config.ip_allowlist).toEqual(['127.0.0.1', '10.0.0.5']);
    });

    test('rejects invalid type', () => {
      expect(triggers.trigger({ action: 'add', type: 'bad', prompt: 'x' })).toMatch(/error/i);
    });

    test('rejects missing prompt', () => {
      expect(triggers.trigger({ action: 'add', type: 'webhook' })).toMatch(/error/i);
    });

    test('rejects file_watch without path', () => {
      expect(triggers.trigger({ action: 'add', type: 'file_watch', prompt: 'x' })).toMatch(/error/i);
    });
  });

  // ── list ──

  describe('list', () => {
    test('empty message when none', () => {
      expect(triggers.trigger({ action: 'list' })).toMatch(/no triggers/i);
    });

    test('lists created triggers', () => {
      triggers.trigger({ action: 'add', type: 'webhook', name: 'T', prompt: 'x' });
      expect(triggers.trigger({ action: 'list' })).toContain('1 trigger');
    });
  });

  // ── remove ──

  describe('remove', () => {
    test('removes by ID', () => {
      triggers.trigger({ action: 'add', type: 'webhook', name: 'R', prompt: 'x' });
      const id = triggers.loadTriggers()[0].id;
      expect(triggers.trigger({ action: 'remove', id })).toContain('Removed');
      expect(triggers.loadTriggers()).toHaveLength(0);
    });

    test('error for bad ID', () => {
      expect(triggers.trigger({ action: 'remove', id: 'nope' })).toMatch(/error/i);
    });
  });

  // ── enable / disable ──

  test('disables and re-enables', () => {
    triggers.trigger({ action: 'add', type: 'webhook', name: 'E', prompt: 'x' });
    const id = triggers.loadTriggers()[0].id;
    triggers.trigger({ action: 'disable', id });
    expect(triggers.loadTriggers()[0].enabled).toBe(false);
    triggers.trigger({ action: 'enable', id });
    expect(triggers.loadTriggers()[0].enabled).toBe(true);
  });

  // ── rotate secret ──

  describe('rotate', () => {
    test('rotates webhook secret', () => {
      triggers.trigger({ action: 'add', type: 'webhook', name: 'Rot', prompt: 'x' });
      const id = triggers.loadTriggers()[0].id;
      const oldSecret = triggers.loadTriggers()[0].config.secret;

      const result = triggers.trigger({ action: 'rotate', id });
      expect(result).toContain('Secret rotated');
      expect(result).toContain('New secret:');
      const newSecret = triggers.loadTriggers()[0].config.secret;
      expect(newSecret).not.toBe(oldSecret);
      expect(newSecret).toHaveLength(64);
    });

    test('error for non-webhook trigger', () => {
      triggers.trigger({ action: 'add', type: 'file_watch', name: 'W', prompt: 'x', path: PRE_DIR });
      const id = triggers.loadTriggers()[0].id;
      expect(triggers.trigger({ action: 'rotate', id })).toMatch(/only webhook/i);
    });

    test('error for missing ID', () => {
      expect(triggers.trigger({ action: 'rotate' })).toMatch(/error/i);
    });
  });

  // ── isWatching ──

  test('isWatching returns false for nonexistent', () => {
    expect(triggers.isWatching('fake')).toBe(false);
  });

  // ── handleWebhook ──

  describe('handleWebhook — Bearer auth', () => {
    test('401 for unknown ID', () => {
      const result = triggers.handleWebhook('x', { body: {}, headers: {} });
      expect(result.status).toBe(401);
    });

    test('401 for disabled trigger', () => {
      triggers.trigger({ action: 'add', type: 'webhook', name: 'D', prompt: 'x' });
      const id = triggers.loadTriggers()[0].id;
      triggers.trigger({ action: 'disable', id });
      expect(triggers.handleWebhook(id, { body: {}, headers: {} }).status).toBe(401);
    });

    test('401 for missing auth header', () => {
      triggers.trigger({ action: 'add', type: 'webhook', name: 'NoAuth', prompt: 'x' });
      const id = triggers.loadTriggers()[0].id;
      const result = triggers.handleWebhook(id, { body: {}, headers: {} });
      expect(result.status).toBe(401);
    });

    test('401 for wrong token', () => {
      triggers.trigger({ action: 'add', type: 'webhook', name: 'Wrong', prompt: 'x' });
      const id = triggers.loadTriggers()[0].id;
      const result = triggers.handleWebhook(id, {
        body: {},
        headers: { authorization: 'Bearer wrong-token' },
      });
      expect(result.status).toBe(401);
    });

    test('succeeds with valid bearer token', () => {
      triggers.trigger({ action: 'add', type: 'webhook', name: 'Valid', prompt: 'x' });
      const t = triggers.loadTriggers()[0];
      triggers.init(jest.fn().mockResolvedValue({})); // need executeCallback
      const result = triggers.handleWebhook(t.id, {
        body: { data: 'test' },
        headers: { authorization: `Bearer ${t.config.secret}` },
      });
      expect(result.ok).toBe(true);
    });

    test('rejects after secret rotation', () => {
      triggers.trigger({ action: 'add', type: 'webhook', name: 'RotCheck', prompt: 'x' });
      const id = triggers.loadTriggers()[0].id;
      const oldSecret = triggers.loadTriggers()[0].config.secret;
      triggers.trigger({ action: 'rotate', id });

      const result = triggers.handleWebhook(id, {
        body: {},
        headers: { authorization: `Bearer ${oldSecret}` },
      });
      expect(result.status).toBe(401);
    });
  });

  describe('handleWebhook — HMAC auth', () => {
    test('401 for missing signature', () => {
      triggers.trigger({ action: 'add', type: 'webhook', name: 'HMAC', prompt: 'x', auth_mode: 'hmac' });
      const id = triggers.loadTriggers()[0].id;
      const result = triggers.handleWebhook(id, {
        body: { test: true },
        headers: {},
        rawBody: Buffer.from('{"test":true}'),
      });
      expect(result.status).toBe(401);
    });

    test('401 for invalid signature', () => {
      triggers.trigger({ action: 'add', type: 'webhook', name: 'HMAC', prompt: 'x', auth_mode: 'hmac' });
      const id = triggers.loadTriggers()[0].id;
      const result = triggers.handleWebhook(id, {
        body: { test: true },
        headers: { 'x-pre-signature': 'sha256=0000000000000000000000000000000000000000000000000000000000000000' },
        rawBody: Buffer.from('{"test":true}'),
      });
      expect(result.status).toBe(401);
    });

    test('succeeds with valid HMAC signature', () => {
      triggers.trigger({ action: 'add', type: 'webhook', name: 'HMAC', prompt: 'x', auth_mode: 'hmac' });
      const t = triggers.loadTriggers()[0];
      triggers.init(jest.fn().mockResolvedValue({}));
      const bodyBuf = Buffer.from('{"test":true}');
      const sig = crypto.createHmac('sha256', t.config.secret).update(bodyBuf).digest('hex');

      const result = triggers.handleWebhook(t.id, {
        body: { test: true },
        headers: { 'x-pre-signature': `sha256=${sig}` },
        rawBody: bodyBuf,
      });
      expect(result.ok).toBe(true);
    });

    test('rejects replay with different body', () => {
      triggers.trigger({ action: 'add', type: 'webhook', name: 'HMAC', prompt: 'x', auth_mode: 'hmac' });
      const t = triggers.loadTriggers()[0];
      const originalBody = Buffer.from('{"test":true}');
      const sig = crypto.createHmac('sha256', t.config.secret).update(originalBody).digest('hex');

      // Replay with different body
      const result = triggers.handleWebhook(t.id, {
        body: { test: false },
        headers: { 'x-pre-signature': `sha256=${sig}` },
        rawBody: Buffer.from('{"test":false}'),
      });
      expect(result.status).toBe(401);
    });
  });

  describe('handleWebhook — IP allowlist', () => {
    test('rejects non-allowed IP', () => {
      triggers.trigger({
        action: 'add', type: 'webhook', name: 'IPBlock', prompt: 'x',
        ip_allowlist: ['10.0.0.1'],
      });
      const t = triggers.loadTriggers()[0];
      const result = triggers.handleWebhook(t.id, {
        body: {},
        headers: { authorization: `Bearer ${t.config.secret}` },
        ip: '192.168.1.1',
      });
      expect(result.status).toBe(401);
    });

    test('allows matching IP', () => {
      triggers.trigger({
        action: 'add', type: 'webhook', name: 'IPAllow', prompt: 'x',
        ip_allowlist: ['127.0.0.1'],
      });
      const t = triggers.loadTriggers()[0];
      triggers.init(jest.fn().mockResolvedValue({}));
      const result = triggers.handleWebhook(t.id, {
        body: {},
        headers: { authorization: `Bearer ${t.config.secret}` },
        ip: '127.0.0.1',
      });
      expect(result.ok).toBe(true);
    });
  });

  describe('handleWebhook — Logging', () => {
    test('logs successful webhook calls', () => {
      triggers.trigger({ action: 'add', type: 'webhook', name: 'Log', prompt: 'x' });
      const t = triggers.loadTriggers()[0];
      triggers.init(jest.fn().mockResolvedValue({}));
      triggers.handleWebhook(t.id, {
        body: {},
        headers: { authorization: `Bearer ${t.config.secret}` },
        ip: '127.0.0.1',
      });

      const log = fs.readFileSync(webhookLogFile, 'utf-8').trim().split('\n');
      const entry = JSON.parse(log[log.length - 1]);
      expect(entry.outcome).toBe('ok');
      expect(entry.trigger_id).toBe(t.id);
      expect(entry.ip).toBe('127.0.0.1');
      expect(entry.ts).toBeGreaterThan(0);
    });

    test('logs rejected webhook calls', () => {
      triggers.trigger({ action: 'add', type: 'webhook', name: 'LogFail', prompt: 'x' });
      const id = triggers.loadTriggers()[0].id;
      triggers.handleWebhook(id, {
        body: {},
        headers: { authorization: 'Bearer wrong' },
        ip: '10.0.0.99',
      });

      const log = fs.readFileSync(webhookLogFile, 'utf-8').trim().split('\n');
      const entry = JSON.parse(log[log.length - 1]);
      expect(entry.outcome).toBe('invalid_token');
      expect(entry.ip).toBe('10.0.0.99');
    });
  });

  // ── restartWatcher ──

  test('restartWatcher error for nonexistent', () => {
    expect(triggers.restartWatcher('fake').error).toBeTruthy();
  });

  test('restartWatcher error for webhook type', () => {
    triggers.trigger({ action: 'add', type: 'webhook', name: 'W', prompt: 'x' });
    const id = triggers.loadTriggers()[0].id;
    expect(triggers.restartWatcher(id).error).toMatch(/not a file watcher/i);
  });

  // ── init ──

  test('init returns stats', () => {
    const stats = triggers.init(jest.fn().mockResolvedValue({}));
    expect(stats).toHaveProperty('watcherCount');
    expect(stats).toHaveProperty('total');
  });
});
