/**
 * Unit tests for service contracts — contract consts, proxy, intent provider, resolve.
 */
import { describe, it } from 'node:test';
import * as assert from 'node:assert/strict';

import type { ServiceContract, IActorService } from '../ServiceContract';
import type { IMatrixContext } from '../IMatrixContext';
import { ContextIntentProvider } from '../ContextIntentProvider';
import type { IServiceLookup } from '../ContextIntentProvider';
import { ActorServiceProxy } from '../ActorServiceProxy';

// Import ALL contract consts
import { IMemoryService } from '../contracts/IMemoryService';
import { ILoggingService } from '../contracts/ILoggingService';
import { IHealthService } from '../contracts/IHealthService';
import { ITracingService } from '../contracts/ITracingService';
import { IInferenceService } from '../contracts/IInferenceService';
import { ISchedulerService } from '../contracts/ISchedulerService';
import { IStorageService } from '../contracts/IStorageService';
import { IShellService } from '../contracts/IShellService';
import { IWorkstreamService } from '../contracts/IWorkstreamService';

const ALL_CONTRACTS: ServiceContract[] = [
  IMemoryService, ILoggingService, IHealthService, ITracingService,
  IInferenceService, ISchedulerService, IStorageService, IShellService, IWorkstreamService,
];

// ═══════════════════════════════════════════════════════════════════════════
// Contract Const Validation
// ═══════════════════════════════════════════════════════════════════════════

describe('Service contracts — contract const shape', () => {
  for (const contract of ALL_CONTRACTS) {
    it(`${contract.id} has valid shape`, () => {
      assert.ok(contract.id, 'id is required');
      assert.ok(contract.description, 'description is required');
      assert.ok(contract.ops, 'ops is required');
      assert.ok(Object.keys(contract.ops).length > 0, 'must have at least one op');

      for (const [opName, op] of Object.entries(contract.ops)) {
        assert.ok(op.description, `${opName} needs description`);
        assert.ok(op.payload !== undefined, `${opName} needs payload (use {} for none)`);
        assert.ok(op.response !== undefined, `${opName} needs response`);

        // Every response has ok: boolean
        assert.ok('ok' in op.response, `${opName} response must have 'ok' field`);
        assert.strictEqual(op.response.ok.type, 'boolean', `${opName} response.ok must be boolean`);
      }
    });
  }

  it('all contracts have unique IDs', () => {
    const ids = ALL_CONTRACTS.map(c => c.id);
    const unique = new Set(ids);
    assert.strictEqual(unique.size, ids.length, `Duplicate IDs: ${ids.join(', ')}`);
  });
});

// ═══════════════════════════════════════════════════════════════════════════
// ActorServiceProxy
// ═══════════════════════════════════════════════════════════════════════════

describe('ActorServiceProxy', () => {
  // Mock context that tracks invoke calls
  const calls: Array<{ mount: string; op: string; payload: unknown }> = [];
  const mockCtx = {
    mount: 'test',
    transport: { publish: () => {}, subscribe: () => () => {} },
    getService: () => undefined,
    setService: () => {},
  } as unknown as IMatrixContext;

  it('supports() returns true for known ops', () => {
    const proxy = new ActorServiceProxy(mockCtx, 'system.memory', IMemoryService);
    assert.strictEqual(proxy.supports('memory.save'), true);
    assert.strictEqual(proxy.supports('memory.search'), true);
    assert.strictEqual(proxy.supports('memory.get'), true);
  });

  it('supports() returns false for unknown ops', () => {
    const proxy = new ActorServiceProxy(mockCtx, 'system.memory', IMemoryService);
    assert.strictEqual(proxy.supports('budget.check'), false);
    assert.strictEqual(proxy.supports('nonexistent'), false);
  });

  it('invoke() rejects unknown ops without calling bus', async () => {
    const proxy = new ActorServiceProxy(mockCtx, 'system.memory', IMemoryService);
    const result = await proxy.invoke('nonexistent.op', {}) as { ok?: boolean; error?: string };
    assert.strictEqual(result.ok, false);
    assert.ok(result.error?.includes('not in contract'));
  });

  it('mount and contract are accessible', () => {
    const proxy = new ActorServiceProxy(mockCtx, 'system.health', IHealthService);
    assert.strictEqual(proxy.mount, 'system.health');
    assert.strictEqual(proxy.contract.id, 'health');
    assert.ok(proxy.supports('health.status'));
  });
});

// ═══════════════════════════════════════════════════════════════════════════
// ContextIntentProvider
// ═══════════════════════════════════════════════════════════════════════════

describe('ContextIntentProvider', () => {
  it('resolves when service is in context', async () => {
    const mockProxy = {
      mount: 'system.memory',
      contract: IMemoryService,
      supports: () => true,
      invoke: async () => ({}),
    } as IActorService;

    const lookup: IServiceLookup = {
      getService: <T>(key: string | symbol) => (key === 'memory' ? mockProxy : undefined) as T | undefined,
    };

    const provider = new ContextIntentProvider(lookup);
    const result = await provider.resolve(IMemoryService);
    assert.ok(result);
    assert.strictEqual(result!.mount, 'system.memory');
  });

  it('returns undefined when service not in context', async () => {
    const lookup: IServiceLookup = {
      getService: <T>(_key: string | symbol) => undefined as T | undefined,
    };
    const provider = new ContextIntentProvider(lookup);
    const result = await provider.resolve(IMemoryService);
    assert.strictEqual(result, undefined);
  });
});

// ═══════════════════════════════════════════════════════════════════════════
// Contract Op Counts (sanity)
// ═══════════════════════════════════════════════════════════════════════════

describe('Contract op counts', () => {
  const expected: Record<string, number> = {
    memory: 4,    // save, search, get, history
    logging: 6,   // query, search, setLevel, getLevel, getConfig, count
    health: 4,    // status, component, setThresholds, getThresholds
    tracing: 9,   // get, query, search, sample, request, thread, state, count, activity
    inference: 3, // infer, infer.stream, infer.messages
    scheduler: 6, // create, cancel, list, get, pause, resume
    sqlite: 7,    // query, execute, batch, resolve-namespace, init-schema, drop-domain, status
    shell: 6,     // create-session, evaluate, destroy-session, list-sessions, fork-session, get-session
    workstream: 4, // status, request, plan, agents
  };

  for (const contract of ALL_CONTRACTS) {
    it(`${contract.id} has ${expected[contract.id]} ops`, () => {
      const count = Object.keys(contract.ops).length;
      assert.strictEqual(count, expected[contract.id], `${contract.id}: expected ${expected[contract.id]} ops, got ${count}`);
    });
  }
});

// ═══════════════════════════════════════════════════════════════════════════
// MatrixContext.resolve() integration
// ═══════════════════════════════════════════════════════════════════════════

describe('MatrixContext.resolve()', () => {
  it('resolves when proxy is registered on context', async () => {
    // Simulate context with a registered memory proxy
    const mockProxy = {
      mount: 'system.memory',
      contract: IMemoryService,
      supports: (op: string) => op in IMemoryService.ops,
      invoke: async (op: string, payload?: Record<string, unknown>) => ({ ok: true, invoked: op }),
    } as IActorService;

    // Create a minimal context-like object
    const services = new Map<string | symbol, unknown>();
    services.set('memory', mockProxy);

    const lookup = {
      getService: <T>(key: string | symbol): T | undefined => services.get(key) as T | undefined,
      setService: <T>(key: string | symbol, val: T) => { services.set(key, val); },
    };

    // ContextIntentProvider finds it
    const provider = new ContextIntentProvider(lookup);
    const resolved = await provider.resolve(IMemoryService);
    assert.ok(resolved, 'should resolve memory proxy');
    assert.strictEqual(resolved!.mount, 'system.memory');

    // Invoke through the resolved proxy
    const result = await resolved!.invoke('memory.save', { content: 'test' }) as {
      ok?: boolean;
      invoked?: string;
    };
    assert.strictEqual(result.ok, true);
    assert.strictEqual(result.invoked, 'memory.save');
  });

  it('returns undefined when no proxy registered', async () => {
    const lookup = {
      getService: () => undefined,
      setService: () => {},
    };
    const provider = new ContextIntentProvider(lookup);
    const resolved = await provider.resolve(ISchedulerService);
    assert.strictEqual(resolved, undefined);
  });
});
