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

import { classifyLiveness, computeFleetLiveness } from '../liveness.js';
import type { HeartbeatObservation, LivenessPolicy } from '../types.js';

// Small, dev-scale policy so the arithmetic is obvious:
//   online window = interval * missTolerance = 1000 * 2 = 2000ms
//   dark threshold = 5000ms
const policy: LivenessPolicy = {
  heartbeatIntervalMs: 1000,
  missTolerance: 2,
  darkThresholdMs: 5000,
};

describe('classifyLiveness (deterministic, heartbeat-age based — RFC-001 §4.5)', () => {
  it('is online when age is within interval * missTolerance', () => {
    expect(classifyLiveness(0, policy)).toBe('online');
    expect(classifyLiveness(1999, policy)).toBe('online');
    expect(classifyLiveness(2000, policy)).toBe('online'); // inclusive boundary
  });

  it('is away when past the online window but before dark threshold', () => {
    expect(classifyLiveness(2001, policy)).toBe('away');
    expect(classifyLiveness(4999, policy)).toBe('away');
  });

  it('is offline/dark at or past the dark threshold', () => {
    expect(classifyLiveness(5000, policy)).toBe('offline');
    expect(classifyLiveness(50_000, policy)).toBe('offline');
  });

  it('treats a never-seen agent (Infinity age) as offline', () => {
    expect(classifyLiveness(Number.POSITIVE_INFINITY, policy)).toBe('offline');
  });

  it('never returns online for a negative-but-huge misconfig (guards NaN)', () => {
    // A NaN age must fail safe to offline, not silently report online.
    expect(classifyLiveness(Number.NaN, policy)).toBe('offline');
  });
});

describe('computeFleetLiveness (A2/A3 core)', () => {
  const now = 100_000;
  const obs = (slug: string, lastSeenTs: number, lastSeq = 1): HeartbeatObservation => ({
    slug,
    mxid: `@agent-${slug}:matrix.localhost`,
    lastSeenTs,
    lastSeq,
    assertedStatus: 'online',
  });

  it('classifies a live fleet: fresh=online, stale=away, dark=offline', () => {
    const result = computeFleetLiveness(
      [
        obs('alpha', now - 500), // 500ms old  -> online
        obs('bravo', now - 3000), // 3000ms old -> away
        obs('charlie', now - 8000), // 8000ms old -> offline
      ],
      now,
      policy,
    );
    const byslug = Object.fromEntries(result.map((r) => [r.slug, r.status]));
    expect(byslug).toEqual({ alpha: 'online', bravo: 'away', charlie: 'offline' });
  });

  it('A3: a previously-online agent flips to offline once age crosses dark threshold', () => {
    const lastBeat = 100_000; // agent was hard-killed right after this beat
    // Just before the threshold it is still merely "away"...
    const justBefore = computeFleetLiveness([obs('victim', lastBeat, 7)], lastBeat + 4999, policy);
    expect(justBefore[0]?.status).toBe('away');
    // ...and the instant age reaches darkThresholdMs it is deterministically offline,
    // with no dependence on native Matrix presence timeouts.
    const atThreshold = computeFleetLiveness([obs('victim', lastBeat, 7)], lastBeat + 5000, policy);
    expect(atThreshold[0]?.status).toBe('offline');
    expect(atThreshold[0]?.ageMs).toBe(5000);
    expect(atThreshold[0]?.lastSeq).toBe(7);
  });

  it('reports ageMs and preserves mxid/slug/seq for the human view', () => {
    const [row] = computeFleetLiveness([obs('alpha', now - 1200, 42)], now, policy);
    expect(row).toMatchObject({
      slug: 'alpha',
      mxid: '@agent-alpha:matrix.localhost',
      ageMs: 1200,
      lastSeq: 42,
      status: 'online',
    });
  });
});
