/**
 * Smoke coverage for the MCP tool-registration surface.
 *
 * Three things we pin:
 *
 * 1. Read-scope baseline — every read tool registers regardless of
 *    write/admin presence: search_knowledge, search_multi_hop,
 *    synthesize, get_entity_profile, get_entity_timeline,
 *    find_related_entities.
 *
 * 2. brain:write gate — record_fact, retract_fact, link_entities
 *    only register when the caller has brain:write. Without it, a
 *    caller can't drive any mutation from an agent loop.
 *
 * 3. brain:admin gate — forget_entity ONLY registers under
 *    brain:admin. GDPR cascade is irreversible; we don't want it on
 *    a key that only carries brain:write.
 *
 * Inspecting registrations: the SDK keeps tools in a private field
 * `_registeredTools` (a record keyed by tool name). We cast to read
 * it for the test only — production code never touches it.
 */
import { McpService } from '../src/mcp/mcp.service';
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import type { BrainScope } from '../src/auth/api-key.types';

const stubEmbedder = {
  cacheStats: () => ({ provider: 'openai:text-embedding-3-small' }),
  getDimensions: () => 1536,
};

function buildWithScopes(scopes: BrainScope[]): McpServer {
  const svc = new McpService(
    {} as never,
    {} as never,
    {} as never,
    {} as never,
    {} as never,
    {} as never,
    {} as never,
    {} as never,
    {} as never,
    {} as never,
    {} as never,
    stubEmbedder as never,
  );
  return svc.buildServer('co_test', scopes);
}

function toolNames(server: McpServer): string[] {
  const internals = server as unknown as {
    _registeredTools: Record<string, unknown>;
  };
  return Object.keys(internals._registeredTools);
}

function resourceNames(server: McpServer): string[] {
  const internals = server as unknown as {
    _registeredResources?: Record<string, unknown>;
    _registeredResourceTemplates?: Record<string, unknown>;
  };
  return [
    ...Object.keys(internals._registeredResources ?? {}),
    ...Object.keys(internals._registeredResourceTemplates ?? {}),
  ];
}

const READ_BASELINE = [
  'search_knowledge',
  'search_multi_hop',
  'synthesize',
  'memory_diff',
  'get_entity_profile',
  'get_entity_timeline',
  'summarize_entity',
  'get_competing_facts',
  'detect_contradiction',
  'match_procedure',
  'list_procedures',
  'search_communities',
  'list_communities',
  'find_entity_communities',
  'find_related_entities',
];

describe('McpService.buildServer — scope-gated tool surface', () => {
  it('registers the read baseline with only brain:read', () => {
    const names = toolNames(buildWithScopes(['brain:read']));
    for (const t of READ_BASELINE) expect(names).toContain(t);
  });

  it('does NOT register mutation tools without brain:write', () => {
    const names = toolNames(buildWithScopes(['brain:read']));
    expect(names).not.toContain('record_fact');
    expect(names).not.toContain('retract_fact');
    expect(names).not.toContain('link_entities');
    expect(names).not.toContain('record_procedure');
    expect(names).not.toContain('retire_procedure');
  });

  it('registers mutation tools when brain:write is present', () => {
    const names = toolNames(buildWithScopes(['brain:read', 'brain:write']));
    expect(names).toContain('record_fact');
    expect(names).toContain('retract_fact');
    expect(names).toContain('link_entities');
    expect(names).toContain('record_procedure');
    expect(names).toContain('retire_procedure');
  });

  it('does NOT register forget_entity without brain:admin (even with write)', () => {
    const names = toolNames(buildWithScopes(['brain:read', 'brain:write']));
    expect(names).not.toContain('forget_entity');
  });

  it('registers forget_entity only with brain:admin', () => {
    const names = toolNames(
      buildWithScopes(['brain:read', 'brain:write', 'brain:admin']),
    );
    expect(names).toContain('forget_entity');
  });

  it('registers brain://entity/ + /timeline resources at brain:read', () => {
    const names = resourceNames(buildWithScopes(['brain:read']));
    expect(names).toContain('entity-profile');
    expect(names).toContain('entity-timeline');
  });
});

describe('McpService.health — unauthenticated probe payload', () => {
  it('returns ok, version, the read-baseline tools, and embedder hint', () => {
    const svc = new McpService(
      {} as never,
      {} as never,
      {} as never,
      {} as never,
      {} as never,
      {} as never,
      {} as never,
      {} as never,
      {} as never,
      {} as never,
      {} as never,
      stubEmbedder as never,
    );
    const health = svc.health();
    expect(health.ok).toBe(true);
    expect(health.version).toMatch(/^\d+\.\d+\.\d+$/);
    for (const t of READ_BASELINE) {
      expect(health.tools).toContain(t);
    }
    // Write- and admin-only tools must NOT leak through the unauth
    // probe — the brain:write / brain:admin gates are the wire's only
    // line of defence; surfacing them in /health would tell a probe
    // exactly what it doesn't have permission to call.
    expect(health.tools).not.toContain('record_fact');
    expect(health.tools).not.toContain('forget_entity');
    expect(health.embedder).toBe('openai:text-embedding-3-small (1536d)');
  });
});
