import { expect, test } from '@playwright/test';
import type { WorkerOperationRequest, WorkerOperationState } from '../../compute/core/src/api-contracts';
import {
  encodeSseFrame,
  explainReplacementReason,
  InMemoryOperationEventStream,
  InMemoryOperationQueue,
  InMemoryOperationStateStore,
  OperationOrchestrator,
  parseSseEventId,
  parseSsePayload,
  shouldReuseExistingOperation,
} from '../../compute/core/src/control-plane';

function buildPdfLayoutRequest(opKey: string): WorkerOperationRequest {
  return {
    kind: 'pdf_layout',
    opKey,
    payload: {
      documentId: `doc-${opKey}`,
      documentObjectKey: `s3://bucket/${opKey}.pdf`,
      namespace: null,
    },
  };
}

test.describe('compute control-plane', () => {
  test('in-memory queue and state store support enqueue/claim/CAS', async () => {
    const queue = new InMemoryOperationQueue();
    const store = new InMemoryOperationStateStore();

    await queue.enqueue({
      jobId: 'job-1',
      opId: 'op-1',
      opKey: 'k-1',
      kind: 'pdf_layout',
      queuedAt: 1000,
      payload: { documentId: 'd1', documentObjectKey: 'obj1', namespace: null },
    });
    await queue.enqueue({
      jobId: 'job-2',
      opId: 'op-2',
      opKey: 'k-2',
      kind: 'whisper_align',
      queuedAt: 1100,
      payload: { text: 'hello', audioObjectKey: 'obj2' },
    });

    expect(queue.size()).toBe(2);
    expect(queue.size('pdf_layout')).toBe(1);
    expect(queue.size('whisper_align')).toBe(1);

    const claimedLayout = await queue.claimNext('pdf_layout');
    expect(claimedLayout?.opId).toBe('op-1');
    expect(queue.size('pdf_layout')).toBe(0);

    const firstCas = await store.compareAndSetOpIndex({
      opKey: 'k-1',
      newOpId: 'op-1',
      expectedOpId: null,
    });
    const secondCas = await store.compareAndSetOpIndex({
      opKey: 'k-1',
      newOpId: 'op-2',
      expectedOpId: null,
    });

    expect(firstCas).toBeTruthy();
    expect(secondCas).toBeFalsy();
    expect(await store.getOpIndex('k-1')).toEqual({ opId: 'op-1' });
  });

  test('in-memory event stream replays from sinceEventId and streams live events', async () => {
    const stream = new InMemoryOperationEventStream();

    const queued: WorkerOperationState = {
      opId: 'op-1',
      opKey: 'k-1',
      kind: 'pdf_layout',
      jobId: 'job-1',
      status: 'queued',
      queuedAt: 1000,
      updatedAt: 1000,
    };
    const running: WorkerOperationState = {
      ...queued,
      status: 'running',
      startedAt: 1200,
      updatedAt: 1200,
    };
    const succeeded: WorkerOperationState = {
      ...running,
      status: 'succeeded',
      updatedAt: 1400,
      result: { ok: true },
    };

    await stream.append('op-1', queued);
    await stream.append('op-1', running);

    const receivedEventIds: number[] = [];
    const unsubscribe = await stream.subscribe({
      opId: 'op-1',
      sinceEventId: 1,
      onEvent: (event) => {
        receivedEventIds.push(event.eventId);
      },
    });

    await stream.append('op-1', succeeded);
    unsubscribe();

    expect(receivedEventIds).toEqual([2, 3]);
  });

  test('orchestrator reuses fresh inflight operations and replaces stale ones', async () => {
    let now = 1_000;
    let nextId = 1;
    const queue = new InMemoryOperationQueue();
    const stateStore = new InMemoryOperationStateStore();
    const eventStream = new InMemoryOperationEventStream();
    const orchestrator = new OperationOrchestrator({
      queue,
      stateStore,
      eventStream,
      config: { opStaleMs: 2_000, maxCasRetries: 5 },
      clock: { now: () => now },
      idFactory: {
        opId: () => `op-${nextId}`,
        jobId: () => `job-${nextId++}`,
      },
    });

    const request = buildPdfLayoutRequest('same-op-key');

    const first = await orchestrator.enqueueOrReuse(request);
    expect(first.opId).toBe('op-1');
    expect(queue.size('pdf_layout')).toBe(1);

    now = 2_000;
    const reused = await orchestrator.enqueueOrReuse(request);
    expect(reused.opId).toBe('op-1');
    expect(queue.size('pdf_layout')).toBe(1);

    await orchestrator.markRunning({ opId: first.opId, updatedAt: 2_100 });

    now = 6_000;
    const replaced = await orchestrator.enqueueOrReuse(request);
    expect(replaced.opId).toBe('op-2');
    expect(queue.size('pdf_layout')).toBe(2);
    expect(await stateStore.getOpIndex('same-op-key')).toEqual({ opId: 'op-2' });

    const op1Events = await eventStream.listSince('op-1', 0);
    const op2Events = await eventStream.listSince('op-2', 0);
    expect(op1Events.map((event) => event.eventId)).toEqual([1, 2]);
    expect(op2Events.map((event) => event.eventId)).toEqual([1]);
  });

  test('state-machine helpers return consistent reuse/replacement decisions', () => {
    const current: WorkerOperationState = {
      opId: 'op-1',
      opKey: 'same-op-key',
      kind: 'pdf_layout',
      jobId: 'job-1',
      status: 'running',
      queuedAt: 1000,
      updatedAt: 2000,
    };

    expect(shouldReuseExistingOperation({
      current,
      requestKind: 'pdf_layout',
      now: 2500,
      opStaleMs: 1_000,
    })).toBeTruthy();

    expect(shouldReuseExistingOperation({
      current,
      requestKind: 'pdf_layout',
      now: 4005,
      opStaleMs: 1_000,
    })).toBeFalsy();

    expect(explainReplacementReason({
      current,
      requestKind: 'pdf_layout',
      now: 4005,
      opStaleMs: 1_000,
    })).toBe('stale_running');
  });

  test('sse helpers encode and decode frame payload and id', () => {
    const frame = encodeSseFrame({
      id: 42,
      event: 'snapshot',
      data: { ok: true },
    });
    expect(parseSseEventId(frame)).toBe(42);
    expect(parseSsePayload(frame)).toBe('{"ok":true}');
  });
});
