import { mkdirSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { ConnectorRepo, openRuntimeDatabase, runRuntimeMigrations } from '@zeno/db/runtime';
import { createLogger } from '@zeno/logger';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
  buildMcpServersMap,
  RESERVED_AUTHORIZATION_KEY,
  RESERVED_MCP_TYPE_KEY,
  toRemoteConfig,
  toStdioConfig,
} from '@/agent/mcp-build';

const ORIGINAL_CWD = process.cwd();

let workDir: string;
const logger = createLogger({ service: 'worker-test' });

beforeEach(() => {
  workDir = join(tmpdir(), `zeno-mcp-build-${Date.now()}-${Math.random().toString(36).slice(2)}`);
  mkdirSync(join(workDir, 'agent'), { recursive: true });
  process.chdir(workDir);
});

afterEach(() => {
  process.chdir(ORIGINAL_CWD);
  rmSync(workDir, { recursive: true, force: true });
});

function makeRepo(): { repo: ConnectorRepo; close: () => void } {
  const opened = openRuntimeDatabase(':memory:');
  runRuntimeMigrations(opened.raw);
  // The new runtime baseline does not auto-seed Playwright; tests that want
  // a clean slate get one for free. (No-op kept for symmetry with prior
  // boots that seeded the connector via migration 20.)
  return {
    repo: new ConnectorRepo(opened.drizzle, {
      masterKey: Buffer.from('a'.repeat(64), 'hex'),
      profileId: 'test',
    }),
    close: opened.close,
  };
}

describe('toStdioConfig', () => {
  it('builds stdio config with env from secrets', () => {
    const config = toStdioConfig(
      {
        id: 'i',
        slug: 'echo',
        displayName: 'Echo',
        description: null,
        source: 'custom',
        catalogId: null,
        transport: 'stdio',
        command: 'node',
        args: ['fixture.js'],
        url: null,
        status: 'enabled',
        lastError: null,
        lastErrorAt: null,
        lastVerifiedAt: null,
        createdAt: '',
        updatedAt: '',
      },
      [
        { connectorId: 'i', key: 'API_KEY', value: 'k' },
        { connectorId: 'i', key: RESERVED_AUTHORIZATION_KEY, value: 'Bearer x' },
        { connectorId: 'i', key: RESERVED_MCP_TYPE_KEY, value: 'http' }, // ignored for stdio
      ],
    );
    expect(config.command).toBe('node');
    expect(config.args).toEqual(['fixture.js']);
    expect(config.env).toEqual({ API_KEY: 'k', AUTHORIZATION: 'Bearer x' });
  });

  it('returns env undefined when no secrets', () => {
    const config = toStdioConfig(
      {
        id: 'i',
        slug: 'a',
        displayName: 'A',
        description: null,
        source: 'custom',
        catalogId: null,
        transport: 'stdio',
        command: 'echo',
        args: null,
        url: null,
        status: 'enabled',
        lastError: null,
        lastErrorAt: null,
        lastVerifiedAt: null,
        createdAt: '',
        updatedAt: '',
      },
      [],
    );
    expect(config.env).toBeUndefined();
    expect(config.args).toEqual([]);
  });

  it('throws when stdio connector lacks command', () => {
    expect(() =>
      toStdioConfig(
        {
          id: 'i',
          slug: 'a',
          displayName: 'A',
          description: null,
          source: 'custom',
          catalogId: null,
          transport: 'stdio',
          command: null,
          args: null,
          url: null,
          status: 'enabled',
          lastError: null,
          lastErrorAt: null,
          lastVerifiedAt: null,
          createdAt: '',
          updatedAt: '',
        },
        [],
      ),
    ).toThrow(/no command/);
  });
});

describe('toRemoteConfig', () => {
  function makeRemote(url: string, secrets: Array<{ key: string; value: string }> = []) {
    return toRemoteConfig(
      {
        id: 'i',
        slug: 'remote',
        displayName: 'Remote',
        description: null,
        source: 'custom',
        catalogId: null,
        transport: 'remote',
        command: null,
        args: null,
        url,
        status: 'enabled',
        lastError: null,
        lastErrorAt: null,
        lastVerifiedAt: null,
        createdAt: '',
        updatedAt: '',
      },
      secrets.map((s) => ({ connectorId: 'i', ...s })),
    );
  }

  it('picks sse for /sse URL', () => {
    expect(makeRemote('https://x/sse').type).toBe('sse');
    expect(makeRemote('https://x/sse/').type).toBe('sse');
  });

  it('picks http for non-sse paths', () => {
    expect(makeRemote('https://x/mcp').type).toBe('http');
    expect(makeRemote('https://x/v1/api').type).toBe('http');
  });

  it('respects __MCP_TYPE__ override', () => {
    expect(makeRemote('https://x/sse', [{ key: RESERVED_MCP_TYPE_KEY, value: 'http' }]).type).toBe(
      'http',
    );
    expect(makeRemote('https://x/api', [{ key: RESERVED_MCP_TYPE_KEY, value: 'sse' }]).type).toBe(
      'sse',
    );
  });

  it('routes __MCP_AUTHORIZATION__ to Authorization header', () => {
    const config = makeRemote('https://x', [
      { key: RESERVED_AUTHORIZATION_KEY, value: 'Bearer abc' },
    ]);
    expect(config.headers).toEqual({ Authorization: 'Bearer abc' });
  });

  it('passes other secrets through as headers', () => {
    const config = makeRemote('https://x', [
      { key: 'X-Custom-Header', value: 'value-1' },
      { key: RESERVED_AUTHORIZATION_KEY, value: 'Bearer t' },
    ]);
    expect(config.headers).toEqual({ 'X-Custom-Header': 'value-1', Authorization: 'Bearer t' });
  });

  it('returns headers undefined when no secrets', () => {
    expect(makeRemote('https://x').headers).toBeUndefined();
  });

  it('throws when remote connector lacks url', () => {
    expect(() =>
      toRemoteConfig(
        {
          id: 'i',
          slug: 'a',
          displayName: 'A',
          description: null,
          source: 'custom',
          catalogId: null,
          transport: 'remote',
          command: null,
          args: null,
          url: null,
          status: 'enabled',
          lastError: null,
          lastErrorAt: null,
          lastVerifiedAt: null,
          createdAt: '',
          updatedAt: '',
        },
        [],
      ),
    ).toThrow(/no url/);
  });
});

describe('buildMcpServersMap', () => {
  it('returns built-ins only when RuntimeDB is empty', () => {
    const { repo, close } = makeRepo();
    const result = buildMcpServersMap({ connectorRepo: repo, logger });
    expect(result).toEqual({});
    close();
  });

  it('loads enabled stdio connector with env', () => {
    const { repo, close } = makeRepo();
    repo.create({
      slug: 'echo',
      displayName: 'Echo',
      source: 'custom',
      transport: 'stdio',
      command: 'node',
      args: ['x.js'],
      secrets: [{ key: 'TOKEN', value: 't' }],
      tools: [],
    });
    const result = buildMcpServersMap({ connectorRepo: repo, logger });
    expect(result.echo).toMatchObject({
      type: 'stdio',
      command: 'node',
      args: ['x.js'],
      env: { TOKEN: 't' },
    });
    close();
  });

  it('skips disabled connector', () => {
    const { repo, close } = makeRepo();
    repo.create({
      slug: 'echo',
      displayName: 'Echo',
      source: 'custom',
      transport: 'stdio',
      command: 'node',
      args: [],
      status: 'disabled',
      secrets: [],
      tools: [],
    });
    const result = buildMcpServersMap({ connectorRepo: repo, logger });
    expect(result.echo).toBeUndefined();
    close();
  });

  it('skips pending connector', () => {
    const { repo, close } = makeRepo();
    repo.create({
      slug: 'echo',
      displayName: 'Echo',
      source: 'custom',
      transport: 'stdio',
      command: 'node',
      args: [],
      status: 'pending',
      secrets: [],
      tools: [],
    });
    const result = buildMcpServersMap({ connectorRepo: repo, logger });
    expect(result.echo).toBeUndefined();
    close();
  });

  it('marks connector with last_error and skips it on build failure', () => {
    const { repo, close } = makeRepo();
    // stdio connector without command — toStdioConfig throws.
    repo.create({
      slug: 'broken',
      displayName: 'Broken',
      source: 'custom',
      transport: 'stdio',
      command: null,
      args: null,
      secrets: [],
      tools: [],
    });
    const result = buildMcpServersMap({ connectorRepo: repo, logger });
    expect(result.broken).toBeUndefined();
    const updated = repo.getBySlug('broken');
    expect(updated?.lastError).toMatch(/no command/);
    expect(updated?.lastErrorAt).not.toBeNull();
    close();
  });

  it('logs override when RuntimeDB connector shadows a built-in', () => {
    const { repo, close } = makeRepo();
    // We can't easily fake an agent-layer entry without writing files, but the
    // override path is exercised by the broader integration. This test verifies
    // the logger contract — we just check no throw + connector is loaded.
    repo.create({
      slug: 'zeno',
      displayName: 'Zeno',
      source: 'custom',
      transport: 'stdio',
      command: 'echo',
      args: [],
      secrets: [],
      tools: [],
    });
    const spy = vi.spyOn(logger, 'info');
    const result = buildMcpServersMap({ connectorRepo: repo, logger });
    expect(result.zeno).toBeDefined();
    spy.mockRestore();
    close();
  });

  // Spec 0042/0044: github-app-* intercept covers runtime spawn path.
  describe('github-app intercept', () => {
    function makeFakeGithubApp(cachedToken: string | null) {
      return {
        getCachedToken: vi.fn(() => cachedToken),
        getToken: vi.fn(),
        invalidateCache: vi.fn(),
        getInstallationNames: vi.fn(() => ['Test']),
        getAppId: vi.fn(() => '1'),
        bootstrap: vi.fn(),
        stop: vi.fn(),
        addInstallation: vi.fn(),
        removeInstallation: vi.fn(),
        appUninstall: vi.fn(),
      } as unknown as Parameters<typeof buildMcpServersMap>[0]['githubApp'];
    }

    function seedGithubAppConnector(
      repo: ConnectorRepo,
      installationName: string,
      envVar: string,
    ): void {
      repo.create({
        slug: `github-app-${installationName.toLowerCase()}`,
        displayName: `GitHub App — ${installationName}`,
        source: 'catalog',
        catalogId: 'github-app',
        transport: 'stdio',
        command: 'github-mcp-server',
        args: ['stdio'],
        secrets: [
          { key: '__GITHUB_INSTALLATION_ID__', value: '999' },
          { key: '__GITHUB_INSTALLATION_NAME__', value: installationName },
          { key: '__GITHUB_ENV_VAR__', value: envVar },
        ],
        tools: [],
      });
    }

    it('synthesizes GITHUB_PERSONAL_ACCESS_TOKEN from cached token', () => {
      const { repo, close } = makeRepo();
      seedGithubAppConnector(repo, 'Acme', 'GITHUB_TOKEN_ACME');
      const githubApp = makeFakeGithubApp('ghs_live_token');
      const result = buildMcpServersMap({ connectorRepo: repo, githubApp, logger });
      expect(result['github-app-acme']).toMatchObject({
        type: 'stdio',
        command: 'github-mcp-server',
        env: { GITHUB_PERSONAL_ACCESS_TOKEN: 'ghs_live_token' },
      });
      // Reserved keys MUST NOT be forwarded as env vars.
      const env = (result['github-app-acme'] as { env?: Record<string, string> } | undefined)?.env;
      expect(env).toBeDefined();
      if (env) {
        expect(env.__GITHUB_INSTALLATION_ID__).toBeUndefined();
        expect(env.__GITHUB_INSTALLATION_NAME__).toBeUndefined();
        expect(env.__GITHUB_ENV_VAR__).toBeUndefined();
      }
      close();
    });

    it('records last_error and skips when github-app singleton is null', () => {
      const { repo, close } = makeRepo();
      seedGithubAppConnector(repo, 'Acme', 'GITHUB_TOKEN_ACME');
      const result = buildMcpServersMap({ connectorRepo: repo, logger });
      expect(result['github-app-acme']).toBeUndefined();
      const updated = repo.getBySlug('github-app-acme');
      expect(updated?.lastError).toMatch(/githubApp/);
      close();
    });

    it('records last_error on cache miss (not a fatal exception)', () => {
      const { repo, close } = makeRepo();
      seedGithubAppConnector(repo, 'Acme', 'GITHUB_TOKEN_ACME');
      const githubApp = makeFakeGithubApp(null); // cache miss
      const result = buildMcpServersMap({ connectorRepo: repo, githubApp, logger });
      expect(result['github-app-acme']).toBeUndefined();
      const updated = repo.getBySlug('github-app-acme');
      expect(updated?.lastError).toMatch(/cache miss/);
      close();
    });
  });

  // Spec 0057: channels share the connectors table with kind='channel' but
  // are NOT MCP servers — the loader MUST skip them. Without this guard, the
  // loader would silently register a broken remote-MCP entry per channel
  // install (transport='remote' is a placeholder for channel rows).
  describe('kind=channel guard (spec 0057)', () => {
    it('skips rows with kind=channel even when status=enabled', () => {
      const { repo, close } = makeRepo();
      // Seed a Slack channel — looks like a remote MCP at the SQL level
      // (transport='remote'), but kind='channel' must keep it out of the map.
      repo.create({
        slug: 'slack',
        displayName: 'Slack',
        source: 'catalog',
        catalogId: 'slack',
        transport: 'remote',
        command: null,
        args: null,
        url: null,
        status: 'enabled',
        kind: 'channel',
        secrets: [
          { key: 'SLACK_APP_TOKEN', value: 'xapp-x' },
          { key: 'SLACK_BOT_TOKEN', value: 'xoxb-x' },
        ],
        tools: [],
      });
      const result = buildMcpServersMap({ connectorRepo: repo, logger });
      // No channel-derived MCP server should appear.
      expect(result.slack).toBeUndefined();
      // No last_error written either — the row is intentionally skipped, not failed.
      const slackRow = repo.getBySlug('slack');
      expect(slackRow?.lastError).toBeNull();
      close();
    });

    it('mixes channel + mcp rows correctly — only mcp ones land in the map', () => {
      const { repo, close } = makeRepo();
      // 1 channel (Slack) + 1 MCP (custom remote) — only the MCP appears.
      repo.create({
        slug: 'slack',
        displayName: 'Slack',
        source: 'catalog',
        catalogId: 'slack',
        transport: 'remote',
        status: 'enabled',
        kind: 'channel',
        secrets: [],
        tools: [],
      });
      repo.create({
        slug: 'sentry',
        displayName: 'Sentry',
        source: 'catalog',
        catalogId: 'sentry',
        transport: 'stdio',
        command: 'npx',
        args: ['-y', '@sentry/mcp-server'],
        status: 'enabled',
        kind: 'mcp',
        secrets: [{ key: 'SENTRY_ACCESS_TOKEN', value: 'tok' }],
        tools: [],
      });
      const result = buildMcpServersMap({ connectorRepo: repo, logger });
      expect(result.sentry).toBeDefined();
      expect(result.slack).toBeUndefined();
      close();
    });
  });
});
