/**
 * Composio REST endpoints — exercised with the same inline-app +
 * http.createServer + fetch pattern as bulkAgentRoute.test.js (no
 * supertest dependency).
 *
 * We don't boot the real webServer; instead, we mount a tiny Express
 * app that wires the same handlers against a stubbed composioService.
 * That keeps the tests fast and avoids the full Loxia bootstrap, while
 * still exercising the actual handler logic (request parsing, status
 * codes, response shape).
 */
import { describe, test, expect, beforeEach, afterEach, jest } from '@jest/globals';
import express from 'express';
import http from 'http';

function makeStubService() {
  return {
    isAvailable:       jest.fn().mockResolvedValue({ available: true, keySource: 'vault' }),
    setApiKey:         jest.fn().mockResolvedValue({ success: true }),
    forgetApiKey:      jest.fn().mockResolvedValue({ success: true, cleared: true }),
    listToolkits:      jest.fn().mockResolvedValue({ success: true, toolkits: [{ slug: 'github', name: 'GitHub' }] }),
    listConnections:   jest.fn().mockResolvedValue({ success: true, connections: [{ id: 'conn-1', toolkitSlug: 'github', status: 'ACTIVE' }] }),
    connectionStatus:  jest.fn().mockResolvedValue({ success: true, connected: true, status: 'ACTIVE', connectionId: 'conn-1' }),
    connect:           jest.fn().mockResolvedValue({ success: true, connectLink: 'https://connect.composio.dev/link/x', connectionId: 'conn-2' }),
    disconnect:        jest.fn().mockResolvedValue({ success: true }),
    listTools:         jest.fn().mockResolvedValue({ success: true, tools: [{ slug: 'GITHUB_X' }] }),
    execute:           jest.fn().mockResolvedValue({ success: true, output: { issueNumber: 42 } }),
  };
}

/**
 * Mount the same set of routes used by webServer.js. The handler bodies
 * below MUST mirror what's in webServer.js — keep them in sync.
 */
function mountRoutes(app, composioService, OPERATOR_USER = 'operator') {
  app.use(express.json());

  app.get('/api/composio/availability', async (req, res) => {
    try { const out = await composioService.isAvailable(); res.json({ success: true, ...out }); }
    catch (err) { res.status(500).json({ success: false, error: err.message }); }
  });

  app.post('/api/composio/apikey', async (req, res) => {
    try {
      const { apiKey } = req.body || {};
      if (typeof apiKey !== 'string' || apiKey.trim().length === 0) {
        return res.status(400).json({ success: false, error: 'apiKey is required' });
      }
      const r = await composioService.setApiKey(apiKey);
      if (!r.success) return res.status(400).json(r);
      const probe = await composioService.listToolkits();
      if (!probe.success) {
        await composioService.forgetApiKey();
        return res.status(400).json({
          success: false,
          error: `Key rejected by Composio: ${probe.error || 'unknown error'}`,
        });
      }
      res.json({ success: true, keySource: 'vault' });
    } catch (err) { res.status(500).json({ success: false, error: err.message }); }
  });

  app.delete('/api/composio/apikey', async (req, res) => {
    try { res.json(await composioService.forgetApiKey()); }
    catch (err) { res.status(500).json({ success: false, error: err.message }); }
  });

  app.get('/api/composio/toolkits', async (req, res) => {
    try { res.json(await composioService.listToolkits()); }
    catch (err) { res.status(500).json({ success: false, error: err.message }); }
  });

  app.get('/api/composio/connections', async (req, res) => {
    try {
      const userId = req.query.userId || OPERATOR_USER;
      res.json(await composioService.listConnections(userId));
    } catch (err) { res.status(500).json({ success: false, error: err.message }); }
  });

  app.get('/api/composio/toolkits/:slug/status', async (req, res) => {
    try {
      const userId = req.query.userId || OPERATOR_USER;
      res.json(await composioService.connectionStatus(userId, req.params.slug));
    } catch (err) { res.status(500).json({ success: false, error: err.message }); }
  });

  app.get('/api/composio/toolkits/:slug/tools', async (req, res) => {
    try {
      const userId = req.query.userId || undefined;
      res.json(await composioService.listTools(req.params.slug, userId));
    } catch (err) { res.status(500).json({ success: false, error: err.message }); }
  });

  app.post('/api/composio/connect', async (req, res) => {
    try {
      const { toolkitSlug, authConfigId } = req.body || {};
      const userId = req.body?.userId || OPERATOR_USER;
      if (!toolkitSlug) return res.status(400).json({ success: false, error: 'toolkitSlug is required' });
      res.json(await composioService.connect(userId, toolkitSlug, { authConfigId }));
    } catch (err) { res.status(500).json({ success: false, error: err.message }); }
  });

  app.delete('/api/composio/connections/:id', async (req, res) => {
    try {
      const userId = req.query.userId || OPERATOR_USER;
      const id = req.params.id;
      if (id && id !== 'by-toolkit') {
        return res.json(await composioService.disconnect({ connectionId: id }));
      }
      const { toolkitSlug } = req.body || {};
      if (!toolkitSlug) return res.status(400).json({ success: false, error: 'toolkitSlug required when no connectionId is given' });
      res.json(await composioService.disconnect({ userId, toolkitSlug }));
    } catch (err) { res.status(500).json({ success: false, error: err.message }); }
  });

  app.post('/api/composio/execute', async (req, res) => {
    try {
      const { toolkitSlug, actionSlug, arguments: args } = req.body || {};
      const userId = req.body?.userId || OPERATOR_USER;
      if (!toolkitSlug || !actionSlug) {
        return res.status(400).json({ success: false, error: 'toolkitSlug and actionSlug are required' });
      }
      res.json(await composioService.execute(userId, toolkitSlug, actionSlug, args || {}));
    } catch (err) { res.status(500).json({ success: false, error: err.message }); }
  });
}

let server;
let baseUrl;
let stubService;

beforeEach(async () => {
  stubService = makeStubService();
  const app = express();
  mountRoutes(app, stubService);
  server = http.createServer(app);
  await new Promise(resolve => server.listen(0, resolve));
  const { port } = server.address();
  baseUrl = `http://127.0.0.1:${port}`;
});
afterEach(async () => {
  if (server) await new Promise(r => server.close(r));
});

async function get(path) {
  const r = await fetch(`${baseUrl}${path}`);
  return { status: r.status, body: await r.json() };
}
async function post(path, body) {
  const r = await fetch(`${baseUrl}${path}`, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify(body),
  });
  return { status: r.status, body: await r.json() };
}
async function del(path, body) {
  const r = await fetch(`${baseUrl}${path}`, {
    method: 'DELETE',
    headers: { 'content-type': 'application/json' },
    body: body ? JSON.stringify(body) : undefined,
  });
  return { status: r.status, body: await r.json() };
}

describe('GET /api/composio/availability', () => {
  test('returns availability result + success:true', async () => {
    const r = await get('/api/composio/availability');
    expect(r.status).toBe(200);
    expect(r.body).toEqual({ success: true, available: true, keySource: 'vault' });
    expect(stubService.isAvailable).toHaveBeenCalled();
  });
});

describe('POST /api/composio/apikey', () => {
  test('rejects missing apiKey with 400', async () => {
    const r = await post('/api/composio/apikey', {});
    expect(r.status).toBe(400);
    expect(r.body.error).toMatch(/apiKey is required/i);
  });

  test('rejects empty string with 400', async () => {
    const r = await post('/api/composio/apikey', { apiKey: '   ' });
    expect(r.status).toBe(400);
  });

  test('persists, probes, returns success with keySource', async () => {
    const r = await post('/api/composio/apikey', { apiKey: 'comp_abcdef123456' });
    expect(r.status).toBe(200);
    expect(r.body).toEqual({ success: true, keySource: 'vault' });
    expect(stubService.setApiKey).toHaveBeenCalledWith('comp_abcdef123456');
    expect(stubService.listToolkits).toHaveBeenCalled();
  });

  test('rejects when Composio rejects the key (rolls it back)', async () => {
    stubService.listToolkits.mockResolvedValue({ success: false, error: '401 Unauthorized' });
    const r = await post('/api/composio/apikey', { apiKey: 'bad-key-but-long-enough' });
    expect(r.status).toBe(400);
    expect(r.body.error).toMatch(/Key rejected/i);
    // Rollback
    expect(stubService.forgetApiKey).toHaveBeenCalled();
  });

  test('surfaces a setApiKey validation failure (e.g. too short)', async () => {
    stubService.setApiKey.mockResolvedValue({ success: false, error: 'API key looks too short.' });
    const r = await post('/api/composio/apikey', { apiKey: 'short' });
    expect(r.status).toBe(400);
    expect(r.body.error).toMatch(/too short/i);
    // Did NOT call listToolkits because setApiKey already failed.
    expect(stubService.listToolkits).not.toHaveBeenCalled();
  });
});

describe('DELETE /api/composio/apikey', () => {
  test('clears the stored key', async () => {
    const r = await del('/api/composio/apikey');
    expect(r.status).toBe(200);
    expect(r.body).toEqual({ success: true, cleared: true });
    expect(stubService.forgetApiKey).toHaveBeenCalled();
  });
});

describe('GET /api/composio/toolkits', () => {
  test('returns the toolkit list', async () => {
    const r = await get('/api/composio/toolkits');
    expect(r.body.success).toBe(true);
    expect(r.body.toolkits[0].slug).toBe('github');
  });
});

describe('GET /api/composio/connections', () => {
  test('defaults to operator userId', async () => {
    await get('/api/composio/connections');
    expect(stubService.listConnections).toHaveBeenCalledWith('operator');
  });
  test('honours ?userId override', async () => {
    await get('/api/composio/connections?userId=agent-42');
    expect(stubService.listConnections).toHaveBeenCalledWith('agent-42');
  });
});

describe('GET /api/composio/toolkits/:slug/status', () => {
  test('forwards slug + default userId', async () => {
    const r = await get('/api/composio/toolkits/github/status');
    expect(r.body.connected).toBe(true);
    expect(stubService.connectionStatus).toHaveBeenCalledWith('operator', 'github');
  });
});

describe('GET /api/composio/toolkits/:slug/tools', () => {
  test('returns the action list', async () => {
    const r = await get('/api/composio/toolkits/github/tools');
    expect(r.body.tools[0].slug).toBe('GITHUB_X');
    expect(stubService.listTools).toHaveBeenCalledWith('github', undefined);
  });
});

describe('POST /api/composio/connect', () => {
  test('rejects missing toolkitSlug with 400', async () => {
    const r = await post('/api/composio/connect', {});
    expect(r.status).toBe(400);
    expect(r.body.error).toMatch(/toolkitSlug/);
  });

  test('happy path returns the Connect Link', async () => {
    const r = await post('/api/composio/connect', { toolkitSlug: 'github' });
    expect(r.status).toBe(200);
    expect(r.body.connectLink).toMatch(/^https:\/\/connect\.composio\.dev/);
    expect(stubService.connect).toHaveBeenCalledWith('operator', 'github', { authConfigId: undefined });
  });

  test('passes through authConfigId + custom userId', async () => {
    await post('/api/composio/connect', { toolkitSlug: 'gh', authConfigId: 'cfg-1', userId: 'opX' });
    expect(stubService.connect).toHaveBeenCalledWith('opX', 'gh', { authConfigId: 'cfg-1' });
  });
});

describe('DELETE /api/composio/connections/:id', () => {
  test('by connectionId', async () => {
    const r = await del('/api/composio/connections/conn-1');
    expect(r.body.success).toBe(true);
    expect(stubService.disconnect).toHaveBeenCalledWith({ connectionId: 'conn-1' });
  });

  test('by toolkitSlug when id="by-toolkit"', async () => {
    const r = await del('/api/composio/connections/by-toolkit', { toolkitSlug: 'github' });
    expect(r.body.success).toBe(true);
    expect(stubService.disconnect).toHaveBeenCalledWith({ userId: 'operator', toolkitSlug: 'github' });
  });

  test('by-toolkit without slug returns 400', async () => {
    const r = await del('/api/composio/connections/by-toolkit', {});
    expect(r.status).toBe(400);
  });
});

describe('POST /api/composio/execute', () => {
  test('rejects missing toolkitSlug/actionSlug with 400', async () => {
    let r = await post('/api/composio/execute', { actionSlug: 'X' });
    expect(r.status).toBe(400);
    r = await post('/api/composio/execute', { toolkitSlug: 'gh' });
    expect(r.status).toBe(400);
  });

  test('happy path forwards arguments', async () => {
    const r = await post('/api/composio/execute', {
      toolkitSlug: 'github',
      actionSlug: 'GITHUB_CREATE_AN_ISSUE',
      arguments: { owner: 'acme', repo: 'api', title: 'Bug' },
    });
    expect(r.body.success).toBe(true);
    expect(r.body.output).toEqual({ issueNumber: 42 });
    expect(stubService.execute).toHaveBeenCalledWith(
      'operator', 'github', 'GITHUB_CREATE_AN_ISSUE',
      { owner: 'acme', repo: 'api', title: 'Bug' },
    );
  });
});
