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 { 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',
] 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['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',
      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: 'discord-channel-001',
    content: 'hello Tess',
    ...overrides,
  };
}

describe('Discord ingress security', () => {
  it('keeps legacy role-only bindings valid while withholding privileged actor identity', () => {
    const [binding] = parseDiscordInteractionBindings(
      JSON.stringify([
        {
          instanceId: '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',
          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.each([
    [
      'binding',
      () => {
        process.env['MOSAIC_AGENT_NAME'] = 'Other';
      },
    ],
    [
      'durable session',
      (durable: { getSnapshot: ReturnType<typeof vi.fn> }) => {
        durable.getSnapshot.mockResolvedValueOnce({
          identity: { agentName: 'Other', providerId: 'fleet', runtimeSessionId: 'runtime-1' },
        });
      },
    ],
  ])(
    'rejects approval when the %s targets a different runtime agent',
    async (_source, configure) => {
      configureDiscordEnv();
      const { gateway, client, durable } = discordGateway('admin');
      configure(durable);

      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 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('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',
          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');
  });
});
