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

const repoMock = {
  listSuites: vi.fn(),
  createSuite: vi.fn(),
  getSuite: vi.fn(),
  updateSuite: vi.fn(),
  listCases: vi.fn(),
  createCase: vi.fn(),
  createRun: vi.fn(),
  getRun: vi.fn(),
  createResults: vi.fn(),
  listResults: vi.fn(),
  updateRun: vi.fn(),
  listRunsBySuite: vi.fn(),
  listRunsByAgent: vi.fn(),
};

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

const agentRepoMock = {
  getAgent: vi.fn(),
  getAgentVersion: vi.fn(),
};

vi.mock('./repository.js', () => repoMock);
vi.mock('../agents/repository.js', () => agentRepoMock);
vi.mock('../jobs/runner.js', () => jobRunnerMock);
vi.mock('../jobs/registry.js', () => jobRegistryMock);

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

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

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

  it('POST /agent-evals/suites creates a suite for an existing agent', async () => {
    agentRepoMock.getAgent.mockResolvedValue({ id: 'agt_1' });
    repoMock.createSuite.mockResolvedValue({ id: 'evals_1', name: 'Release Gate' });

    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });
    const res = await app.inject({
      method: 'POST',
      url: '/api/agent-evals/suites',
      payload: {
        agentId: 'agt_1',
        name: 'Release Gate',
        passThreshold: 0.9,
      },
    });

    expect(res.statusCode).toBe(200);
    expect(agentRepoMock.getAgent).toHaveBeenCalledWith('agt_1', 'lysnrai');
    expect(repoMock.createSuite).toHaveBeenCalledWith(
      expect.objectContaining({
        agentId: 'agt_1',
        passThreshold: 0.9,
      })
    );
  });

  it('POST /agent-evals/suites/:id/runs creates a version-aware run', async () => {
    repoMock.getSuite.mockResolvedValue({
      id: 'evals_1',
      productId: 'lysnrai',
      agentId: 'agt_1',
      passThreshold: 0.85,
      targetVersion: 2,
    });
    agentRepoMock.getAgent.mockResolvedValue({ id: 'agt_1', currentVersion: 3 });
    agentRepoMock.getAgentVersion.mockResolvedValue({ id: 'agt_1:v2', version: 2 });
    repoMock.listCases.mockResolvedValue([{ id: 'case_1' }, { id: 'case_2' }]);
    repoMock.createRun.mockResolvedValue({ id: 'evalrun_1' });

    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });
    const res = await app.inject({
      method: 'POST',
      url: '/api/agent-evals/suites/evals_1/runs',
      payload: {
        releaseGate: true,
      },
    });

    expect(res.statusCode).toBe(200);
    expect(agentRepoMock.getAgentVersion).toHaveBeenCalledWith('agt_1:v2', 'agt_1');
    expect(repoMock.createRun).toHaveBeenCalledWith(
      expect.objectContaining({
        releaseGate: true,
        totalCases: 2,
        agentVersion: 2,
      })
    );
  });

  it('POST /agent-evals/runs/:id/results finalizes the run verdict', async () => {
    repoMock.getRun.mockResolvedValue({
      id: 'evalrun_1',
      productId: 'lysnrai',
      suiteId: 'evals_1',
      passThreshold: 0.8,
    });
    repoMock.listCases.mockResolvedValue([
      { id: 'case_1', critical: true },
      { id: 'case_2', critical: false },
    ]);
    repoMock.createResults.mockResolvedValue([]);
    repoMock.updateRun.mockResolvedValue({ id: 'evalrun_1', verdict: 'needs_review' });

    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });
    const res = await app.inject({
      method: 'POST',
      url: '/api/agent-evals/runs/evalrun_1/results',
      payload: {
        summary: 'Critical regression detected',
        results: [
          { caseId: 'case_1', passed: false, score: 0.2 },
          { caseId: 'case_2', passed: true, score: 1 },
        ],
      },
    });

    expect(res.statusCode).toBe(200);
    expect(repoMock.createResults).toHaveBeenCalled();
    expect(repoMock.updateRun).toHaveBeenCalledWith(
      'evalrun_1',
      'lysnrai',
      expect.objectContaining({
        verdict: 'needs_review',
        reviewRequired: true,
      })
    );
  });

  // ── Run History ─────────────────────────────────────────

  it('GET /agent-evals/suites/:id/runs returns run history', async () => {
    repoMock.listRunsBySuite.mockResolvedValue([{ id: 'evalrun_1' }]);
    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });

    const res = await app.inject({ method: 'GET', url: '/api/agent-evals/suites/evals_1/runs' });
    expect(res.statusCode).toBe(200);
    expect(repoMock.listRunsBySuite).toHaveBeenCalledWith('lysnrai', 'evals_1', 20);
  });

  // ── Regression Comparison ───────────────────────────────

  it('GET /agent-evals/suites/:id/regression detects score regression', async () => {
    repoMock.listRunsBySuite.mockResolvedValue([
      {
        id: 'run_2',
        status: 'completed',
        score: 0.7,
        agentVersion: 2,
        verdict: 'fail',
        passedCases: 7,
        failedCases: 3,
        startedAt: '2026-03-16',
      },
      {
        id: 'run_1',
        status: 'completed',
        score: 0.9,
        agentVersion: 1,
        verdict: 'pass',
        passedCases: 9,
        failedCases: 1,
        startedAt: '2026-03-15',
      },
    ]);
    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });

    const res = await app.inject({
      method: 'GET',
      url: '/api/agent-evals/suites/evals_1/regression',
    });
    expect(res.statusCode).toBe(200);
    const body = res.json();
    expect(body.regression).toBe(true);
    expect(body.scoreDelta).toBeLessThan(0);
  });

  it('GET /agent-evals/suites/:id/regression returns false with insufficient runs', async () => {
    repoMock.listRunsBySuite.mockResolvedValue([{ id: 'run_1', status: 'completed', score: 0.9 }]);
    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });

    const res = await app.inject({
      method: 'GET',
      url: '/api/agent-evals/suites/evals_1/regression',
    });
    expect(res.statusCode).toBe(200);
    expect(res.json().regression).toBe(false);
  });

  // ── Release Gate ────────────────────────────────────────

  it('GET /agent-evals/suites/:id/gate returns allowed when gate passed', async () => {
    repoMock.getSuite.mockResolvedValue({ id: 'evals_1', passThreshold: 0.8 });
    repoMock.listRunsBySuite.mockResolvedValue([
      {
        id: 'run_1',
        releaseGate: true,
        status: 'completed',
        verdict: 'pass',
        agentVersion: 2,
        score: 0.95,
      },
    ]);
    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });

    const res = await app.inject({ method: 'GET', url: '/api/agent-evals/suites/evals_1/gate' });
    expect(res.statusCode).toBe(200);
    expect(res.json().allowed).toBe(true);
  });

  it('GET /agent-evals/suites/:id/gate returns not allowed when no gate run', async () => {
    repoMock.getSuite.mockResolvedValue({ id: 'evals_1', passThreshold: 0.8 });
    repoMock.listRunsBySuite.mockResolvedValue([]);
    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });

    const res = await app.inject({ method: 'GET', url: '/api/agent-evals/suites/evals_1/gate' });
    expect(res.statusCode).toBe(200);
    expect(res.json().allowed).toBe(false);
  });

  // ── Compliance Report ───────────────────────────────────

  it('GET /agent-evals/agents/:agentId/report returns compliance summary', async () => {
    repoMock.listSuites.mockResolvedValue([
      { id: 'evals_1', status: 'active' },
      { id: 'evals_2', status: 'draft' },
    ]);
    repoMock.listRunsByAgent.mockResolvedValue([
      {
        id: 'run_1',
        status: 'completed',
        verdict: 'pass',
        score: 0.95,
        agentVersion: 1,
        suiteId: 'evals_1',
        startedAt: '2026-03-15',
        completedAt: '2026-03-15',
      },
      {
        id: 'run_2',
        status: 'completed',
        verdict: 'fail',
        score: 0.6,
        agentVersion: 2,
        suiteId: 'evals_1',
        startedAt: '2026-03-16',
        completedAt: '2026-03-16',
      },
      { id: 'run_3', status: 'running' },
    ]);
    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });

    const res = await app.inject({ method: 'GET', url: '/api/agent-evals/agents/agt_1/report' });
    expect(res.statusCode).toBe(200);
    const body = res.json();
    expect(body.totalSuites).toBe(2);
    expect(body.activeSuites).toBe(1);
    expect(body.completedRuns).toBe(2);
    expect(body.passed).toBe(1);
    expect(body.failed).toBe(1);
    expect(body.passRate).toBe(50);
  });

  // ── Schedule Eval ───────────────────────────────────────

  it('POST /agent-evals/suites/:id/schedule creates scheduled eval job', async () => {
    repoMock.getSuite.mockResolvedValue({ id: 'evals_1', name: 'Release Gate', agentId: 'agt_1' });
    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/agent-evals/suites/evals_1/schedule',
      payload: { cronExpression: '0 0 * * *' },
    });

    expect(res.statusCode).toBe(200);
    expect(res.json()).toEqual({
      scheduled: true,
      jobName: 'eval:evals_1',
      cronExpression: '0 0 * * *',
    });
  });

  it('POST /agent-evals/suites/:id/schedule requires cronExpression', async () => {
    repoMock.getSuite.mockResolvedValue({ id: 'evals_1' });
    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });

    const res = await app.inject({
      method: 'POST',
      url: '/api/agent-evals/suites/evals_1/schedule',
      payload: {},
    });
    expect(res.statusCode).toBe(400);
  });
});
