import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import express from 'express';
import request from 'supertest';
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { createDelegateRunsRouter } from './delegate-runs-api.js';
import type { Repository } from '../db/repository.js';

// ---------------------------------------------------------------------------
// Subtask-event helper: write events.jsonl to {dir}/logs/events.jsonl
// ---------------------------------------------------------------------------
function writeSubEvents(dir: string, lines: object[]) {
  mkdirSync(join(dir, 'logs'), { recursive: true });
  writeFileSync(
    join(dir, 'logs', 'events.jsonl'),
    lines.map((l) => JSON.stringify(l)).join('\n') + '\n',
    'utf-8',
  );
}

// ---------------------------------------------------------------------------
// Minimal repo mock — mirrors subtask-activity-api.test.ts pattern
// ---------------------------------------------------------------------------
function makeRepo(overrides: Partial<Repository> = {}): Repository {
  return {
    getLocalTask: vi.fn(),
    getLatestJobForIssue: vi.fn(),
    getSubJobs: vi.fn(),
    getJob: vi.fn(),
    userCanViewSpace: vi.fn().mockReturnValue(false),
    ...overrides,
  } as unknown as Repository;
}

// ---------------------------------------------------------------------------
// Seed helpers
// ---------------------------------------------------------------------------

/** Write lines to {workspacePath}/logs/events.jsonl (logRoot fallback path). */
function writeEventsJsonl(workspacePath: string, lines: object[]) {
  mkdirSync(join(workspacePath, 'logs'), { recursive: true });
  writeFileSync(
    join(workspacePath, 'logs', 'events.jsonl'),
    lines.map((l) => JSON.stringify(l)).join('\n') + '\n',
    'utf-8',
  );
}

// Canonical minimal event factories
const START = (id: string) => ({
  v: 1,
  ts: '2026-06-25T00:00:01.000Z',
  seq: 1,
  eventId: 's',
  runId: 'r',
  kind: 'delegate_start',
  payload: { delegateRunId: id, parentRunId: null, description: 'd', depth: 1 },
});

const TOOL = (id: string) => ({
  v: 1,
  ts: '2026-06-25T00:00:02.000Z',
  seq: 2,
  eventId: 't',
  runId: 'r',
  kind: 'tool_call',
  correlationId: id,
  payload: { toolName: 'Read', toolCallId: 'tc1' },
});

const DONE = (id: string) => ({
  v: 1,
  ts: '2026-06-25T00:00:03.000Z',
  seq: 3,
  eventId: 'c',
  runId: 'r',
  kind: 'delegate_complete',
  payload: { delegateRunId: id, parentRunId: null, description: 'd', depth: 1, next: 'COMPLETE', status: 'success' },
});

// ---------------------------------------------------------------------------
// seedTaskWithWorkspace: build a test Express app + mocked repo + a task that
// has a non-null workspacePath backed by a real tmp directory.
// Also provides otherUsersTaskId whose getLocalTask mock returns null (viewer
// cannot see it), to test the visibility gate.
// ---------------------------------------------------------------------------
async function seedTaskWithWorkspace() {
  const workspacePath = mkdtempSync(join(tmpdir(), 'delegate-runs-test-'));

  const TASK = {
    id: 1,
    title: 'test task',
    workspacePath,
    runtimeDir: null,
    ownerId: 'alice-id',
    visibility: 'private' as const,
    visibilityScopeOrgId: null,
    spaceId: null,
  };

  const repo = makeRepo();
  // By default task 1 is visible (no user set on req, so canViewTask returns true)
  vi.mocked(repo.getLocalTask).mockImplementation(async (id: number) => {
    if (id === 1) return TASK as never;
    return null as never;
  });

  const app = express();
  app.use(express.json());
  app.use('/api/local/tasks', createDelegateRunsRouter(repo));

  return { app, repo, taskId: 1, workspacePath, otherUsersTaskId: 99 };
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe('GET /api/local/tasks/:id/delegate-runs', () => {
  let tmpDirs: string[] = [];

  afterEach(() => {
    for (const dir of tmpDirs) {
      try { rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
    }
    tmpDirs = [];
    vi.clearAllMocks();
  });

  it('events.jsonl から run 一覧を返す', async () => {
    const { app, taskId, workspacePath } = await seedTaskWithWorkspace();
    tmpDirs.push(workspacePath);
    writeEventsJsonl(workspacePath, [START('R1'), TOOL('R1'), DONE('R1')]);

    const res = await request(app).get(`/api/local/tasks/${taskId}/delegate-runs`);

    expect(res.status).toBe(200);
    expect(res.body.runs).toHaveLength(1);
    expect(res.body.runs[0]).toMatchObject({ delegateRunId: 'R1', status: 'success', toolCalls: 1 });
  });

  it('events.jsonl 不在なら空配列（エラーにしない）', async () => {
    const { app, taskId, workspacePath } = await seedTaskWithWorkspace();
    tmpDirs.push(workspacePath);
    // No events.jsonl written

    const res = await request(app).get(`/api/local/tasks/${taskId}/delegate-runs`);

    expect(res.status).toBe(200);
    expect(res.body.runs).toEqual([]);
  });

  it('複数 run を delegateRunId で区別して返す', async () => {
    const { app, taskId, workspacePath } = await seedTaskWithWorkspace();
    tmpDirs.push(workspacePath);
    writeEventsJsonl(workspacePath, [
      START('R1'), TOOL('R1'), DONE('R1'),
      START('R2'), DONE('R2'),
    ]);

    const res = await request(app).get(`/api/local/tasks/${taskId}/delegate-runs`);

    expect(res.status).toBe(200);
    expect(res.body.runs).toHaveLength(2);
    expect(res.body.runs.map((r: { delegateRunId: string }) => r.delegateRunId)).toEqual(['R1', 'R2']);
  });

  it('非可視タスクは 404 を返す', async () => {
    const { app, otherUsersTaskId } = await seedTaskWithWorkspace();

    const res = await request(app).get(`/api/local/tasks/${otherUsersTaskId}/delegate-runs`);

    expect([403, 404]).toContain(res.status);
  });

  it('authenticated non-owner viewer blocked from private task (IDOR protection)', async () => {
    const { repo, taskId, workspacePath } = await seedTaskWithWorkspace();
    const tmpDirs = [workspacePath];
    const mockGetLocalTask = vi.mocked(repo.getLocalTask);

    // Configure mock: when viewer is passed (authenticated request),
    // return null (simulating DB-level visibility filter)
    mockGetLocalTask.mockImplementation(async (id: number, opts?: { viewer?: Express.User }) => {
      if (id === taskId) {
        if (opts?.viewer) {
          // Non-owner viewer: visibility filter returns null
          return null as never;
        }
        // No viewer (unauthenticated): allowed to view
        return {
          id: taskId,
          title: 'test task',
          workspacePath,
          runtimeDir: null,
          ownerId: 'alice-id',
          visibility: 'private' as const,
          visibilityScopeOrgId: null,
          spaceId: null,
        } as never;
      }
      return null as never;
    });

    // Create app with middleware that injects non-owner viewer
    const nonOwnerUser: Express.User = { id: 'bob-id', name: 'Bob' } as Express.User;
    const app = express();
    app.use((req: any, _res: any, next: any) => {
      req.user = nonOwnerUser;
      next();
    });
    app.use(express.json());
    app.use('/api/local/tasks', createDelegateRunsRouter(repo));

    const res = await request(app).get(`/api/local/tasks/${taskId}/delegate-runs`);

    // Verify: request is blocked (404 or 403) and no data is leaked
    expect([403, 404]).toContain(res.status);
    expect(res.body.runs).toBeUndefined();
    expect(res.body.error).toBeDefined();

    // Verify: getLocalTask was called WITH the viewer object
    expect(mockGetLocalTask).toHaveBeenCalledWith(
      taskId,
      expect.objectContaining({ viewer: nonOwnerUser })
    );

    for (const dir of tmpDirs) {
      try { rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
    }
  });
});

describe('GET /api/local/tasks/:id/delegate-runs/:delegateRunId/timeline', () => {
  let tmpDirs: string[] = [];

  afterEach(() => {
    for (const dir of tmpDirs) {
      try { rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
    }
    tmpDirs = [];
    vi.clearAllMocks();
  });

  it('drill-down: その run の内部イベントのみ返す', async () => {
    const { app, taskId, workspacePath } = await seedTaskWithWorkspace();
    tmpDirs.push(workspacePath);
    writeEventsJsonl(workspacePath, [START('R1'), TOOL('R1'), DONE('R1')]);

    const res = await request(app).get(`/api/local/tasks/${taskId}/delegate-runs/R1/timeline`);

    expect(res.status).toBe(200);
    // Events that have correlationId === 'R1' — TOOL has it; START/DONE do not since they
    // are keyed by kind not correlationId in the filter
    expect(Array.isArray(res.body.events)).toBe(true);
    // The TOOL event has correlationId === 'R1', others do not
    expect(res.body.events.some((e: { correlationId?: string }) => e.correlationId === 'R1')).toBe(true);
  });

  it('timeline: events.jsonl 不在なら空配列', async () => {
    const { app, taskId, workspacePath } = await seedTaskWithWorkspace();
    tmpDirs.push(workspacePath);

    const res = await request(app).get(`/api/local/tasks/${taskId}/delegate-runs/R1/timeline`);

    expect(res.status).toBe(200);
    expect(res.body.events).toEqual([]);
  });

  it('timeline: 非可視タスクは 404', async () => {
    const { app, otherUsersTaskId } = await seedTaskWithWorkspace();

    const res = await request(app).get(`/api/local/tasks/${otherUsersTaskId}/delegate-runs/R1/timeline`);

    expect([403, 404]).toContain(res.status);
  });

  it('timeline: authenticated non-owner viewer blocked from private task (IDOR protection)', async () => {
    const { repo, taskId, workspacePath } = await seedTaskWithWorkspace();
    const tmpDirs = [workspacePath];
    const mockGetLocalTask = vi.mocked(repo.getLocalTask);

    // Configure mock: when viewer is passed (authenticated request),
    // return null (simulating DB-level visibility filter)
    mockGetLocalTask.mockImplementation(async (id: number, opts?: { viewer?: Express.User }) => {
      if (id === taskId) {
        if (opts?.viewer) {
          // Non-owner viewer: visibility filter returns null
          return null as never;
        }
        // No viewer (unauthenticated): allowed to view
        return {
          id: taskId,
          title: 'test task',
          workspacePath,
          runtimeDir: null,
          ownerId: 'alice-id',
          visibility: 'private' as const,
          visibilityScopeOrgId: null,
          spaceId: null,
        } as never;
      }
      return null as never;
    });

    // Create app with middleware that injects non-owner viewer
    const nonOwnerUser: Express.User = { id: 'bob-id', name: 'Bob' } as Express.User;
    const app = express();
    app.use((req: any, _res: any, next: any) => {
      req.user = nonOwnerUser;
      next();
    });
    app.use(express.json());
    app.use('/api/local/tasks', createDelegateRunsRouter(repo));

    const res = await request(app).get(`/api/local/tasks/${taskId}/delegate-runs/R1/timeline`);

    // Verify: request is blocked (404 or 403) and no data is leaked
    expect([403, 404]).toContain(res.status);
    expect(res.body.events).toBeUndefined();
    expect(res.body.error).toBeDefined();

    // Verify: getLocalTask was called WITH the viewer object
    expect(mockGetLocalTask).toHaveBeenCalledWith(
      taskId,
      expect.objectContaining({ viewer: nonOwnerUser })
    );

    for (const dir of tmpDirs) {
      try { rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
    }
  });
});

// ---------------------------------------------------------------------------
// Timeline jobId scope tests (Task 3)
// ---------------------------------------------------------------------------

describe('GET /api/local/tasks/:id/delegate-runs/:delegateRunId/timeline?jobId=', () => {
  let tmpDirs: string[] = [];

  afterEach(() => {
    for (const dir of tmpDirs) {
      try { rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
    }
    tmpDirs = [];
    vi.clearAllMocks();
  });

  it('timeline は jobId 指定でサブジョブの events を読む', async () => {
    const parentDir = mkdtempSync(join(tmpdir(), 'parent-'));
    const subDir = join(parentDir, 'subtasks', 'sub1');
    tmpDirs.push(parentDir);

    // Only write events in the sub-job dir (not the parent)
    writeSubEvents(subDir, [
      { v: 1, ts: '2026-06-29T00:00:00.000Z', seq: 1, eventId: 's1', runId: 'r', kind: 'tool_call', correlationId: 'subRun', payload: { toolName: 'Read', toolCallId: 'tc1' } },
    ]);

    const repo = makeRepo({
      getLocalTask: vi.fn().mockResolvedValue({
        id: 1,
        workspacePath: parentDir,
        runtimeDir: null,
        ownerId: 'alice-id',
        visibility: 'private' as const,
        visibilityScopeOrgId: null,
        spaceId: null,
      }),
      getJob: vi.fn().mockImplementation(async (id: string) => {
        if (id === 'sub1') {
          return { id: 'sub1', issueNumber: 1, status: 'succeeded', worktreePath: subDir, parentJobId: 'root' };
        }
        return null;
      }),
    });

    const app = express();
    app.use(express.json());
    app.use('/api/local/tasks', createDelegateRunsRouter(repo));

    const res = await request(app).get('/api/local/tasks/1/delegate-runs/subRun/timeline?jobId=sub1');

    expect(res.status).toBe(200);
    expect(Array.isArray(res.body.events)).toBe(true);
    expect(res.body.events.length).toBeGreaterThan(0);
    expect(res.body.events.every((e: { correlationId?: string }) => e.correlationId === 'subRun')).toBe(true);
  });

  it('timeline jobId: ジョブが見つからない場合は 404', async () => {
    const parentDir = mkdtempSync(join(tmpdir(), 'parent-'));
    tmpDirs.push(parentDir);

    const repo = makeRepo({
      getLocalTask: vi.fn().mockResolvedValue({
        id: 1,
        workspacePath: parentDir,
        runtimeDir: null,
        ownerId: 'alice-id',
        visibility: 'private' as const,
        visibilityScopeOrgId: null,
        spaceId: null,
      }),
      getJob: vi.fn().mockResolvedValue(null),
    });

    const app = express();
    app.use(express.json());
    app.use('/api/local/tasks', createDelegateRunsRouter(repo));

    const res = await request(app).get('/api/local/tasks/1/delegate-runs/subRun/timeline?jobId=nonexistent');

    expect(res.status).toBe(404);
  });

  it('timeline jobId: workspacePath 外の worktree は fail-closed で 404', async () => {
    const parentDir = mkdtempSync(join(tmpdir(), 'parent-'));
    const outsideDir = mkdtempSync(join(tmpdir(), 'evil-'));
    tmpDirs.push(parentDir, outsideDir);

    writeSubEvents(outsideDir, [
      { v: 1, ts: '2026-06-29T00:00:00.000Z', seq: 1, eventId: 's1', runId: 'r', kind: 'tool_call', correlationId: 'evilRun', payload: { toolName: 'Read', toolCallId: 'tc1' } },
    ]);

    const repo = makeRepo({
      getLocalTask: vi.fn().mockResolvedValue({
        id: 1,
        workspacePath: parentDir,
        runtimeDir: null,
        ownerId: 'alice-id',
        visibility: 'private' as const,
        visibilityScopeOrgId: null,
        spaceId: null,
      }),
      getJob: vi.fn().mockResolvedValue({
        id: 'evil', issueNumber: 1, status: 'succeeded', worktreePath: outsideDir, parentJobId: 'root',
      }),
    });

    const app = express();
    app.use(express.json());
    app.use('/api/local/tasks', createDelegateRunsRouter(repo));

    const res = await request(app).get('/api/local/tasks/1/delegate-runs/evilRun/timeline?jobId=evil');

    expect(res.status).toBe(404);
  });

  it('timeline jobId 未指定: 親 events に correlationId 一致なければサブジョブを線形探索', async () => {
    const parentDir = mkdtempSync(join(tmpdir(), 'parent-'));
    const subDir = join(parentDir, 'subtasks', 'sub1');
    tmpDirs.push(parentDir);

    // Parent has no matching correlationId events; sub-job does
    writeSubEvents(subDir, [
      { v: 1, ts: '2026-06-29T00:00:00.000Z', seq: 1, eventId: 's1', runId: 'r', kind: 'tool_call', correlationId: 'subRun', payload: { toolName: 'Read', toolCallId: 'tc1' } },
    ]);

    const repo = makeRepo({
      getLocalTask: vi.fn().mockResolvedValue({
        id: 1,
        workspacePath: parentDir,
        runtimeDir: null,
        ownerId: 'alice-id',
        visibility: 'private' as const,
        visibilityScopeOrgId: null,
        spaceId: null,
      }),
      getLatestJobForIssue: vi.fn().mockResolvedValue({ id: 'root' }),
      getSubJobs: vi.fn().mockImplementation(async (pid: string) => {
        if (pid === 'root') {
          return [{ id: 'sub1', issueNumber: 1, status: 'succeeded', worktreePath: subDir, parentJobId: 'root' }];
        }
        return [];
      }),
    });

    const app = express();
    app.use(express.json());
    app.use('/api/local/tasks', createDelegateRunsRouter(repo));

    const res = await request(app).get('/api/local/tasks/1/delegate-runs/subRun/timeline');

    expect(res.status).toBe(200);
    expect(res.body.events.length).toBeGreaterThan(0);
    expect(res.body.events.every((e: { correlationId?: string }) => e.correlationId === 'subRun')).toBe(true);
  });
});

// ---------------------------------------------------------------------------
// Subtask aggregation tests (Task 2)
// ---------------------------------------------------------------------------

describe('GET /api/local/tasks/:id/delegate-runs — subtask aggregation', () => {
  let tmpDirs: string[] = [];

  afterEach(() => {
    for (const dir of tmpDirs) {
      try { rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
    }
    tmpDirs = [];
    vi.clearAllMocks();
  });

  // Event factories reusing the same shape as START/DONE above
  const mkStart = (id: string) => ({
    v: 1, ts: '2026-06-29T00:00:00.000Z', seq: 1, eventId: `s-${id}`, runId: 'r',
    kind: 'delegate_start',
    payload: { delegateRunId: id, parentRunId: null, description: 'x', depth: 1 },
  });
  const mkDone = (id: string) => ({
    v: 1, ts: '2026-06-29T00:00:01.000Z', seq: 2, eventId: `c-${id}`, runId: 'r',
    kind: 'delegate_complete',
    payload: { delegateRunId: id, parentRunId: null, description: 'x', depth: 1, next: 'COMPLETE', status: 'success' },
  });

  it('親 run とサブタスク run を分けて返す', async () => {
    const parentDir = mkdtempSync(join(tmpdir(), 'parent-'));
    const subDir = join(parentDir, 'subtasks', 'sub1');
    tmpDirs.push(parentDir);

    // Write parent events
    writeSubEvents(parentDir, [mkStart('parentRun'), mkDone('parentRun')]);
    // Write sub-job events
    writeSubEvents(subDir, [mkStart('subRun'), mkDone('subRun')]);

    const repo = makeRepo({
      getLocalTask: vi.fn().mockResolvedValue({
        id: 1,
        workspacePath: parentDir,
        runtimeDir: null,
        ownerId: 'alice-id',
        visibility: 'private' as const,
        visibilityScopeOrgId: null,
        spaceId: null,
      }),
      getLatestJobForIssue: vi.fn().mockResolvedValue({ id: 'root' }),
      getSubJobs: vi.fn().mockImplementation(async (pid: string) => {
        if (pid === 'root') {
          return [{
            id: 'sub1',
            issueNumber: 1,
            status: 'succeeded',
            worktreePath: subDir,
            parentJobId: 'root',
          }];
        }
        return [];
      }),
    });

    const app = express();
    app.use(express.json());
    app.use('/api/local/tasks', createDelegateRunsRouter(repo));

    const res = await request(app).get('/api/local/tasks/1/delegate-runs');

    expect(res.status).toBe(200);
    // Parent runs
    expect(res.body.runs).toHaveLength(1);
    expect(res.body.runs[0].delegateRunId).toBe('parentRun');
    // Subtask runs
    expect(res.body.subtasks).toHaveLength(1);
    expect(res.body.subtasks[0].jobId).toBe('sub1');
    expect(res.body.subtasks[0].depth).toBe(1);
    expect(res.body.subtasks[0].status).toBe('succeeded');
    expect(res.body.subtasks[0].runs).toHaveLength(1);
    expect(res.body.subtasks[0].runs[0].delegateRunId).toBe('subRun');
  });

  it('親ワークスペース外の worktree を持つサブジョブは除外 (auth containment)', async () => {
    const parentDir = mkdtempSync(join(tmpdir(), 'parent-'));
    const outsideDir = mkdtempSync(join(tmpdir(), 'evil-'));
    tmpDirs.push(parentDir, outsideDir);

    // Write parent events (no delegate runs → empty)
    // Write events to the outside dir to prove it's excluded, not missing
    writeSubEvents(outsideDir, [mkStart('evilRun'), mkDone('evilRun')]);

    const repo = makeRepo({
      getLocalTask: vi.fn().mockResolvedValue({
        id: 1,
        workspacePath: parentDir,
        runtimeDir: null,
        ownerId: 'alice-id',
        visibility: 'private' as const,
        visibilityScopeOrgId: null,
        spaceId: null,
      }),
      getLatestJobForIssue: vi.fn().mockResolvedValue({ id: 'root' }),
      getSubJobs: vi.fn().mockImplementation(async (pid: string) => {
        if (pid === 'root') {
          return [{
            id: 'sub1',
            issueNumber: 1,
            status: 'succeeded',
            worktreePath: outsideDir,
            parentJobId: 'root',
          }];
        }
        return [];
      }),
    });

    const app = express();
    app.use(express.json());
    app.use('/api/local/tasks', createDelegateRunsRouter(repo));

    const res = await request(app).get('/api/local/tasks/1/delegate-runs');

    expect(res.status).toBe(200);
    // Outside sub-job must be excluded
    expect(res.body.subtasks).toHaveLength(0);
  });

  it('workspacePath が null のタスクはサブジョブ containment を評価できないため subtasks が空 (fail-closed regression)', async () => {
    // Regression: old code used `task.workspacePath && !isJobWithinWorkspace(...)` — when
    // workspacePath is null the && short-circuits and the check is BYPASSED, leaking events.
    // Fixed to: `!isJobWithinWorkspace(null, path)` which returns false → skips the sub-job.
    const subDir = mkdtempSync(join(tmpdir(), 'sub-'));
    tmpDirs.push(subDir);

    // Write delegate events into the sub-job dir so it would appear if included
    writeSubEvents(subDir, [mkStart('leakedRun'), mkDone('leakedRun')]);

    const repo = makeRepo({
      getLocalTask: vi.fn().mockResolvedValue({
        id: 1,
        workspacePath: null, // ← null triggers the bypass in old code
        runtimeDir: null,
        ownerId: 'alice-id',
        visibility: 'private' as const,
        visibilityScopeOrgId: null,
        spaceId: null,
      }),
      getLatestJobForIssue: vi.fn().mockResolvedValue({ id: 'root' }),
      getSubJobs: vi.fn().mockImplementation(async (pid: string) => {
        if (pid === 'root') {
          return [{
            id: 'sub1',
            issueNumber: 1,
            status: 'succeeded',
            worktreePath: subDir,
            parentJobId: 'root',
          }];
        }
        return [];
      }),
    });

    const app = express();
    app.use(express.json());
    app.use('/api/local/tasks', createDelegateRunsRouter(repo));

    const res = await request(app).get('/api/local/tasks/1/delegate-runs');

    expect(res.status).toBe(200);
    // Sub-job must be skipped because workspacePath is null (containment cannot be evaluated)
    expect(res.body.subtasks).toHaveLength(0);
  });

  it('既存テスト互換: サブジョブ無し時は subtasks が空配列', async () => {
    const { app, taskId, workspacePath } = await seedTaskWithWorkspace();
    tmpDirs.push(workspacePath);
    writeSubEvents(workspacePath, [mkStart('R1'), mkDone('R1')]);

    const res = await request(app).get(`/api/local/tasks/${taskId}/delegate-runs`);

    expect(res.status).toBe(200);
    expect(res.body.runs).toHaveLength(1);
    // getLatestJobForIssue is vi.fn() returning undefined → latestJob falsy → empty subtasks
    expect(res.body.subtasks).toEqual([]);
  });
});
