/**
 * Addressing Modes Test
 *
 * Tests addressing modes implemented in @open-matrix/federation:
 * 1. Realm-only addressing (anonymous)
 * 2. Tab realm addressing (flowpad-demo.tab.xyz)
 * 3. ~tab URI normalization
 * 4. Principal-based addressing (anon, jwt, service)
 * 5. Cross-realm with principal context
 *
 * Note: Session directory tests removed (SessionDirectory/SessionsFacade deleted in cleanup)
 *
 * Run:
 *   npx tsx --test tests/integration/federation/addressing-modes.spec.ts
 */

import { describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import {
  FederationPeer, ExactInstanceResolver,
  WIRE_PROFILES,
  InMemoryFilterBroker,
  InMemoryExactBroker,
  getLegacyIngressTopic,
  getMxIngressTopic,
  getInternalTopic,
  createLegacyRequest,
  createMxEnvV1,
  serializeLegacyEnvelope,
  serializeMxEnvV1,
  parseMatrixUri,
  CrossRealmClient,
} from '@open-matrix/federation';

function sleep(ms: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, ms));
}

function generateCorrelationId(): string {
  return `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
}

// =============================================================================
// 1. REALM-ONLY ADDRESSING (Anonymous)
// =============================================================================

describe('Realm-Only Addressing', () => {
  it('anonymous request from realm A to realm B', async () => {
    const REALM_A = 'client-realm';
    const REALM_B = 'server-realm';

    const backbone = new InMemoryExactBroker();
    const localBusA = new InMemoryFilterBroker();
    const localBusB = new InMemoryFilterBroker();

    const gatewayA = new FederationPeer({
      localRealm: REALM_A,
      profile: WIRE_PROFILES.matrix3,
      backbone,
      localBus: localBusA,
    });

    const gatewayB = new FederationPeer({
      localRealm: REALM_B,
      profile: WIRE_PROFILES.matrix3,
      backbone,
      localBus: localBusB,
    });

    gatewayA.start();
    gatewayB.start();

    // Handler on B - receives internal format, replies to replyTo topic
    localBusB.subscribe(getInternalTopic(REALM_B, 'echo.accepts.ping'), (topic, payload) => {
      const req = JSON.parse(payload);
      // Reply to the internal replyTo topic - gateway will forward to backbone
      localBusB.publish(req.replyTo, JSON.stringify({
        ok: true,
        correlationId: req.correlationId,
        result: { pong: true, from: REALM_B },
      }));
    });

    await sleep(100);

    // A listens on its backbone mailbox for replies
    const correlationId = generateCorrelationId();
    let reply: unknown = null;

    backbone.subscribe(getLegacyIngressTopic(REALM_A), (topic, payload) => {
      const parsed = JSON.parse(payload);
      if (parsed.correlationId === correlationId) {
        reply = parsed;
      }
    });

    await sleep(100);

    const request = createLegacyRequest({
      toRealm: REALM_B,
      toPath: 'echo.accepts.ping',
      fromRealm: REALM_A,
      replyTo: '_client.inbox',
      correlationId,
      body: { msg: 'hello' },
    });

    backbone.publish(getLegacyIngressTopic(REALM_B), serializeLegacyEnvelope(request));

    await sleep(500);

    assert.ok(reply, 'Should receive reply');
    // Reply body is in 'body' field, not 'result'
    assert.strictEqual((reply as { body: { pong: boolean } }).body.pong, true);

    console.log('  ✓ Anonymous realm-only addressing works');

    gatewayA.stop();
    gatewayB.stop();
  });
});

// =============================================================================
// 2. TAB REALM ADDRESSING
// =============================================================================

describe('Tab Realm Addressing', () => {
  it('tab realm format: home.tab.xyz communicates with home realm', async () => {
    const HOME_REALM = 'flowpad-demo';
    const TAB_REALM = 'flowpad-demo.tab.abc123';

    const backbone = new InMemoryExactBroker();
    const homeBus = new InMemoryFilterBroker();
    const tabBus = new InMemoryFilterBroker();

    // Home realm gateway
    const homeGateway = new FederationPeer({
      localRealm: HOME_REALM,
      profile: WIRE_PROFILES.mxenv1,
      backbone,
      localBus: homeBus,
    });

    // Tab realm gateway
    const tabGateway = new FederationPeer({
      localRealm: TAB_REALM,
      profile: WIRE_PROFILES.mxenv1,
      backbone,
      localBus: tabBus,
    });

    homeGateway.start();
    tabGateway.start();

    // Home realm handler - replies to internal replyTo topic (gateway forwards to backbone)
    homeBus.subscribe(getInternalTopic(HOME_REALM, 'service.accepts.getData'), (topic, payload) => {
      const req = JSON.parse(payload);
      // Reply to the internal replyTo topic - gateway handles forwarding
      homeBus.publish(req.replyTo, JSON.stringify({
        ok: true,
        correlationId: req.correlationId,
        result: { data: 'from home realm', tabRealm: TAB_REALM },
      }));
    });

    await sleep(100);

    // Tab listens on its backbone mailbox for replies
    let tabReceived: unknown = null;
    backbone.subscribe(getMxIngressTopic(TAB_REALM, '00'), (topic, payload) => {
      const parsed = JSON.parse(payload);
      if (parsed.msgType === 'Result') {
        tabReceived = parsed;
      }
    });

    await sleep(100);

    const correlationId = generateCorrelationId();
    // NOTE: createMxEnvV1 uses replyToRealm/replyToPath, NOT replyTo object
    const request = createMxEnvV1({
      msgType: 'Cmd',
      toRealm: HOME_REALM,
      toPath: 'service.accepts.getData',
      fromRealm: TAB_REALM,
      fromPath: '_client',
      replyToRealm: TAB_REALM,
      replyToPath: '_inbox.emits.reply',
      correlationId,
      ttl: 8,
      body: { query: 'test' },
    });

    backbone.publish(getMxIngressTopic(HOME_REALM, '00'), serializeMxEnvV1(request));

    await sleep(500);

    assert.ok(tabReceived, 'Tab should receive reply from home');
    assert.strictEqual((tabReceived as { body: { tabRealm: string } }).body.tabRealm, TAB_REALM);

    console.log('  ✓ Tab realm addressing (home.tab.xyz) works');

    homeGateway.stop();
    tabGateway.stop();
  });

  it('multiple tabs under same home realm are isolated', async () => {
    const HOME_REALM = 'app';
    const TAB_1 = 'app.tab.tab1';
    const TAB_2 = 'app.tab.tab2';
    const TAB_3 = 'app.tab.tab3';

    const backbone = new InMemoryExactBroker();
    const tab1Bus = new InMemoryFilterBroker();
    const tab2Bus = new InMemoryFilterBroker();
    const tab3Bus = new InMemoryFilterBroker();

    const tab1Gw = new FederationPeer({ localRealm: TAB_1, profile: WIRE_PROFILES.mxenv1, backbone, localBus: tab1Bus });
    const tab2Gw = new FederationPeer({ localRealm: TAB_2, profile: WIRE_PROFILES.mxenv1, backbone, localBus: tab2Bus });
    const tab3Gw = new FederationPeer({ localRealm: TAB_3, profile: WIRE_PROFILES.mxenv1, backbone, localBus: tab3Bus });

    tab1Gw.start();
    tab2Gw.start();
    tab3Gw.start();

    const received: Record<string, number> = { tab1: 0, tab2: 0, tab3: 0 };

    tab1Bus.subscribe(getInternalTopic(TAB_1, 'notify.emits.event'), () => { received.tab1++; });
    tab2Bus.subscribe(getInternalTopic(TAB_2, 'notify.emits.event'), () => { received.tab2++; });
    tab3Bus.subscribe(getInternalTopic(TAB_3, 'notify.emits.event'), () => { received.tab3++; });

    await sleep(100);

    // Send event ONLY to tab2
    const evt = createMxEnvV1({
      msgType: 'Evt',
      toRealm: TAB_2,
      toPath: 'notify.emits.event',
      fromRealm: 'broadcaster',
      fromPath: '_sender',
      ttl: 8,
      body: { msg: 'only for tab2' },
    });

    backbone.publish(getMxIngressTopic(TAB_2, '00'), serializeMxEnvV1(evt));

    await sleep(300);

    assert.strictEqual(received.tab1, 0, 'Tab1 should NOT receive');
    assert.strictEqual(received.tab2, 1, 'Tab2 should receive');
    assert.strictEqual(received.tab3, 0, 'Tab3 should NOT receive');

    console.log('  ✓ Multiple tabs are isolated - only tab2 received');

    tab1Gw.stop();
    tab2Gw.stop();
    tab3Gw.stop();
  });
});

// =============================================================================
// 3. ~TAB URI NORMALIZATION
// =============================================================================

describe('~tab URI Normalization', () => {
  it('parses ~tab in legacy compact form', () => {
    const uri = 'flowpad-demo/~tab/xyz/root.ui.app.accepts.ping';
    const parsed = parseMatrixUri(uri);

    assert.ok(parsed, 'Should parse URI');
    assert.strictEqual(parsed.realm, 'flowpad-demo.tab.xyz', 'Realm should be normalized');
    assert.strictEqual(parsed.toPath, 'root.ui.app.accepts.ping');

    console.log('  ✓ ~tab normalization in legacy compact form');
  });

  it('parses ~tab in segmented MXURI/1 form', () => {
    const uri = 'flowpad-demo/~tab/abc123/-/root/ui/app/accepts/ping';
    const parsed = parseMatrixUri(uri);

    assert.ok(parsed, 'Should parse URI');
    assert.strictEqual(parsed.realm, 'flowpad-demo.tab.abc123', 'Realm should be normalized');
    assert.strictEqual(parsed.componentPath, 'root.ui.app');
    assert.strictEqual(parsed.plane, 'accepts');
    assert.strictEqual(parsed.messageName, 'ping');

    console.log('  ✓ ~tab normalization in segmented MXURI/1 form');
  });

  it('handles query parameters with ~tab', () => {
    const uri = 'home/~tab/session-42/service.accepts.query?depth=3&format=json';
    const parsed = parseMatrixUri(uri);

    assert.ok(parsed, 'Should parse URI');
    assert.strictEqual(parsed.realm, 'home.tab.session-42');
    assert.strictEqual(parsed.query.depth, '3');
    assert.strictEqual(parsed.query.format, 'json');

    console.log('  ✓ ~tab with query parameters');
  });
});

// =============================================================================
// 4. PRINCIPAL-BASED ADDRESSING
// =============================================================================

describe('Principal-Based Addressing', () => {
  it('MXENV/1 envelope with JWT principal', async () => {
    const REALM_A = 'client';
    const REALM_B = 'server';

    const backbone = new InMemoryExactBroker();
    const busA = new InMemoryFilterBroker();
    const busB = new InMemoryFilterBroker();

    const gwA = new FederationPeer({ localRealm: REALM_A, profile: WIRE_PROFILES.mxenv1, backbone, localBus: busA });
    const gwB = new FederationPeer({ localRealm: REALM_B, profile: WIRE_PROFILES.mxenv1, backbone, localBus: busB });

    gwA.start();
    gwB.start();

    // EdgeGateway passes principalKey (string), not full principal object
    // JWT principal derives to: `${iss}:${sub}` = "https://auth.example.com:user-123"
    let receivedPrincipalKey: string | undefined = undefined;

    busB.subscribe(getInternalTopic(REALM_B, 'auth.accepts.check'), (topic, payload) => {
      const req = JSON.parse(payload);
      receivedPrincipalKey = req._mx?.principalKey;
      busB.publish(req.replyTo, JSON.stringify({
        ok: true,
        correlationId: req.correlationId,
        result: { authenticated: true },
      }));
    });

    await sleep(100);

    // Send request with JWT principal
    // NOTE: createMxEnvV1 doesn't support principal directly - we add it manually
    const request = createMxEnvV1({
      msgType: 'Cmd',
      toRealm: REALM_B,
      toPath: 'auth.accepts.check',
      fromRealm: REALM_A,
      fromPath: '_client',
      replyToRealm: REALM_A,
      replyToPath: '_inbox',
      correlationId: generateCorrelationId(),
      ttl: 8,
      body: { action: 'verify' },
    });
    // Add principal manually (createMxEnvV1 doesn't support it)
    request.principal = {
      kind: 'jwt',
      iss: 'https://auth.example.com',
      sub: 'user-123',
      aud: ['server'],
      scopes: ['read', 'write'],
    };

    backbone.publish(getMxIngressTopic(REALM_B, '00'), serializeMxEnvV1(request));

    await sleep(300);

    // Check principalKey is derived correctly: iss:sub = "https://auth.example.com:user-123"
    assert.ok(receivedPrincipalKey, 'Handler should receive principalKey');
    assert.strictEqual(receivedPrincipalKey, 'https://auth.example.com:user-123');

    console.log('  ✓ JWT principal in MXENV/1 envelope → principalKey: ' + receivedPrincipalKey);

    gwA.stop();
    gwB.stop();
  });

  it('MXENV/1 envelope with service principal', async () => {
    const REALM = 'api-server';
    const FROM_REALM = 'scheduler-service';

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

    const gw = new FederationPeer({ localRealm: REALM, profile: WIRE_PROFILES.mxenv1, backbone, localBus });
    gw.start();

    // Service principal derives to: `service:${sub}` = "service:cron-scheduler"
    let receivedPrincipalKey: string | undefined = undefined;

    localBus.subscribe(getInternalTopic(REALM, 'internal.accepts.sync'), (topic, payload) => {
      const req = JSON.parse(payload);
      receivedPrincipalKey = req._mx?.principalKey;
      // Reply so the gateway doesn't timeout
      localBus.publish(req.replyTo, JSON.stringify({
        ok: true,
        correlationId: req.correlationId,
        result: { synced: true },
      }));
    });

    await sleep(100);

    // Send with service principal - MUST include replyTo and correlationId for Cmd messages
    const request = createMxEnvV1({
      msgType: 'Cmd',
      toRealm: REALM,
      toPath: 'internal.accepts.sync',
      fromRealm: FROM_REALM,
      fromPath: '_scheduler',
      replyToRealm: FROM_REALM,
      replyToPath: '_inbox',
      correlationId: generateCorrelationId(),
      ttl: 8,
      body: { task: 'cleanup' },
    });
    // Add principal manually
    request.principal = {
      kind: 'service',
      sub: 'cron-scheduler',
      scopes: ['internal:sync'],
    };

    backbone.publish(getMxIngressTopic(REALM, '00'), serializeMxEnvV1(request));

    await sleep(300);

    // Check principalKey is derived correctly: service:sub = "service:cron-scheduler"
    assert.ok(receivedPrincipalKey, 'Handler should receive principalKey');
    assert.strictEqual(receivedPrincipalKey, 'service:cron-scheduler');

    console.log('  ✓ Service principal in MXENV/1 envelope → principalKey: ' + receivedPrincipalKey);

    gw.stop();
  });

  it('anonymous principal when none specified', async () => {
    const REALM = 'public-api';
    const FROM_REALM = 'anonymous-client';

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

    const gw = new FederationPeer({ localRealm: REALM, profile: WIRE_PROFILES.mxenv1, backbone, localBus });
    gw.start();

    // Anonymous principal derives to: `anon:${fromRealm}` = "anon:anonymous-client"
    let receivedPrincipalKey: string | undefined = undefined;

    localBus.subscribe(getInternalTopic(REALM, 'public.accepts.query'), (topic, payload) => {
      const req = JSON.parse(payload);
      receivedPrincipalKey = req._mx?.principalKey;
      // Reply so gateway doesn't timeout
      localBus.publish(req.replyTo, JSON.stringify({
        ok: true,
        correlationId: req.correlationId,
        result: { found: true },
      }));
    });

    await sleep(100);

    // Send without principal (anonymous)
    const request = createMxEnvV1({
      msgType: 'Cmd',
      toRealm: REALM,
      toPath: 'public.accepts.query',
      fromRealm: FROM_REALM,
      fromPath: '_anon',
      // NO principal field - will derive to anon
      replyToRealm: FROM_REALM,
      replyToPath: '_inbox',
      correlationId: generateCorrelationId(),
      ttl: 8,
      body: { q: 'search' },
    });

    backbone.publish(getMxIngressTopic(REALM, '00'), serializeMxEnvV1(request));

    await sleep(300);

    // Check principalKey is derived correctly: anon:fromRealm = "anon:anonymous-client"
    assert.ok(receivedPrincipalKey, 'Handler should receive principalKey');
    assert.strictEqual(receivedPrincipalKey, 'anon:anonymous-client');

    console.log('  ✓ Anonymous principal → principalKey: ' + receivedPrincipalKey);

    gw.stop();
  });
});

// =============================================================================
// 5. CROSS-REALM CLIENT WITH DISCOVERY
// =============================================================================

describe('Cross-Realm Client', () => {
  it('invoke with automatic reply handling', async () => {
    const SERVER_REALM = 'calculator-service';
    const CLIENT_REALM = 'client-app';

    const backbone = new InMemoryExactBroker();
    const serverBus = new InMemoryFilterBroker();

    const serverGw = new FederationPeer({
      localRealm: SERVER_REALM,
      profile: WIRE_PROFILES.mxenv1,
      backbone,
      localBus: serverBus,
    });
    serverGw.start();

    // Server handler
    serverBus.subscribe(getInternalTopic(SERVER_REALM, 'math.accepts.add'), (topic, payload) => {
      const req = JSON.parse(payload);
      const { a, b } = req.params;
      const result = a + b;

      // Reply
      const reply = createMxEnvV1({
        msgType: 'Result',
        toRealm: req._mx?.replyTo?.realm ?? CLIENT_REALM,
        toPath: req._mx?.replyTo?.path ?? '_inbox',
        fromRealm: SERVER_REALM,
        fromPath: 'math',
        correlationId: req.correlationId,
        body: { sum: result },
      });
      backbone.publish(getMxIngressTopic(req._mx?.replyTo?.realm ?? CLIENT_REALM, '00'), serializeMxEnvV1(reply));
    });

    await sleep(100);

    // Client uses CrossRealmClient
    const client = new CrossRealmClient({
      localRealm: CLIENT_REALM,
      transport: backbone,
      profile: WIRE_PROFILES.mxenv1,
      defaultTimeoutMs: 5000,
    });

    const result = await client.invoke<{ sum: number }>(SERVER_REALM, 'math.accepts.add', { a: 15, b: 27 });

    assert.strictEqual(result.sum, 42);
    console.log('  ✓ CrossRealmClient invoke: 15 + 27 = 42');

    serverGw.stop();
  });
});

// =============================================================================
// SUMMARY
// =============================================================================

describe('Addressing Modes Summary', () => {
  it('documents all tested addressing modes', () => {
    const modes = [
      { name: 'Realm-only (anonymous)', tested: true },
      { name: 'Tab realm (home.tab.xyz)', tested: true },
      { name: '~tab URI normalization', tested: true },
      { name: 'JWT principal', tested: true },
      { name: 'Service principal', tested: true },
      { name: 'Anonymous principal', tested: true },
      { name: 'CrossRealmClient invoke', tested: true },
    ];

    console.log('\n  ┌────────────────────────────────────────────┐');
    console.log('  │         ADDRESSING MODES COVERAGE          │');
    console.log('  ├────────────────────────────────────────────┤');
    for (const m of modes) {
      console.log(`  │ ${m.tested ? '✓' : '✗'} ${m.name.padEnd(38)} │`);
    }
    console.log('  └────────────────────────────────────────────┘');

    assert.ok(modes.every(m => m.tested), 'All addressing modes should be tested');
  });
});
