/**
 * Layer 6: Runtime Unit Tests
 *
 * Tests for runtime infrastructure: RealmRegistry, ShardAllocator, etc.
 */
import test from 'node:test';
import assert from 'node:assert/strict';
import { RealmRegistry, type RealmEndpoint } from '@open-matrix/federation/runtime/realm_registry';
import {
  ShardAllocator,
  DEFAULT_SHARD,
  allocateShardForTab,
  getGlobalShardAllocator,
} from '@open-matrix/federation/runtime/shard_allocator';

// ============================================================================
// RealmRegistry Tests
// ============================================================================

test('RealmRegistry: register and resolve single endpoint', () => {
  const registry = new RealmRegistry();

  registry.register('acme.com', [
    { type: 'nats', url: 'nats://broker.acme.com', priority: 0 },
  ]);

  const endpoint = registry.resolve('acme.com');
  assert.ok(endpoint !== null);
  assert.equal(endpoint.type, 'nats');
  assert.equal(endpoint.url, 'nats://broker.acme.com');
  assert.equal(endpoint.priority, 0);
});

test('RealmRegistry: resolve returns highest priority endpoint', () => {
  const registry = new RealmRegistry();

  registry.register('example.com', [
    { type: 'ws', url: 'ws://secondary.example.com', priority: 10 },
    { type: 'nats', url: 'nats://primary.example.com', priority: 0 },
    { type: 'wss', url: 'wss://backup.example.com', priority: 20 },
  ]);

  const endpoint = registry.resolve('example.com');
  assert.ok(endpoint !== null);
  assert.equal(endpoint.priority, 0);
  assert.equal(endpoint.url, 'nats://primary.example.com');
});

test('RealmRegistry: resolveAll returns sorted endpoints', () => {
  const registry = new RealmRegistry();

  registry.register('multi.com', [
    { type: 'wss', url: 'wss://c.multi.com', priority: 30 },
    { type: 'ws', url: 'ws://a.multi.com', priority: 10 },
    { type: 'nats', url: 'nats://b.multi.com', priority: 20 },
  ]);

  const endpoints = registry.resolveAll('multi.com');
  assert.equal(endpoints.length, 3);
  assert.equal(endpoints[0].priority, 10);
  assert.equal(endpoints[1].priority, 20);
  assert.equal(endpoints[2].priority, 30);
});

test('RealmRegistry: resolve returns null for unknown realm', () => {
  const registry = new RealmRegistry();
  const endpoint = registry.resolve('unknown.realm');
  assert.equal(endpoint, null);
});

test('RealmRegistry: resolveAll returns empty array for unknown realm', () => {
  const registry = new RealmRegistry();
  const endpoints = registry.resolveAll('unknown.realm');
  assert.deepEqual(endpoints, []);
});

test('RealmRegistry: has() checks realm registration', () => {
  const registry = new RealmRegistry();

  assert.equal(registry.has('test.com'), false);

  registry.register('test.com', [
    { type: 'nats', url: 'nats://test.com', priority: 0 },
  ]);

  assert.equal(registry.has('test.com'), true);
  assert.equal(registry.has('other.com'), false);
});

test('RealmRegistry: realms() lists all registered realms', () => {
  const registry = new RealmRegistry();

  registry.register('alpha.com', [{ type: 'nats', url: 'nats://alpha.com', priority: 0 }]);
  registry.register('beta.com', [{ type: 'nats', url: 'nats://beta.com', priority: 0 }]);
  registry.register('gamma.com', [{ type: 'nats', url: 'nats://gamma.com', priority: 0 }]);

  const realms = registry.realms();
  assert.equal(realms.length, 3);
  assert.ok(realms.includes('alpha.com'));
  assert.ok(realms.includes('beta.com'));
  assert.ok(realms.includes('gamma.com'));
});

test('RealmRegistry: register merges endpoints', () => {
  const registry = new RealmRegistry();

  // Register initial endpoint
  registry.register('merge.com', [
    { type: 'nats', url: 'nats://a.merge.com', priority: 10 },
  ]);

  // Register additional endpoint
  registry.register('merge.com', [
    { type: 'ws', url: 'ws://b.merge.com', priority: 5 },
  ]);

  const endpoints = registry.resolveAll('merge.com');
  assert.equal(endpoints.length, 2);

  // ws has higher priority (lower number)
  assert.equal(endpoints[0].type, 'ws');
  assert.equal(endpoints[1].type, 'nats');
});

test('RealmRegistry: register updates existing endpoint by type+url', () => {
  const registry = new RealmRegistry();

  registry.register('update.com', [
    { type: 'nats', url: 'nats://broker.update.com', priority: 10 },
  ]);

  // Update same type+url with different priority
  registry.register('update.com', [
    { type: 'nats', url: 'nats://broker.update.com', priority: 5, shard: '01' },
  ]);

  const endpoints = registry.resolveAll('update.com');
  assert.equal(endpoints.length, 1);
  assert.equal(endpoints[0].priority, 5);
  assert.equal(endpoints[0].shard, '01');
});

test('RealmRegistry: deregister removes entire realm', () => {
  const registry = new RealmRegistry();

  registry.register('remove.com', [
    { type: 'nats', url: 'nats://remove.com', priority: 0 },
    { type: 'ws', url: 'ws://remove.com', priority: 1 },
  ]);

  assert.equal(registry.has('remove.com'), true);

  registry.deregister('remove.com');

  assert.equal(registry.has('remove.com'), false);
  assert.equal(registry.resolve('remove.com'), null);
});

test('RealmRegistry: deregister removes specific endpoint', () => {
  const registry = new RealmRegistry();

  registry.register('partial.com', [
    { type: 'nats', url: 'nats://a.partial.com', priority: 0 },
    { type: 'ws', url: 'ws://b.partial.com', priority: 1 },
  ]);

  registry.deregister('partial.com', { type: 'nats', url: 'nats://a.partial.com', priority: 0 });

  const endpoints = registry.resolveAll('partial.com');
  assert.equal(endpoints.length, 1);
  assert.equal(endpoints[0].type, 'ws');
});

test('RealmRegistry: deregister last endpoint removes realm', () => {
  const registry = new RealmRegistry();

  registry.register('single.com', [
    { type: 'nats', url: 'nats://single.com', priority: 0 },
  ]);

  registry.deregister('single.com', { type: 'nats', url: 'nats://single.com', priority: 0 });

  assert.equal(registry.has('single.com'), false);
});

test('RealmRegistry: watch notifies on changes', () => {
  const registry = new RealmRegistry();
  const notifications: RealmEndpoint[][] = [];

  const unsub = registry.watch('watch.com', (endpoints) => {
    notifications.push([...endpoints]);
  });

  // Register
  registry.register('watch.com', [
    { type: 'nats', url: 'nats://watch.com', priority: 0 },
  ]);

  assert.equal(notifications.length, 1);
  assert.equal(notifications[0].length, 1);

  // Add another endpoint
  registry.register('watch.com', [
    { type: 'ws', url: 'ws://watch.com', priority: 1 },
  ]);

  assert.equal(notifications.length, 2);
  assert.equal(notifications[1].length, 2);

  // Deregister
  registry.deregister('watch.com');

  assert.equal(notifications.length, 3);
  assert.equal(notifications[2].length, 0);

  unsub();
});

test('RealmRegistry: watch unsub stops notifications', () => {
  const registry = new RealmRegistry();
  let notificationCount = 0;

  const unsub = registry.watch('unsub.com', () => {
    notificationCount++;
  });

  registry.register('unsub.com', [{ type: 'nats', url: 'nats://unsub.com', priority: 0 }]);
  assert.equal(notificationCount, 1);

  unsub();

  registry.register('unsub.com', [{ type: 'ws', url: 'ws://unsub.com', priority: 1 }]);
  assert.equal(notificationCount, 1); // No change after unsub
});

test('RealmRegistry: resolve uses cache', () => {
  const registry = new RealmRegistry();

  registry.register('cache.com', [
    { type: 'nats', url: 'nats://cache.com', priority: 0 },
  ]);

  const first = registry.resolve('cache.com');
  const second = registry.resolve('cache.com');

  // Both should return the same object (cached)
  assert.equal(first, second);
});

test('RealmRegistry: register invalidates cache', () => {
  const registry = new RealmRegistry();

  registry.register('invalidate.com', [
    { type: 'nats', url: 'nats://old.invalidate.com', priority: 10 },
  ]);

  const before = registry.resolve('invalidate.com');
  assert.equal(before?.url, 'nats://old.invalidate.com');

  // Register new higher-priority endpoint
  registry.register('invalidate.com', [
    { type: 'nats', url: 'nats://new.invalidate.com', priority: 0 },
  ]);

  const after = registry.resolve('invalidate.com');
  assert.equal(after?.url, 'nats://new.invalidate.com');
});

// ============================================================================
// ShardAllocator Tests
// ============================================================================

test('ShardAllocator: allocate always returns DEFAULT_SHARD', () => {
  const allocator = new ShardAllocator();

  // Different realms should all get same shard (single-shard model)
  assert.equal(allocator.allocate('realm-1'), DEFAULT_SHARD);
  assert.equal(allocator.allocate('realm-2'), DEFAULT_SHARD);
  assert.equal(allocator.allocate('realm-3'), DEFAULT_SHARD);
});

test('ShardAllocator: get always returns DEFAULT_SHARD', () => {
  const allocator = new ShardAllocator();

  assert.equal(allocator.get('any-realm'), DEFAULT_SHARD);
  assert.equal(allocator.get('another-realm'), DEFAULT_SHARD);
});

test('ShardAllocator: release is no-op', () => {
  const allocator = new ShardAllocator();

  // Should not throw
  allocator.release('realm');
  allocator.release('nonexistent');

  // Get still works
  assert.equal(allocator.get('realm'), DEFAULT_SHARD);
});

test('ShardAllocator: allocations returns empty map (single-shard model)', () => {
  const allocator = new ShardAllocator();

  allocator.allocate('realm-1');
  allocator.allocate('realm-2');

  // Single-shard model doesn't track allocations
  const allocations = allocator.allocations();
  assert.equal(allocations.size, 0);
});

test('DEFAULT_SHARD is "00"', () => {
  assert.equal(DEFAULT_SHARD, '00');
});

test('allocateShardForTab always returns DEFAULT_SHARD', () => {
  assert.equal(allocateShardForTab('flowpad'), DEFAULT_SHARD);
  assert.equal(allocateShardForTab('flowpad.tab-abc'), DEFAULT_SHARD);
  assert.equal(allocateShardForTab('matrix-3'), DEFAULT_SHARD);
});

test('getGlobalShardAllocator returns singleton', () => {
  const a1 = getGlobalShardAllocator();
  const a2 = getGlobalShardAllocator();

  assert.equal(a1, a2);
});

// ============================================================================
// TransportSelector Tests (if exported)
// ============================================================================

test('TransportSelector: disabled mode returns memory endpoint', async () => {
  const { TransportSelector } = await import('../../src/runtime/transport_selector');
  const registry = new RealmRegistry();

  // In 'disabled' mode, always returns memory endpoint
  const selector = new TransportSelector(registry, 'disabled');
  const endpoint = await selector.select('any.realm');

  assert.equal(endpoint.type, 'memory');
  assert.equal(endpoint.url, '');
});

test('TransportSelector: auto mode checks registry first', async () => {
  const { TransportSelector } = await import('../../src/runtime/transport_selector');
  const registry = new RealmRegistry();

  // Register an endpoint
  registry.register('selectable.com', [
    { type: 'nats', url: 'nats://selectable.com', priority: 0 },
  ]);

  // In auto mode, it will check registry and probe
  // Since we can't easily probe NATS in tests, just verify getCandidates behavior
  const selector = new TransportSelector(registry, 'auto');

  // Auto mode with no working probes falls back to memory
  const endpoint = await selector.select('selectable.com');
  // Falls back to memory since NATS probe fails without real server
  assert.equal(endpoint.type, 'memory');
});

// ============================================================================
// TransportPool Tests
// ============================================================================

test('TransportPool: stores and retrieves transport', async () => {
  const { TransportPool } = await import('../../src/runtime/transport_pool');
  const pool = new TransportPool();

  const mockTransport = {
    publish: () => {},
    subscribe: () => () => {},
  };
  const mockEndpoint = { type: 'memory' as const, url: 'memory://test', priority: 0 };

  pool.set('test-realm', mockEndpoint, mockTransport);

  const result = pool.get('test-realm');
  assert.ok(result !== undefined && result !== null);
  assert.equal(result!.transport, mockTransport);
  assert.equal(result!.endpoint, mockEndpoint);
});

test('TransportPool: returns null for unknown realm', async () => {
  const { TransportPool } = await import('../../src/runtime/transport_pool');
  const pool = new TransportPool();

  const result = pool.get('nonexistent');
  assert.equal(result, null);
});

test('TransportPool: clear removes all entries', async () => {
  const { TransportPool } = await import('../../src/runtime/transport_pool');
  const pool = new TransportPool();

  const mockTransport = {
    publish: () => {},
    subscribe: () => () => {},
  };
  const mockEndpoint = { type: 'memory' as const, url: 'memory://test', priority: 0 };

  pool.set('realm-1', mockEndpoint, mockTransport);
  pool.set('realm-2', mockEndpoint, mockTransport);

  pool.clear();

  assert.equal(pool.get('realm-1'), null);
  assert.equal(pool.get('realm-2'), null);
});

// ============================================================================
// Clock Tests
// ============================================================================

test('SystemClock: nowMs returns number', async () => {
  const { SystemClock } = await import('../../src/runtime/clock');
  const clock = new SystemClock();

  const now = clock.nowMs();
  assert.equal(typeof now, 'number');
  assert.ok(now > 0);
});

test('SystemClock: nowIso returns ISO string', async () => {
  const { SystemClock } = await import('../../src/runtime/clock');
  const clock = new SystemClock();

  const iso = clock.nowIso();
  assert.equal(typeof iso, 'string');
  assert.ok(iso.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/));
});

test('TestClock: returns configured time', async () => {
  const { TestClock } = await import('../../src/runtime/clock');
  const fixedTime = 1700000000000;
  const clock = new TestClock(fixedTime);

  assert.equal(clock.nowMs(), fixedTime);

  // ISO should match fixed time
  const iso = clock.nowIso();
  const parsed = new Date(iso).getTime();
  assert.equal(parsed, fixedTime);
});

test('TestClock: advanceMs moves time forward', async () => {
  const { TestClock } = await import('../../src/runtime/clock');
  const clock = new TestClock(1000);

  assert.equal(clock.nowMs(), 1000);

  clock.advanceMs(500);
  assert.equal(clock.nowMs(), 1500);

  clock.advanceMs(1000);
  assert.equal(clock.nowMs(), 2500);
});
