import Fastify from 'fastify';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

const executorMock = {
  executeAgent: vi.fn(),
  cancelAgentRun: vi.fn(),
  getActiveAgentRuns: vi.fn(),
  onStepEvent: vi.fn(),
};

const toolRegistryMock = {
  listTools: vi.fn(),
  validateToolBindings: vi.fn(),
};

const runRepoMock = {
  getRun: vi.fn(),
  listRuns: vi.fn(),
  listChildRuns: vi.fn(),
  listRunSteps: vi.fn(),
  updateRun: vi.fn(),
};

const jobRunnerMock = {
  ensureJobDefinitions: vi.fn(),
};

const jobRegistryMock = {
  registerJob: vi.fn(),
};

vi.mock('./executor.js', () => executorMock);
vi.mock('./tool-registry.js', () => toolRegistryMock);
vi.mock('../runs/repository.js', () => runRepoMock);
vi.mock('../runs/types.js', () => ({}));
vi.mock('../jobs/runner.js', () => jobRunnerMock);
vi.mock('../jobs/registry.js', () => jobRegistryMock);

async function buildApp(payload?: { sub: string; productId: string; role?: string }) {
  const { agentExecutorRoutes } = await import('./executor-routes.js');
  const app = Fastify({ logger: false });
  if (payload) {
    app.addHook('onRequest', async req => {
      req.jwtPayload = payload;
    });
  }
  await app.register(agentExecutorRoutes, { prefix: '/api' });
  return app;
}

describe('agentExecutorRoutes', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  afterEach(() => {
    vi.restoreAllMocks();
  });

  // ── Execute agent ───────────────────────────────────────

  it('POST /agents/execute executes an agent', async () => {
    executorMock.executeAgent.mockResolvedValue({
      runId: 'run_1',
      status: 'succeeded',
      tokenUsage: { prompt: 100, completion: 50, total: 150 },
      stepsCompleted: 3,
      durationMs: 500,
    });
    const app = await buildApp({ sub: 'user_1', productId: 'lysnrai' });

    const res = await app.inject({
      method: 'POST',
      url: '/api/agents/execute',
      payload: { agentId: 'agt_1', input: { query: 'hello' } },
    });

    expect(res.statusCode).toBe(200);
    expect(executorMock.executeAgent).toHaveBeenCalledWith(
      expect.objectContaining({ agentId: 'agt_1', userId: 'user_1' })
    );
  });

  it('POST /agents/execute rejects invalid input', async () => {
    const app = await buildApp({ sub: 'user_1', productId: 'lysnrai' });

    const res = await app.inject({
      method: 'POST',
      url: '/api/agents/execute',
      payload: {},
    });

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

  it('POST /agents/execute rejects unauthenticated', async () => {
    const app = await buildApp();
    const res = await app.inject({
      method: 'POST',
      url: '/api/agents/execute',
      payload: { agentId: 'agt_1' },
    });
    expect(res.statusCode).toBe(403);
  });

  // ── Cancel run ──────────────────────────────────────────

  it('POST /runs/:id/cancel cancels an active run', async () => {
    executorMock.cancelAgentRun.mockReturnValue(true);
    const app = await buildApp({ sub: 'user_1', productId: 'lysnrai' });

    const res = await app.inject({ method: 'POST', url: '/api/runs/run_1/cancel' });
    expect(res.statusCode).toBe(200);
    expect(JSON.parse(res.body)).toEqual({ cancelled: true, runId: 'run_1' });
  });

  it('POST /runs/:id/cancel falls back to DB update', async () => {
    executorMock.cancelAgentRun.mockReturnValue(false);
    runRepoMock.updateRun.mockResolvedValue({ id: 'run_1', status: 'cancelled' });
    const app = await buildApp({ sub: 'user_1', productId: 'lysnrai' });

    const res = await app.inject({ method: 'POST', url: '/api/runs/run_1/cancel' });
    expect(res.statusCode).toBe(200);
    expect(runRepoMock.updateRun).toHaveBeenCalledWith(
      'run_1',
      'lysnrai',
      expect.objectContaining({ status: 'cancelled' })
    );
  });

  // ── Active runs ─────────────────────────────────────────

  it('GET /agents/active-runs returns active runs', async () => {
    executorMock.getActiveAgentRuns.mockReturnValue(['run_1', 'run_2']);
    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });

    const res = await app.inject({ method: 'GET', url: '/api/agents/active-runs' });
    expect(res.statusCode).toBe(200);
    expect(JSON.parse(res.body)).toEqual({ activeRuns: ['run_1', 'run_2'] });
  });

  it('GET /agents/active-runs requires admin', async () => {
    const app = await buildApp({ sub: 'user_1', productId: 'lysnrai', role: 'viewer' });
    const res = await app.inject({ method: 'GET', url: '/api/agents/active-runs' });
    expect(res.statusCode).toBe(403);
  });

  // ── DAG / children ──────────────────────────────────────

  it('GET /runs/:id/children returns child runs', async () => {
    runRepoMock.listChildRuns.mockResolvedValue([{ id: 'run_child_1', parentRunId: 'run_1' }]);
    const app = await buildApp({ sub: 'user_1', productId: 'lysnrai' });

    const res = await app.inject({ method: 'GET', url: '/api/runs/run_1/children' });
    expect(res.statusCode).toBe(200);
    expect(runRepoMock.listChildRuns).toHaveBeenCalledWith('lysnrai', 'run_1');
  });

  it('GET /runs/:id/tree returns full run tree', async () => {
    runRepoMock.getRun.mockResolvedValue({ id: 'run_1', status: 'succeeded' });
    runRepoMock.listChildRuns.mockResolvedValue([{ id: 'run_child_1' }]);
    runRepoMock.listRunSteps.mockResolvedValue([]);
    const app = await buildApp({ sub: 'user_1', productId: 'lysnrai' });

    const res = await app.inject({ method: 'GET', url: '/api/runs/run_1/tree' });
    expect(res.statusCode).toBe(200);
    const body = JSON.parse(res.body);
    expect(body.run.id).toBe('run_1');
    expect(body.children).toHaveLength(1);
  });

  // ── Agent metrics ───────────────────────────────────────

  it('GET /agents/:id/metrics computes agent metrics', async () => {
    runRepoMock.listRuns.mockResolvedValue([
      {
        id: 'run_1',
        status: 'succeeded',
        metadata: { agentId: 'agt_1' },
        output: { tokenUsage: { prompt: 100, completion: 50, total: 150 }, durationMs: 500 },
        createdAt: '2026-01-01',
      },
      {
        id: 'run_2',
        status: 'failed',
        metadata: { agentId: 'agt_1' },
        createdAt: '2026-01-02',
      },
      {
        id: 'run_3',
        status: 'succeeded',
        metadata: { agentId: 'other_agent' },
        createdAt: '2026-01-02',
      },
    ]);
    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });

    const res = await app.inject({ method: 'GET', url: '/api/agents/agt_1/metrics' });
    expect(res.statusCode).toBe(200);
    const body = JSON.parse(res.body);
    expect(body.totalRuns).toBe(2);
    expect(body.succeeded).toBe(1);
    expect(body.failed).toBe(1);
    expect(body.successRate).toBe(50);
    expect(body.tokenUsage.total).toBe(150);
  });

  // ── Tool registry ───────────────────────────────────────

  it('GET /tools lists registered tools', async () => {
    toolRegistryMock.listTools.mockReturnValue([{ name: 'search', description: 'Search the web' }]);
    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });

    const res = await app.inject({ method: 'GET', url: '/api/tools' });
    expect(res.statusCode).toBe(200);
    expect(JSON.parse(res.body)).toHaveLength(1);
  });

  it('POST /tools/validate validates tool bindings', async () => {
    toolRegistryMock.validateToolBindings.mockReturnValue({
      valid: ['search'],
      invalid: ['unknown_tool'],
    });
    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });

    const res = await app.inject({
      method: 'POST',
      url: '/api/tools/validate',
      payload: { bindings: ['search', 'unknown_tool'] },
    });
    expect(res.statusCode).toBe(200);
    expect(JSON.parse(res.body).invalid).toContain('unknown_tool');
  });

  // ── Agent scheduling ────────────────────────────────────

  it('POST /agents/:id/schedule creates a scheduled job', async () => {
    jobRunnerMock.ensureJobDefinitions.mockResolvedValue(undefined);
    jobRegistryMock.registerJob.mockReturnValue(undefined);
    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });

    const res = await app.inject({
      method: 'POST',
      url: '/api/agents/agt_1/schedule',
      payload: { cronExpression: '0 */6 * * *', input: { mode: 'daily' } },
    });

    expect(res.statusCode).toBe(200);
    expect(JSON.parse(res.body)).toEqual({
      scheduled: true,
      jobName: 'agent:agt_1',
      cronExpression: '0 */6 * * *',
    });
    expect(jobRegistryMock.registerJob).toHaveBeenCalledWith('agent:agt_1', expect.any(Function));
  });

  it('POST /agents/:id/schedule requires cronExpression', async () => {
    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });

    const res = await app.inject({
      method: 'POST',
      url: '/api/agents/agt_1/schedule',
      payload: {},
    });
    expect(res.statusCode).toBe(400);
  });
});
