/*
 * Priority Forge — V4.2 lifecycle-synthesis regression tests.
 *
 * When completeTask is called on a task that was never transitioned through
 * in_progress (callers skipping the explicit hop), the storage layer should
 * synthesize the missing selection event so the ML pipeline still gets a
 * signal. This file pins that behavior.
 *
 * Run via:  npm test -- tests/lifecycle-synthesis.test.ts
 */

import { describe, it, expect, afterEach } from 'vitest';
import { JsonStorage } from '../src/storage/jsonStorage';

const instances: JsonStorage[] = [];
const tasksToCleanup: Array<{ storage: JsonStorage; id: string }> = [];

afterEach(async () => {
  for (const { storage, id } of tasksToCleanup.splice(0)) {
    try {
      await storage.deleteTask(id);
    } catch {
      /* fine — task may already be gone */
    }
  }
  for (const s of instances.splice(0)) {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (s as any).stopFileWatcher?.();
  }
});

function makeStorage(): JsonStorage {
  const s = new JsonStorage();
  instances.push(s);
  return s;
}

describe('V4.2 — synthesized selection on direct not_started → completed', () => {
  it('completeTask on a task with no startedAt sets synthesizedSelection=true', async () => {
    const s = makeStorage();
    const task = await s.createTask({
      task: 'V4.2 test: synthesized direct completion',
      priority: 'P3',
      project: 'priority-forge',
      effort: 'low',
    });
    tasksToCleanup.push({ storage: s, id: task.id });

    const record = await s.completeTask(task.id, 'completed');
    expect(record).not.toBeNull();
    expect(record!.synthesizedSelection).toBe(true);
    // startedAt is synthesized to task.createdAt — both should match
    expect(record!.startedAt).toBe(task.createdAt);
    // Synthesized workTime is computed from createdAt → completedAt; since
    // the test runs in milliseconds, rounded to 2 decimals it's 0.
    expect(record!.actualWorkTime).toBeDefined();
    expect(record!.actualWorkTime).toBeGreaterThanOrEqual(0);
  });

  it('completeTask after explicit in_progress does NOT mark synthesizedSelection', async () => {
    const s = makeStorage();
    const task = await s.createTask({
      task: 'V4.2 test: explicit in_progress completion',
      priority: 'P3',
      project: 'priority-forge',
      effort: 'low',
    });
    tasksToCleanup.push({ storage: s, id: task.id });

    // Walk the lifecycle properly
    const updated = await s.updateTask(task.id, { status: 'in_progress' });
    expect(updated).not.toBeNull();
    expect(updated!.startedAt).toBeDefined();
    const realStartedAt = updated!.startedAt;

    // Brief delay so workTime is nonzero (not strictly required but realistic)
    await new Promise((r) => setTimeout(r, 5));

    const record = await s.completeTask(task.id, 'completed');
    expect(record).not.toBeNull();
    expect(record!.synthesizedSelection).toBeUndefined();
    expect(record!.startedAt).toBe(realStartedAt);
  });

  it('synthesized completion fires a TaskSelectionEvent (the ML signal we were missing)', async () => {
    const s = makeStorage();
    const before = (s as unknown as { globalML: { taskSelectionEvents: unknown[] } })
      .globalML.taskSelectionEvents.length;

    const task = await s.createTask({
      task: 'V4.2 test: ensure selection event fires',
      priority: 'P3',
      project: 'priority-forge',
      effort: 'low',
    });
    tasksToCleanup.push({ storage: s, id: task.id });

    await s.completeTask(task.id, 'completed');

    const after = (s as unknown as { globalML: { taskSelectionEvents: unknown[] } })
      .globalML.taskSelectionEvents.length;

    // Synthesizing the missing in_progress hop should have appended exactly
    // one selection event — the same one that updateTask({status:in_progress})
    // would have fired.
    expect(after - before).toBe(1);
  });

  it('cancelled / deferred outcomes also get synthesized when called direct', async () => {
    const s = makeStorage();
    const task = await s.createTask({
      task: 'V4.2 test: cancelled outcome via direct completion',
      priority: 'P3',
      project: 'priority-forge',
      effort: 'low',
    });
    tasksToCleanup.push({ storage: s, id: task.id });

    const record = await s.completeTask(task.id, 'cancelled');
    expect(record).not.toBeNull();
    expect(record!.outcome).toBe('cancelled');
    // Cancelled / deferred are still completions for ML purposes — synthesis
    // applies just the same.
    expect(record!.synthesizedSelection).toBe(true);
    expect(record!.startedAt).toBe(task.createdAt);
  });
});
