// Tests for @agor/stores health store
// The store uses Svelte 5 runes — we replicate and test the pure logic.

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

// ── Replicated types from health.svelte.ts ──────────────────────────────────

type ActivityState = 'inactive' | 'running' | 'idle' | 'stalled';

interface ProjectHealth {
  projectId: string;
  activityState: ActivityState;
  activeTool: string | null;
  idleDurationMs: number;
  burnRatePerHour: number;
  contextPressure: number | null;
  fileConflictCount: number;
  attentionScore: number;
  attentionReason: string | null;
}

// ── Replicated pure functions from health.svelte.ts (Electrobun version) ────

const DEFAULT_STALL_THRESHOLD_MS = 15 * 60 * 1000;
const BURN_RATE_WINDOW_MS = 5 * 60 * 1000;
const DEFAULT_CONTEXT_LIMIT = 200_000;

function scoreAttention(
  activityState: ActivityState,
  contextPressure: number | null,
  fileConflictCount: number,
  status: string,
): { score: number; reason: string | null } {
  if (status === 'error') return { score: 90, reason: 'Agent error' };
  if (activityState === 'stalled') return { score: 100, reason: 'Agent stalled (>15 min)' };
  if (contextPressure !== null && contextPressure > 0.9) return { score: 80, reason: 'Context >90%' };
  if (fileConflictCount > 0) return { score: 70, reason: `${fileConflictCount} file conflict(s)` };
  if (contextPressure !== null && contextPressure > 0.75) return { score: 40, reason: 'Context >75%' };
  return { score: 0, reason: null };
}

function computeBurnRate(snapshots: Array<[number, number]>): number {
  if (snapshots.length < 2) return 0;
  const now = Date.now();
  const windowStart = now - BURN_RATE_WINDOW_MS;
  const recent = snapshots.filter(([ts]) => ts >= windowStart);
  if (recent.length < 2) return 0;
  const first = recent[0];
  const last = recent[recent.length - 1];
  const elapsedHours = (last[0] - first[0]) / 3_600_000;
  if (elapsedHours < 0.001) return 0;
  const costDelta = last[1] - first[1];
  return Math.max(0, costDelta / elapsedHours);
}

// Simplified tracker for testing
interface ProjectTracker {
  projectId: string;
  lastActivityTs: number;
  lastToolName: string | null;
  toolsInFlight: number;
  activeToolMap: Map<string, { startTime: number; count: number }>;
  costSnapshots: Array<[number, number]>;
  totalTokens: number;
  totalCost: number;
  status: 'inactive' | 'running' | 'idle' | 'done' | 'error';
}

function makeTracker(overrides: Partial<ProjectTracker> = {}): ProjectTracker {
  return {
    projectId: 'test-project',
    lastActivityTs: Date.now(),
    lastToolName: null,
    toolsInFlight: 0,
    activeToolMap: new Map(),
    costSnapshots: [],
    totalTokens: 0,
    totalCost: 0,
    status: 'inactive',
    ...overrides,
  };
}

function computeHealth(tracker: ProjectTracker, now: number): ProjectHealth {
  let activityState: ActivityState;
  let idleDurationMs = 0;
  let activeTool: string | null = null;

  if (tracker.status === 'inactive' || tracker.status === 'done' || tracker.status === 'error') {
    activityState = 'inactive';
  } else if (tracker.toolsInFlight > 0) {
    activityState = 'running';
    activeTool = tracker.lastToolName;
  } else {
    idleDurationMs = now - tracker.lastActivityTs;
    activityState = idleDurationMs >= DEFAULT_STALL_THRESHOLD_MS ? 'stalled' : 'idle';
  }

  let contextPressure: number | null = null;
  if (tracker.totalTokens > 0) {
    contextPressure = Math.min(1, tracker.totalTokens / DEFAULT_CONTEXT_LIMIT);
  }

  const burnRatePerHour = computeBurnRate(tracker.costSnapshots);
  const attention = scoreAttention(activityState, contextPressure, 0, tracker.status);

  return {
    projectId: tracker.projectId,
    activityState,
    activeTool,
    idleDurationMs,
    burnRatePerHour,
    contextPressure,
    fileConflictCount: 0,
    attentionScore: attention.score,
    attentionReason: attention.reason,
  };
}

// ── Tests ───────────────────────────────────────────────────────────────────

describe('scoreAttention', () => {
  it('returns 0 when no attention needed', () => {
    const r = scoreAttention('inactive', null, 0, 'inactive');
    expect(r.score).toBe(0);
    expect(r.reason).toBeNull();
  });

  it('scores stalled highest (100)', () => {
    const r = scoreAttention('stalled', null, 0, 'running');
    expect(r.score).toBe(100);
    expect(r.reason).toContain('stalled');
  });

  it('scores error at 90', () => {
    const r = scoreAttention('inactive', null, 0, 'error');
    expect(r.score).toBe(90);
    expect(r.reason).toContain('error');
  });

  it('scores context >90% at 80', () => {
    const r = scoreAttention('running', 0.95, 0, 'running');
    expect(r.score).toBe(80);
    expect(r.reason).toContain('90%');
  });

  it('scores file conflict at 70', () => {
    const r = scoreAttention('running', 0.5, 2, 'running');
    expect(r.score).toBe(70);
    expect(r.reason).toContain('2 file conflict');
  });

  it('scores context >75% at 40', () => {
    const r = scoreAttention('running', 0.8, 0, 'running');
    expect(r.score).toBe(40);
    expect(r.reason).toContain('75%');
  });

  it('priority: stalled beats context pressure', () => {
    const r = scoreAttention('stalled', 0.95, 3, 'running');
    expect(r.score).toBe(100);
  });
});

describe('computeBurnRate', () => {
  it('returns 0 with fewer than 2 snapshots', () => {
    expect(computeBurnRate([])).toBe(0);
    expect(computeBurnRate([[Date.now(), 1.0]])).toBe(0);
  });

  it('calculates $/hr from cost snapshots', () => {
    const now = Date.now();
    const snapshots: Array<[number, number]> = [
      [now - 60_000, 0.50],  // 1 min ago, $0.50
      [now, 1.50],            // now, $1.50
    ];
    const rate = computeBurnRate(snapshots);
    // $1.00 over 1 minute = $60/hr
    expect(rate).toBeCloseTo(60, 0);
  });

  it('returns 0 when snapshots outside window', () => {
    const now = Date.now();
    const snapshots: Array<[number, number]> = [
      [now - BURN_RATE_WINDOW_MS - 10_000, 0.50],
      [now - BURN_RATE_WINDOW_MS - 5_000, 1.50],
    ];
    expect(computeBurnRate(snapshots)).toBe(0);
  });
});

describe('computeHealth', () => {
  it('inactive tracker returns inactive state', () => {
    const tracker = makeTracker({ status: 'inactive' });
    const h = computeHealth(tracker, Date.now());
    expect(h.activityState).toBe('inactive');
    expect(h.attentionScore).toBe(0);
  });

  it('running tracker with tool in flight returns running state', () => {
    const tracker = makeTracker({ status: 'running', toolsInFlight: 1, lastToolName: 'Bash' });
    const h = computeHealth(tracker, Date.now());
    expect(h.activityState).toBe('running');
    expect(h.activeTool).toBe('Bash');
  });

  it('detects stall when idle exceeds threshold', () => {
    const stalledTs = Date.now() - DEFAULT_STALL_THRESHOLD_MS - 1000;
    const tracker = makeTracker({ status: 'running', lastActivityTs: stalledTs });
    const h = computeHealth(tracker, Date.now());
    expect(h.activityState).toBe('stalled');
    expect(h.attentionScore).toBe(100);
  });

  it('calculates context pressure from tokens', () => {
    const tracker = makeTracker({ status: 'running', totalTokens: 180_000, toolsInFlight: 1 });
    const h = computeHealth(tracker, Date.now());
    expect(h.contextPressure).toBeCloseTo(0.9, 2);
  });

  it('context pressure capped at 1.0', () => {
    const tracker = makeTracker({ status: 'running', totalTokens: 300_000, toolsInFlight: 1 });
    const h = computeHealth(tracker, Date.now());
    expect(h.contextPressure).toBe(1.0);
  });

  it('toolsInFlight counter tracks concurrent tools', () => {
    const tracker = makeTracker({ status: 'running', toolsInFlight: 3, lastToolName: 'Read' });
    const h = computeHealth(tracker, Date.now());
    expect(h.activityState).toBe('running');
    expect(h.activeTool).toBe('Read');
  });
});

describe('tool histogram logic', () => {
  it('activeToolMap tracks per-tool starts', () => {
    const tracker = makeTracker({ status: 'running' });
    // Simulate recordActivity with toolName
    tracker.activeToolMap.set('Bash', { startTime: Date.now(), count: 1 });
    tracker.toolsInFlight = 1;
    tracker.lastToolName = 'Bash';
    expect(tracker.activeToolMap.size).toBe(1);
    expect(tracker.activeToolMap.get('Bash')!.count).toBe(1);
  });

  it('incrementing count for same tool', () => {
    const tracker = makeTracker({ status: 'running' });
    tracker.activeToolMap.set('Bash', { startTime: Date.now(), count: 1 });
    // Second Bash call
    const entry = tracker.activeToolMap.get('Bash')!;
    entry.count++;
    tracker.toolsInFlight = 2;
    expect(entry.count).toBe(2);
    expect(tracker.toolsInFlight).toBe(2);
  });

  it('decrement removes entry when count reaches 0', () => {
    const tracker = makeTracker({ status: 'running' });
    tracker.activeToolMap.set('Read', { startTime: Date.now(), count: 1 });
    tracker.toolsInFlight = 1;
    // Simulate recordToolDone
    tracker.toolsInFlight = Math.max(0, tracker.toolsInFlight - 1);
    const entry = tracker.activeToolMap.get('Read')!;
    entry.count--;
    if (entry.count <= 0) tracker.activeToolMap.delete('Read');
    expect(tracker.activeToolMap.size).toBe(0);
    expect(tracker.toolsInFlight).toBe(0);
  });
});

describe('attention queue sorting', () => {
  it('sorts by score descending', () => {
    const trackers = [
      makeTracker({ projectId: 'a', status: 'running', totalTokens: 180_000, toolsInFlight: 1 }), // ctx 90%
      makeTracker({ projectId: 'b', status: 'error' }),                                            // error
      makeTracker({ projectId: 'c', status: 'inactive' }),                                         // no attention
    ];
    const now = Date.now();
    const healths = trackers.map(t => computeHealth(t, now));
    const queue = healths
      .filter(h => h.attentionScore > 0)
      .sort((a, b) => b.attentionScore - a.attentionScore);

    expect(queue).toHaveLength(2);
    expect(queue[0].projectId).toBe('b'); // error = 90
    expect(queue[1].projectId).toBe('a'); // ctx 90% = 80
  });
});
