/*
 * Tests for src/orchestration/shared-tasks.ts — verifies that the
 * bridge module emits the expected memory-plane observations after
 * each successful orchestration mutation.
 *
 * The bridge had a subtle gap when first shipped (Plan 2): it called
 * storage methods directly, bypassing syncEmitter, and didn't
 * explicitly emit shared_task_* TeamMemoryEvents. Result: the activity
 * feed only saw `task_completed` (the one event handler.ts fires) per
 * cross-forge cycle — the rest of the lifecycle was invisible.
 *
 * Fixed in 3816bfa. This test pins that contract so future refactors
 * can't silently regress it.
 *
 * Strategy: mock the four PF singletons the bridge depends on
 * (storage, syncEmitter, getMemoryHub, getCurrentIntegration), wire
 * recorders behind them, exercise each bridge function, assert the
 * right events fired. No real DB or network.
 */

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { EventEmitter } from 'events';
import type { SharedTask, TeamMemoryEvent } from 'priority-forge-hub';

// vi.hoisted lets the mock factories below reference these. Without
// hoisted, the mock factory captures the var BEFORE it's initialized
// because vi.mock is itself hoisted to the top of the module.
const recorders = vi.hoisted(() => {
  return {
    memoryWrites: [] as TeamMemoryEvent[],
    syncEmits: [] as Array<{ method: string; args: unknown[] }>,
    storageCalls: [] as Array<{ method: string; args: unknown[] }>,
    orchCalls: [] as Array<{ method: string; args: unknown[] }>,
  };
});

const makeSharedTask = (overrides: Partial<SharedTask> = {}): SharedTask => ({
  id: 'st_test_001',
  teamId: 'test-team',
  title: 'Test SharedTask',
  description: 'For bridge emission tests',
  originatorAgentId: 'test@agent',
  originatorTeamId: 'test-team',
  createdAt: '2026-05-21T00:00:00.000Z',
  currentClaim: null,
  claimHistory: [],
  status: 'claimable',
  ...overrides,
});

const makeClaimInfo = () => ({
  holderAgentId: 'test@agent',
  holderTeamId: 'test-team',
  claimedAt: '2026-05-21T00:01:00.000Z',
  leaseUntil: '2026-05-21T00:31:00.000Z',
});

// Mock the priority-forge-hub package — override config + integration
// + memory-hub getters while keeping all real types/utilities.
vi.mock('priority-forge-hub', async () => {
  const actual = await vi.importActual<typeof import('priority-forge-hub')>(
    'priority-forge-hub',
  );
  return {
    ...actual,
    loadMemoryHubConfig: () => ({
      enabled: true,
      backend: 'pgvector' as const,
      teamId: 'test-team',
      agentId: 'test@agent',
      syncIntervalMs: 60000,
      xmem: { baseUrl: '', apiKey: '', orgID: '', environment: 'production' as const },
      pgvector: { connectionString: '', agentToken: '' },
    }),
    getMemoryHub: () => ({
      backend: 'pgvector' as const,
      write: vi.fn(async (event: TeamMemoryEvent) => {
        recorders.memoryWrites.push(event);
        return { accepted: true, remoteId: 'mock' };
      }),
      read: vi.fn(async () => []),
      search: vi.fn(async () => []),
      blockersFor: vi.fn(async () => []),
      health: vi.fn(async () => ({ ok: true, backend: 'pgvector' as const })),
    }),
    getCurrentIntegration: () => ({
      reader: {} as never,
      rederive: async () => undefined,
      orchestrationHub: {
        supportsOrchestration: true,
        create: vi.fn(async (input: { title: string; description?: string }) => {
          recorders.orchCalls.push({ method: 'create', args: [input] });
          return makeSharedTask({ title: input.title, description: input.description });
        }),
        claim: vi.fn(async (sharedTaskId: string, agentId: string, agentTeamId: string) => {
          recorders.orchCalls.push({ method: 'claim', args: [sharedTaskId, agentId, agentTeamId] });
          const claim = makeClaimInfo();
          return {
            ok: true as const,
            sharedTask: makeSharedTask({
              id: sharedTaskId,
              currentClaim: claim,
              status: 'claimed',
              claimHistory: [
                { event: 'claimed', agentId, agentTeamId, at: claim.claimedAt },
              ],
            }),
            claim,
          };
        }),
        release: vi.fn(async (sharedTaskId: string, agentId: string, outcome: string) => {
          recorders.orchCalls.push({ method: 'release', args: [sharedTaskId, agentId, outcome] });
          return {
            ok: true,
            sharedTask: makeSharedTask({
              id: sharedTaskId,
              currentClaim: null,
              status: outcome === 'completed' ? 'completed' : 'claimable',
            }),
          };
        }),
        renewLease: vi.fn(async (sharedTaskId: string, agentId: string) => {
          recorders.orchCalls.push({ method: 'renewLease', args: [sharedTaskId, agentId] });
          return { ok: true, claim: makeClaimInfo() };
        }),
        get: vi.fn(async () => null),
        list: vi.fn(async () => []),
        getMyClaims: vi.fn(async () => []),
      },
      detach: () => undefined,
    }),
  };
});

// Mock PF's storage singleton — return shapes the bridge uses.
vi.mock('../src/storage/jsonStorage', () => {
  return {
    storage: {
      getCurrentWorkspaceId: vi.fn(async () => 'test-workspace'),
      getTask: vi.fn(async (id: string) => {
        recorders.storageCalls.push({ method: 'getTask', args: [id] });
        return {
          id,
          task: 'Local task content',
          notes: 'Some notes',
          priority: 'P2',
          status: 'in_progress',
          project: 'shared-work',
          version: 1,
        };
      }),
      getTaskBySharedTaskId: vi.fn(async (sid: string) => {
        recorders.storageCalls.push({ method: 'getTaskBySharedTaskId', args: [sid] });
        return {
          id: 'local-task-1',
          task: 'Projection',
          status: 'in_progress',
          sharedTaskId: sid,
          notes: '',
          priority: 'P2',
          project: 'shared-work',
        };
      }),
      createTask: vi.fn(async (input: unknown) => {
        recorders.storageCalls.push({ method: 'createTask', args: [input] });
        return {
          id: 'local-task-fresh',
          task: 'Materialized',
          status: 'not_started',
          priority: 'P2',
          project: 'shared-work',
          version: 1,
          ...(typeof input === 'object' && input ? input : {}),
        };
      }),
      claimTask: vi.fn(async (id: string, agentId: string) => {
        recorders.storageCalls.push({ method: 'claimTask', args: [id, agentId] });
        return null;
      }),
      completeTask: vi.fn(async (id: string, outcome: string) => {
        recorders.storageCalls.push({ method: 'completeTask', args: [id, outcome] });
        return null;
      }),
      linkSharedTask: vi.fn(async (localId: string, sharedId: string) => {
        recorders.storageCalls.push({ method: 'linkSharedTask', args: [localId, sharedId] });
        return {
          id: localId,
          task: 'Linked',
          status: 'in_progress',
          priority: 'P2',
          project: 'shared-work',
          sharedTaskId: sharedId,
        };
      }),
      updateTask: vi.fn(async () => null),
      getTasks: vi.fn(async () => []),
    },
  };
});

// Mock PF's syncEmitter — record every emit* call.
vi.mock('../src/sync', () => {
  const emitter = new EventEmitter() as EventEmitter & {
    emitTaskCreated: (task: Record<string, unknown>) => void;
    emitTaskSelected: (task: Record<string, unknown>) => void;
    emitTaskCompleted: (task: Record<string, unknown>, outcome: string) => void;
    emitTaskUpdated: (id: string, changes: Record<string, unknown>, prev: Record<string, unknown>) => void;
  };
  emitter.emitTaskCreated = (task) => {
    recorders.syncEmits.push({ method: 'emitTaskCreated', args: [task] });
  };
  emitter.emitTaskSelected = (task) => {
    recorders.syncEmits.push({ method: 'emitTaskSelected', args: [task] });
  };
  emitter.emitTaskCompleted = (task, outcome) => {
    recorders.syncEmits.push({ method: 'emitTaskCompleted', args: [task, outcome] });
  };
  emitter.emitTaskUpdated = (id, changes, prev) => {
    recorders.syncEmits.push({ method: 'emitTaskUpdated', args: [id, changes, prev] });
  };
  return { syncEmitter: emitter };
});

// Import the module under test AFTER the mocks are set up.
import {
  claimSharedTask,
  createSharedTask,
  publishSharedTask,
  releaseSharedTask,
  renewSharedTaskLease,
  autoReleaseOnLocalComplete,
} from '../src/orchestration/shared-tasks';

beforeEach(() => {
  recorders.memoryWrites.length = 0;
  recorders.syncEmits.length = 0;
  recorders.storageCalls.length = 0;
  recorders.orchCalls.length = 0;
});

afterEach(() => {
  vi.clearAllMocks();
});

describe('orchestration bridge — emits shared_task_* observations to memory plane', () => {
  it('createSharedTask emits shared_task_published', async () => {
    const result = await createSharedTask({ title: 'created via bridge' });
    expect(result.sharedTask.title).toBe('created via bridge');

    const sharedTaskEvents = recorders.memoryWrites.filter((e) =>
      e.eventType.startsWith('shared_task_'),
    );
    expect(sharedTaskEvents).toHaveLength(1);
    expect(sharedTaskEvents[0].eventType).toBe('shared_task_published');
    expect(sharedTaskEvents[0].agentId).toBe('test@agent');
    expect(sharedTaskEvents[0].teamId).toBe('test-team');
    expect(sharedTaskEvents[0].summary).toContain('published');
  });

  it('claimSharedTask emits shared_task_claimed + task_created + task_selected', async () => {
    const result = await claimSharedTask('st_test_001', { priority: 'P1' });
    expect(result.ok).toBe(true);

    const sharedTaskEvents = recorders.memoryWrites.filter((e) =>
      e.eventType.startsWith('shared_task_'),
    );
    expect(sharedTaskEvents).toHaveLength(1);
    expect(sharedTaskEvents[0].eventType).toBe('shared_task_claimed');

    const syncMethods = recorders.syncEmits.map((s) => s.method);
    expect(syncMethods).toContain('emitTaskCreated');
    expect(syncMethods).toContain('emitTaskSelected');
  });

  it('releaseSharedTask with outcome=completed emits shared_task_completed + task_completed', async () => {
    await releaseSharedTask('st_test_001', 'completed');

    const sharedTaskEvents = recorders.memoryWrites.filter((e) =>
      e.eventType.startsWith('shared_task_'),
    );
    expect(sharedTaskEvents).toHaveLength(1);
    expect(sharedTaskEvents[0].eventType).toBe('shared_task_completed');

    const syncMethods = recorders.syncEmits.map((s) => s.method);
    expect(syncMethods).toContain('emitTaskCompleted');
  });

  it('releaseSharedTask with outcome=deferred emits shared_task_released (not completed)', async () => {
    await releaseSharedTask('st_test_001', 'deferred');

    const sharedTaskEvents = recorders.memoryWrites.filter((e) =>
      e.eventType.startsWith('shared_task_'),
    );
    expect(sharedTaskEvents).toHaveLength(1);
    expect(sharedTaskEvents[0].eventType).toBe('shared_task_released');
  });

  it('autoReleaseOnLocalComplete with PF outcome=completed emits shared_task_completed', async () => {
    await autoReleaseOnLocalComplete('st_test_001', 'completed');

    const sharedTaskEvents = recorders.memoryWrites.filter((e) =>
      e.eventType.startsWith('shared_task_'),
    );
    expect(sharedTaskEvents).toHaveLength(1);
    expect(sharedTaskEvents[0].eventType).toBe('shared_task_completed');
    expect(sharedTaskEvents[0].payload.autoReleased).toBe(true);
  });

  it('renewSharedTaskLease emits shared_task_lease_renewed', async () => {
    await renewSharedTaskLease('st_test_001');

    const sharedTaskEvents = recorders.memoryWrites.filter((e) =>
      e.eventType.startsWith('shared_task_'),
    );
    expect(sharedTaskEvents).toHaveLength(1);
    expect(sharedTaskEvents[0].eventType).toBe('shared_task_lease_renewed');
  });

  it('publishSharedTask emits shared_task_published + shared_task_claimed + emitTaskUpdated', async () => {
    const result = await publishSharedTask('local-task-1');
    expect(result.sharedTask.id).toBeDefined();

    const sharedTaskEvents = recorders.memoryWrites
      .filter((e) => e.eventType.startsWith('shared_task_'))
      .map((e) => e.eventType);
    expect(sharedTaskEvents).toContain('shared_task_published');
    expect(sharedTaskEvents).toContain('shared_task_claimed');

    const syncMethods = recorders.syncEmits.map((s) => s.method);
    expect(syncMethods).toContain('emitTaskUpdated');
  });
});

describe('orchestration bridge — full lifecycle observation count', () => {
  it('a complete create→claim→release cycle emits >= 6 lifecycle events on the memory plane', async () => {
    // Mirror the live smoke we ran post-bridge-patch: a cycle should
    // produce shared_task_published + shared_task_claimed +
    // task_created + task_selected + shared_task_completed
    // (5 from the bridge) + task_completed (which handler.ts emits in
    // the real flow but the bridge's releaseSharedTask also emits via
    // emitTaskCompleted on the archival path) = 6 in total.
    await createSharedTask({ title: 'lifecycle test' });
    await claimSharedTask('st_test_001');
    await releaseSharedTask('st_test_001', 'completed');

    const allEvents = [
      ...recorders.memoryWrites.map((e) => `mem:${e.eventType}`),
      ...recorders.syncEmits.map((s) => `sync:${s.method}`),
    ];

    // Expected emissions (set membership, not strict ordering):
    expect(allEvents).toContain('mem:shared_task_published');
    expect(allEvents).toContain('mem:shared_task_claimed');
    expect(allEvents).toContain('mem:shared_task_completed');
    expect(allEvents).toContain('sync:emitTaskCreated');
    expect(allEvents).toContain('sync:emitTaskSelected');
    expect(allEvents).toContain('sync:emitTaskCompleted');

    // Total >= 6 — protect against silent regressions where one of the
    // emit calls gets removed without breaking the others.
    expect(allEvents.length).toBeGreaterThanOrEqual(6);
  });
});
