import { describe, expect, it, vi } from 'vitest';
import { ChatGateway } from './chat.gateway.js';

const CONVERSATION_ID = 'conversation-1';
const CANARY = 'sk_canary12345678';

type GatewayInternals = {
  clientSessions: Map<string, unknown>;
  relayEvent(client: unknown, conversationId: string, event: unknown): void;
};

function buildGateway() {
  const brain = {
    conversations: {
      addMessage: vi.fn().mockResolvedValue(undefined),
    },
  };
  const agentService = {
    getSession: vi.fn().mockReturnValue(undefined),
  };
  const gateway = new ChatGateway(
    agentService as never,
    {} as never,
    brain as never,
    {} as never,
    {} as never,
    {} as never,
  );

  return { gateway: gateway as unknown as GatewayInternals, brain };
}

describe('ChatGateway redaction boundary', (): void => {
  it('redacts a secret split across assistant deltas before egress and persistence', (): void => {
    const { gateway } = buildGateway();
    const client = {
      connected: true,
      id: 'client-1',
      data: { user: { id: 'user-1' } },
      emit: vi.fn(),
    };
    const session = {
      conversationId: CONVERSATION_ID,
      cleanup: vi.fn(),
      assistantText: '',
      toolCalls: [],
      pendingToolCalls: new Map(),
      scope: { userId: 'user-1', tenantId: 'tenant-1' },
    };
    gateway.clientSessions.set(client.id, session);

    gateway.relayEvent(client, CONVERSATION_ID, {
      type: 'message_update',
      assistantMessageEvent: { type: 'text_delta', delta: 'sk_canary' },
    });

    expect(JSON.stringify(client.emit.mock.calls)).not.toContain('sk_canary');

    gateway.relayEvent(client, CONVERSATION_ID, {
      type: 'message_update',
      assistantMessageEvent: { type: 'text_delta', delta: '12345678 ' },
    });

    expect(client.emit).toHaveBeenCalledWith('agent:text', {
      conversationId: CONVERSATION_ID,
      text: '[REDACTED_SECRET] ',
    });
    expect(session.assistantText).toBe(`${CANARY} `);
    expect(JSON.stringify(client.emit.mock.calls)).not.toContain(CANARY);
  });

  it('retains a split secret label until its value can be redacted', (): void => {
    const { gateway } = buildGateway();
    const client = {
      connected: true,
      id: 'client-1',
      data: { user: { id: 'user-1' } },
      emit: vi.fn(),
    };

    gateway.relayEvent(client, CONVERSATION_ID, {
      type: 'message_update',
      assistantMessageEvent: { type: 'text_delta', delta: 'token ' },
    });
    gateway.relayEvent(client, CONVERSATION_ID, {
      type: 'message_update',
      assistantMessageEvent: { type: 'text_delta', delta: '=canaryvalue123 ' },
    });

    expect(client.emit).toHaveBeenCalledWith('agent:text', {
      conversationId: CONVERSATION_ID,
      text: '[REDACTED_SECRET] ',
    });
    expect(JSON.stringify(client.emit.mock.calls)).not.toContain('canaryvalue123');
  });

  it('holds a streamed private key until it can be redacted', (): void => {
    const { gateway } = buildGateway();
    const client = {
      connected: true,
      id: 'client-1',
      data: { user: { id: 'user-1' } },
      emit: vi.fn(),
    };

    gateway.relayEvent(client, CONVERSATION_ID, {
      type: 'message_update',
      assistantMessageEvent: { type: 'text_delta', delta: '-----BEGIN PRIVATE KEY-----\ncanary' },
    });
    gateway.relayEvent(client, CONVERSATION_ID, {
      type: 'message_update',
      assistantMessageEvent: { type: 'text_delta', delta: '\n-----END PRIVATE KEY-----' },
    });

    expect(client.emit).toHaveBeenCalledWith('agent:text', {
      conversationId: CONVERSATION_ID,
      text: '[REDACTED_SECRET]',
    });
    expect(JSON.stringify(client.emit.mock.calls)).not.toContain('canary');
  });

  it('drops an oversized unterminated stream fragment rather than retaining it', (): void => {
    const { gateway } = buildGateway();
    const client = {
      connected: true,
      id: 'client-1',
      data: { user: { id: 'user-1' } },
      emit: vi.fn(),
    };

    gateway.relayEvent(client, CONVERSATION_ID, {
      type: 'message_update',
      assistantMessageEvent: { type: 'text_delta', delta: 'x'.repeat(8_193) },
    });

    expect(client.emit).toHaveBeenCalledWith('agent:text', {
      conversationId: CONVERSATION_ID,
      text: '[REDACTED_STREAM_OVERFLOW]',
    });
  });

  it('persists only redacted assistant content with classifications', (): void => {
    const { gateway, brain } = buildGateway();
    const client = {
      connected: true,
      id: 'client-1',
      data: { user: { id: 'user-1' } },
      emit: vi.fn(),
    };
    gateway.clientSessions.set(client.id, {
      conversationId: CONVERSATION_ID,
      cleanup: vi.fn(),
      assistantText: CANARY,
      toolCalls: [],
      pendingToolCalls: new Map(),
      scope: { userId: 'user-1', tenantId: 'tenant-1' },
    });

    gateway.relayEvent(client, CONVERSATION_ID, { type: 'agent_end' });

    expect(brain.conversations.addMessage).toHaveBeenCalledWith(
      expect.objectContaining({
        content: '[REDACTED_SECRET]',
        metadata: expect.objectContaining({ classifications: ['secret'] }),
      }),
      'user-1',
    );
    expect(JSON.stringify(brain.conversations.addMessage.mock.calls)).not.toContain(CANARY);
  });
});
