/**
 * SET 14 — metrics helpers: passesTotal derivation, H:MM:SS, monotonicity.
 */
import { describe, expect, it } from 'vitest';
import {
  derivePassesTotal,
  formatHms,
  mergeMonotonicLiveMetrics,
  selectTier1StatusMetrics,
} from '../src/tier1/metricsHelpers';
import {
  cyclesFromMetrics,
  displayPipelineMessage,
  elapsedMinutesSince,
  formatHms as formatHmsWeb,
  formatRunningTabTitle,
  SPIN_FAVICON_FRAMES,
} from '../web/src/tier1Metrics';

describe('SET 14 metrics helpers', () => {
  it('passesTotal equals sum of passesPerFeature', () => {
    const ppf = { F1: 3, F2: 5, F3: 0, F4: 2 };
    expect(derivePassesTotal(ppf)).toBe(10);
    expect(derivePassesTotal({})).toBe(0);
    expect(derivePassesTotal(null)).toBe(0);
    expect(derivePassesTotal(undefined)).toBe(0);
    expect(cyclesFromMetrics({ passesPerFeature: ppf, passesTotal: 99 })).toBe(10);
    expect(cyclesFromMetrics({ passesTotal: 7 })).toBe(7);
  });

  it('formatHms renders H:MM:SS', () => {
    expect(formatHms(0)).toBe('0:00:00');
    expect(formatHms(9)).toBe('0:00:09');
    expect(formatHms(65)).toBe('0:01:05');
    expect(formatHms(3600)).toBe('1:00:00');
    expect(formatHms(3661)).toBe('1:01:01');
    expect(formatHms(12 * 3600 + 34 * 60 + 56)).toBe('12:34:56');
    expect(formatHms(-5)).toBe('0:00:00');
    // web helper stays lockstep with src
    expect(formatHmsWeb(3661)).toBe(formatHms(3661));
  });

  it('mergeMonotonicLiveMetrics never decreases cycles/tokens/calls', () => {
    const a = mergeMonotonicLiveMetrics(null, {
      passesPerFeature: { F1: 10, F2: 10 },
      passesTotal: 20,
      tokensIn: 1000,
      tokensOut: 500,
      llmCalls: 12,
    });
    expect(a.passesTotal).toBe(20);

    // Race: recomputed sum dips while usage aggregate is briefly stale.
    const b = mergeMonotonicLiveMetrics(a, {
      passesPerFeature: { F1: 9, F2: 10 },
      passesTotal: 19,
      tokensIn: 900,
      tokensOut: 400,
      llmCalls: 11,
    });
    expect(b.passesTotal).toBe(20);
    expect(b.tokensIn).toBe(1000);
    expect(b.tokensOut).toBe(500);
    expect(b.llmCalls).toBe(12);

    const c = mergeMonotonicLiveMetrics(b, {
      passesPerFeature: { F1: 11, F2: 10 },
      tokensIn: 1100,
      tokensOut: 600,
      llmCalls: 14,
    });
    expect(c.passesTotal).toBe(21);
    expect(c.tokensIn).toBe(1100);
    expect(c.tokensOut).toBe(600);
    expect(c.llmCalls).toBe(14);
  });

  it('prefers Σ passesPerFeature over a stale passesTotal field', () => {
    const m = mergeMonotonicLiveMetrics(null, {
      passesPerFeature: { F1: 2, F2: 3 },
      passesTotal: 99, // independently tracked — ignore when ppf present
      tokensIn: 0,
      tokensOut: 0,
      llmCalls: 0,
    });
    expect(m.passesTotal).toBe(5);
  });

  it('selectTier1StatusMetrics never leaks a prior run\'s stale metrics while running (SET 24a)', () => {
    const staleFromPriorRun = { tokensIn: 5000, tokensOut: 3000, llmCalls: 40 };
    const freshLive = { tokensIn: 0, tokensOut: 0, llmCalls: 0 };
    // A new run just started: `stored` is still the OLD run's finished result
    // (offscreen.js hasn't overwritten it yet), but running=true — the status
    // read must NOT surface the old run's numbers.
    expect(selectTier1StatusMetrics({ running: true, stored: staleFromPriorRun, live: freshLive }))
      .toEqual(freshLive);
    // Once the run finishes, the stored (final) result becomes authoritative again.
    expect(selectTier1StatusMetrics({ running: false, stored: staleFromPriorRun, live: freshLive }))
      .toEqual(staleFromPriorRun);
    // No prior run at all — stored is null while idle.
    expect(selectTier1StatusMetrics({ running: false, stored: null, live: freshLive })).toBeNull();
  });

  it('displayPipelineMessage normalizes substrate 0 tokens to (0 tok)', () => {
    expect(displayPipelineMessage('substrate: fixed — 0 tokens')).toBe('substrate: fixed — (0 tok)');
    expect(displayPipelineMessage('case rebound via clade (0 tok)')).toBe('case rebound via clade (0 tok)');
    expect(displayPipelineMessage('llm call (2,341→ / 1,876← tok)')).toBe('llm call (2,341→ / 1,876← tok)');
  });
});

describe('SET 29b tab-stub indicator helpers', () => {
  it('formatRunningTabTitle prefixes the spinner + minute count', () => {
    expect(formatRunningTabTitle(0)).toBe('⟳ 0m — RHA Wrapper');
    expect(formatRunningTabTitle(12)).toBe('⟳ 12m — RHA Wrapper');
    expect(formatRunningTabTitle(12, 'Custom Title')).toBe('⟳ 12m — Custom Title');
  });

  it('formatRunningTabTitle floors fractional/negative minutes at 0', () => {
    expect(formatRunningTabTitle(2.9)).toBe('⟳ 2m — RHA Wrapper');
    expect(formatRunningTabTitle(-5)).toBe('⟳ 0m — RHA Wrapper');
    expect(formatRunningTabTitle(Number.NaN)).toBe('⟳ 0m — RHA Wrapper');
  });

  it('elapsedMinutesSince computes whole minutes, floored at 0', () => {
    const start = new Date('2026-07-26T10:00:00.000Z').toISOString();
    expect(elapsedMinutesSince(start, Date.parse('2026-07-26T10:00:30.000Z'))).toBe(0);
    expect(elapsedMinutesSince(start, Date.parse('2026-07-26T10:01:00.000Z'))).toBe(1);
    expect(elapsedMinutesSince(start, Date.parse('2026-07-26T10:12:59.000Z'))).toBe(12);
    // Clock skew / bad input never goes negative.
    expect(elapsedMinutesSince(start, Date.parse('2026-07-26T09:59:00.000Z'))).toBe(0);
    expect(elapsedMinutesSince('not-a-date', Date.now())).toBe(0);
  });

  it('SPIN_FAVICON_FRAMES is a small, distinct set of inline SVG data URIs', () => {
    expect(SPIN_FAVICON_FRAMES.length).toBeGreaterThanOrEqual(2);
    expect(SPIN_FAVICON_FRAMES.length).toBeLessThanOrEqual(8);
    for (const frame of SPIN_FAVICON_FRAMES) {
      expect(frame.startsWith('data:image/svg+xml,')).toBe(true);
    }
    expect(new Set(SPIN_FAVICON_FRAMES).size).toBe(SPIN_FAVICON_FRAMES.length);
  });
});
