/*
 * Hub-boundary regression test (root CLAUDE.md "Hub boundary").
 *
 * The in-process 'sync_event' bus carries a rich task object, but the memory
 * plane must persist OBSERVATION METADATA only. This pins the redaction at the
 * hub-intake seam (projectMemoryPayload) so a future change to the emitter or
 * subscriber can't silently leak task content / learning signals into the hub.
 *
 * If you intentionally change what the memory plane carries, update both this
 * test AND the boundary section of CLAUDE.md in the same change.
 */

import { describe, expect, it } from 'vitest';
import { projectMemoryPayload } from '../subscriber';

// Fields that must NEVER appear in a memory-plane payload, regardless of event.
const FORBIDDEN = [
  'priority',      // learning signal (priority changes) + content
  'effort',        // content
  'notes',         // content
  'body',          // content
  'deadline',      // content
  'actualWorkTime',// learning signal (work duration)
  'changes',       // raw new values (content)
  'previousValues',// the entire prior task (content)
  'weights',       // learning signal
  'priorityScore', // learning signal
];

// Rich payloads shaped exactly like src/sync/event-emitter.ts emits them.
const richCreated = {
  taskId: 'TASK-1', task: 'Fix the auth bug', project: 'builds',
  priority: 'P0', status: 'not_started', effort: 'high',
  blocking: 'TASK-9', dependencies: ['TASK-2'],
};
const richSelected = {
  taskId: 'TASK-1', task: 'Fix the auth bug', project: 'builds',
  priority: 'P0', status: 'in_progress', startedAt: '2026-05-21T14:05:00Z',
  effort: 'high', blocking: 'TASK-9', dependencies: ['TASK-2'],
};
const richCompleted = {
  taskId: 'TASK-1', task: 'Fix the auth bug', project: 'builds',
  priority: 'P0', outcome: 'completed', completedAt: '2026-05-21T17:00:00Z',
  startedAt: '2026-05-21T14:05:00Z', actualWorkTime: 2.9,
};
const richUpdated = {
  taskId: 'TASK-1',
  changes: { priority: 'P1', notes: 'secret internal detail', effort: 'low' },
  previousValues: {
    taskId: 'TASK-1', task: 'Fix the auth bug', project: 'builds',
    priority: 'P0', notes: 'older secret', body: 'long body', deadline: '2026-06-01',
  },
  project: 'builds',
};

describe('projectMemoryPayload — hub boundary redaction', () => {
  it('never leaks forbidden fields for any event type', () => {
    const cases: Array<[string, Record<string, unknown>]> = [
      ['task_created', richCreated],
      ['task_selected', richSelected],
      ['task_completed', richCompleted],
      ['task_updated', richUpdated],
    ];
    for (const [type, data] of cases) {
      const out = projectMemoryPayload(type, data);
      for (const f of FORBIDDEN) {
        expect(out, `${type} leaked "${f}"`).not.toHaveProperty(f);
      }
    }
  });

  it('keeps the observation metadata the activity feed + current-focus need', () => {
    const created = projectMemoryPayload('task_created', richCreated);
    expect(created).toMatchObject({ taskId: 'TASK-1', task: 'Fix the auth bug', project: 'builds', status: 'not_started' });
    expect(created).toMatchObject({ blocking: 'TASK-9', dependencies: ['TASK-2'] });

    // current-focus reads taskId, task, project, startedAt.
    const selected = projectMemoryPayload('task_selected', richSelected);
    expect(selected).toMatchObject({ taskId: 'TASK-1', task: 'Fix the auth bug', project: 'builds', startedAt: '2026-05-21T14:05:00Z' });

    const completed = projectMemoryPayload('task_completed', richCompleted);
    expect(completed).toMatchObject({ taskId: 'TASK-1', outcome: 'completed' });
  });

  it('task_updated carries only changed-field NAMES, never values or the prior task', () => {
    const out = projectMemoryPayload('task_updated', richUpdated);
    expect(out.changedFields).toEqual(['priority', 'notes', 'effort']);
    expect(out).not.toHaveProperty('changes');
    expect(out).not.toHaveProperty('previousValues');
    expect(JSON.stringify(out)).not.toContain('secret');
    expect(JSON.stringify(out)).not.toContain('long body');
  });

  it('derived_blocker carries only edge identity, no content', () => {
    const out = projectMemoryPayload('derived_blocker', {
      blockedTaskId: 'TASK-3', blockedAgentId: 'bob', blockingTaskId: 'TASK-1',
      blockingAgentId: 'alice', status: 'active', observedAt: '2026-05-21T14:00:00Z',
      task: 'should not be here', priority: 'P0',
    });
    expect(out).toMatchObject({ blockingTaskId: 'TASK-1', blockedTaskId: 'TASK-3', status: 'active' });
    expect(out).not.toHaveProperty('task');
    expect(out).not.toHaveProperty('priority');
  });

  it('unknown event types fall back to the minimal id+project shape', () => {
    const out = projectMemoryPayload('some_future_event', { taskId: 'X', project: 'p', priority: 'P0', notes: 'leak' });
    expect(out).toEqual({ taskId: 'X', project: 'p' });
  });
});
