/**
 * Security Guardrails Integration Tests
 *
 * Tests the EdgeGateway's security features:
 * - Anti-relay protection
 * - Replay detection
 * - Rate limiting
 */

import { describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import {
  FederationPeer, ExactInstanceResolver,
  WIRE_PROFILES,
  InMemoryFilterBroker,
  getLegacyIngressTopic,
  getMxIngressTopic,
  ReplayCache,
  TokenBucketRateLimiter,
  type SecurityBlockReason,
  createMxEnvV1,
  serializeMxEnvV1,
} from '@open-matrix/federation';

describe('Security Guardrails', () => {

  it('rejects open-relay attempt (mxenv1)', async () => {
    const REALM = 'target-realm';

    const backbone = new InMemoryFilterBroker();
    const localBus = new InMemoryFilterBroker();

    const securityBlocks: { reason: SecurityBlockReason }[] = [];

    const gateway = new FederationPeer({
      localRealm: REALM,
      profile: WIRE_PROFILES.mxenv1,
      backbone,
      localBus,
      enforceAntiRelay: true,
      onSecurityBlock: (reason) => {
        securityBlocks.push({ reason });
      },
    });

    gateway.start();

    await new Promise(r => setTimeout(r, 50));

    // Send open-relay attempt: from attacker-realm, but replyTo victim-realm
    const maliciousEnvelope = createMxEnvV1({
      msgType: 'Cmd',
      toRealm: REALM,
      toPath: 'victim.accepts.cmd',
      fromRealm: 'attacker-realm',
      fromPath: 'attacker',
      replyToRealm: 'victim-realm',  // Different from fromRealm!
      replyToPath: '_inbox',
      correlationId: 'attack-1',
      body: { attack: true },
    });

    backbone.publish(getMxIngressTopic(REALM), serializeMxEnvV1(maliciousEnvelope));

    await new Promise(r => setTimeout(r, 200));

    assert.ok(securityBlocks.length > 0, 'Should detect open-relay attempt');
    assert.strictEqual(securityBlocks[0].reason, 'anti_relay', 'Block reason should be anti_relay');

    const metrics = gateway.getMetrics();
    assert.ok(metrics.securityBlocks.antiRelay > 0, 'Anti-relay metric should increase');

    gateway.stop();
  });

  it('rejects replay attempt (mxenv1)', async () => {
    const REALM = 'test-realm';

    const backbone = new InMemoryFilterBroker();
    const localBus = new InMemoryFilterBroker();

    const replayCache = new ReplayCache(100);
    const securityBlocks: { reason: SecurityBlockReason }[] = [];

    const gateway = new FederationPeer({
      localRealm: REALM,
      profile: WIRE_PROFILES.mxenv1,
      backbone,
      localBus,
      replayCache,
      onSecurityBlock: (reason) => {
        securityBlocks.push({ reason });
      },
    });

    gateway.start();

    // Set up a handler so the first request can complete
    localBus.subscribe(`test.accepts.cmd`, (topic, payload) => {
      const req = JSON.parse(payload) as { replyTo: string; correlationId: string };
      localBus.publish(req.replyTo, JSON.stringify({ ok: true, correlationId: req.correlationId }));
    });

    await new Promise(r => setTimeout(r, 50));

    // Create envelope with a fixed msgId
    const envelope = createMxEnvV1({
      msgType: 'Cmd',
      msgId: 'unique-msg-12345',
      toRealm: REALM,
      toPath: 'test.accepts.cmd',
      fromRealm: 'sender-realm',
      fromPath: 'sender',
      replyToRealm: 'sender-realm',
      replyToPath: '_inbox',
      correlationId: 'req-1',
      body: {},
    });

    // Send same message twice
    backbone.publish(getMxIngressTopic(REALM), serializeMxEnvV1(envelope));
    await new Promise(r => setTimeout(r, 100));
    backbone.publish(getMxIngressTopic(REALM), serializeMxEnvV1(envelope));

    await new Promise(r => setTimeout(r, 200));

    assert.ok(securityBlocks.length >= 1, 'Should detect replay attempt');
    assert.strictEqual(securityBlocks[0].reason, 'replay', 'Block reason should be replay');

    const metrics = gateway.getMetrics();
    assert.ok(metrics.securityBlocks.replay > 0, 'Replay metric should increase');

    gateway.stop();
  });

  it('enforces rate limits (mxenv1)', async () => {
    const REALM = 'rate-test';

    const backbone = new InMemoryFilterBroker();
    const localBus = new InMemoryFilterBroker();

    // Very restrictive rate limiter for testing: 3 capacity, 0 refill (never refills)
    const rateLimiter = new TokenBucketRateLimiter(3, 0);
    const securityBlocks: { reason: SecurityBlockReason }[] = [];

    const gateway = new FederationPeer({
      localRealm: REALM,
      profile: WIRE_PROFILES.mxenv1,
      backbone,
      localBus,
      rateLimiter,
      onSecurityBlock: (reason) => {
        securityBlocks.push({ reason });
      },
    });

    gateway.start();

    // Set up a handler so requests can complete
    localBus.subscribe(`test.accepts.cmd`, (topic, payload) => {
      const req = JSON.parse(payload) as { replyTo: string; correlationId: string };
      localBus.publish(req.replyTo, JSON.stringify({ ok: true, correlationId: req.correlationId }));
    });

    await new Promise(r => setTimeout(r, 50));

    // Send 6 requests rapidly (limit is 3)
    for (let i = 0; i < 6; i++) {
      const envelope = createMxEnvV1({
        msgType: 'Cmd',
        toRealm: REALM,
        toPath: 'test.accepts.cmd',
        fromRealm: 'sender',
        fromPath: 'sender',
        replyToRealm: 'sender',
        replyToPath: '_inbox',
        correlationId: `req-${i}`,
        body: {},
      });
      backbone.publish(getMxIngressTopic(REALM), serializeMxEnvV1(envelope));
    }

    await new Promise(r => setTimeout(r, 300));

    // Should have blocked at least 3 requests (6 - 3 = 3)
    const rateLimitBlocks = securityBlocks.filter(b => b.reason === 'rate_limit').length;
    assert.ok(rateLimitBlocks >= 3, `Should rate limit excess requests (blocked ${rateLimitBlocks})`);

    const metrics = gateway.getMetrics();
    assert.ok(metrics.securityBlocks.rateLimit >= 3, 'Rate limit metric should be >= 3');

    gateway.stop();
  });

  it('allows legitimate requests with anti-relay', async () => {
    const REALM = 'legit-realm';

    const backbone = new InMemoryFilterBroker();
    const localBus = new InMemoryFilterBroker();

    let receivedRequest = false;

    const gateway = new FederationPeer({
      localRealm: REALM,
      profile: WIRE_PROFILES.mxenv1,
      backbone,
      localBus,
      enforceAntiRelay: true,
    });

    gateway.start();

    // Handler that marks success
    localBus.subscribe(`test.accepts.ping`, (topic, payload) => {
      receivedRequest = true;
      const req = JSON.parse(payload) as { replyTo: string; correlationId: string };
      localBus.publish(req.replyTo, JSON.stringify({ ok: true, correlationId: req.correlationId }));
    });

    await new Promise(r => setTimeout(r, 50));

    // Legitimate request: from and replyTo have same realm
    const envelope = createMxEnvV1({
      msgType: 'Cmd',
      toRealm: REALM,
      toPath: 'test.accepts.ping',
      fromRealm: 'client-realm',
      fromPath: 'client',
      replyToRealm: 'client-realm',  // Same as fromRealm - legitimate
      replyToPath: '_inbox',
      correlationId: 'legit-1',
      body: { message: 'hello' },
    });

    backbone.publish(getMxIngressTopic(REALM), serializeMxEnvV1(envelope));

    await new Promise(r => setTimeout(r, 200));

    assert.ok(receivedRequest, 'Legitimate request should reach handler');

    const metrics = gateway.getMetrics();
    assert.strictEqual(metrics.securityBlocks.antiRelay, 0, 'No anti-relay blocks for legitimate request');

    gateway.stop();
  });

  it('tracks security metrics correctly', async () => {
    const REALM = 'metrics-test';

    const backbone = new InMemoryFilterBroker();
    const localBus = new InMemoryFilterBroker();

    const replayCache = new ReplayCache(100);
    const rateLimiter = new TokenBucketRateLimiter(2, 0);

    const gateway = new FederationPeer({
      localRealm: REALM,
      profile: WIRE_PROFILES.mxenv1,
      backbone,
      localBus,
      enforceAntiRelay: true,
      replayCache,
      rateLimiter,
    });

    gateway.start();

    // Handler
    localBus.subscribe(`test.accepts.cmd`, (topic, payload) => {
      const req = JSON.parse(payload) as { replyTo: string; correlationId: string };
      localBus.publish(req.replyTo, JSON.stringify({ ok: true, correlationId: req.correlationId }));
    });

    await new Promise(r => setTimeout(r, 50));

    const initialMetrics = gateway.getMetrics();

    // 1. Anti-relay violation
    const antiRelayEnv = createMxEnvV1({
      msgType: 'Cmd',
      toRealm: REALM,
      toPath: 'test.accepts.cmd',
      fromRealm: 'attacker',
      fromPath: 'a',
      replyToRealm: 'victim',
      replyToPath: '_inbox',
      correlationId: 'ar-1',
      body: {},
    });
    backbone.publish(getMxIngressTopic(REALM), serializeMxEnvV1(antiRelayEnv));

    // 2. Replay (send same twice)
    const replayEnv = createMxEnvV1({
      msgType: 'Cmd',
      msgId: 'replay-test-id',
      toRealm: REALM,
      toPath: 'test.accepts.cmd',
      fromRealm: 'sender',
      fromPath: 's',
      replyToRealm: 'sender',
      replyToPath: '_inbox',
      correlationId: 'rp-1',
      body: {},
    });
    backbone.publish(getMxIngressTopic(REALM), serializeMxEnvV1(replayEnv));
    await new Promise(r => setTimeout(r, 50));
    backbone.publish(getMxIngressTopic(REALM), serializeMxEnvV1(replayEnv));

    // 3. Rate limit (send 3 more, only 1 capacity left after replay)
    for (let i = 0; i < 3; i++) {
      const rlEnv = createMxEnvV1({
        msgType: 'Cmd',
        toRealm: REALM,
        toPath: 'test.accepts.cmd',
        fromRealm: 'sender',
        fromPath: 's',
        replyToRealm: 'sender',
        replyToPath: '_inbox',
        correlationId: `rl-${i}`,
        body: {},
      });
      backbone.publish(getMxIngressTopic(REALM), serializeMxEnvV1(rlEnv));
    }

    await new Promise(r => setTimeout(r, 300));

    const finalMetrics = gateway.getMetrics();

    assert.ok(
      finalMetrics.securityBlocks.antiRelay > initialMetrics.securityBlocks.antiRelay,
      'Anti-relay metric should increase'
    );
    assert.ok(
      finalMetrics.securityBlocks.replay > initialMetrics.securityBlocks.replay,
      'Replay metric should increase'
    );
    assert.ok(
      finalMetrics.securityBlocks.rateLimit > initialMetrics.securityBlocks.rateLimit,
      'Rate limit metric should increase'
    );

    gateway.stop();
  });
});
