import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { ChatRuntimeMode } from '../chat/chat-runtime.js';
import { ConversationsController } from './conversations.controller.js';

/**
 * Task 5 harness fence for the conversations REST write path.
 *
 * Under `pi-rpc` the durable/harness conversation path (Task 15) owns message persistence, so the
 * legacy direct-repository write via `POST /api/conversations/:id/messages` must be refused with a
 * fixed typed `runtime_unsupported` BEFORE the repository is touched — never a duplicate write.
 * Under `legacy` the endpoint keeps its current behaviour and writes through `brain.conversations`.
 *
 * Item 3 (single runtime-mode source of truth): the mode is the router's ONE init-time resolution,
 * injected into the controller and read as `router.runtimeMode`. It is NOT re-derived from
 * `process.env` at request time. The two "env is flipped after construction" tests below are the
 * load-bearing guard: they pass only because the controller reads the fixed injected mode, and turn
 * RED the instant the fence is reverted to `resolveChatRuntimeMode(process.env)`.
 */
const CONVERSATION_ID = '22222222-2222-4222-8222-222222222222';
const USER = { id: 'user-1' };

function sendMessageDto() {
  return {
    role: 'user' as const,
    content: 'hello from the legacy REST write path',
    metadata: undefined,
  };
}

function brainWithMessageSpy() {
  const addMessage = vi.fn().mockResolvedValue({
    id: 'message-1',
    conversationId: CONVERSATION_ID,
    role: 'user',
    content: 'hello from the legacy REST write path',
  });
  return {
    brain: { conversations: { addMessage } } as never,
    addMessage,
  };
}

/** The controller only needs the router's immutable `runtimeMode`; supply exactly that. */
function routerFixedTo(mode: ChatRuntimeMode) {
  return { runtimeMode: mode };
}

let priorMode: string | undefined;

describe('conversations REST write path — Task 5 harness fence', () => {
  beforeEach(() => {
    priorMode = process.env['CHAT_HARNESS_RUNTIME'];
  });

  afterEach(() => {
    if (priorMode === undefined) delete process.env['CHAT_HARNESS_RUNTIME'];
    else process.env['CHAT_HARNESS_RUNTIME'] = priorMode;
  });

  it('refuses the legacy repository write when the router resolved pi-rpc, before any write', async () => {
    const { brain, addMessage } = brainWithMessageSpy();
    const controller = new ConversationsController(brain, routerFixedTo('pi-rpc'));

    await expect(
      controller.addMessage(CONVERSATION_ID, sendMessageDto(), USER),
    ).rejects.toMatchObject({ code: 'runtime_unsupported' });

    // Load-bearing: the durable/harness path owns pi-rpc persistence — the legacy repo must not be
    // written, so no duplicate message can be produced.
    expect(addMessage).not.toHaveBeenCalled();
  });

  it('writes through the repository when the router resolved legacy (GREEN control)', async () => {
    const { brain, addMessage } = brainWithMessageSpy();
    const controller = new ConversationsController(brain, routerFixedTo('legacy'));

    const result = await controller.addMessage(CONVERSATION_ID, sendMessageDto(), USER);

    expect(addMessage).toHaveBeenCalledWith(
      {
        conversationId: CONVERSATION_ID,
        role: 'user',
        content: 'hello from the legacy REST write path',
        metadata: undefined,
      },
      USER.id,
    );
    expect(result).toMatchObject({ id: 'message-1', conversationId: CONVERSATION_ID });
  });

  it('keeps refusing under a pi-rpc router even when CHAT_HARNESS_RUNTIME is flipped to legacy after startup', async () => {
    // The runtime mode is fixed at module init. A later env mutation must not reopen the fence:
    // a request-time `resolveChatRuntimeMode(process.env)` read would see `legacy` and wrongly write.
    process.env['CHAT_HARNESS_RUNTIME'] = 'legacy';
    const { brain, addMessage } = brainWithMessageSpy();
    const controller = new ConversationsController(brain, routerFixedTo('pi-rpc'));

    await expect(
      controller.addMessage(CONVERSATION_ID, sendMessageDto(), USER),
    ).rejects.toMatchObject({ code: 'runtime_unsupported' });

    expect(addMessage).not.toHaveBeenCalled();
  });

  it('keeps writing under a legacy router even when CHAT_HARNESS_RUNTIME is flipped to pi-rpc after startup', async () => {
    // Symmetric guard: a legacy-resolved router must keep writing regardless of the live env, so a
    // request-time env read of `pi-rpc` cannot spuriously refuse a legitimate legacy write.
    process.env['CHAT_HARNESS_RUNTIME'] = 'pi-rpc';
    const { brain, addMessage } = brainWithMessageSpy();
    const controller = new ConversationsController(brain, routerFixedTo('legacy'));

    await controller.addMessage(CONVERSATION_ID, sendMessageDto(), USER);

    expect(addMessage).toHaveBeenCalledTimes(1);
  });
});
