import { describe, expect, it } from 'vitest';
import type {
  AttachConversation,
  ConversationSnapshot,
  DetachConversation,
  HarnessActorContext,
  HarnessConversationService,
  HarnessEventEnvelope,
  HarnessSelection,
  SendHarnessTurn,
  TurnReceipt,
} from '@mosaicstack/types';
import { HarnessChatRuntime } from './harness-chat.runtime.js';

/**
 * Task Five, Step One (harness runtime). Proves the `pi-rpc` runtime executes
 * exclusively through the {@link HarnessConversationService} RPC boundary and
 * forwards the caller's exact selection tuple and idempotency key without
 * substitution. Red-first: the runtime is an unimplemented stub, so every
 * delegation assertion fails until Step Three.
 */

const context: HarnessActorContext = {
  actorId: 'actor-1',
  tenantId: 'tenant-1',
  seatId: 'seat-1',
  correlationId: 'corr-1',
};

const selection: HarnessSelection = {
  harnessId: 'pi',
  providerId: 'anthropic',
  modelId: 'claude-opus-4-8',
};

const conversationId = '11111111-1111-4111-8111-111111111111';
const idempotencyKey = '22222222-2222-4222-8222-222222222222';

const sendInput: SendHarnessTurn & { idempotencyKey: string } = {
  context,
  conversationId,
  selection,
  turnId: 'turn-abc',
  correlationId: 'corr-1',
  content: 'hello',
  idempotencyKey,
};

const attachInput: AttachConversation & { afterSequence?: number } = {
  context,
  conversationId,
  clientId: 'client-1',
  selection,
  afterSequence: 0,
};

const detachInput: DetachConversation = {
  context,
  conversationId,
  clientId: 'client-1',
};

interface RecordedCalls {
  attach: (AttachConversation & { afterSequence?: number })[];
  detach: DetachConversation[];
  send: (SendHarnessTurn & { idempotencyKey: string })[];
  subscribeFrom: { conversationId: string; afterSequence: number }[];
}

const snapshot: ConversationSnapshot = {
  session: {
    conversationId,
    nativeSessionId: 'native-1',
    seatId: 'seat-1',
    selection,
    state: 'idle',
    attachedClientIds: ['client-1'],
  },
  lastSequence: 0,
  replay: [],
};

function build(): { runtime: HarnessChatRuntime; calls: RecordedCalls } {
  const calls: RecordedCalls = { attach: [], detach: [], send: [], subscribeFrom: [] };
  const service: HarnessConversationService = {
    attach: (input) => {
      calls.attach.push(input);
      return Promise.resolve(snapshot);
    },
    detach: (input) => {
      calls.detach.push(input);
      return Promise.resolve();
    },
    send: (input) => {
      calls.send.push(input);
      // The service echoes only the requested tuple; there is no representable substitute.
      const receipt: TurnReceipt = {
        conversationId: input.conversationId,
        turnId: 'turn-server',
        correlationId: input.correlationId,
        state: 'accepted',
        selection: input.selection,
      };
      return Promise.resolve(receipt);
    },
    subscribeFrom: (id, afterSequence) => {
      calls.subscribeFrom.push({ conversationId: id, afterSequence });

      return (async function* (): AsyncIterable<HarnessEventEnvelope> {
        return;
      })();
    },
  };
  return { runtime: new HarnessChatRuntime(service), calls };
}

describe('HarnessChatRuntime', () => {
  it('is the harness runtime kind and needs only a HarnessConversationService', () => {
    const { runtime } = build();
    expect(runtime.kind).toBe('harness');
  });

  it('delegates send to the conversation service with the exact tuple and idempotency key', async () => {
    const { runtime, calls } = build();

    const receipt = await runtime.send(sendInput);

    expect(calls.send).toHaveLength(1);
    const firstSend = calls.send[0]!;
    expect(firstSend).toEqual(sendInput);
    expect(firstSend.idempotencyKey).toBe(idempotencyKey);
    expect(firstSend.selection).toEqual(selection);
    // The runtime must not substitute an effective tuple onto the receipt.
    expect(receipt.selection).toEqual(selection);
  });

  it('delegates attach to the conversation service and returns its snapshot', async () => {
    const { runtime, calls } = build();

    const result = await runtime.attach(attachInput);

    expect(calls.attach).toHaveLength(1);
    expect(calls.attach[0]).toEqual(attachInput);
    expect(result).toBe(snapshot);
  });

  it('delegates detach to the conversation service', async () => {
    const { runtime, calls } = build();

    await runtime.detach(detachInput);

    expect(calls.detach).toHaveLength(1);
    expect(calls.detach[0]).toEqual(detachInput);
  });

  it('delegates subscribeFrom to the conversation service journal replay', async () => {
    const { runtime, calls } = build();

    const iterable = runtime.subscribeFrom(conversationId, 7);
    // Drain to prove it is the service-backed async iterable, not a fabricated one.
    const drained: unknown[] = [];
    for await (const event of iterable) {
      drained.push(event);
    }
    expect(drained).toHaveLength(0);

    expect(calls.subscribeFrom).toHaveLength(1);
    expect(calls.subscribeFrom[0]).toEqual({ conversationId, afterSequence: 7 });
  });
});
