import { describe, expect, it, vi } from 'vitest';
import type { SlashCommandPayload } from '@mosaicstack/types';
import { ChatGateway } from './chat.gateway.js';

const payload: SlashCommandPayload = {
  command: 'gc',
  conversationId: 'conversation-1',
  approvalId: 'approval-1',
};

/**
 * Task 5 fence (F, existing control): gateway-owned command authorization/approval must
 * cause ZERO chat-runtime dispatch. Placed in the gateway's chat-runtime-router slot (the
 * former direct `AgentService` slot) so any accidental chat-runtime resolution throws
 * loudly instead of silently passing. Because execute/approval run entirely through the
 * command executor dependency and never resolve a chat runtime, this fixture is never
 * triggered and the ingress stays a GREEN control.
 */
function failIfUsedChatRuntimeRouter() {
  return {
    onModuleInit: () => {
      throw new Error('chat runtime router must not initialise on the command approval path');
    },
    get active(): never {
      throw new Error('chat runtime must not be resolved on the command approval path');
    },
  };
}

function buildGateway(commandExecutor: {
  execute: ReturnType<typeof vi.fn>;
  createApproval: ReturnType<typeof vi.fn>;
}): ChatGateway {
  return new ChatGateway(
    failIfUsedChatRuntimeRouter() as never,
    {} as never,
    {} as never,
    {} as never,
    commandExecutor as never,
    {} as never,
  );
}

describe('ChatGateway command approval ingress', () => {
  it('passes the client approval ID through to command execution while deriving the actor server-side', async (): Promise<void> => {
    const commandExecutor = {
      execute: vi.fn().mockResolvedValue({ ...payload, success: true }),
      createApproval: vi.fn(),
    };
    const gateway = buildGateway(commandExecutor);
    const client = { data: { user: { id: 'admin-1' } }, emit: vi.fn() };

    await gateway.handleCommandExecute(client as never, payload);

    expect(commandExecutor.execute).toHaveBeenCalledWith(payload, {
      userId: 'admin-1',
      tenantId: 'admin-1',
    });
    expect(client.emit).toHaveBeenCalledWith(
      'command:result',
      expect.objectContaining({ success: true }),
    );
  });

  it('issues a durable approval only for the authenticated actor', async (): Promise<void> => {
    const commandExecutor = {
      execute: vi.fn(),
      createApproval: vi.fn().mockResolvedValue({
        approvalId: 'approval-1',
        expiresAt: '2026-07-12T00:05:00.000Z',
      }),
    };
    const gateway = buildGateway(commandExecutor);
    const client = { data: { user: { id: 'admin-1' } }, emit: vi.fn() };

    await gateway.handleCommandApproval(client as never, {
      command: 'gc',
      conversationId: 'conversation-1',
    });

    expect(commandExecutor.createApproval).toHaveBeenCalledWith(
      { command: 'gc', conversationId: 'conversation-1' },
      { userId: 'admin-1', tenantId: 'admin-1' },
    );
    expect(client.emit).toHaveBeenCalledWith('command:approval', {
      command: 'gc',
      conversationId: 'conversation-1',
      success: true,
      approvalId: 'approval-1',
      expiresAt: '2026-07-12T00:05:00.000Z',
    });
  });
});

/**
 * Task 5 (G3) command runtime fence. Under pi-rpc there is no embedded chat session, so
 * embedded slash-commands (/model, /agent, and every other non-audited command) are fixed
 * "unsupported" and MUST fail closed BEFORE reaching the command executor — never a silent
 * fall-through to embedded execution. Only runtime-independent audited system commands
 * (/reload) pass through as a positive control, and the approval path stays runtime-independent.
 * The router stub here carries `runtimeMode: 'pi-rpc'` and throws if any runtime is resolved, so
 * a fence bypass surfaces as a thrown error rather than a silent embedded dispatch.
 */
function buildPiRpcGateway(commandExecutor: {
  execute: ReturnType<typeof vi.fn>;
  createApproval: ReturnType<typeof vi.fn>;
}): ChatGateway {
  const piRpcRouter = {
    runtimeMode: 'pi-rpc' as const,
    onModuleInit: () => {
      throw new Error('chat runtime router must not initialise on the pi-rpc command path');
    },
    get active(): never {
      throw new Error('chat runtime must not be resolved on the pi-rpc command path');
    },
  };
  return new ChatGateway(
    piRpcRouter as never,
    {} as never,
    {} as never,
    {} as never,
    commandExecutor as never,
    {} as never,
  );
}

describe('ChatGateway command runtime fence (Task 5 G3, pi-rpc)', () => {
  const UNSUPPORTED = 'Slash commands are not available on this deployment.';

  it.each(['model', 'agent', 'gc'])(
    'fails /%s closed before the executor under pi-rpc (execute never called)',
    async (command): Promise<void> => {
      const commandExecutor = {
        execute: vi
          .fn()
          .mockResolvedValue({ command, conversationId: 'conversation-1', success: true }),
        createApproval: vi.fn(),
      };
      const gateway = buildPiRpcGateway(commandExecutor);
      const client = { data: { user: { id: 'admin-1' } }, emit: vi.fn() };

      await gateway.handleCommandExecute(client as never, {
        command,
        conversationId: 'conversation-1',
      });

      expect(commandExecutor.execute).toHaveBeenCalledTimes(0);
      expect(client.emit).toHaveBeenCalledWith('command:result', {
        command,
        conversationId: 'conversation-1',
        success: false,
        message: UNSUPPORTED,
      });
    },
  );

  it('passes the audited /reload system command through as a positive control under pi-rpc', async (): Promise<void> => {
    const reloadResult = { command: 'reload', conversationId: 'conversation-1', success: true };
    const commandExecutor = {
      execute: vi.fn().mockResolvedValue(reloadResult),
      createApproval: vi.fn(),
    };
    const gateway = buildPiRpcGateway(commandExecutor);
    const client = { data: { user: { id: 'admin-1' } }, emit: vi.fn() };

    await gateway.handleCommandExecute(client as never, {
      command: 'reload',
      conversationId: 'conversation-1',
    });

    expect(commandExecutor.execute).toHaveBeenCalledTimes(1);
    expect(commandExecutor.execute).toHaveBeenCalledWith(
      { command: 'reload', conversationId: 'conversation-1' },
      { userId: 'admin-1', tenantId: 'admin-1' },
    );
    expect(client.emit).toHaveBeenCalledWith('command:result', reloadResult);
  });

  it('keeps command approval runtime-independent under pi-rpc (createApproval still runs)', async (): Promise<void> => {
    const commandExecutor = {
      execute: vi.fn(),
      createApproval: vi.fn().mockResolvedValue({
        approvalId: 'approval-1',
        expiresAt: '2026-07-12T00:05:00.000Z',
      }),
    };
    const gateway = buildPiRpcGateway(commandExecutor);
    const client = { data: { user: { id: 'admin-1' } }, emit: vi.fn() };

    await gateway.handleCommandApproval(client as never, {
      command: 'gc',
      conversationId: 'conversation-1',
    });

    expect(commandExecutor.createApproval).toHaveBeenCalledWith(
      { command: 'gc', conversationId: 'conversation-1' },
      { userId: 'admin-1', tenantId: 'admin-1' },
    );
    expect(client.emit).toHaveBeenCalledWith(
      'command:approval',
      expect.objectContaining({ success: true, approvalId: 'approval-1' }),
    );
  });
});
