import { describe, it, expect, vi, beforeEach } from 'vitest';
import { HookBus, pluginErrorTracker, removeAllPluginHooks, clearAllHooks, emailHooks, calendarHooks } from '../plugin-hooks';

beforeEach(() => {
  pluginErrorTracker.resetAll();
  clearAllHooks();
});

describe('HookBus', () => {
  describe('register / size / dispose', () => {
    it('registers a handler', () => {
      const bus = new HookBus();
      bus.register('p1', vi.fn());
      expect(bus.size).toBe(1);
    });

    it('dispose removes the handler', () => {
      const bus = new HookBus();
      const d = bus.register('p1', vi.fn());
      d.dispose();
      expect(bus.size).toBe(0);
    });

    it('registering multiple handlers', () => {
      const bus = new HookBus();
      bus.register('p1', vi.fn());
      bus.register('p2', vi.fn());
      expect(bus.size).toBe(2);
    });

    it('removePlugin removes all handlers for that plugin', () => {
      const bus = new HookBus();
      bus.register('p1', vi.fn());
      bus.register('p1', vi.fn());
      bus.register('p2', vi.fn());
      bus.removePlugin('p1');
      expect(bus.size).toBe(1);
    });

    it('clear removes all handlers', () => {
      const bus = new HookBus();
      bus.register('p1', vi.fn());
      bus.register('p2', vi.fn());
      bus.clear();
      expect(bus.size).toBe(0);
    });
  });

  describe('emit (observer)', () => {
    it('calls all handlers with args', async () => {
      const bus = new HookBus<(x: number) => void>();
      const fn1 = vi.fn();
      const fn2 = vi.fn();
      bus.register('p1', fn1);
      bus.register('p2', fn2);
      await bus.emit(42);
      expect(fn1).toHaveBeenCalledWith(42);
      expect(fn2).toHaveBeenCalledWith(42);
    });

    it('calls handlers in order', async () => {
      const bus = new HookBus<() => void>();
      const order: number[] = [];
      bus.register('p1', () => order.push(200), 200);
      bus.register('p2', () => order.push(50), 50);
      bus.register('p3', () => order.push(100), 100);
      await bus.emit();
      expect(order).toEqual([50, 100, 200]);
    });

    it('catches handler errors and records them', async () => {
      const bus = new HookBus<() => void>();
      const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
      bus.register('p1', () => { throw new Error('fail'); });
      await bus.emit();
      expect(consoleSpy).toHaveBeenCalled();
      consoleSpy.mockRestore();
    });

    it('skips disabled plugins', async () => {
      const bus = new HookBus<() => void>();
      const fn = vi.fn();
      bus.register('p1', fn);

      // Manually trigger circuit breaker
      const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
      for (let i = 0; i < 3; i++) {
        pluginErrorTracker.record('p1', new Error('test'));
      }
      consoleSpy.mockRestore();

      expect(pluginErrorTracker.isDisabled('p1')).toBe(true);
      await bus.emit();
      expect(fn).not.toHaveBeenCalled();
    });
  });

  describe('emitSync', () => {
    it('calls handlers synchronously', () => {
      const bus = new HookBus<(x: string) => void>();
      const fn = vi.fn();
      bus.register('p1', fn);
      bus.emitSync('hello');
      expect(fn).toHaveBeenCalledWith('hello');
    });

    it('catches errors without throwing', () => {
      const bus = new HookBus<() => void>();
      const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
      bus.register('p1', () => { throw new Error('boom'); });
      expect(() => bus.emitSync()).not.toThrow();
      consoleSpy.mockRestore();
    });
  });

  describe('intercept', () => {
    it('returns true when all handlers pass', async () => {
      const bus = new HookBus<() => boolean>();
      bus.register('p1', () => true);
      bus.register('p2', () => true);
      const result = await bus.intercept();
      expect(result).toBe(true);
    });

    it('returns false when any handler returns false', async () => {
      const bus = new HookBus<() => boolean>();
      bus.register('p1', () => true);
      bus.register('p2', () => false);
      const result = await bus.intercept();
      expect(result).toBe(false);
    });

    it('stops early on false (short-circuits)', async () => {
      const bus = new HookBus<() => boolean>();
      const fn3 = vi.fn(() => true);
      bus.register('p1', () => true, 10);
      bus.register('p2', () => false, 20);
      bus.register('p3', fn3, 30);
      await bus.intercept();
      expect(fn3).not.toHaveBeenCalled();
    });

    it('returns true when no handlers registered', async () => {
      const bus = new HookBus<() => boolean>();
      expect(await bus.intercept()).toBe(true);
    });
  });

  describe('transform', () => {
    it('chains values through handlers', async () => {
      const bus = new HookBus<(val: number) => number>();
      bus.register('p1', (val: number) => val * 2);
      bus.register('p2', (val: number) => val + 1);
      const result = await bus.transform(5);
      expect(result).toBe(11); // (5 * 2) + 1
    });

    it('returns initial value when no handlers', async () => {
      const bus = new HookBus<(val: string) => string>();
      const result = await bus.transform('hello');
      expect(result).toBe('hello');
    });

    it('skips handler that returns undefined', async () => {
      const bus = new HookBus<(val: number) => number | undefined>();
      bus.register('p1', () => undefined);
      bus.register('p2', (val: number) => val + 10);
      const result = await bus.transform(5);
      expect(result).toBe(15);
    });
  });
});

describe('PluginErrorTracker', () => {
  it('is not disabled initially', () => {
    expect(pluginErrorTracker.isDisabled('some-plugin')).toBe(false);
  });

  it('disables after threshold errors', () => {
    const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
    pluginErrorTracker.record('p1', new Error('1'));
    pluginErrorTracker.record('p1', new Error('2'));
    expect(pluginErrorTracker.isDisabled('p1')).toBe(false);
    pluginErrorTracker.record('p1', new Error('3'));
    expect(pluginErrorTracker.isDisabled('p1')).toBe(true);
    consoleSpy.mockRestore();
  });

  it('calls auto-disable callback', () => {
    const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
    const cb = vi.fn();
    pluginErrorTracker.setAutoDisableCallback(cb);
    for (let i = 0; i < 3; i++) {
      pluginErrorTracker.record('p2', new Error(`err-${i}`));
    }
    expect(cb).toHaveBeenCalledWith('p2', expect.any(Error));
    consoleSpy.mockRestore();
    pluginErrorTracker.setAutoDisableCallback(() => {});
  });

  it('reset re-enables a plugin', () => {
    const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
    for (let i = 0; i < 3; i++) {
      pluginErrorTracker.record('p3', new Error(`err-${i}`));
    }
    expect(pluginErrorTracker.isDisabled('p3')).toBe(true);
    pluginErrorTracker.reset('p3');
    expect(pluginErrorTracker.isDisabled('p3')).toBe(false);
    consoleSpy.mockRestore();
  });
});

describe('Hook domain instances', () => {
  it('emailHooks has expected buses', () => {
    expect(emailHooks.onEmailOpen).toBeInstanceOf(HookBus);
    expect(emailHooks.onBeforeEmailSend).toBeInstanceOf(HookBus);
    expect(emailHooks.onAfterEmailDelete).toBeInstanceOf(HookBus);
    expect(emailHooks.onNewEmailReceived).toBeInstanceOf(HookBus);
  });

  it('calendarHooks has expected buses', () => {
    expect(calendarHooks.onCalendarEventOpen).toBeInstanceOf(HookBus);
    expect(calendarHooks.onBeforeEventCreate).toBeInstanceOf(HookBus);
    expect(calendarHooks.onEventRsvp).toBeInstanceOf(HookBus);
  });
});

describe('removeAllPluginHooks', () => {
  it('removes handlers from all buses for a plugin', () => {
    emailHooks.onEmailOpen.register('test-p', vi.fn());
    calendarHooks.onCalendarEventOpen.register('test-p', vi.fn());
    emailHooks.onEmailOpen.register('other-p', vi.fn());

    removeAllPluginHooks('test-p');

    expect(emailHooks.onEmailOpen.size).toBe(1); // other-p remains
    expect(calendarHooks.onCalendarEventOpen.size).toBe(0);
  });
});

describe('clearAllHooks', () => {
  it('removes all handlers from all buses', () => {
    emailHooks.onEmailOpen.register('p1', vi.fn());
    calendarHooks.onCalendarEventOpen.register('p2', vi.fn());
    clearAllHooks();
    expect(emailHooks.onEmailOpen.size).toBe(0);
    expect(calendarHooks.onCalendarEventOpen.size).toBe(0);
  });
});
