import { afterEach, describe, expect, it, vi } from 'vitest';
import {
  createDiscordIngressEnvelope,
  verifyDiscordIngressEnvelope,
  DiscordPlugin,
  type DiscordIngressPayload,
  parseDiscordInteractionBindings,
  resolveDiscordInteractionActorId,
  resolveDiscordInteractionBinding,
} from '@mosaicstack/discord-plugin';
import { RuntimeProviderService } from '../agent/runtime-provider-registry.service.js';
import { ChatGateway } from '../chat/chat.gateway.js';
import { CommandAuthorizationService } from '../commands/command-authorization.service.js';
import { validateDiscordServiceToken } from '../chat/chat.gateway-auth.js';
import { ChatRuntimeRouter } from '../chat/chat-runtime-router.js';
import { EmbeddedChatRuntime } from '../chat/embedded-chat.runtime.js';
import { HarnessChatRuntime } from '../chat/harness-chat.runtime.js';
import { HarnessRegistry } from '../harness/harness.registry.js';
import { DiscordReplayProtector } from './discord-replay-protector.js';

const SERVICE_TOKEN = 'test-service-token';
const ENV_KEYS = [
  'DISCORD_SERVICE_TOKEN',
  'DISCORD_SERVICE_USER_ID',
  'DISCORD_SERVICE_TENANT_ID',
  'DISCORD_INTERACTION_BINDINGS',
  'DISCORD_ALLOWED_GUILD_IDS',
  'DISCORD_ALLOWED_CHANNEL_IDS',
  'DISCORD_ALLOWED_USER_IDS',
  'MOSAIC_AGENT_NAME',
  'MOSAIC_AGENT_CONFIG_ID',
  'CHAT_HARNESS_RUNTIME',
] as const;
const savedEnv = new Map<string, string | undefined>();

function configureDiscordEnv(role: 'admin' | 'member' = 'admin'): void {
  for (const key of ENV_KEYS) savedEnv.set(key, process.env[key]);
  process.env['DISCORD_SERVICE_TOKEN'] = SERVICE_TOKEN;
  process.env['DISCORD_SERVICE_USER_ID'] = 'discord-service';
  process.env['DISCORD_SERVICE_TENANT_ID'] = 'tenant-discord';
  process.env['MOSAIC_AGENT_NAME'] = 'Nova';
  process.env['MOSAIC_AGENT_CONFIG_ID'] = 'agent-config-nova';
  process.env['DISCORD_ALLOWED_GUILD_IDS'] = 'guild-001';
  process.env['DISCORD_ALLOWED_CHANNEL_IDS'] = 'channel-001';
  process.env['DISCORD_ALLOWED_USER_IDS'] = 'user-001';
  process.env['DISCORD_INTERACTION_BINDINGS'] = JSON.stringify([
    {
      instanceId: 'Nova',
      agentConfigId: 'agent-config-nova',
      guildId: 'guild-001',
      channelId: 'channel-001',
      pairedUsers: {
        'user-001': {
          role: role === 'admin' ? 'admin' : 'operator',
          mosaicUserId: 'mosaic-admin-001',
        },
      },
    },
  ]);
}

afterEach((): void => {
  for (const key of ENV_KEYS) {
    const value = savedEnv.get(key);
    if (value === undefined) delete process.env[key];
    else process.env[key] = value;
  }
  savedEnv.clear();
});

function commandAuthorization(role: 'admin' | 'member'): CommandAuthorizationService {
  const entries = new Map<string, string>();
  const db = {
    select: () => ({ from: () => ({ where: () => ({ limit: async () => [{ role }] }) }) }),
  };
  const redis = {
    get: async (key: string) => entries.get(key) ?? null,
    set: async (key: string, value: string) => entries.set(key, value),
    del: async (key: string) => Number(entries.delete(key)),
  };
  return new CommandAuthorizationService(db as never, redis);
}

function discordGateway(role: 'admin' | 'member'): {
  gateway: ChatGateway;
  client: { data: { discordService: boolean }; emit: ReturnType<typeof vi.fn> };
  consumedActions: Array<{ actorId: string; correlationId: string }>;
  durable: { getSnapshot: ReturnType<typeof vi.fn> };
  audit: { record: ReturnType<typeof vi.fn> };
} {
  const authorization = commandAuthorization(role);
  const consumedActions: Array<{ actorId: string; correlationId: string }> = [];
  const durable = {
    getSnapshot: vi.fn().mockResolvedValue({
      identity: { agentName: 'Nova', providerId: 'fleet', runtimeSessionId: 'runtime-1' },
    }),
  };
  const audit = { record: vi.fn().mockResolvedValue(undefined) };
  const runtimeRegistry = new RuntimeProviderService(
    {
      require: () => ({
        capabilities: async () => ({ supported: ['session.terminate'] }),
        terminate: async () => undefined,
      }),
    } as never,
    { record: async () => undefined } as never,
    {
      consume: async (approvalId, action) => {
        consumedActions.push({ actorId: action.actorId, correlationId: action.correlationId });
        return authorization.consumeRuntimeTerminationApproval(approvalId, action);
      },
    },
  );
  return {
    gateway: new ChatGateway(
      {} as never,
      {} as never,
      {} as never,
      {} as never,
      {} as never,
      {} as never,
      authorization,
      runtimeRegistry,
      durable as never,
      audit as never,
    ),
    client: { data: { discordService: true }, emit: vi.fn() },
    consumedActions,
    durable,
    audit,
  };
}

function ingressEnvelope(
  content: string,
  messageId: string,
  overrides: Partial<DiscordIngressPayload> = {},
): ReturnType<typeof createDiscordIngressEnvelope> {
  return createDiscordIngressEnvelope(
    createPayload({ content, messageId, ...overrides }),
    SERVICE_TOKEN,
  );
}

function createPayload(overrides: Partial<DiscordIngressPayload> = {}): DiscordIngressPayload {
  return {
    correlationId: 'correlation-001',
    messageId: 'discord-message-001',
    guildId: 'guild-001',
    channelId: 'channel-001',
    userId: 'user-001',
    conversationId: 'Nova:discord:channel-001',
    content: 'hello Tess',
    ...overrides,
  };
}

/**
 * Task 5 fence (C): the Discord SEND path runs through the exclusive {@link ChatRuntimeRouter},
 * constructed here in `pi-rpc` mode with a fully-resolved runtime (`active` = harness). A verified
 * Discord *service* turn must nonetheless execute on the {@link EmbeddedChatRuntime} — never the
 * harness, never the routing engine — per the Q1/Q2 adjudication: the router owns a dedicated
 * verified-ingress dispatch that delegates to embedded regardless of mode, with zero harness
 * fallback. The gateway is given the router in the former direct-`AgentService` constructor slot.
 *
 * RED today: production still reads that slot as a bare `AgentService`, so `this.agentService`
 * resolves to the router, `getSession(...)` is not a function, the send path throws and is caught
 * (an `error` is emitted and the handler returns) BEFORE it ever reaches the embedded runtime. The
 * failure is behavioural wiring — collection, DI, and `onModuleInit` all succeed. GREEN re-routes
 * the verified Discord dispatch through the router into the embedded runtime, satisfying the
 * preserved create/prompt assertions without weakening any control. `harnessConversations.append`
 * proves the harness path is never touched even though the pi-rpc router resolved it as `active`.
 *
 * Correction #4 is proved behaviourally, not by naming an accessor: the verified-ingress dispatch
 * is reachable only from the fully-verified `discordService` branch (the create/prompt tests below)
 * and never from a browser-emittable socket event (the browser-forgery refusal test).
 */
function readyPiRpcRegistry(): HarnessRegistry {
  const registry = new HarnessRegistry();
  // A registered 'pi' adapter + an available (non-sentinel) conversation service let the pi-rpc
  // router resolve `active` = harness instead of failing closed at init, so these tests model the
  // real hostile condition — the harness runtime IS live — rather than a degraded router.
  registry.register({ id: 'pi' } as never);
  return registry;
}

function piRpcRouterFronting(
  agentService: unknown,
  harnessConversations: { append: ReturnType<typeof vi.fn> },
): ChatRuntimeRouter {
  const routerConversationServiceTripwire = {
    append: () => {
      throw new Error('router conversation service must not be resolved on the Discord path');
    },
  };
  const embedded = new EmbeddedChatRuntime(agentService as never);
  const harness = new HarnessChatRuntime(harnessConversations as never);
  const router = new ChatRuntimeRouter(
    readyPiRpcRegistry(),
    routerConversationServiceTripwire as never,
    embedded,
    harness,
    'pi-rpc',
  );
  router.onModuleInit();
  return router;
}

describe('Discord ingress security', () => {
  it('keeps legacy role-only bindings valid while withholding privileged actor identity', () => {
    const [binding] = parseDiscordInteractionBindings(
      JSON.stringify([
        {
          instanceId: 'Nova',
          agentConfigId: 'agent-config-nova',
          guildId: 'guild-001',
          channelId: 'channel-001',
          pairedUsers: { 'user-001': 'admin' },
        },
      ]),
    );
    expect(
      resolveDiscordInteractionBinding([binding!], 'guild-001', 'channel-001', 'user-001', 'send'),
    ).toEqual(binding);
    expect(resolveDiscordInteractionActorId(binding!, 'user-001')).toBeNull();
  });

  it('binds a differently named configured interaction instance without code changes', () => {
    const binding = resolveDiscordInteractionBinding(
      [
        {
          instanceId: 'Nova',
          agentConfigId: 'agent-config-nova',
          guildId: 'guild-001',
          channelId: 'channel-001',
          pairedUsers: { 'user-001': { role: 'operator', mosaicUserId: 'mosaic-operator-001' } },
        },
      ],
      'guild-001',
      'channel-001',
      'user-001',
      'send',
    );

    expect(binding?.instanceId).toBe('Nova');
  });

  it('accepts only the configured Discord service identity', () => {
    expect(validateDiscordServiceToken(SERVICE_TOKEN, SERVICE_TOKEN)).toBe(true);
    expect(validateDiscordServiceToken('wrong-service-token', SERVICE_TOKEN)).toBe(false);
    expect(validateDiscordServiceToken(undefined, SERVICE_TOKEN)).toBe(false);
  });

  it('rejects unauthenticated or tampered service envelopes', () => {
    const envelope = createDiscordIngressEnvelope(createPayload(), SERVICE_TOKEN);

    expect(verifyDiscordIngressEnvelope(envelope, SERVICE_TOKEN)).toEqual(createPayload());
    expect(verifyDiscordIngressEnvelope(envelope, 'wrong-service-token')).toBeNull();
    expect(
      verifyDiscordIngressEnvelope(
        { ...envelope, payload: { ...envelope.payload, content: 'forged command' } },
        SERVICE_TOKEN,
      ),
    ).toBeNull();
  });

  it.each([
    ['guild', { guildId: 'unlisted-guild' }],
    ['channel', { channelId: 'unlisted-channel' }],
    ['user', { userId: 'unlisted-user' }],
  ])(
    'rejects an unallowlisted Discord %s',
    (_kind: string, overrides: Partial<DiscordIngressPayload>) => {
      const envelope = createDiscordIngressEnvelope(createPayload(overrides), SERVICE_TOKEN);

      expect(
        verifyDiscordIngressEnvelope(envelope, SERVICE_TOKEN, {
          guildIds: ['guild-001'],
          channelIds: ['channel-001'],
          userIds: ['user-001'],
        }),
      ).toBeNull();
    },
  );

  it('retains Discord message and correlation IDs after authenticated allowlisted validation', () => {
    const payload = createPayload({
      correlationId: 'correlation-trace-123',
      messageId: 'discord-snowflake-987',
    });
    const envelope = createDiscordIngressEnvelope(payload, SERVICE_TOKEN);

    expect(
      verifyDiscordIngressEnvelope(envelope, SERVICE_TOKEN, {
        guildIds: ['guild-001'],
        channelIds: ['channel-001'],
        userIds: ['user-001'],
      }),
    ).toEqual(payload);
  });

  it('rejects a replayed Discord message ID while retaining bounded replay state', () => {
    const replayProtector = new DiscordReplayProtector(60_000, 2);

    expect(replayProtector.claim('discord-message-001')).toBe(true);
    expect(replayProtector.claim('discord-message-001')).toBe(false);
    expect(replayProtector.claim('discord-message-002')).toBe(true);
    expect(replayProtector.claim('discord-message-003')).toBe(true);
    expect(replayProtector.size).toBe(2);
  });

  it('consumes the exact target once when approval and stop are separate Discord messages', async () => {
    configureDiscordEnv();
    const { gateway, client, consumedActions } = discordGateway('admin');
    await gateway.handleDiscordApproval(
      client as never,
      ingressEnvelope('/approve', 'approve-message', {
        correlationId: 'approval-ingress-correlation',
      }),
    );
    const approval = client.emit.mock.calls.find(
      ([event]) => event === 'discord:approval',
    )?.[1] as {
      approvalId: string;
      success: boolean;
    };
    expect(approval.success).toBe(true);

    await gateway.handleDiscordStop(
      client as never,
      ingressEnvelope(`/stop ${approval.approvalId}`, 'stop-message', {
        correlationId: 'stop-ingress-correlation',
      }),
    );
    expect(client.emit).toHaveBeenCalledWith('discord:stop', {
      correlationId: 'stop-ingress-correlation',
      success: true,
    });
    expect(consumedActions).toEqual([
      {
        actorId: 'mosaic-admin-001',
        correlationId: expect.stringMatching(/^discord-action:v1:/),
      },
    ]);
  });

  it('audits a Discord mint-side authorization denial', async () => {
    configureDiscordEnv();
    const { gateway, client, audit } = discordGateway('member');

    await gateway.handleDiscordApproval(
      client as never,
      ingressEnvelope('/approve', 'denied-approve'),
    );

    expect(client.emit).toHaveBeenCalledWith('discord:approval', {
      correlationId: 'correlation-001',
      success: false,
      approvalId: undefined,
      expiresAt: undefined,
    });
    expect(audit.record).toHaveBeenCalledWith(
      expect.objectContaining({
        outcome: 'denied',
        operation: 'session.terminate',
        errorCode: 'policy_denied',
      }),
    );
  });

  it('rejects approval when the durable session targets a different logical agent', async () => {
    configureDiscordEnv();
    const { gateway, client, durable } = discordGateway('admin');
    durable.getSnapshot.mockResolvedValueOnce({
      identity: { agentName: 'Other', providerId: 'fleet', runtimeSessionId: 'runtime-1' },
    });

    await gateway.handleDiscordApproval(
      client as never,
      ingressEnvelope('/approve', 'mismatched-agent-approve'),
    );

    expect(client.emit).toHaveBeenCalledWith('discord:approval', {
      correlationId: 'correlation-001',
      success: false,
      approvalId: undefined,
      expiresAt: undefined,
    });
  });

  it('rejects privileged envelopes with a forged current conversation route', async () => {
    configureDiscordEnv();
    const { gateway, client } = discordGateway('admin');

    await gateway.handleDiscordApproval(
      client as never,
      ingressEnvelope('/approve', 'forged-approval-route', {
        conversationId: 'Nova:discord:other-channel',
      }),
    );
    await gateway.handleDiscordStop(
      client as never,
      ingressEnvelope('/stop forged', 'forged-stop-route', {
        conversationId: 'Nova:discord:other-channel',
      }),
    );

    expect(client.emit).not.toHaveBeenCalledWith('discord:approval', expect.anything());
    expect(client.emit).not.toHaveBeenCalledWith('discord:stop', expect.anything());
  });

  it('rejects unpaired and non-admin Discord users for approval and stop', async () => {
    configureDiscordEnv();
    const { gateway, client } = discordGateway('member');
    await gateway.handleDiscordApproval(
      client as never,
      ingressEnvelope('/approve', 'member-approve'),
    );
    expect(client.emit).toHaveBeenCalledWith('discord:approval', {
      correlationId: 'correlation-001',
      success: false,
      approvalId: undefined,
      expiresAt: undefined,
    });

    process.env['DISCORD_INTERACTION_BINDINGS'] = JSON.stringify([]);
    await gateway.handleDiscordStop(
      client as never,
      ingressEnvelope('/stop forged', 'unpaired-stop'),
    );
    expect(client.emit).not.toHaveBeenCalledWith('discord:stop', expect.anything());
  });

  it('rejects replaying a Discord-created termination approval', async () => {
    configureDiscordEnv();
    const { gateway, client } = discordGateway('admin');
    await gateway.handleDiscordApproval(
      client as never,
      ingressEnvelope('/approve', 'replay-approve', {
        correlationId: 'replay-approval-correlation',
      }),
    );
    const approval = client.emit.mock.calls.find(
      ([event]) => event === 'discord:approval',
    )?.[1] as {
      approvalId: string;
    };
    await gateway.handleDiscordStop(
      client as never,
      ingressEnvelope(`/stop ${approval.approvalId}`, 'replay-stop-one', {
        correlationId: 'replay-stop-correlation-one',
      }),
    );
    await gateway.handleDiscordStop(
      client as never,
      ingressEnvelope(`/stop ${approval.approvalId}`, 'replay-stop-two', {
        correlationId: 'replay-stop-correlation-two',
      }),
    );
    const stopResults = client.emit.mock.calls.filter(([event]) => event === 'discord:stop');
    expect(stopResults.map(([, result]) => (result as { success: boolean }).success)).toEqual([
      true,
      false,
    ]);
  });

  it.each([
    'https://user:password@cdn.example.test/diagram.png',
    'https://cdn.example.test/diagram.png?token=secret',
    'https://cdn.example.test/diagram.png?X-Amz-Signature=secret',
    'https://cdn.example.test/diagram.png?auth=secret',
    'https://cdn.example.test/diagram.png?hm=secret',
  ])('rejects credential-bearing attachment URLs before gateway dispatch', async (url) => {
    configureDiscordEnv();
    const { gateway, client } = discordGateway('admin');

    await gateway.handleMessage(
      client as never,
      ingressEnvelope('', `credential-url-${url.length}`, {
        conversationId: 'Nova:discord:channel-001',
        attachments: [
          { id: 'attachment-credential', name: 'diagram.png', url, contentType: 'image/png' },
        ],
      }),
    );

    expect(client.emit).not.toHaveBeenCalledWith('message:ack', expect.anything());
  });

  it("selects each binding's trusted logical-agent config when creating Discord sessions", async () => {
    configureDiscordEnv();
    process.env['CHAT_HARNESS_RUNTIME'] = 'pi-rpc';
    process.env['DISCORD_ALLOWED_CHANNEL_IDS'] = 'channel-001,channel-002';
    process.env['DISCORD_INTERACTION_BINDINGS'] = JSON.stringify([
      {
        instanceId: 'Nova',
        agentConfigId: 'agent-config-nova',
        guildId: 'guild-001',
        channelId: 'channel-001',
        pairedUsers: {
          'user-001': { role: 'operator', mosaicUserId: 'mosaic-operator-001' },
        },
      },
      {
        instanceId: 'Orion',
        agentConfigId: 'agent-config-orion',
        guildId: 'guild-001',
        channelId: 'channel-002',
        pairedUsers: {
          'user-001': { role: 'operator', mosaicUserId: 'mosaic-operator-001' },
        },
      },
    ]);
    const session = {
      provider: 'configured-provider',
      modelId: 'configured-model',
      piSession: {
        thinkingLevel: 'medium',
        getAvailableThinkingLevels: (): string[] => ['medium'],
      },
    };
    const createSession = vi.fn().mockResolvedValue(session);
    const agentService = {
      getSession: vi.fn().mockReturnValue(undefined),
      createSession,
      recordMessage: vi.fn(),
      onEvent: vi.fn().mockReturnValue((): void => undefined),
      addChannel: vi.fn(),
      prompt: vi.fn().mockResolvedValue(undefined),
    };
    const brain = {
      agents: {
        findById: vi.fn((id: string) =>
          Promise.resolve({
            id,
            name: id === 'agent-config-orion' ? 'Orion' : 'Nova',
          }),
        ),
      },
      conversations: {
        findById: vi.fn().mockResolvedValue({ id: 'Nova:discord:channel-001' }),
        findMessages: vi.fn().mockResolvedValue([]),
        create: vi.fn().mockResolvedValue(undefined),
        update: vi.fn().mockResolvedValue(undefined),
        addMessage: vi.fn().mockResolvedValue(undefined),
      },
    };
    const routingEngine = { resolve: vi.fn() };
    const harnessConversations = { append: vi.fn() };
    const gateway = new ChatGateway(
      piRpcRouterFronting(agentService, harnessConversations) as never,
      {} as never,
      brain as never,
      {} as never,
      {} as never,
      routingEngine as never,
    );
    const client = {
      id: 'discord-client-new-session',
      data: { discordService: true },
      emit: vi.fn(),
    };

    await gateway.handleMessage(
      client as never,
      ingressEnvelope('start configured session', 'configured-session-001', {
        conversationId: 'Nova:discord:channel-001',
      }),
    );

    await gateway.handleMessage(
      client as never,
      ingressEnvelope('start second configured session', 'configured-session-002', {
        channelId: 'channel-002',
        conversationId: 'Orion:discord:channel-002',
      }),
    );

    expect(createSession).toHaveBeenCalledWith(
      'Nova:discord:channel-001',
      expect.objectContaining({
        agentConfigId: 'agent-config-nova',
        userId: 'discord-service',
        tenantId: 'tenant-discord',
      }),
    );
    expect(createSession).toHaveBeenCalledWith(
      'Orion:discord:channel-002',
      expect.objectContaining({ agentConfigId: 'agent-config-orion' }),
    );
    expect(routingEngine.resolve).not.toHaveBeenCalled();
    // Even though the pi-rpc router resolved the harness as `active`, verified Discord ingress must
    // never touch it — the create path stays on the embedded runtime.
    expect(harnessConversations.append).not.toHaveBeenCalled();
  });

  it('dispatches a verified Discord SEND once and drops a byte-identical replay with zero additional dispatch/persist/ack (Task 5 G4)', async () => {
    configureDiscordEnv();
    process.env['CHAT_HARNESS_RUNTIME'] = 'pi-rpc';
    process.env['DISCORD_ALLOWED_CHANNEL_IDS'] = 'channel-001';
    process.env['DISCORD_INTERACTION_BINDINGS'] = JSON.stringify([
      {
        instanceId: 'Nova',
        agentConfigId: 'agent-config-nova',
        guildId: 'guild-001',
        channelId: 'channel-001',
        pairedUsers: {
          'user-001': { role: 'operator', mosaicUserId: 'mosaic-operator-001' },
        },
      },
    ]);
    const session = {
      provider: 'configured-provider',
      modelId: 'configured-model',
      agentConfigId: 'agent-config-nova',
      agentName: 'Nova',
      piSession: {
        thinkingLevel: 'medium',
        getAvailableThinkingLevels: (): string[] => ['medium'],
      },
    };
    const createSession = vi.fn().mockResolvedValue(session);
    const prompt = vi.fn().mockResolvedValue(undefined);
    const agentService = {
      getSession: vi.fn().mockReturnValue(undefined),
      createSession,
      recordMessage: vi.fn(),
      onEvent: vi.fn().mockReturnValue((): void => undefined),
      addChannel: vi.fn(),
      removeChannel: vi.fn(),
      prompt,
    };
    const addMessage = vi.fn().mockResolvedValue({ id: 'discord-persisted-message' });
    const brain = {
      agents: { findById: vi.fn((id: string) => Promise.resolve({ id, name: 'Nova' })) },
      conversations: {
        findById: vi.fn().mockResolvedValue({ id: 'Nova:discord:channel-001' }),
        findMessages: vi.fn().mockResolvedValue([]),
        create: vi.fn().mockResolvedValue(undefined),
        update: vi.fn().mockResolvedValue(undefined),
        addMessage,
      },
    };
    const harnessConversations = { append: vi.fn() };
    const gateway = new ChatGateway(
      piRpcRouterFronting(agentService, harnessConversations) as never,
      {} as never,
      brain as never,
      {} as never,
      {} as never,
      { resolve: vi.fn() } as never,
    );
    const client = {
      id: 'discord-client-replay',
      data: { discordService: true },
      emit: vi.fn(),
    };
    const ackCount = (): number =>
      client.emit.mock.calls.filter((call) => call[0] === 'message:ack').length;

    // One fully-valid signed envelope; the replay reuses the SAME object (same messageId).
    const envelope = ingressEnvelope('verified once', 'discord-replay-001', {
      conversationId: 'Nova:discord:channel-001',
    });

    // First delivery: the verified-Discord SEND runs the full embedded dispatch exactly once.
    await gateway.handleMessage(client as never, envelope);
    expect(createSession).toHaveBeenCalledTimes(1);
    expect(prompt).toHaveBeenCalledTimes(1);
    expect(addMessage).toHaveBeenCalledTimes(1);
    expect(ackCount()).toBe(1);

    // Byte-identical replay: the messageId is already claimed, so resolveDiscordIngress returns
    // null and the SEND handler bails before dispatch/persist/ack. Every effect stays at exactly one.
    await gateway.handleMessage(client as never, envelope);
    expect(createSession).toHaveBeenCalledTimes(1);
    expect(prompt).toHaveBeenCalledTimes(1);
    expect(addMessage).toHaveBeenCalledTimes(1);
    expect(ackCount()).toBe(1);
    // The harness runtime is never touched on either delivery.
    expect(harnessConversations.append).not.toHaveBeenCalled();
  });

  it('a verified SEND that fails the configured service identity consumes no replay claim, so a corrected byte-identical retry dispatches/persists/acks exactly once and a later duplicate stays fail-closed (Task 5 item 4 — claim ordering)', async () => {
    configureDiscordEnv();
    process.env['CHAT_HARNESS_RUNTIME'] = 'pi-rpc';
    process.env['DISCORD_ALLOWED_CHANNEL_IDS'] = 'channel-001';
    process.env['DISCORD_INTERACTION_BINDINGS'] = JSON.stringify([
      {
        instanceId: 'Nova',
        agentConfigId: 'agent-config-nova',
        guildId: 'guild-001',
        channelId: 'channel-001',
        pairedUsers: {
          'user-001': { role: 'operator', mosaicUserId: 'mosaic-operator-001' },
        },
      },
    ]);
    const session = {
      provider: 'configured-provider',
      modelId: 'configured-model',
      agentConfigId: 'agent-config-nova',
      agentName: 'Nova',
      piSession: {
        thinkingLevel: 'medium',
        getAvailableThinkingLevels: (): string[] => ['medium'],
      },
    };
    const createSession = vi.fn().mockResolvedValue(session);
    const prompt = vi.fn().mockResolvedValue(undefined);
    const agentService = {
      getSession: vi.fn().mockReturnValue(undefined),
      createSession,
      recordMessage: vi.fn(),
      onEvent: vi.fn().mockReturnValue((): void => undefined),
      addChannel: vi.fn(),
      removeChannel: vi.fn(),
      prompt,
    };
    const addMessage = vi.fn().mockResolvedValue({ id: 'discord-persisted-message' });
    const brain = {
      agents: { findById: vi.fn((id: string) => Promise.resolve({ id, name: 'Nova' })) },
      conversations: {
        findById: vi.fn().mockResolvedValue({ id: 'Nova:discord:channel-001' }),
        findMessages: vi.fn().mockResolvedValue([]),
        create: vi.fn().mockResolvedValue(undefined),
        update: vi.fn().mockResolvedValue(undefined),
        addMessage,
      },
    };
    const harnessConversations = { append: vi.fn() };
    const gateway = new ChatGateway(
      piRpcRouterFronting(agentService, harnessConversations) as never,
      {} as never,
      brain as never,
      {} as never,
      {} as never,
      { resolve: vi.fn() } as never,
    );
    const client = {
      id: 'discord-client-claim-ordering',
      data: { discordService: true },
      emit: vi.fn(),
    };
    const ackCount = (): number =>
      client.emit.mock.calls.filter((call) => call[0] === 'message:ack').length;

    // A single fully-valid signed envelope, reused byte-for-byte across all three deliveries.
    const envelope = ingressEnvelope('verified once with late identity', 'discord-order-001', {
      conversationId: 'Nova:discord:channel-001',
    });

    // (1) Configured service identity is MISSING. The envelope is validly signed and passes the
    //     binding + route checks, but the SEND must refuse at the identity gate BEFORE any claim
    //     or effect. If the claim fires ahead of that gate, this delivery silently burns the
    //     replay claim for `discord-order-001` even though nothing dispatched.
    delete process.env['DISCORD_SERVICE_USER_ID'];
    await gateway.handleMessage(client as never, envelope);
    expect(createSession).toHaveBeenCalledTimes(0);
    expect(prompt).toHaveBeenCalledTimes(0);
    expect(addMessage).toHaveBeenCalledTimes(0);
    expect(ackCount()).toBe(0);

    // (2) Identity is now configured; the operator resends the SAME envelope byte-for-byte. Because
    //     step (1) consumed no claim, this corrected retry claims once and runs the full embedded
    //     dispatch exactly once. (Under the pre-fix ordering the claim was already spent in step (1),
    //     so this retry is dropped as a replay and never dispatches — the RED this test drives.)
    process.env['DISCORD_SERVICE_USER_ID'] = 'discord-service';
    await gateway.handleMessage(client as never, envelope);
    expect(createSession).toHaveBeenCalledTimes(1);
    expect(prompt).toHaveBeenCalledTimes(1);
    expect(addMessage).toHaveBeenCalledTimes(1);
    expect(ackCount()).toBe(1);

    // (3) A genuine duplicate after a committed turn stays fail-closed: the claim taken in step (2)
    //     blocks it, so every effect remains at exactly one.
    await gateway.handleMessage(client as never, envelope);
    expect(createSession).toHaveBeenCalledTimes(1);
    expect(prompt).toHaveBeenCalledTimes(1);
    expect(addMessage).toHaveBeenCalledTimes(1);
    expect(ackCount()).toBe(1);
    expect(harnessConversations.append).not.toHaveBeenCalled();
  });

  it('a verified SEND whose configured agent record fails reconciliation consumes no replay claim, so a corrected byte-identical retry dispatches/persists/acks exactly once (Task 5 finding 3)', async () => {
    configureDiscordEnv();
    process.env['CHAT_HARNESS_RUNTIME'] = 'pi-rpc';
    process.env['DISCORD_ALLOWED_CHANNEL_IDS'] = 'channel-001';
    process.env['DISCORD_INTERACTION_BINDINGS'] = JSON.stringify([
      {
        instanceId: 'Nova',
        agentConfigId: 'agent-config-nova',
        guildId: 'guild-001',
        channelId: 'channel-001',
        pairedUsers: {
          'user-001': { role: 'operator', mosaicUserId: 'mosaic-operator-001' },
        },
      },
    ]);
    const session = {
      provider: 'configured-provider',
      modelId: 'configured-model',
      agentConfigId: 'agent-config-nova',
      agentName: 'Nova',
      piSession: {
        thinkingLevel: 'medium',
        getAvailableThinkingLevels: (): string[] => ['medium'],
      },
    };
    const createSession = vi.fn().mockResolvedValue(session);
    const prompt = vi.fn().mockResolvedValue(undefined);
    const agentService = {
      getSession: vi.fn().mockReturnValue(undefined),
      createSession,
      recordMessage: vi.fn(),
      onEvent: vi.fn().mockReturnValue((): void => undefined),
      addChannel: vi.fn(),
      removeChannel: vi.fn(),
      prompt,
    };
    const addMessage = vi.fn().mockResolvedValue({ id: 'discord-persisted-message' });
    // The durable agent record does not reconcile on the first delivery (its name no longer matches
    // the verified binding's instance id), then reconciles cleanly on the corrected retry.
    const findAgent = vi
      .fn()
      .mockResolvedValueOnce({ id: 'agent-config-nova', name: 'Renamed-Away' })
      .mockResolvedValue({ id: 'agent-config-nova', name: 'Nova' });
    const brain = {
      agents: { findById: findAgent },
      conversations: {
        findById: vi.fn().mockResolvedValue({ id: 'Nova:discord:channel-001' }),
        findMessages: vi.fn().mockResolvedValue([]),
        create: vi.fn().mockResolvedValue(undefined),
        update: vi.fn().mockResolvedValue(undefined),
        addMessage,
      },
    };
    const harnessConversations = { append: vi.fn() };
    const gateway = new ChatGateway(
      piRpcRouterFronting(agentService, harnessConversations) as never,
      {} as never,
      brain as never,
      {} as never,
      {} as never,
      { resolve: vi.fn() } as never,
    );
    const client = {
      id: 'discord-client-reconcile',
      data: { discordService: true },
      emit: vi.fn(),
    };
    const ackCount = (): number =>
      client.emit.mock.calls.filter((call) => call[0] === 'message:ack').length;

    const envelope = ingressEnvelope(
      'verified once with stale agent record',
      'discord-reconcile-001',
      {
        conversationId: 'Nova:discord:channel-001',
      },
    );

    // (1) The configured-agent reconcile runs BEFORE the replay claim. A mismatch refuses the turn
    //     and, crucially, consumes no claim for discord-reconcile-001 — nothing dispatches.
    await gateway.handleMessage(client as never, envelope);
    expect(createSession).toHaveBeenCalledTimes(0);
    expect(prompt).toHaveBeenCalledTimes(0);
    expect(addMessage).toHaveBeenCalledTimes(0);
    expect(ackCount()).toBe(0);

    // (2) The record now reconciles; because step (1) took no claim, this byte-identical retry claims
    //     once and runs the full embedded dispatch exactly once. (Pre-fix, the claim was spent ahead
    //     of the reconcile in step (1), so this retry was dropped as a replay — the RED this drives.)
    await gateway.handleMessage(client as never, envelope);
    expect(createSession).toHaveBeenCalledTimes(1);
    expect(prompt).toHaveBeenCalledTimes(1);
    expect(addMessage).toHaveBeenCalledTimes(1);
    expect(ackCount()).toBe(1);

    // (3) A genuine duplicate after the committed turn stays fail-closed.
    await gateway.handleMessage(client as never, envelope);
    expect(createSession).toHaveBeenCalledTimes(1);
    expect(prompt).toHaveBeenCalledTimes(1);
    expect(addMessage).toHaveBeenCalledTimes(1);
    expect(ackCount()).toBe(1);
    expect(harnessConversations.append).not.toHaveBeenCalled();
  });

  it('a verified SEND refuses to reuse a same-scope embedded session minted under a different configured identity, with zero prompt/persist/ack (Task 5 finding 3)', async () => {
    configureDiscordEnv();
    process.env['CHAT_HARNESS_RUNTIME'] = 'pi-rpc';
    process.env['DISCORD_ALLOWED_CHANNEL_IDS'] = 'channel-001';
    process.env['DISCORD_INTERACTION_BINDINGS'] = JSON.stringify([
      {
        instanceId: 'Nova',
        agentConfigId: 'agent-config-nova',
        guildId: 'guild-001',
        channelId: 'channel-001',
        pairedUsers: {
          'user-001': { role: 'operator', mosaicUserId: 'mosaic-operator-001' },
        },
      },
    ]);
    // A live session already exists for this conversation/scope, but it was minted under a DIFFERENT
    // configured agent (Orion). The verified binding reconciles to Nova, so reusing this session would
    // execute one agent's turn under another agent's verified label — the reuse guard must refuse it.
    const foreignIdentitySession = {
      provider: 'configured-provider',
      modelId: 'configured-model',
      agentConfigId: 'agent-config-orion',
      agentName: 'Orion',
      piSession: {
        thinkingLevel: 'medium',
        getAvailableThinkingLevels: (): string[] => ['medium'],
      },
    };
    const prompt = vi.fn().mockResolvedValue(undefined);
    const createSession = vi.fn().mockResolvedValue(foreignIdentitySession);
    const agentService = {
      getSession: vi.fn().mockReturnValue(foreignIdentitySession),
      createSession,
      recordMessage: vi.fn(),
      onEvent: vi.fn().mockReturnValue((): void => undefined),
      addChannel: vi.fn(),
      removeChannel: vi.fn(),
      prompt,
    };
    const addMessage = vi.fn().mockResolvedValue({ id: 'discord-persisted-message' });
    const brain = {
      agents: { findById: vi.fn((id: string) => Promise.resolve({ id, name: 'Nova' })) },
      conversations: {
        findById: vi.fn().mockResolvedValue({ id: 'Nova:discord:channel-001' }),
        findMessages: vi.fn().mockResolvedValue([]),
        create: vi.fn().mockResolvedValue(undefined),
        update: vi.fn().mockResolvedValue(undefined),
        addMessage,
      },
    };
    const harnessConversations = { append: vi.fn() };
    const gateway = new ChatGateway(
      piRpcRouterFronting(agentService, harnessConversations) as never,
      {} as never,
      brain as never,
      {} as never,
      {} as never,
      { resolve: vi.fn() } as never,
    );
    const client = {
      id: 'discord-client-identity-swap',
      data: { discordService: true },
      emit: vi.fn(),
    };

    await gateway.handleMessage(
      client as never,
      ingressEnvelope('reuse under a different identity', 'discord-identity-swap-001', {
        conversationId: 'Nova:discord:channel-001',
      }),
    );

    // Refused at the embedded reuse guard: no prompt, no persist, no ack — only a typed refusal.
    expect(prompt).not.toHaveBeenCalled();
    expect(addMessage).not.toHaveBeenCalled();
    expect(client.emit).not.toHaveBeenCalledWith('message:ack', expect.anything());
    expect(client.emit).toHaveBeenCalledWith(
      'error',
      expect.objectContaining({ conversationId: 'Nova:discord:channel-001' }),
    );
    expect(harnessConversations.append).not.toHaveBeenCalled();
  });

  it('a verified SEND whose configured agent record resolves under a different id fails reconciliation, consumes no replay claim, and a corrected byte-identical retry dispatches/persists/acks exactly once (Task 5 finding 3 — id axis)', async () => {
    configureDiscordEnv();
    process.env['CHAT_HARNESS_RUNTIME'] = 'pi-rpc';
    process.env['DISCORD_ALLOWED_CHANNEL_IDS'] = 'channel-001';
    process.env['DISCORD_INTERACTION_BINDINGS'] = JSON.stringify([
      {
        instanceId: 'Nova',
        agentConfigId: 'agent-config-nova',
        guildId: 'guild-001',
        channelId: 'channel-001',
        pairedUsers: {
          'user-001': { role: 'operator', mosaicUserId: 'mosaic-operator-001' },
        },
      },
    ]);
    const session = {
      provider: 'configured-provider',
      modelId: 'configured-model',
      agentConfigId: 'agent-config-nova',
      agentName: 'Nova',
      piSession: {
        thinkingLevel: 'medium',
        getAvailableThinkingLevels: (): string[] => ['medium'],
      },
    };
    const createSession = vi.fn().mockResolvedValue(session);
    const prompt = vi.fn().mockResolvedValue(undefined);
    const agentService = {
      getSession: vi.fn().mockReturnValue(undefined),
      createSession,
      recordMessage: vi.fn(),
      onEvent: vi.fn().mockReturnValue((): void => undefined),
      addChannel: vi.fn(),
      removeChannel: vi.fn(),
      prompt,
    };
    const addMessage = vi.fn().mockResolvedValue({ id: 'discord-persisted-message' });
    // The name matches the verified binding, but the record's own id is a DIFFERENT agent config —
    // an aliased/substituted lookup. Exact-id reconciliation must refuse it on the first delivery,
    // then admit the corrected record whose id matches the binding.
    const findAgent = vi
      .fn()
      .mockResolvedValueOnce({ id: 'agent-config-elsewhere', name: 'Nova' })
      .mockResolvedValue({ id: 'agent-config-nova', name: 'Nova' });
    const brain = {
      agents: { findById: findAgent },
      conversations: {
        findById: vi.fn().mockResolvedValue({ id: 'Nova:discord:channel-001' }),
        findMessages: vi.fn().mockResolvedValue([]),
        create: vi.fn().mockResolvedValue(undefined),
        update: vi.fn().mockResolvedValue(undefined),
        addMessage,
      },
    };
    const harnessConversations = { append: vi.fn() };
    const gateway = new ChatGateway(
      piRpcRouterFronting(agentService, harnessConversations) as never,
      {} as never,
      brain as never,
      {} as never,
      {} as never,
      { resolve: vi.fn() } as never,
    );
    const client = {
      id: 'discord-client-reconcile-id',
      data: { discordService: true },
      emit: vi.fn(),
    };
    const ackCount = (): number =>
      client.emit.mock.calls.filter((call) => call[0] === 'message:ack').length;

    const envelope = ingressEnvelope(
      'verified once with aliased agent id',
      'discord-reconcile-id-001',
      {
        conversationId: 'Nova:discord:channel-001',
      },
    );

    // (1) The record's id differs from the binding's agentConfigId. Exact-id reconcile refuses the
    //     turn BEFORE the replay claim, so nothing dispatches and the claim stays available.
    await gateway.handleMessage(client as never, envelope);
    expect(createSession).toHaveBeenCalledTimes(0);
    expect(prompt).toHaveBeenCalledTimes(0);
    expect(addMessage).toHaveBeenCalledTimes(0);
    expect(ackCount()).toBe(0);

    // (2) The record now reconciles on both id and name; because step (1) took no claim, this
    //     byte-identical retry claims once and runs the full embedded dispatch exactly once.
    await gateway.handleMessage(client as never, envelope);
    expect(createSession).toHaveBeenCalledTimes(1);
    expect(prompt).toHaveBeenCalledTimes(1);
    expect(addMessage).toHaveBeenCalledTimes(1);
    expect(ackCount()).toBe(1);

    // (3) A genuine duplicate after the committed turn stays fail-closed.
    await gateway.handleMessage(client as never, envelope);
    expect(createSession).toHaveBeenCalledTimes(1);
    expect(prompt).toHaveBeenCalledTimes(1);
    expect(addMessage).toHaveBeenCalledTimes(1);
    expect(ackCount()).toBe(1);
    expect(harnessConversations.append).not.toHaveBeenCalled();
  });

  it('a verified SEND refuses a freshly minted same-scope session whose identity differs from the reconciled configured agent, with zero prompt/persist/ack (Task 5 finding 3 — post-create)', async () => {
    configureDiscordEnv();
    process.env['CHAT_HARNESS_RUNTIME'] = 'pi-rpc';
    process.env['DISCORD_ALLOWED_CHANNEL_IDS'] = 'channel-001';
    process.env['DISCORD_INTERACTION_BINDINGS'] = JSON.stringify([
      {
        instanceId: 'Nova',
        agentConfigId: 'agent-config-nova',
        guildId: 'guild-001',
        channelId: 'channel-001',
        pairedUsers: {
          'user-001': { role: 'operator', mosaicUserId: 'mosaic-operator-001' },
        },
      },
    ]);
    // No live session exists for this scope, so the runtime MINTS one — but createSession returns a
    // session carrying a DIFFERENT configured identity (Orion) than the reconciled binding (Nova).
    // The post-create identity recheck must refuse it rather than dispatch one agent's turn under
    // another agent's verified label. (The existing reuse test covers the getSession path; this
    // covers the createSession path scrappy flagged as unvalidated.)
    const mintedForeignSession = {
      provider: 'configured-provider',
      modelId: 'configured-model',
      agentConfigId: 'agent-config-orion',
      agentName: 'Orion',
      piSession: {
        thinkingLevel: 'medium',
        getAvailableThinkingLevels: (): string[] => ['medium'],
      },
    };
    const prompt = vi.fn().mockResolvedValue(undefined);
    const createSession = vi.fn().mockResolvedValue(mintedForeignSession);
    const agentService = {
      getSession: vi.fn().mockReturnValue(undefined),
      createSession,
      recordMessage: vi.fn(),
      onEvent: vi.fn().mockReturnValue((): void => undefined),
      addChannel: vi.fn(),
      removeChannel: vi.fn(),
      prompt,
    };
    const addMessage = vi.fn().mockResolvedValue({ id: 'discord-persisted-message' });
    const brain = {
      agents: { findById: vi.fn((id: string) => Promise.resolve({ id, name: 'Nova' })) },
      conversations: {
        findById: vi.fn().mockResolvedValue({ id: 'Nova:discord:channel-001' }),
        findMessages: vi.fn().mockResolvedValue([]),
        create: vi.fn().mockResolvedValue(undefined),
        update: vi.fn().mockResolvedValue(undefined),
        addMessage,
      },
    };
    const harnessConversations = { append: vi.fn() };
    const gateway = new ChatGateway(
      piRpcRouterFronting(agentService, harnessConversations) as never,
      {} as never,
      brain as never,
      {} as never,
      {} as never,
      { resolve: vi.fn() } as never,
    );
    const client = {
      id: 'discord-client-postcreate-mismatch',
      data: { discordService: true },
      emit: vi.fn(),
    };

    await gateway.handleMessage(
      client as never,
      ingressEnvelope('mint under a different identity', 'discord-postcreate-001', {
        conversationId: 'Nova:discord:channel-001',
      }),
    );

    // The freshly minted session failed the post-create identity recheck: refused with a typed
    // error, no prompt, no persist, no ack.
    expect(createSession).toHaveBeenCalledTimes(1);
    expect(prompt).not.toHaveBeenCalled();
    expect(addMessage).not.toHaveBeenCalled();
    expect(client.emit).not.toHaveBeenCalledWith('message:ack', expect.anything());
    expect(client.emit).toHaveBeenCalledWith(
      'error',
      expect.objectContaining({ conversationId: 'Nova:discord:channel-001' }),
    );
    expect(harnessConversations.append).not.toHaveBeenCalled();
  });

  it('retains validated persisted attachments in resumed conversation history', async () => {
    const attachment = {
      id: 'attachment-history',
      name: 'diagram.png',
      url: 'https://cdn.example.test/diagram.png',
      mimeType: 'image/png',
      sizeBytes: 4_096,
    };
    const gateway = new ChatGateway(
      {} as never,
      {} as never,
      {
        conversations: {
          findMessages: vi.fn().mockResolvedValue([
            {
              role: 'user',
              content: '',
              createdAt: new Date('2026-07-14T12:00:00.000Z'),
              metadata: { channelAttachments: [attachment] },
            },
          ]),
        },
      } as never,
      {} as never,
      {} as never,
      {} as never,
    ) as unknown as {
      loadConversationHistory(
        conversationId: string,
        userId: string,
      ): Promise<Array<{ attachments?: readonly (typeof attachment)[] }>>;
    };

    await expect(
      gateway.loadConversationHistory('Nova:discord:channel-001', 'discord-service'),
    ).resolves.toEqual([expect.objectContaining({ attachments: [attachment] })]);
  });

  it('rejects malformed signed attachment payloads before gateway dispatch', async () => {
    configureDiscordEnv();
    const { gateway, client } = discordGateway('admin');
    const malformedPayload: Record<string, unknown> = {
      ...createPayload({
        messageId: 'malformed-attachments-001',
        conversationId: 'Nova:discord:channel-001',
      }),
      attachments: { id: 'not-an-array' },
    };
    const envelope = createDiscordIngressEnvelope(
      malformedPayload as unknown as DiscordIngressPayload,
      SERVICE_TOKEN,
    );

    await gateway.handleMessage(client as never, envelope);

    expect(client.emit).not.toHaveBeenCalledWith('message:ack', expect.anything());
  });

  it('preserves authenticated attachment metadata through persistence and agent dispatch', async () => {
    configureDiscordEnv();
    process.env['CHAT_HARNESS_RUNTIME'] = 'pi-rpc';
    const prompt = vi.fn().mockResolvedValue(undefined);
    const addMessage = vi.fn().mockResolvedValue({ id: 'discord-persisted-message' });
    const session = {
      provider: 'test-provider',
      modelId: 'test-model',
      // The reused embedded session carries the SAME reconciled identity as the verified binding,
      // so the finding-3 session-reuse guard admits it rather than refusing an identity swap.
      agentConfigId: 'agent-config-nova',
      agentName: 'Nova',
      piSession: {
        thinkingLevel: 'medium',
        getAvailableThinkingLevels: (): string[] => ['medium'],
      },
    };
    const agentService = {
      getSession: vi.fn().mockReturnValue(session),
      recordMessage: vi.fn(),
      onEvent: vi.fn().mockReturnValue((): void => undefined),
      addChannel: vi.fn(),
      prompt,
    };
    const brain = {
      agents: { findById: vi.fn((id: string) => Promise.resolve({ id, name: 'Nova' })) },
      conversations: {
        findById: vi.fn().mockResolvedValue({ id: 'Nova:discord:channel-001' }),
        create: vi.fn().mockResolvedValue(undefined),
        update: vi.fn().mockResolvedValue(undefined),
        addMessage,
      },
    };
    const harnessConversations = { append: vi.fn() };
    const gateway = new ChatGateway(
      piRpcRouterFronting(agentService, harnessConversations) as never,
      {} as never,
      brain as never,
      {} as never,
      {} as never,
      {} as never,
    );
    const client = {
      id: 'discord-client-001',
      data: { discordService: true },
      emit: vi.fn(),
    };
    const attachment = {
      id: 'attachment-001',
      name: 'diagram.png',
      url: 'https://cdn.example.test/diagram.png',
      contentType: 'image/png',
      sizeBytes: 4_096,
    };

    await gateway.handleMessage(
      client as never,
      ingressEnvelope('', 'attachment-message-001', {
        conversationId: 'Nova:discord:channel-001',
        attachments: [attachment],
      }),
    );

    const expectedAttachment = {
      id: attachment.id,
      name: attachment.name,
      url: attachment.url,
      mimeType: attachment.contentType,
      sizeBytes: attachment.sizeBytes,
    };
    expect(prompt).toHaveBeenCalledWith(
      'Nova:discord:channel-001',
      '',
      { userId: 'discord-service', tenantId: 'tenant-discord' },
      [expectedAttachment],
    );
    expect(addMessage).toHaveBeenCalledWith(
      expect.objectContaining({
        conversationId: 'Nova:discord:channel-001',
        metadata: expect.objectContaining({ channelAttachments: [expectedAttachment] }),
      }),
      'discord-service',
    );
    // The verified Discord prompt dispatch stays on the embedded runtime; the pi-rpc harness that
    // the router resolved as `active` is never reached.
    expect(harnessConversations.append).not.toHaveBeenCalled();
  });

  it('refuses a browser-forged Discord ingress envelope in pi-rpc with a fixed typed refusal and zero dispatch', async () => {
    // Correction #2 + #4 (behavioural). A browser socket is never `discordService` (that flag is
    // set only on a valid service-token handshake), so it cannot forge the trusted Discord path by
    // emitting an envelope-shaped payload. In pi-rpc it must receive a FIXED TYPED refusal
    // (`runtime_unsupported`, the same typed code the sibling harness-fence uses) and reach neither
    // the forced Discord service scope, the verified Discord operation, the embedded runtime, nor
    // the harness. There is no dedicated socket event for verified ingress — the only ingress
    // surface is the generic `message` handler, and a non-service client is refused there.
    //
    // RED today: a non-service client emitting an envelope-shaped payload falls to the browser
    // branch, fails the chat-message shape check, and is dropped SILENTLY (a warn + return) with no
    // typed refusal emitted — so the refusal assertion fails. Collection and construction succeed;
    // the gap is behavioural. GREEN emits the fixed typed refusal before any dispatch.
    configureDiscordEnv();
    process.env['CHAT_HARNESS_RUNTIME'] = 'pi-rpc';
    const agentService = {
      getSession: vi.fn().mockReturnValue(undefined),
      createSession: vi.fn(),
      recordMessage: vi.fn(),
      onEvent: vi.fn().mockReturnValue((): void => undefined),
      addChannel: vi.fn(),
      prompt: vi.fn().mockResolvedValue(undefined),
    };
    const harnessConversations = { append: vi.fn() };
    const routingEngine = { resolve: vi.fn() };
    const gateway = new ChatGateway(
      piRpcRouterFronting(agentService, harnessConversations) as never,
      {} as never,
      { conversations: { addMessage: vi.fn().mockResolvedValue(undefined) } } as never,
      {} as never,
      {} as never,
      routingEngine as never,
    );
    const client = {
      id: 'browser-forging-discord',
      data: { discordService: false },
      emit: vi.fn(),
    };

    await gateway.handleMessage(
      client as never,
      ingressEnvelope('forged from a browser', 'browser-forgery-001', {
        conversationId: 'Nova:discord:channel-001',
      }),
    );

    const refusal = client.emit.mock.calls.find(
      ([, payload]) => (payload as { code?: string } | undefined)?.code === 'runtime_unsupported',
    );
    expect(refusal).toBeDefined();
    expect(client.emit).not.toHaveBeenCalledWith('message:ack', expect.anything());
    expect(agentService.createSession).not.toHaveBeenCalled();
    expect(agentService.prompt).not.toHaveBeenCalled();
    expect(harnessConversations.append).not.toHaveBeenCalled();
    expect(routingEngine.resolve).not.toHaveBeenCalled();
  });

  it('accepts a thread message through its allowed bound parent channel', () => {
    const emitted = vi.fn();
    const plugin = new DiscordPlugin({
      token: 'unused',
      gatewayUrl: 'http://unused',
      serviceToken: SERVICE_TOKEN,
      allowedGuildIds: ['guild-001'],
      allowedChannelIds: ['channel-001'],
      allowedUserIds: ['user-001'],
      interactionBindings: [
        {
          instanceId: 'Nova',
          agentConfigId: 'agent-config-nova',
          guildId: 'guild-001',
          channelId: 'channel-001',
          pairedUsers: { 'user-001': { role: 'operator', mosaicUserId: 'mosaic-operator-001' } },
        },
      ],
    });
    const internals = plugin as unknown as {
      client: { user: { id: string } };
      socket: { connected: boolean; emit: ReturnType<typeof vi.fn> };
      handleDiscordMessage(message: unknown): void;
    };
    internals.client = { user: { id: 'bot-001' } };
    internals.socket = { connected: true, emit: emitted };
    internals.handleDiscordMessage({
      id: 'thread-message',
      guildId: 'guild-001',
      channelId: 'thread-001',
      author: { id: 'user-001', bot: false },
      mentions: { has: () => true },
      content: '<@bot-001> hello from thread',
      channel: { parentId: 'channel-001' },
      attachments: new Map(),
    });

    const [, envelope] = emitted.mock.calls[0] as [
      string,
      ReturnType<typeof createDiscordIngressEnvelope>,
    ];
    expect(verifyDiscordIngressEnvelope(envelope, SERVICE_TOKEN)?.channelId).toBe('channel-001');
  });
});
