/*
 * Tests for the pure workspace-freshness computation.
 * Run via: npm test -- tests/workspaceFreshness.test.ts
 */

import { describe, it, expect } from 'vitest';
import { computeFreshness } from '../src/utils/workspaceFreshness';
import {
  DEFAULT_WORKSPACE_FRESHNESS_THRESHOLDS,
  type WorkspaceFreshnessThresholds,
} from '../src/types/schema';
import type { WeightedTask } from '../src/types/schema';

const now = new Date('2026-05-12T12:00:00Z');

function makeTask(opts: Partial<WeightedTask>): WeightedTask {
  // If only one of createdAt / updatedAt is provided, mirror it onto the
  // other so the test's intended "last touch" actually matches max(both).
  // Otherwise default both to test-fixed `now`.
  const createdAt = opts.createdAt || opts.updatedAt || now.toISOString();
  const updatedAt = opts.updatedAt || opts.createdAt || now.toISOString();
  const base: WeightedTask = {
    id: opts.id || 'T-' + Math.random().toString(36).slice(2, 8),
    priority: opts.priority || 'P1',
    task: opts.task || 'test task',
    project: opts.project || 'test',
    status: opts.status || 'not_started',
    createdAt,
    updatedAt,
    priorityScore: opts.priorityScore ?? 0,
    weights: opts.weights || {
      blockingCount: 0,
      crossProjectImpact: 0,
      timeSensitivity: 0,
      effortValueRatio: 0,
      dependencyDepth: 0,
    },
    deadline: opts.deadline,
    effort: opts.effort,
  };
  return base;
}

function daysAgo(d: number): string {
  return new Date(now.getTime() - d * 86400 * 1000).toISOString();
}

function daysAhead(d: number): string {
  return new Date(now.getTime() + d * 86400 * 1000).toISOString();
}

describe('computeFreshness', () => {
  it('empty workspace is dormant with infinite daysSinceTouch', () => {
    const f = computeFreshness([], DEFAULT_WORKSPACE_FRESHNESS_THRESHOLDS, now);
    expect(f.dormant).toBe(true);
    expect(f.active).toBe(false);
    expect(f.activeReason).toBeNull();
    expect(f.openTaskCount).toBe(0);
    expect(f.futureDeadlineCount).toBe(0);
    expect(f.lastTouchedAt).toBeNull();
    expect(f.daysSinceTouch).toBe(Number.POSITIVE_INFINITY);
    expect(f.freshnessScore).toBe(0);
  });

  it('recent task makes workspace active via interaction', () => {
    const tasks = [makeTask({ updatedAt: daysAgo(2), status: 'not_started' })];
    const f = computeFreshness(tasks, DEFAULT_WORKSPACE_FRESHNESS_THRESHOLDS, now);
    expect(f.active).toBe(true);
    expect(f.activeReason).toBe('recent_interaction');
    expect(f.daysSinceTouch).toBeCloseTo(2, 1);
  });

  it('old task with no future deadline → dormant', () => {
    const tasks = [
      makeTask({
        createdAt: daysAgo(30),
        updatedAt: daysAgo(30),
        status: 'not_started',
      }),
    ];
    const f = computeFreshness(tasks, DEFAULT_WORKSPACE_FRESHNESS_THRESHOLDS, now);
    expect(f.dormant).toBe(true);
    expect(f.activeReason).toBeNull();
  });

  it('old task with future deadline → active via future_deadline', () => {
    const tasks = [
      makeTask({
        createdAt: daysAgo(60),
        updatedAt: daysAgo(60),
        deadline: daysAhead(10),
        status: 'not_started',
      }),
    ];
    const f = computeFreshness(tasks, DEFAULT_WORKSPACE_FRESHNESS_THRESHOLDS, now);
    expect(f.active).toBe(true);
    expect(f.activeReason).toBe('future_deadline');
    expect(f.futureDeadlineCount).toBe(1);
  });

  it('completed tasks do not count toward openTaskCount or futureDeadlineCount', () => {
    const tasks = [
      makeTask({
        status: 'completed',
        deadline: daysAhead(5),
        updatedAt: daysAgo(2),
      }),
    ];
    const f = computeFreshness(tasks, DEFAULT_WORKSPACE_FRESHNESS_THRESHOLDS, now);
    expect(f.openTaskCount).toBe(0);
    expect(f.futureDeadlineCount).toBe(0);
    // But the completed task's recent updatedAt still keeps the workspace active
    // via interaction — that's intentional (you just touched it).
    expect(f.active).toBe(true);
    expect(f.activeReason).toBe('recent_interaction');
  });

  it('future deadline beyond horizon does not count', () => {
    const tasks = [
      makeTask({
        createdAt: daysAgo(30),
        updatedAt: daysAgo(30),
        deadline: daysAhead(90), // > 60d horizon
      }),
    ];
    const f = computeFreshness(tasks, DEFAULT_WORKSPACE_FRESHNESS_THRESHOLDS, now);
    expect(f.futureDeadlineCount).toBe(0);
    expect(f.dormant).toBe(true);
  });

  it('past deadline does not count as future work', () => {
    const tasks = [
      makeTask({
        createdAt: daysAgo(30),
        updatedAt: daysAgo(30),
        deadline: daysAgo(5),
      }),
    ];
    const f = computeFreshness(tasks, DEFAULT_WORKSPACE_FRESHNESS_THRESHOLDS, now);
    expect(f.futureDeadlineCount).toBe(0);
  });

  it('freshness decays exponentially with τ', () => {
    const t: WorkspaceFreshnessThresholds = {
      interactionWindowDays: 14,
      futureDeadlineHorizonDays: 60,
      decayTauDays: 7,
    };
    // One interaction at exactly τ=7 days ago contributes e^-1 ≈ 0.3679
    const tasks = [makeTask({ createdAt: daysAgo(7), updatedAt: daysAgo(7) })];
    const f = computeFreshness(tasks, t, now);
    // Two interactions (createdAt + updatedAt) at same instant → 2 × e^-1
    expect(f.freshnessScore).toBeCloseTo(2 * Math.exp(-1), 3);
  });

  it('many recent interactions stack (Ebbinghaus rehearsal)', () => {
    const tasks = [
      makeTask({ createdAt: daysAgo(0.1) }),
      makeTask({ createdAt: daysAgo(0.5) }),
      makeTask({ createdAt: daysAgo(1) }),
    ];
    const f = computeFreshness(tasks, DEFAULT_WORKSPACE_FRESHNESS_THRESHOLDS, now);
    expect(f.freshnessScore).toBeGreaterThan(3); // each contributes ~1 (very recent)
    expect(f.active).toBe(true);
  });

  it('interaction exactly at the cutoff window is still active', () => {
    const tasks = [makeTask({ updatedAt: daysAgo(14) })];
    const f = computeFreshness(tasks, DEFAULT_WORKSPACE_FRESHNESS_THRESHOLDS, now);
    // Boundary: exactly 14d ago is on the edge — treat as still active.
    expect(f.active).toBe(true);
  });

  it('15 days ago is dormant (just past the cutoff)', () => {
    const tasks = [makeTask({ updatedAt: daysAgo(15) })];
    const f = computeFreshness(tasks, DEFAULT_WORKSPACE_FRESHNESS_THRESHOLDS, now);
    expect(f.active).toBe(false);
    expect(f.dormant).toBe(true);
  });

  it('recent_interaction wins when both rules apply', () => {
    const tasks = [
      makeTask({
        updatedAt: daysAgo(1),
        deadline: daysAhead(10),
      }),
    ];
    const f = computeFreshness(tasks, DEFAULT_WORKSPACE_FRESHNESS_THRESHOLDS, now);
    expect(f.active).toBe(true);
    expect(f.activeReason).toBe('recent_interaction');
  });

  it('malformed timestamps are ignored, not thrown', () => {
    const tasks = [
      makeTask({ createdAt: 'not-a-date', updatedAt: 'also-bogus' }),
      makeTask({ createdAt: daysAgo(2) }),
    ];
    const f = computeFreshness(tasks, DEFAULT_WORKSPACE_FRESHNESS_THRESHOLDS, now);
    expect(f.active).toBe(true);
    expect(f.openTaskCount).toBe(2);
  });
});
