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

import { HeartbeatEmitter, startHeartbeatLoop } from '../heartbeat.js';
import type { PresenceHeartbeatContent } from '../types.js';

const agent = { mxid: '@agent-alpha:matrix.localhost', slug: 'alpha', harness: 'claude-code' };

describe('HeartbeatEmitter', () => {
  it('increments seq starting at 1 and stamps the envelope', () => {
    let t = 1000;
    const em = new HeartbeatEmitter({ agent, intervalMs: 5000, now: () => t });
    const a = em.next();
    t = 6000;
    const b = em.next('away');

    expect(a.seq).toBe(1);
    expect(a.ts).toBe(1000);
    expect(a.status).toBe('online');
    expect(a.macp_type).toBe('presence');
    expect(a.msgtype).toBe('mosaic.presence');
    expect(a.macp_version).toBe('1.0');
    expect(a.interval_ms).toBe(5000);
    expect(a.agent).toEqual(agent);
    expect(a.body).toContain('alpha');

    expect(b.seq).toBe(2);
    expect(b.ts).toBe(6000);
    expect(b.status).toBe('away');
    expect(em.currentSeq).toBe(2);
  });

  it('includes mission_id only when provided', () => {
    const withMission = new HeartbeatEmitter({
      agent,
      intervalMs: 1000,
      missionId: 'KBN-101',
    }).next();
    const without = new HeartbeatEmitter({ agent, intervalMs: 1000 }).next();
    expect(withMission.mission_id).toBe('KBN-101');
    expect(without.mission_id).toBeUndefined();
  });
});

describe('startHeartbeatLoop', () => {
  afterEach(() => vi.useRealTimers());

  it('emits immediately, then once per interval, until stopped', () => {
    vi.useFakeTimers();
    const sent: PresenceHeartbeatContent[] = [];
    const em = new HeartbeatEmitter({ agent, intervalMs: 1000, now: () => Date.now() });
    const loop = startHeartbeatLoop({
      emitter: em,
      intervalMs: 1000,
      send: (c) => {
        sent.push(c);
      },
    });

    expect(sent).toHaveLength(1); // immediate beat
    vi.advanceTimersByTime(3000);
    expect(sent).toHaveLength(4); // +3 beats
    expect(sent.map((s) => s.seq)).toEqual([1, 2, 3, 4]);

    loop.stop();
    vi.advanceTimersByTime(5000);
    expect(sent).toHaveLength(4); // no more after stop
    loop.stop(); // idempotent
  });

  it('routes a rejected async send to onError without killing the loop', async () => {
    vi.useFakeTimers();
    const onError = vi.fn();
    let n = 0;
    const em = new HeartbeatEmitter({ agent, intervalMs: 1000 });
    const loop = startHeartbeatLoop({
      emitter: em,
      intervalMs: 1000,
      onError,
      send: () => {
        n += 1;
        return Promise.reject(new Error(`boom ${n}`));
      },
    });

    await vi.advanceTimersByTimeAsync(2000); // immediate + 2
    expect(n).toBe(3);
    expect(onError).toHaveBeenCalledTimes(3);
    loop.stop();
  });
});
