/**
 * Fleet correlation-id tracing (§4) — end-to-end across the coordinator↔runner
 * boundary: a job minted with a correlation id propagates onto the run, lease,
 * and every lifecycle event, and the routes echo it on the `x-correlation-id`
 * response header so an external factory can carry it forward.
 */

import Fastify, { type FastifyInstance } from 'fastify';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { MemoryDatastoreProvider } from '@bytelyst/datastore';
import { _resetDatastoreProvider, setProvider } from '../../lib/datastore.js';
import * as repo from './repository.js';
import * as coord from './coordinator.js';
import { CORRELATION_HEADER } from './trace.js';
import type { SubmitJobInput } from './types.js';

vi.mock('../../lib/auth.js', () => ({
  extractAuth: vi.fn(async () => ({ sub: 'user_1', role: 'admin' })),
}));
vi.mock('../../lib/request-context.js', () => ({
  getRequestProductId: () => 'lysnrai',
  requireProductAccess: async () => 'lysnrai',
}));

const PID = 'lysnrai';

function input(over: Partial<SubmitJobInput> = {}): SubmitJobInput {
  return {
    idempotencyKey: 'trace-1',
    bodyMd: '# do the thing',
    priority: 'medium',
    capabilities: [],
    prefersEngine: [],
    allowedScope: [],
    deps: [],
    kind: 'leaf',
    ...over,
  };
}

const factory = (over = {}) => ({
  productId: PID,
  factoryId: 'fac_1',
  capabilities: [] as string[],
  leaseSeconds: 900,
  ...over,
});

async function buildApp(): Promise<FastifyInstance> {
  const { fleetRoutes } = await import('./routes.js');
  const app = Fastify({ logger: false });
  await app.register(fleetRoutes, { prefix: '/api' });
  return app;
}

describe('fleet correlation-id tracing (coordinator)', () => {
  beforeEach(() => setProvider(new MemoryDatastoreProvider()));
  afterEach(() => {
    _resetDatastoreProvider();
    vi.clearAllMocks();
  });

  it('persists the correlation id on the job and stamps the submitted event', async () => {
    const { job } = await coord.submitJob(PID, input(), { correlationId: 'cid-abc' });
    expect(job.correlationId).toBe('cid-abc');
    const events = await repo.listEvents(job.id);
    const submitted = events.find(e => e.type === 'submitted');
    expect(submitted?.correlationId).toBe('cid-abc');
  });

  it('run + lease inherit the job correlation id at claim, and the assigned event carries it', async () => {
    await coord.submitJob(PID, input(), { correlationId: 'cid-claim' });
    const claim = await coord.claimNextJob(factory());
    expect(claim).not.toBeNull();
    expect(claim!.run.correlationId).toBe('cid-claim');
    expect(claim!.lease.correlationId).toBe('cid-claim');
    const events = await repo.listEvents(claim!.job.id);
    expect(events.find(e => e.type === 'assigned')?.correlationId).toBe('cid-claim');
  });

  it('threads the correlation id across transition → release lifecycle events', async () => {
    await coord.submitJob(PID, input(), { correlationId: 'cid-life' });
    const claim = await coord.claimNextJob(factory());
    const jobId = claim!.job.id;
    const epoch = claim!.lease.leaseEpoch;

    await coord.patchJobFenced(jobId, PID, { leaseEpoch: epoch, stage: 'building' });
    await coord.renewLease(jobId, PID, epoch, 900);
    await coord.releaseLease(jobId, PID, epoch, 'shipped', { result: 'shipped' });

    const events = await repo.listEvents(jobId);
    // Every lifecycle event for this job shares the one correlation id.
    for (const e of events) expect(e.correlationId).toBe('cid-life');
    expect(events.map(e => e.type)).toEqual(
      expect.arrayContaining([
        'submitted',
        'assigned',
        'transition',
        'lease_renewed',
        'lease_released',
      ])
    );
  });

  it('children of a composite job share the parent correlation id', async () => {
    const { job } = await coord.submitJob(
      PID,
      input({
        idempotencyKey: 'parent-1',
        children: [
          {
            idempotencyKey: 'child-1',
            bodyMd: '# child',
            priority: 'medium',
            capabilities: [],
            prefersEngine: [],
            allowedScope: [],
            deps: [],
          },
        ],
      }),
      { correlationId: 'cid-dag' }
    );
    expect(job.correlationId).toBe('cid-dag');
    const children = await repo.findJobsByIdempotencyKey(PID, 'child-1');
    expect(children[0]?.correlationId).toBe('cid-dag');
  });
});

describe('fleet correlation-id tracing (routes)', () => {
  beforeEach(() => setProvider(new MemoryDatastoreProvider()));
  afterEach(() => {
    _resetDatastoreProvider();
    vi.clearAllMocks();
  });

  it('POST /fleet/jobs honors an inbound x-correlation-id and echoes it back', async () => {
    const app = await buildApp();
    const res = await app.inject({
      method: 'POST',
      url: '/api/fleet/jobs',
      headers: { [CORRELATION_HEADER]: 'inbound-cid' },
      payload: { idempotencyKey: 'r-1', bodyMd: '# task' },
    });
    expect(res.statusCode).toBe(201);
    expect(res.headers[CORRELATION_HEADER]).toBe('inbound-cid');
    expect(JSON.parse(res.body).job.correlationId).toBe('inbound-cid');
  });

  it('POST /fleet/jobs mints + echoes a correlation id when the request carries none', async () => {
    const app = await buildApp();
    const res = await app.inject({
      method: 'POST',
      url: '/api/fleet/jobs',
      payload: { idempotencyKey: 'r-2', bodyMd: '# task' },
    });
    expect(res.statusCode).toBe(201);
    const echoed = res.headers[CORRELATION_HEADER];
    expect(echoed).toMatch(/^ftr_/);
    expect(JSON.parse(res.body).job.correlationId).toBe(echoed);
  });

  it('POST /fleet/claim hands the job correlation id back on the response header', async () => {
    const app = await buildApp();
    await app.inject({
      method: 'POST',
      url: '/api/fleet/jobs',
      headers: { [CORRELATION_HEADER]: 'claim-cid' },
      payload: { idempotencyKey: 'r-3', bodyMd: '# task' },
    });
    const res = await app.inject({
      method: 'POST',
      url: '/api/fleet/claim',
      payload: { factoryId: 'fac_1', capabilities: [] },
    });
    expect(res.statusCode).toBe(200);
    expect(res.headers[CORRELATION_HEADER]).toBe('claim-cid');
    const body = JSON.parse(res.body);
    expect(body.claimed).toBe(true);
    expect(body.run.correlationId).toBe('claim-cid');
  });
});
