import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import Fastify from 'fastify';
import { pluginRoutes } from './routes.js';
import { setIpcBridge, type IpcBridge } from '../../lib/ipc-bridge.js';

vi.mock('../../lib/product-config.js', () => ({
  PRODUCT_ID: 'clawcowork',
  productConfig: {
    backendPort: 4009,
  },
}));

vi.mock('../../lib/request-context.js', () => ({
  getUserId: vi.fn(() => 'demo-user'),
}));

let app: ReturnType<typeof Fastify>;
const call = vi.fn();

beforeEach(async () => {
  setIpcBridge({
    isRunning: true,
    call,
  } as unknown as IpcBridge);

  app = Fastify({ logger: false });
  app.decorateRequest('jwtPayload', null);
  await app.register(pluginRoutes);
  call.mockReset();
});

afterEach(async () => {
  setIpcBridge(null);
  await app.close();
});

describe('plugin routes', () => {
  it('lists plugins via IPC', async () => {
    call.mockResolvedValue({
      result: { plugins: [{ name: 'alpha', version: '1.0.0' }] },
    });

    const res = await app.inject({ method: 'GET', url: '/api/plugins' });
    expect(res.statusCode).toBe(200);
    expect(JSON.parse(res.payload).plugins).toHaveLength(1);
    expect(call).toHaveBeenCalledWith(
      'list_plugins',
      expect.objectContaining({
        auth: expect.objectContaining({ userId: 'demo-user' }),
      })
    );
  });

  it('returns a single plugin via IPC', async () => {
    call.mockResolvedValue({
      result: { plugin: { name: 'alpha', version: '1.0.0' } },
    });

    const res = await app.inject({ method: 'GET', url: '/api/plugins/alpha' });
    expect(res.statusCode).toBe(200);
    expect(JSON.parse(res.payload).plugin.name).toBe('alpha');
  });

  it('maps missing plugin to 404', async () => {
    call.mockResolvedValue({
      error: { code: -32001, message: 'not found' },
    });

    const res = await app.inject({ method: 'GET', url: '/api/plugins/missing' });
    expect(res.statusCode).toBe(404);
  });

  it('installs plugin via IPC', async () => {
    call.mockResolvedValue({
      result: { installed: true, pluginId: 'alpha' },
    });

    const res = await app.inject({
      method: 'POST',
      url: '/api/plugins/install',
      payload: { pluginId: 'alpha' },
    });

    expect(res.statusCode).toBe(200);
    expect(JSON.parse(res.payload).installed).toBe(true);
    expect(call).toHaveBeenCalledWith(
      'install_plugin',
      expect.objectContaining({
        pluginId: 'alpha',
      })
    );
  });

  it('surfaces install errors', async () => {
    call.mockResolvedValue({
      error: { code: -32603, message: 'install failed' },
    });

    const res = await app.inject({
      method: 'POST',
      url: '/api/plugins/install',
      payload: { pluginId: 'alpha' },
    });

    expect(res.statusCode).toBe(400);
  });

  it('uninstalls plugin via IPC', async () => {
    call.mockResolvedValue({
      result: { deleted: true, pluginId: 'alpha' },
    });

    const res = await app.inject({ method: 'DELETE', url: '/api/plugins/alpha' });
    expect(res.statusCode).toBe(200);
    expect(JSON.parse(res.payload).deleted).toBe(true);
  });

  describe('POST /api/plugins/reload', () => {
    it('returns 200 + ReloadStats when admin role is set on jwtPayload', async () => {
      const stats = {
        loaded: 3,
        added: ['a'],
        removed: [],
        updated: [],
        errors: [],
      };
      call.mockResolvedValue({ result: stats });

      // Spin up a fresh app with a preHandler that injects an admin jwtPayload.
      const adminApp = Fastify({ logger: false });
      adminApp.decorateRequest('jwtPayload', null);
      adminApp.addHook('preHandler', async req => {
        (req as unknown as { jwtPayload: unknown }).jwtPayload = {
          sub: 'demo-user',
          role: 'admin',
        };
      });
      await adminApp.register(pluginRoutes);

      const res = await adminApp.inject({ method: 'POST', url: '/api/plugins/reload' });
      expect(res.statusCode).toBe(200);
      expect(JSON.parse(res.payload)).toEqual(stats);
      expect(call).toHaveBeenCalledWith(
        'reload_plugins',
        expect.objectContaining({
          auth: expect.objectContaining({ role: 'admin', isPlatformAuth: true }),
        })
      );
      await adminApp.close();
    });

    it('returns 403 when role=user and no platform auth', async () => {
      const res = await app.inject({ method: 'POST', url: '/api/plugins/reload' });
      expect(res.statusCode).toBe(403);
      // Bridge must not be called because authz rejects before IPC.
      expect(call).not.toHaveBeenCalled();
    });

    it('returns 400 when IPC bridge is unavailable', async () => {
      setIpcBridge({ isRunning: false, call } as unknown as IpcBridge);

      const downApp = Fastify({ logger: false });
      downApp.decorateRequest('jwtPayload', null);
      downApp.addHook('preHandler', async req => {
        (req as unknown as { jwtPayload: unknown }).jwtPayload = { role: 'admin' };
      });
      await downApp.register(pluginRoutes);

      const res = await downApp.inject({ method: 'POST', url: '/api/plugins/reload' });
      expect(res.statusCode).toBe(400);
      expect(JSON.parse(res.payload).message).toMatch(/IPC bridge unavailable/i);
      await downApp.close();
    });

    it('propagates orchestrator -32003 as ForbiddenError', async () => {
      call.mockResolvedValue({ error: { code: -32003, message: 'admin required' } });

      const adminApp = Fastify({ logger: false });
      adminApp.decorateRequest('jwtPayload', null);
      adminApp.addHook('preHandler', async req => {
        (req as unknown as { jwtPayload: unknown }).jwtPayload = { role: 'admin' };
      });
      await adminApp.register(pluginRoutes);

      const res = await adminApp.inject({ method: 'POST', url: '/api/plugins/reload' });
      expect(res.statusCode).toBe(403);
      await adminApp.close();
    });

    it('maps other IPC errors to BadRequestError', async () => {
      call.mockResolvedValue({ error: { code: -32603, message: 'internal boom' } });

      const adminApp = Fastify({ logger: false });
      adminApp.decorateRequest('jwtPayload', null);
      adminApp.addHook('preHandler', async req => {
        (req as unknown as { jwtPayload: unknown }).jwtPayload = { role: 'admin' };
      });
      await adminApp.register(pluginRoutes);

      const res = await adminApp.inject({ method: 'POST', url: '/api/plugins/reload' });
      expect(res.statusCode).toBe(400);
      expect(JSON.parse(res.payload).message).toContain('internal boom');
      await adminApp.close();
    });

    it('rejects malformed orchestrator payloads', async () => {
      call.mockResolvedValue({ result: { loaded: 'not-a-number' } });

      const adminApp = Fastify({ logger: false });
      adminApp.decorateRequest('jwtPayload', null);
      adminApp.addHook('preHandler', async req => {
        (req as unknown as { jwtPayload: unknown }).jwtPayload = { role: 'admin' };
      });
      await adminApp.register(pluginRoutes);

      const res = await adminApp.inject({ method: 'POST', url: '/api/plugins/reload' });
      expect(res.statusCode).toBe(400);
      await adminApp.close();
    });
  });
});
