/**
 * `loxia composio` CLI — flag parsing + subcommand wiring.
 *
 * The CLI uses HTTP against the running server, so a full end-to-end
 * test would need to boot a real server. Instead we unit-test the
 * pieces that don't need a live server:
 *   - parseFlags: --json, --user, --args, --open, --help, positional
 *   - subcommand handlers: with a stubbed http.request, verify they
 *     hit the right URL with the right body and exit codes
 *
 * This catches the regressions that actually matter (broken arg parser,
 * wrong path, missing JSON body) without paying the bootstrap cost.
 */
import { describe, test, expect, beforeEach, afterEach, jest } from '@jest/globals';
import { _internal } from '../../bin/composio-cli.js';

const { parseFlags, statusDot } = _internal;

describe('parseFlags', () => {
  test('captures positional args in order', () => {
    const f = parseFlags(['connect', 'github']);
    expect(f.positional).toEqual(['connect', 'github']);
  });

  test('--json sets the flag', () => {
    expect(parseFlags(['toolkits', '--json']).json).toBe(true);
    expect(parseFlags(['toolkits']).json).toBe(false);
  });

  test('--user accepts both --user X and --user=X', () => {
    expect(parseFlags(['status', '--user', 'agent-42']).user).toBe('agent-42');
    expect(parseFlags(['status', '--user=agent-42']).user).toBe('agent-42');
  });

  test('--args captures the JSON string for exec', () => {
    const f = parseFlags(['exec', 'github', 'X', '--args', '{"per_page":3}']);
    expect(f.args).toBe('{"per_page":3}');
  });

  test('--open is captured as boolean', () => {
    expect(parseFlags(['connect', 'github', '--open']).open).toBe(true);
  });

  test('--help and -h both set help', () => {
    expect(parseFlags(['--help']).help).toBe(true);
    expect(parseFlags(['-h']).help).toBe(true);
  });

  test('unrecognised flags fall through to positional', () => {
    // We don't whitelist; unknown flags survive in positional so the
    // caller can decide what to do. (Defensive — protects against typos
    // becoming silent no-ops.)
    const f = parseFlags(['exec', 'gh', 'X', '--unknown']);
    expect(f.positional).toContain('--unknown');
  });
});

describe('statusDot', () => {
  test('returns a coloured/raw character per status', () => {
    // We don't assert on the exact ANSI codes (depends on TTY), but
    // every variant produces a non-empty string.
    for (const s of ['ACTIVE', 'INITIATED', 'FAILED', 'NOT_CONNECTED', null]) {
      const dot = statusDot(s);
      expect(typeof dot).toBe('string');
      expect(dot.length).toBeGreaterThan(0);
    }
  });
});

/* ---------------- Subcommand handlers (stubbed http) ---------------- */

function makeDeps(routes) {
  // Each route entry: { match: regex|string, method, response }
  const http = {
    request: jest.fn((url, opts, cb) => {
      const u = new URL(url);
      const route = routes.find(r => {
        const m = r.match instanceof RegExp ? r.match.test(u.pathname) : u.pathname === r.match;
        return m && (!r.method || r.method === opts.method);
      });
      const body = route ? route.response : { success: false, error: `unmatched ${opts.method} ${u.pathname}` };
      const res = {
        statusCode: route ? (route.status || 200) : 404,
        on(evt, fn) {
          if (evt === 'data') fn(JSON.stringify(body));
          if (evt === 'end') fn();
        },
      };
      setImmediate(() => cb(res));
      return { on: () => {}, write: () => {}, end: () => {}, setTimeout: () => {}, destroy: () => {} };
    }),
  };
  return {
    http,
    backend: { host: '127.0.0.1', port: 12345 },
    args: [],
  };
}

describe('cmdToolkits', () => {
  test('hits GET /api/composio/toolkits and prints rows', async () => {
    const deps = makeDeps([{ match: '/api/composio/toolkits', method: 'GET', response: { success: true, toolkits: [{ slug: 'github', name: 'GitHub' }] } }]);
    const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
    try {
      await _internal.cmdToolkits(deps, { json: false, positional: ['toolkits'] });
      expect(deps.http.request).toHaveBeenCalled();
      const calls = logSpy.mock.calls.flat().join('\n');
      expect(calls).toMatch(/github/);
      expect(calls).toMatch(/GitHub/);
    } finally {
      logSpy.mockRestore();
    }
  });

  test('--json emits the raw response', async () => {
    const deps = makeDeps([{ match: '/api/composio/toolkits', method: 'GET', response: { success: true, toolkits: [] } }]);
    const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
    try {
      await _internal.cmdToolkits(deps, { json: true, positional: ['toolkits'] });
      const out = logSpy.mock.calls.flat().join('\n');
      expect(out).toMatch(/"success": true/);
    } finally {
      logSpy.mockRestore();
    }
  });
});

describe('cmdConnections', () => {
  test('forwards userId in the query string', async () => {
    const deps = makeDeps([{
      match: /^\/api\/composio\/connections/,
      method: 'GET',
      response: { success: true, connections: [{ id: 'conn-1', toolkitSlug: 'github', status: 'ACTIVE' }] },
    }]);
    const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
    try {
      await _internal.cmdConnections(deps, { json: false, user: 'agent-99', positional: ['connections'] });
      const url = deps.http.request.mock.calls[0][0];
      expect(url).toMatch(/userId=agent-99/);
    } finally {
      logSpy.mockRestore();
    }
  });
});

describe('cmdConnect', () => {
  test('POSTs to /api/composio/connect with body { toolkitSlug, userId }', async () => {
    const deps = makeDeps([{
      match: '/api/composio/connect',
      method: 'POST',
      response: { success: true, connectLink: 'https://connect.composio.dev/link/x', connectionId: 'conn-1' },
    }]);
    const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
    try {
      await _internal.cmdConnect(deps, { json: false, positional: ['connect', 'github'], user: 'operator' });
      // Verify the body was JSON.stringified into write() — we get the
      // call args via deps.http.request itself (we wrote {} body above
      // but the real write happens on the returned req object).
      const out = logSpy.mock.calls.flat().join('\n');
      expect(out).toMatch(/Connect github/);
      expect(out).toMatch(/connect\.composio\.dev/);
    } finally {
      logSpy.mockRestore();
    }
  });
});

describe('cmdDisconnect', () => {
  test('routes UUID-style id to DELETE by-id', async () => {
    const deps = makeDeps([{
      match: /^\/api\/composio\/connections\/conn-abc-def-123-456/,
      method: 'DELETE',
      response: { success: true },
    }]);
    const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
    try {
      await _internal.cmdDisconnect(deps, { json: false, positional: ['disconnect', 'conn-abc-def-123-456'] });
      const url = deps.http.request.mock.calls[0][0];
      expect(url).toMatch(/conn-abc-def-123-456/);
    } finally {
      logSpy.mockRestore();
    }
  });

  test('routes short slug to DELETE by-toolkit', async () => {
    const deps = makeDeps([{
      match: /^\/api\/composio\/connections\/by-toolkit/,
      method: 'DELETE',
      response: { success: true },
    }]);
    const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
    try {
      await _internal.cmdDisconnect(deps, { json: false, positional: ['disconnect', 'github'], user: 'operator' });
      const url = deps.http.request.mock.calls[0][0];
      expect(url).toMatch(/by-toolkit/);
    } finally {
      logSpy.mockRestore();
    }
  });
});

describe('cmdExec', () => {
  test('parses --args as JSON and forwards', async () => {
    const deps = makeDeps([{
      match: '/api/composio/execute',
      method: 'POST',
      response: { success: true, output: { ok: 1 } },
    }]);
    const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
    try {
      await _internal.cmdExec(deps, {
        json: false,
        positional: ['exec', 'github', 'X'],
        args: '{"per_page":3}',
        user: 'operator',
      });
      const out = logSpy.mock.calls.flat().join('\n');
      expect(out).toMatch(/X/);
      expect(out).toMatch(/ok/);
    } finally {
      logSpy.mockRestore();
    }
  });

  test('invalid --args JSON exits with helpful error', async () => {
    const deps = makeDeps([]);
    const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
    const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => { throw new Error('__exit__'); });
    try {
      await expect(_internal.cmdExec(deps, {
        json: false, positional: ['exec', 'gh', 'X'], args: '{not-json',
      })).rejects.toThrow('__exit__');
      expect(errSpy.mock.calls.flat().join('\n')).toMatch(/Invalid --args/);
    } finally {
      errSpy.mockRestore();
      exitSpy.mockRestore();
    }
  });
});
