import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import express, { type RequestHandler } from 'express';
import request from 'supertest';
import {
  mountToolsApi,
  _resetToolCatalogCacheForTests,
  type McpCatalogDeps,
  type ToolCatalogEntry,
} from './tools-api.js';
import { setSshSubsystem, type SshSubsystem } from '../engine/tools/ssh.js';

/**
 * Build an express app exposing /api/tools. `user` controls what
 * (req as any).user is set to before the catalog handler runs.
 */
function makeApp(opts?: {
  user?: { id?: string; role?: string } | null;
  authActive?: boolean;
  mcp?: McpCatalogDeps | null;
}): express.Application {
  const app = express();
  app.use(express.json());
  if (opts?.user !== null) {
    const u = opts?.user ?? { id: 'u1', role: 'user' };
    app.use((req, _res, next) => {
      (req as unknown as { user: typeof u }).user = u;
      // supertest doesn't have passport; fake isAuthenticated.
      (req as unknown as { isAuthenticated: () => boolean }).isAuthenticated = () => true;
      next();
    });
  }
  const requireAuth: RequestHandler = (req, res, next) => {
    if ((req as unknown as { user?: { id?: string } }).user?.id) {
      next();
    } else {
      res.status(401).json({ error: 'Unauthorized' });
    }
  };
  mountToolsApi(app, {
    authActive: opts?.authActive ?? false,
    requireAuth,
    mcp: opts?.mcp ?? null,
  });
  return app;
}

describe('GET /api/tools (runtime catalog)', () => {
  beforeEach(() => {
    _resetToolCatalogCacheForTests();
    setSshSubsystem(null);
  });

  afterEach(() => {
    setSshSubsystem(null);
    _resetToolCatalogCacheForTests();
  });

  it('returns ToolCatalogResponse with builtin tools', async () => {
    const res = await request(makeApp()).get('/api/tools');
    expect(res.status).toBe(200);
    expect(Array.isArray(res.body.tools)).toBe(true);
    const names = (res.body.tools as ToolCatalogEntry[]).map((t) => t.name);
    // Core builtin tools must be present.
    expect(names).toContain('Read');
    expect(names).toContain('Write');
    expect(names).toContain('Bash');
  });

  it('tags core tools with source=builtin and category=core', async () => {
    const res = await request(makeApp()).get('/api/tools');
    const read = (res.body.tools as ToolCatalogEntry[]).find((t) => t.name === 'Read');
    expect(read).toBeDefined();
    expect(read!.source).toBe('builtin');
    expect(read!.category).toBe('core');
    expect(read!.scope).toBe('piece');
    expect(read!.available).toBe(true);
  });

  it('includes meta tools tagged source=meta scope=global', async () => {
    const res = await request(makeApp()).get('/api/tools');
    const tools = res.body.tools as ToolCatalogEntry[];
    const readDoc = tools.find((t) => t.name === 'ReadToolDoc');
    expect(readDoc).toBeDefined();
    expect(readDoc!.source).toBe('meta');
    expect(readDoc!.scope).toBe('global');
    expect(readDoc!.available).toBe(true);

    const brainstorm = tools.find((t) => t.name === 'Brainstorm');
    expect(brainstorm?.source).toBe('meta');
    expect(brainstorm?.scope).toBe('global');
  });

  it('marks ssh tools available=false with reason when SSH subsystem is not initialised', async () => {
    setSshSubsystem(null);
    const res = await request(makeApp()).get('/api/tools');
    const ssh = (res.body.tools as ToolCatalogEntry[]).find((t) => t.name === 'SshExec');
    // SSH module may not exist in some lean builds; only assert when present.
    if (ssh) {
      expect(ssh.category).toBe('ssh');
      expect(ssh.available).toBe(false);
      expect(ssh.reason).toMatch(/SSH subsystem not initialised/);
      expect(ssh.scope).toBe('piece');
    }
  });

  it('marks ssh tools available=true when subsystem is initialised', async () => {
    setSshSubsystem({} as SshSubsystem);
    const res = await request(makeApp()).get('/api/tools');
    const ssh = (res.body.tools as ToolCatalogEntry[]).find((t) => t.name === 'SshExec');
    if (ssh) {
      expect(ssh.available).toBe(true);
      expect(ssh.reason).toBeUndefined();
    }
  });

  it('includes MCP tools for authenticated user', async () => {
    const mcp: McpCatalogDeps = {
      registry: {
        listEnabledForUser: () => [
          { id: 'canva', name: 'Canva', enabled: true },
        ],
      },
      tokenManager: {
        hasToken: () => true,
      },
      toolCache: {
        getAllForServers: () => [
          { serverId: 'canva', toolName: 'createDesign' },
          { serverId: 'canva', toolName: 'listDesigns' },
        ],
      },
    };
    const res = await request(makeApp({ mcp })).get('/api/tools');
    const tools = res.body.tools as ToolCatalogEntry[];
    const create = tools.find((t) => t.name === 'mcp__canva__createDesign');
    expect(create).toBeDefined();
    expect(create!.source).toBe('mcp');
    expect(create!.category).toBe('mcp:canva');
    expect(create!.serverId).toBe('canva');
    expect(create!.scope).toBe('user');
    expect(create!.available).toBe(true);
  });

  it('marks MCP tools unavailable with reason when user is not connected (offline)', async () => {
    const mcp: McpCatalogDeps = {
      registry: {
        listEnabledForUser: () => [{ id: 'gh', name: 'GitHub', enabled: true }],
      },
      tokenManager: {
        hasToken: () => false, // user not connected
      },
      toolCache: {
        getAllForServers: () => [{ serverId: 'gh', toolName: 'listIssues' }],
      },
    };
    const res = await request(makeApp({ mcp })).get('/api/tools');
    const tool = (res.body.tools as ToolCatalogEntry[]).find(
      (t) => t.name === 'mcp__gh__listIssues',
    );
    expect(tool).toBeDefined();
    expect(tool!.available).toBe(false);
    expect(tool!.reason).toMatch(/offline/);
  });

  it('omits MCP tools when caller has no user id (unauthenticated)', async () => {
    const mcp: McpCatalogDeps = {
      registry: {
        listEnabledForUser: () => [{ id: 'canva', name: 'Canva', enabled: true }],
      },
      tokenManager: { hasToken: () => true },
      toolCache: {
        getAllForServers: () => [{ serverId: 'canva', toolName: 'createDesign' }],
      },
    };
    const res = await request(makeApp({ user: null, mcp })).get('/api/tools');
    expect(res.status).toBe(200);
    const tools = res.body.tools as ToolCatalogEntry[];
    expect(tools.some((t) => t.source === 'mcp')).toBe(false);
  });

  it('returns 401 when authActive=true and caller is not authenticated', async () => {
    const res = await request(makeApp({ user: null, authActive: true })).get('/api/tools');
    expect(res.status).toBe(401);
  });

  it('?legacy=1 returns flat array of tool names', async () => {
    const res = await request(makeApp()).get('/api/tools?legacy=1');
    expect(res.status).toBe(200);
    expect(Array.isArray(res.body.tools)).toBe(true);
    // Must be string[], NOT objects.
    for (const t of res.body.tools as unknown[]) {
      expect(typeof t).toBe('string');
    }
    expect(res.body.tools).toContain('Read');
    expect(res.body.tools).toContain('ReadToolDoc');
  });

  it('?legacy=1 includes per-user MCP names when authenticated', async () => {
    const mcp: McpCatalogDeps = {
      registry: {
        listEnabledForUser: () => [{ id: 'canva', name: 'Canva', enabled: true }],
      },
      tokenManager: { hasToken: () => true },
      toolCache: {
        getAllForServers: () => [{ serverId: 'canva', toolName: 'createDesign' }],
      },
    };
    const res = await request(makeApp({ mcp })).get('/api/tools?legacy=1');
    expect(res.body.tools).toContain('mcp__canva__createDesign');
  });

  it('surfaces a placeholder entry when MCP server has no cached tools', async () => {
    const mcp: McpCatalogDeps = {
      registry: {
        listEnabledForUser: () => [{ id: 'fresh', name: 'Fresh', enabled: true }],
      },
      tokenManager: { hasToken: () => false },
      toolCache: { getAllForServers: () => [] },
    };
    const res = await request(makeApp({ mcp })).get('/api/tools');
    const placeholder = (res.body.tools as ToolCatalogEntry[]).find(
      (t) => t.serverId === 'fresh',
    );
    expect(placeholder).toBeDefined();
    expect(placeholder!.available).toBe(false);
    expect(placeholder!.reason).toMatch(/offline|no cached tools/);
    expect(placeholder!.scope).toBe('user');
  });
});
