import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { mkdtempSync, rmSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { Repository, localTaskRepoName } from './repository.js';

describe('Repository worker-aware scheduling', () => {
  let tempDir = '';

  afterEach(() => {
    if (tempDir) {
      rmSync(tempDir, { recursive: true, force: true });
      tempDir = '';
    }
  });

  function makeRepo(): Repository {
    tempDir = mkdtempSync(join(tmpdir(), 'maestro-repo-'));
    return new Repository(join(tempDir, 'orchestrator.db'));
  }

  it('claims only jobs matching a healthy worker role set', async () => {
    const repo = makeRepo();
    try {
      await repo.upsertWorkerNode({
        workerId: 'worker-fast',
        endpoint: 'http://fast.example/v1',
        enabled: true,
        healthy: true,
        roles: ['auto', 'fast'],
      });
      await repo.upsertWorkerNode({
        workerId: 'worker-quality',
        endpoint: 'http://quality.example/v1',
        enabled: true,
        healthy: true,
        roles: ['auto', 'quality'],
      });

      await repo.createJob({
        repo: 'acme/demo',
        issueNumber: 1,
        instruction: 'quality job',
        pieceName: 'general',
        role: 'quality',
      });
      const fastJob = await repo.createJob({
        repo: 'acme/demo',
        issueNumber: 2,
        instruction: 'fast job',
        pieceName: 'general',
        role: 'fast',
      });

      const claimedByFast = await repo.claimNextJob('worker-fast');
      const claimedByQuality = await repo.claimNextJob('worker-quality');

      expect(claimedByFast?.id).toBe(fastJob.id);
      expect(claimedByFast?.requiredRole).toBe('fast');
      expect(claimedByQuality?.requiredRole).toBe('quality');
    } finally {
      repo.close();
    }
  });

  it('does not let unhealthy workers claim queued jobs', async () => {
    const repo = makeRepo();
    try {
      await repo.upsertWorkerNode({
        workerId: 'worker-fast',
        endpoint: 'http://fast.example/v1',
        enabled: true,
        healthy: false,
        roles: ['auto', 'fast'],
        lastError: 'fetch failed',
      });

      await repo.createJob({
        repo: 'acme/demo',
        issueNumber: 3,
        instruction: 'fast job',
        pieceName: 'general',
        role: 'fast',
      });

      await expect(repo.claimNextJob('worker-fast')).resolves.toBeNull();
    } finally {
      repo.close();
    }
  });
});

describe('Repository deleteLocalTask', () => {
  let tempDir = '';

  afterEach(() => {
    if (tempDir) {
      rmSync(tempDir, { recursive: true, force: true });
      tempDir = '';
    }
  });

  function makeRepo(): Repository {
    tempDir = mkdtempSync(join(tmpdir(), 'maestro-repo-'));
    return new Repository(join(tempDir, 'orchestrator.db'));
  }

  it('タスクを削除すると取得できなくなる', async () => {
    const repo = makeRepo();
    try {
      const task = await repo.createLocalTask({ title: 'test task', body: 'hello' });
      const taskId = task.id;

      // 削除前は取得できる
      const before = await repo.getLocalTask(taskId);
      expect(before).not.toBeNull();

      await repo.deleteLocalTask(taskId);

      // 削除後は取得できない
      const after = await repo.getLocalTask(taskId);
      expect(after).toBeNull();
    } finally {
      repo.close();
    }
  });

  it('タスク削除時に関連ジョブも削除される', async () => {
    const repo = makeRepo();
    try {
      const task = await repo.createLocalTask({ title: 'task with job', body: 'test' });
      const taskId = task.id;
      const repoName = localTaskRepoName(taskId);

      await repo.createJob({ repo: repoName, issueNumber: taskId, instruction: 'do something' });

      await repo.deleteLocalTask(taskId);

      // タスクが消えていることを確認
      const after = await repo.getLocalTask(taskId);
      expect(after).toBeNull();

      // ジョブも消えていることを確認
      const job = await repo.getLatestJobForIssue(repoName, taskId);
      expect(job).toBeNull();
    } finally {
      repo.close();
    }
  });

  it('running ジョブがある場合は削除を拒否する', async () => {
    const repo = makeRepo();
    try {
      await repo.upsertWorkerNode({
        workerId: 'worker-test',
        endpoint: 'http://test.example/v1',
        enabled: true,
        healthy: true,
        roles: ['auto'],
      });

      const task = await repo.createLocalTask({ title: 'active task', body: 'running' });
      const taskId = task.id;
      const repoName = localTaskRepoName(taskId);

      await repo.createJob({ repo: repoName, issueNumber: taskId, instruction: 'run me' });

      // ジョブを running 状態にする
      await repo.claimNextJob('worker-test');

      // 削除は失敗するはず
      await expect(repo.deleteLocalTask(taskId)).rejects.toThrow('has an active job');

      // タスクはまだ存在するはず
      const still = await repo.getLocalTask(taskId);
      expect(still).not.toBeNull();
    } finally {
      repo.close();
    }
  });

  it('存在しないタスクの削除はエラーになる', async () => {
    const repo = makeRepo();
    try {
      await expect(repo.deleteLocalTask(99999)).rejects.toThrow('not found');
    } finally {
      repo.close();
    }
  });
});

describe('Repository.requestJobCancel', () => {
  let tempDir = '';

  afterEach(() => {
    if (tempDir) {
      rmSync(tempDir, { recursive: true, force: true });
      tempDir = '';
    }
  });

  function makeRepo(): Repository {
    tempDir = mkdtempSync(join(tmpdir(), 'maestro-cancel-'));
    return new Repository(join(tempDir, 'orchestrator.db'));
  }

  it('running ジョブをキャンセルして cancelled ステータスに変更する', async () => {
    const repo = makeRepo();
    try {
      await repo.upsertWorkerNode({
        workerId: 'worker-1',
        endpoint: 'http://localhost/v1',
        enabled: true,
        healthy: true,
        roles: ['auto'],
      });
      await repo.createJob({
        repo: 'acme/demo',
        issueNumber: 1,
        instruction: 'test job',
        pieceName: 'general',
      });
      const claimed = await repo.claimNextJob('worker-1');
      expect(claimed).not.toBeNull();
      expect(claimed!.status).toBe('running');

      const result = repo.requestJobCancel(claimed!.id);
      expect(result).toBe(true);

      const updated = await repo.getJob(claimed!.id);
      expect(updated?.status).toBe('cancelled');
    } finally {
      repo.close();
    }
  });

  it('queued ジョブに対しては false を返し状態は変化しない', async () => {
    const repo = makeRepo();
    try {
      const job = await repo.createJob({
        repo: 'acme/demo',
        issueNumber: 2,
        instruction: 'test job',
        pieceName: 'general',
      });
      expect(job.status).toBe('queued');

      const result = repo.requestJobCancel(job.id);
      expect(result).toBe(false);

      const unchanged = await repo.getJob(job.id);
      expect(unchanged?.status).toBe('queued');
    } finally {
      repo.close();
    }
  });

  it('存在しないジョブ ID に対しては false を返す', async () => {
    const repo = makeRepo();
    try {
      const result = repo.requestJobCancel('non-existent-id');
      expect(result).toBe(false);
    } finally {
      repo.close();
    }
  });
});

describe('listLocalTasks', () => {
  let tempDir = '';

  afterEach(() => {
    if (tempDir) {
      rmSync(tempDir, { recursive: true, force: true });
      tempDir = '';
    }
  });

  function makeRepo(): Repository {
    tempDir = mkdtempSync(join(tmpdir(), 'maestro-list-'));
    return new Repository(join(tempDir, 'orchestrator.db'));
  }

  it('タスク 0 件で空配列を返す', async () => {
    const repo = makeRepo();
    try {
      const tasks = await repo.listLocalTasks();
      expect(tasks).toEqual([]);
    } finally {
      repo.close();
    }
  });

  it('タスクあり・ジョブなしで latestJob が undefined', async () => {
    const repo = makeRepo();
    try {
      await repo.createLocalTask({ title: 'task without job', body: 'no job' });
      const tasks = await repo.listLocalTasks();
      expect(tasks).toHaveLength(1);
      expect(tasks[0].title).toBe('task without job');
      expect(tasks[0].latestJob).toBeNull();
    } finally {
      repo.close();
    }
  });

  it('タスクあり・ジョブありで latestJob が設定される', async () => {
    const repo = makeRepo();
    try {
      const task = await repo.createLocalTask({ title: 'task with job', body: 'has job' });
      const repoName = localTaskRepoName(task.id);
      await repo.createJob({
        repo: repoName,
        issueNumber: task.id,
        instruction: 'do work',
        pieceName: 'general',
      });
      const tasks = await repo.listLocalTasks();
      expect(tasks).toHaveLength(1);
      expect(tasks[0].latestJob).not.toBeNull();
      expect(tasks[0].latestJob!.instruction).toBe('do work');
      expect(tasks[0].latestJob!.pieceName).toBe('general');
    } finally {
      repo.close();
    }
  });

  it('同一タスクに複数ジョブがある場合、最新が返る', async () => {
    const repo = makeRepo();
    try {
      const task = await repo.createLocalTask({ title: 'multi-job task', body: 'multiple jobs' });
      const repoName = localTaskRepoName(task.id);
      await repo.createJob({
        repo: repoName,
        issueNumber: task.id,
        instruction: 'first job',
        pieceName: 'general',
      });
      // Create second job (should be the latest)
      await repo.createJob({
        repo: repoName,
        issueNumber: task.id,
        instruction: 'second job',
        pieceName: 'general',
      });
      const tasks = await repo.listLocalTasks();
      expect(tasks).toHaveLength(1);
      expect(tasks[0].latestJob).not.toBeNull();
      expect(tasks[0].latestJob!.instruction).toBe('second job');
    } finally {
      repo.close();
    }
  });

  it('タスクあり・ジョブあり・サブジョブありで subtask 情報が付与される', async () => {
    const repo = makeRepo();
    try {
      const task = await repo.createLocalTask({ title: 'parent task', body: 'with subtasks' });
      const repoName = localTaskRepoName(task.id);
      const parentJob = await repo.createJob({
        repo: repoName,
        issueNumber: task.id,
        instruction: 'parent instruction',
        pieceName: 'general',
      });
      await repo.updateJob(parentJob.id, { status: 'waiting_subtasks' });

      const sub1 = await repo.createJob({
        repo: `subtask/${parentJob.id}`,
        issueNumber: 1,
        instruction: 'sub 1',
        pieceName: 'general',
        parentJobId: parentJob.id,
        subtaskDepth: 1,
      });
      await repo.updateJob(sub1.id, { status: 'succeeded' });

      await repo.createJob({
        repo: `subtask/${parentJob.id}`,
        issueNumber: 2,
        instruction: 'sub 2',
        pieceName: 'general',
        parentJobId: parentJob.id,
        subtaskDepth: 1,
      });

      const tasks = await repo.listLocalTasks();
      const found = tasks.find(t => t.id === task.id);
      expect(found).toBeDefined();
      expect(found!.subtasks).toHaveLength(2);
      expect(found!.subtaskCount).toBe(2);
      expect(found!.subtaskCompleted).toBe(1);
    } finally {
      repo.close();
    }
  });

  it('populates ownerName and visibilityScopeOrgName from JOINed users / user_gitea_orgs', async () => {
    const repo = makeRepo();
    try {
      const alice = repo.createUser({ email: 'alice@x.com', name: 'Alice', role: 'user', status: 'active' });
      repo.replaceUserGiteaOrgs(alice.id, [
        { orgId: 'org-42', orgName: 'Acme' },
      ]);
      await repo.createLocalTask({
        title: 'org-visible task',
        body: 'body',
        ownerId: alice.id,
        visibility: 'org',
        visibilityScopeOrgId: 'org-42',
      });
      const tasks = await repo.listLocalTasks();
      expect(tasks).toHaveLength(1);
      expect(tasks[0].ownerId).toBe(alice.id);
      expect(tasks[0].ownerName).toBe('Alice');
      expect(tasks[0].visibility).toBe('org');
      expect(tasks[0].visibilityScopeOrgId).toBe('org-42');
      expect(tasks[0].visibilityScopeOrgName).toBe('Acme');
    } finally {
      repo.close();
    }
  });
});

describe('listLocalTasks with subtasks', () => {
  let tempDir = '';

  afterEach(() => {
    if (tempDir) {
      rmSync(tempDir, { recursive: true, force: true });
      tempDir = '';
    }
  });

  function makeRepo(): Repository {
    tempDir = mkdtempSync(join(tmpdir(), 'maestro-subtasks-'));
    return new Repository(join(tempDir, 'orchestrator.db'));
  }

  it('should include subtask info when parent job has subtasks', async () => {
    const repo = makeRepo();
    try {
      const task = await repo.createLocalTask({
        title: 'Test task',
        body: 'Test body',
        pieceName: 'brainstorming',
      });

      const parentJob = await repo.createJob({
        repo: `local/task-${task.id}`,
        issueNumber: task.id,
        instruction: 'Parent instruction',
        pieceName: 'brainstorming',
        maxAttempts: 1,
        role: 'auto',
      });
      await repo.updateJob(parentJob.id, { status: 'waiting_subtasks' });

      const subJob1 = await repo.createJob({
        repo: `subtask/${parentJob.id}`,
        issueNumber: 1,
        instruction: 'Subtask 1 instruction',
        pieceName: 'general',
        parentJobId: parentJob.id,
        subtaskDepth: 1,
        maxAttempts: 1,
        role: 'auto',
      });
      await repo.updateJob(subJob1.id, { status: 'succeeded' });

      await repo.createJob({
        repo: `subtask/${parentJob.id}`,
        issueNumber: 2,
        instruction: 'Subtask 2 instruction',
        pieceName: 'general',
        parentJobId: parentJob.id,
        subtaskDepth: 1,
        maxAttempts: 1,
        role: 'auto',
      });

      const tasks = await repo.listLocalTasks();
      const found = tasks.find(t => t.id === task.id);

      expect(found).toBeDefined();
      expect(found!.subtasks).toHaveLength(2);
      expect(found!.subtaskCount).toBe(2);
      expect(found!.subtaskCompleted).toBe(1);
      expect(found!.subtasks![0].instruction).toBe('Subtask 1 instruction');
      expect(found!.subtasks![0].status).toBe('succeeded');
      expect(found!.subtasks![1].status).toBe('queued');
    } finally {
      repo.close();
    }
  });

  it('should not include subtasks when job has none', async () => {
    const repo = makeRepo();
    try {
      const task = await repo.createLocalTask({
        title: 'Simple task',
        body: 'No subtasks',
        pieceName: 'general',
      });

      await repo.createJob({
        repo: `local/task-${task.id}`,
        issueNumber: task.id,
        instruction: 'Simple instruction',
        pieceName: 'general',
        maxAttempts: 1,
        role: 'auto',
      });

      const tasks = await repo.listLocalTasks();
      const found = tasks.find(t => t.id === task.id);

      expect(found).toBeDefined();
      expect(found!.subtasks).toBeUndefined();
      expect(found!.subtaskCount).toBeUndefined();
    } finally {
      repo.close();
    }
  });
});

describe('ScheduledTasks', () => {
  let tempDir = '';

  afterEach(() => {
    if (tempDir) {
      rmSync(tempDir, { recursive: true, force: true });
      tempDir = '';
    }
  });

  function makeRepo(): Repository {
    tempDir = mkdtempSync(join(tmpdir(), 'maestro-sched-'));
    return new Repository(join(tempDir, 'orchestrator.db'));
  }

  it('should create and retrieve a scheduled task', async () => {
    const repo = makeRepo();
    try {
      const task = await repo.createScheduledTask({
        title: 'テスト',
        body: 'テストプロンプト',
        cronExpression: '0 9 * * *',
        nextRunAt: '2026-04-01T00:00:00Z',
      });
      expect(task.id).toBeGreaterThan(0);
      expect(task.body).toBe('テストプロンプト');
      expect(task.cronExpression).toBe('0 9 * * *');
      expect(task.isActive).toBe(true);

      const fetched = await repo.getScheduledTask(task.id);
      expect(fetched).not.toBeNull();
      expect(fetched!.title).toBe('テスト');
    } finally {
      repo.close();
    }
  });

  it('should list all scheduled tasks', async () => {
    const repo = makeRepo();
    try {
      await repo.createScheduledTask({ body: 'task1', cronExpression: '0 9 * * *', nextRunAt: '2026-04-01T00:00:00Z' });
      await repo.createScheduledTask({ body: 'task2', cronExpression: '0 10 * * *', nextRunAt: '2026-04-01T01:00:00Z' });
      const list = await repo.listScheduledTasks();
      expect(list.length).toBeGreaterThanOrEqual(2);
    } finally {
      repo.close();
    }
  });

  it('should return due tasks only', async () => {
    const repo = makeRepo();
    try {
      await repo.createScheduledTask({ body: 'past', cronExpression: '0 9 * * *', nextRunAt: '2020-01-01T00:00:00Z' });
      await repo.createScheduledTask({ body: 'future', cronExpression: '0 9 * * *', nextRunAt: '2099-01-01T00:00:00Z' });
      const due = await repo.getScheduledTasksDue();
      const bodies = due.map(t => t.body);
      expect(bodies).toContain('past');
      expect(bodies).not.toContain('future');
    } finally {
      repo.close();
    }
  });

  it('should update a scheduled task', async () => {
    const repo = makeRepo();
    try {
      const task = await repo.createScheduledTask({ body: 'original', cronExpression: '0 9 * * *', nextRunAt: '2026-04-01T00:00:00Z' });
      const updated = await repo.updateScheduledTask(task.id, { body: 'updated', isActive: false });
      expect(updated!.body).toBe('updated');
      expect(updated!.isActive).toBe(false);
    } finally {
      repo.close();
    }
  });

  it('should delete a scheduled task', async () => {
    const repo = makeRepo();
    try {
      const task = await repo.createScheduledTask({ body: 'to-delete', cronExpression: '0 9 * * *', nextRunAt: '2026-04-01T00:00:00Z' });
      const deleted = await repo.deleteScheduledTask(task.id);
      expect(deleted).toBe(true);
      const fetched = await repo.getScheduledTask(task.id);
      expect(fetched).toBeNull();
    } finally {
      repo.close();
    }
  });
});

describe('Feedback', () => {
  let tempDir = '';

  afterEach(() => {
    if (tempDir) {
      rmSync(tempDir, { recursive: true, force: true });
      tempDir = '';
    }
  });

  function makeRepo(): Repository {
    tempDir = mkdtempSync(join(tmpdir(), 'maestro-feedback-'));
    return new Repository(join(tempDir, 'orchestrator.db'));
  }

  it('updates and retrieves feedback on a local task', async () => {
    const repo = makeRepo();
    try {
      const task = await repo.createLocalTask({
        title: 'feedback test',
        body: 'test body',
      });

      await repo.updateFeedback(task.id, {
        rating: 'good',
        tags: ['精度が高い', 'フォーマットが適切'],
        comment: 'よくできました',
      });

      const updated = await repo.getLocalTask(task.id);
      expect(updated).toBeTruthy();
      expect(updated!.feedbackRating).toBe('good');
      expect(updated!.feedbackTags).toEqual(['精度が高い', 'フォーマットが適切']);
      expect(updated!.feedbackComment).toBe('よくできました');
      expect(updated!.feedbackAt).toBeTruthy();
    } finally {
      repo.close();
    }
  });

  it('returns null feedback fields when not set', async () => {
    const repo = makeRepo();
    try {
      const task = await repo.createLocalTask({
        title: 'no feedback',
        body: '',
      });

      const fetched = await repo.getLocalTask(task.id);
      expect(fetched!.feedbackRating).toBeNull();
      expect(fetched!.feedbackTags).toBeNull();
      expect(fetched!.feedbackComment).toBeNull();
      expect(fetched!.feedbackAt).toBeNull();
    } finally {
      repo.close();
    }
  });

  it('overwrites feedback on second update', async () => {
    const repo = makeRepo();
    try {
      const task = await repo.createLocalTask({
        title: 'overwrite test',
        body: '',
      });

      await repo.updateFeedback(task.id, { rating: 'good', tags: [], comment: null });
      await repo.updateFeedback(task.id, { rating: 'bad', tags: ['精度が低い'], comment: '改善必要' });

      const updated = await repo.getLocalTask(task.id);
      expect(updated!.feedbackRating).toBe('bad');
      expect(updated!.feedbackTags).toEqual(['精度が低い']);
      expect(updated!.feedbackComment).toBe('改善必要');
    } finally {
      repo.close();
    }
  });
});

describe('Share feature', () => {
  let tempDir = '';

  afterEach(() => {
    if (tempDir) {
      rmSync(tempDir, { recursive: true, force: true });
      tempDir = '';
    }
  });

  function makeRepo(): Repository {
    tempDir = mkdtempSync(join(tmpdir(), 'maestro-share-'));
    return new Repository(join(tempDir, 'orchestrator.db'));
  }

  it('shareLocalTask generates a token and returns it', async () => {
    const repo = makeRepo();
    const task = await repo.createLocalTask({ title: 'test', body: 'test body' });
    const token = await repo.shareLocalTask(task.id);
    expect(token).toBeTruthy();
    expect(typeof token).toBe('string');
    expect(token.length).toBeGreaterThan(10);

    const updated = await repo.getLocalTask(task.id);
    expect(updated?.shareToken).toBe(token);
    expect(updated?.sharedAt).toBeTruthy();
  });

  it('shareLocalTask returns existing token if already shared', async () => {
    const repo = makeRepo();
    const task = await repo.createLocalTask({ title: 'test', body: 'test body' });
    const token1 = await repo.shareLocalTask(task.id);
    const token2 = await repo.shareLocalTask(task.id);
    expect(token1).toBe(token2);
  });

  it('unshareLocalTask clears the token', async () => {
    const repo = makeRepo();
    const task = await repo.createLocalTask({ title: 'test', body: 'test body' });
    await repo.shareLocalTask(task.id);
    await repo.unshareLocalTask(task.id);
    const updated = await repo.getLocalTask(task.id);
    expect(updated?.shareToken).toBeNull();
    expect(updated?.sharedAt).toBeNull();
  });

  it('getLocalTaskByShareToken returns the task', async () => {
    const repo = makeRepo();
    const task = await repo.createLocalTask({ title: 'shared', body: 'body' });
    const token = await repo.shareLocalTask(task.id);
    const found = await repo.getLocalTaskByShareToken(token);
    expect(found).not.toBeNull();
    expect(found?.id).toBe(task.id);
    expect(found?.title).toBe('shared');
  });

  it('getLocalTaskByShareToken returns null for unknown token', async () => {
    const repo = makeRepo();
    const found = await repo.getLocalTaskByShareToken('nonexistent-token');
    expect(found).toBeNull();
  });

  it('getLocalTaskByShareToken returns null after unshare', async () => {
    const repo = makeRepo();
    const task = await repo.createLocalTask({ title: 'test', body: 'body' });
    const token = await repo.shareLocalTask(task.id);
    await repo.unshareLocalTask(task.id);
    const found = await repo.getLocalTaskByShareToken(token);
    expect(found).toBeNull();
  });
});

describe('Repository ownership migration', () => {
  let tempDir = '';
  afterEach(() => {
    if (tempDir) { rmSync(tempDir, { recursive: true, force: true }); tempDir = ''; }
  });
  function makeRepo(): Repository {
    tempDir = mkdtempSync(join(tmpdir(), 'maestro-repo-'));
    return new Repository(join(tempDir, 'orchestrator.db'));
  }

  it('adds owner_id/visibility/visibility_scope_org_id to local_tasks, scheduled_tasks, jobs', () => {
    const repo = makeRepo();
    try {
      const db = (repo as unknown as { db: import('better-sqlite3').Database }).db;
      for (const table of ['local_tasks', 'scheduled_tasks', 'jobs']) {
        const cols = db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>;
        const names = cols.map(c => c.name);
        expect(names).toContain('owner_id');
        expect(names).toContain('visibility');
        expect(names).toContain('visibility_scope_org_id');
      }
      const userCols = db.prepare('PRAGMA table_info(users)').all() as Array<{ name: string }>;
      const userNames = userCols.map(c => c.name);
      expect(userNames).toContain('default_visibility');
      expect(userNames).toContain('default_visibility_org_id');
      const orgCols = db.prepare('PRAGMA table_info(user_gitea_orgs)').all() as Array<{ name: string }>;
      expect(orgCols.length).toBeGreaterThan(0);
    } finally {
      repo.close();
    }
  });
});

describe('Repository user_gitea_orgs', () => {
  let tempDir = '';
  afterEach(() => {
    if (tempDir) { rmSync(tempDir, { recursive: true, force: true }); tempDir = ''; }
  });
  function makeRepo(): Repository {
    tempDir = mkdtempSync(join(tmpdir(), 'maestro-repo-'));
    return new Repository(join(tempDir, 'orchestrator.db'));
  }

  it('replaces user_gitea_orgs atomically (removes old, inserts new)', () => {
    const repo = makeRepo();
    try {
      const user = repo.createUser({
        email: 'u@example.com', name: 'U', role: 'user', status: 'active',
      });
      repo.replaceUserGiteaOrgs(user.id, [
        { orgId: '10', orgName: 'marketing' },
        { orgId: '20', orgName: 'platform' },
      ]);
      expect(repo.listUserGiteaOrgs(user.id).map(o => o.orgName).sort())
        .toEqual(['marketing', 'platform']);
      repo.replaceUserGiteaOrgs(user.id, [{ orgId: '10', orgName: 'marketing' }]);
      expect(repo.listUserGiteaOrgs(user.id).map(o => o.orgName)).toEqual(['marketing']);
    } finally {
      repo.close();
    }
  });
});

describe('findOrCreateUserByOAuth dummy email upgrade', () => {
  let tempDir = '';
  afterEach(() => {
    if (tempDir) { rmSync(tempDir, { recursive: true, force: true }); tempDir = ''; }
  });
  function makeRepo(): Repository {
    tempDir = mkdtempSync(join(tmpdir(), 'oauth-test-'));
    return new Repository(join(tempDir, 'db.sqlite'));
  }

  it('upgrades gitea.local dummy email to real email on next login', () => {
    const repo = makeRepo();
    try {
      const u1 = repo.findOrCreateUserByOAuth({
        provider: 'gitea', providerId: '42', email: 'bob@gitea.local', name: 'bob',
      });
      expect(u1.email).toBe('bob@gitea.local');
      const u2 = repo.findOrCreateUserByOAuth({
        provider: 'gitea', providerId: '42', email: 'bob@realmail.com', name: 'bob',
      });
      expect(u2.id).toBe(u1.id);
      expect(u2.email).toBe('bob@realmail.com');
    } finally {
      repo.close();
    }
  });

  it('syncs name and avatarUrl from the provider on re-login', () => {
    const repo = makeRepo();
    try {
      const u1 = repo.findOrCreateUserByOAuth({
        provider: 'gitea', providerId: '42', email: 'bob@x.com', name: 'bob', avatarUrl: null,
      });
      expect(u1.name).toBe('bob');
      expect(u1.avatarUrl).toBeNull();

      const u2 = repo.findOrCreateUserByOAuth({
        provider: 'gitea', providerId: '42', email: 'bob@x.com',
        name: 'Bob Full', avatarUrl: 'http://g/a.png',
      });
      expect(u2.id).toBe(u1.id);
      expect(u2.name).toBe('Bob Full');
      expect(u2.avatarUrl).toBe('http://g/a.png');
    } finally {
      repo.close();
    }
  });

  it('does not overwrite a populated name with an empty string on re-login', () => {
    const repo = makeRepo();
    try {
      const u1 = repo.findOrCreateUserByOAuth({
        provider: 'gitea', providerId: '42', email: 'bob@x.com', name: 'Bob Full',
      });
      const u2 = repo.findOrCreateUserByOAuth({
        provider: 'gitea', providerId: '42', email: 'bob@x.com', name: '',
      });
      expect(u2.name).toBe('Bob Full');
    } finally {
      repo.close();
    }
  });
});

describe('listLocalTasks visibility filter', () => {
  let tempDir = '';
  afterEach(() => {
    if (tempDir) { rmSync(tempDir, { recursive: true, force: true }); tempDir = ''; }
  });
  function makeRepo(): Repository {
    tempDir = mkdtempSync(join(tmpdir(), 'list-lt-'));
    return new Repository(join(tempDir, 'db.sqlite'));
  }

  it('filters local_tasks by owner / visibility / org membership', async () => {
    const repo = makeRepo();
    try {
      const alice = repo.createUser({ email: 'a@x.com', name: 'a', role: 'user', status: 'active' });
      const bob = repo.createUser({ email: 'b@x.com', name: 'b', role: 'user', status: 'active' });
      repo.replaceUserGiteaOrgs(alice.id, [{ orgId: '10', orgName: 'mkt' }]);
      repo.replaceUserGiteaOrgs(bob.id, [{ orgId: '20', orgName: 'eng' }]);

      const tAlicePriv = await repo.createLocalTask({
        title: 't1', body: '', ownerId: alice.id, visibility: 'private', visibilityScopeOrgId: null,
      });
      const tAliceOrg = await repo.createLocalTask({
        title: 't2', body: '', ownerId: alice.id, visibility: 'org', visibilityScopeOrgId: '10',
      });
      const tBobPub = await repo.createLocalTask({
        title: 't3', body: '', ownerId: bob.id, visibility: 'public', visibilityScopeOrgId: null,
      });
      void tAlicePriv; void tAliceOrg; void tBobPub;

      const aliceUser = { ...alice, orgIds: ['10'], defaultVisibility: 'private' as const, defaultVisibilityOrgId: null };
      const listAsAlice = await repo.listLocalTasks({ viewer: aliceUser });
      expect(listAsAlice.map(t => t.title).sort()).toEqual(['t1', 't2', 't3']);

      const bobUser = { ...bob, orgIds: ['20'], defaultVisibility: 'private' as const, defaultVisibilityOrgId: null };
      const listAsBob = await repo.listLocalTasks({ viewer: bobUser });
      expect(listAsBob.map(t => t.title).sort()).toEqual(['t3']);

      const admin = repo.createUser({ email: 'adm@x.com', name: 'adm', role: 'admin', status: 'active' });
      const adminUser = { ...admin, orgIds: [], defaultVisibility: 'private' as const, defaultVisibilityOrgId: null };
      const listAsAdmin = await repo.listLocalTasks({ viewer: adminUser });
      expect(listAsAdmin.length).toBe(3);
    } finally {
      repo.close();
    }
  });
});

describe('updateLocalTask visibility fields', () => {
  let tempDir = '';
  afterEach(() => {
    if (tempDir) { rmSync(tempDir, { recursive: true, force: true }); tempDir = ''; }
  });
  function makeRepo(): Repository {
    tempDir = mkdtempSync(join(tmpdir(), 'update-lt-'));
    return new Repository(join(tempDir, 'db.sqlite'));
  }

  it('updates visibility and visibilityScopeOrgId', async () => {
    const repo = makeRepo();
    try {
      const alice = repo.createUser({ email: 'a@x.com', name: 'a', role: 'user', status: 'active' });
      const task = await repo.createLocalTask({
        title: 't', body: '', ownerId: alice.id, visibility: 'private', visibilityScopeOrgId: null,
      });

      await repo.updateLocalTask(task.id, { visibility: 'org', visibilityScopeOrgId: '10' });
      const after = await repo.getLocalTask(task.id);
      expect(after!.visibility).toBe('org');
      expect(after!.visibilityScopeOrgId).toBe('10');

      await repo.updateLocalTask(task.id, { visibility: 'public', visibilityScopeOrgId: null });
      const after2 = await repo.getLocalTask(task.id);
      expect(after2!.visibility).toBe('public');
      expect(after2!.visibilityScopeOrgId).toBeNull();
    } finally {
      repo.close();
    }
  });
});

describe('getLocalTask / getJob visibility', () => {
  let tempDir = '';
  afterEach(() => {
    if (tempDir) { rmSync(tempDir, { recursive: true, force: true }); tempDir = ''; }
  });
  function makeRepo(): Repository {
    tempDir = mkdtempSync(join(tmpdir(), 'get-vis-'));
    return new Repository(join(tempDir, 'db.sqlite'));
  }

  it('returns null when viewer has no access', async () => {
    const repo = makeRepo();
    try {
      const alice = repo.createUser({ email: 'a@x.com', name: 'a', role: 'user', status: 'active' });
      const bob = repo.createUser({ email: 'b@x.com', name: 'b', role: 'user', status: 'active' });
      const t = await repo.createLocalTask({
        title: 'secret', body: '', ownerId: alice.id, visibility: 'private',
      });
      const bobUser = { ...bob, orgIds: [], defaultVisibility: 'private' as const, defaultVisibilityOrgId: null };
      const aliceUser = { ...alice, orgIds: [], defaultVisibility: 'private' as const, defaultVisibilityOrgId: null };
      expect(await repo.getLocalTask(t.id, { viewer: bobUser })).toBeNull();
      expect(await repo.getLocalTask(t.id, { viewer: aliceUser })).not.toBeNull();
    } finally {
      repo.close();
    }
  });
});

describe('createJob owner/visibility', () => {
  let tempDir = '';
  afterEach(() => {
    if (tempDir) { rmSync(tempDir, { recursive: true, force: true }); tempDir = ''; }
  });
  function makeRepo(): Repository {
    tempDir = mkdtempSync(join(tmpdir(), 'createjob-'));
    return new Repository(join(tempDir, 'db.sqlite'));
  }

  it('jobs inherit owner/visibility from createJob params', async () => {
    const repo = makeRepo();
    try {
      const alice = repo.createUser({ email: 'a@x.com', name: 'a', role: 'user', status: 'active' });
      const job = await repo.createJob({
        repo: 'local/task-1', issueNumber: 1, instruction: 'go', pieceName: 'chat',
        ownerId: alice.id, visibility: 'org', visibilityScopeOrgId: '10',
      });
      expect(job.ownerId).toBe(alice.id);
      expect(job.visibility).toBe('org');
      expect(job.visibilityScopeOrgId).toBe('10');
    } finally {
      repo.close();
    }
  });
});

describe('createScheduledTask owner/visibility', () => {
  let tempDir = '';
  afterEach(() => {
    if (tempDir) { rmSync(tempDir, { recursive: true, force: true }); tempDir = ''; }
  });
  function makeRepo(): Repository {
    tempDir = mkdtempSync(join(tmpdir(), 'sched-own-'));
    return new Repository(join(tempDir, 'db.sqlite'));
  }

  it('defaults to private / null owner / null scope when not provided', async () => {
    const repo = makeRepo();
    try {
      const st = await repo.createScheduledTask({
        body: 'b', cronExpression: '* * * * *', nextRunAt: new Date().toISOString(),
      });
      expect(st.ownerId).toBeNull();
      expect(st.visibility).toBe('private');
      expect(st.visibilityScopeOrgId).toBeNull();
    } finally {
      repo.close();
    }
  });

  it('persists owner/visibility/scope when provided', async () => {
    const repo = makeRepo();
    try {
      const alice = repo.createUser({ email: 'a@x.com', name: 'a', role: 'user', status: 'active' });
      const st = await repo.createScheduledTask({
        body: 'b', cronExpression: '* * * * *', nextRunAt: new Date().toISOString(),
        ownerId: alice.id, visibility: 'org', visibilityScopeOrgId: '10',
      });
      expect(st.ownerId).toBe(alice.id);
      expect(st.visibility).toBe('org');
      expect(st.visibilityScopeOrgId).toBe('10');

      const fetched = await repo.getScheduledTask(st.id);
      expect(fetched!.ownerId).toBe(alice.id);
      expect(fetched!.visibility).toBe('org');
      expect(fetched!.visibilityScopeOrgId).toBe('10');
    } finally {
      repo.close();
    }
  });
});

describe('scheduler createJob inheritance', () => {
  let tempDir = '';
  afterEach(() => {
    if (tempDir) { rmSync(tempDir, { recursive: true, force: true }); tempDir = ''; }
  });
  function makeRepo(): Repository {
    tempDir = mkdtempSync(join(tmpdir(), 'sched-inh-'));
    return new Repository(join(tempDir, 'db.sqlite'));
  }

  it('scheduler-spawned job inherits owner/visibility from scheduled_task', async () => {
    const repo = makeRepo();
    try {
      const alice = repo.createUser({ email: 'a@x.com', name: 'a', role: 'user', status: 'active' });
      const st = await repo.createScheduledTask({
        title: 't', body: 'b',
        cronExpression: '* * * * *',
        nextRunAt: new Date().toISOString(),
        ownerId: alice.id, visibility: 'public', visibilityScopeOrgId: null,
      });
      const job = await repo.createJob({
        repo: `local/task-${st.id}`, issueNumber: st.id, instruction: 'go', pieceName: 'chat',
        ownerId: st.ownerId, visibility: st.visibility, visibilityScopeOrgId: st.visibilityScopeOrgId,
      });
      expect(job.ownerId).toBe(alice.id);
      expect(job.visibility).toBe('public');
      expect(job.visibilityScopeOrgId).toBeNull();
    } finally {
      repo.close();
    }
  });
});

describe('subtask inheritance', () => {
  let tempDir = '';
  afterEach(() => {
    if (tempDir) { rmSync(tempDir, { recursive: true, force: true }); tempDir = ''; }
  });
  function makeRepo(): Repository {
    tempDir = mkdtempSync(join(tmpdir(), 'subtask-inh-'));
    return new Repository(join(tempDir, 'db.sqlite'));
  }

  it('subtask job inherits owner/visibility from parent job', async () => {
    const repo = makeRepo();
    try {
      const alice = repo.createUser({ email: 'a@x.com', name: 'a', role: 'user', status: 'active' });
      const parent = await repo.createJob({
        repo: 'local/task-1', issueNumber: 1, instruction: 'root', pieceName: 'chat',
        ownerId: alice.id, visibility: 'org', visibilityScopeOrgId: '10',
      });
      const child = await repo.createJob({
        repo: 'local/task-1', issueNumber: 2, instruction: 'sub', pieceName: 'chat',
        parentJobId: parent.id, subtaskDepth: 1,
        ownerId: parent.ownerId,
        visibility: parent.visibility,
        visibilityScopeOrgId: parent.visibilityScopeOrgId,
      });
      expect(child.ownerId).toBe(alice.id);
      expect(child.visibility).toBe('org');
      expect(child.visibilityScopeOrgId).toBe('10');
    } finally {
      repo.close();
    }
  });
});

describe('Repository updateUser preferences', () => {
  let tempDir = '';
  afterEach(() => {
    if (tempDir) { rmSync(tempDir, { recursive: true, force: true }); tempDir = ''; }
  });
  function makeRepo(): Repository {
    tempDir = mkdtempSync(join(tmpdir(), 'maestro-prefs-'));
    return new Repository(join(tempDir, 'orchestrator.db'));
  }

  it('writes defaultVisibility and defaultVisibilityOrgId columns', () => {
    const repo = makeRepo();
    try {
      const u = repo.createUser({
        email: 'u@example.com', name: 'U', role: 'user', status: 'active',
      });
      expect(u.defaultVisibility).toBe('private');
      expect(u.defaultVisibilityOrgId).toBeNull();

      repo.updateUser(u.id, { defaultVisibility: 'org', defaultVisibilityOrgId: 'acme' });
      const after = repo.getUserById(u.id);
      expect(after).not.toBeNull();
      expect(after!.defaultVisibility).toBe('org');
      expect(after!.defaultVisibilityOrgId).toBe('acme');
    } finally {
      repo.close();
    }
  });

  it('allows clearing defaultVisibilityOrgId by passing null', () => {
    const repo = makeRepo();
    try {
      const u = repo.createUser({
        email: 'u2@example.com', name: 'U2', role: 'user', status: 'active',
      });
      repo.updateUser(u.id, { defaultVisibility: 'org', defaultVisibilityOrgId: 'acme' });
      repo.updateUser(u.id, { defaultVisibility: 'public', defaultVisibilityOrgId: null });
      const after = repo.getUserById(u.id);
      expect(after!.defaultVisibility).toBe('public');
      expect(after!.defaultVisibilityOrgId).toBeNull();
    } finally {
      repo.close();
    }
  });
});

describe('Repository job context tracking', () => {
  let tempDir = '';
  afterEach(() => {
    if (tempDir) {
      rmSync(tempDir, { recursive: true, force: true });
      tempDir = '';
    }
  });
  function makeRepo(): Repository {
    tempDir = mkdtempSync(join(tmpdir(), 'maestro-ctx-'));
    return new Repository(join(tempDir, 'orchestrator.db'));
  }

  it('persists context tokens and limit on a job', async () => {
    const repo = makeRepo();
    try {
      const job = await repo.createJob({
        repo: 'acme/demo',
        issueNumber: 1,
        instruction: 'test',
        pieceName: 'general',
      });
      await repo.updateJobContext(job.id, { promptTokens: 12_345, limitTokens: 128_000 });
      const reloaded = await repo.getJob(job.id);
      expect(reloaded?.contextPromptTokens).toBe(12_345);
      expect(reloaded?.contextLimitTokens).toBe(128_000);
      expect(reloaded?.contextUpdatedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
    } finally {
      repo.close();
    }
  });

  it('updateJobContext is idempotent and overwrites previous values', async () => {
    const repo = makeRepo();
    try {
      const job = await repo.createJob({
        repo: 'acme/demo',
        issueNumber: 2,
        instruction: 'test',
        pieceName: 'general',
      });
      await repo.updateJobContext(job.id, { promptTokens: 1_000, limitTokens: 64_000 });
      await repo.updateJobContext(job.id, { promptTokens: 50_000, limitTokens: 128_000 });
      const reloaded = await repo.getJob(job.id);
      expect(reloaded?.contextPromptTokens).toBe(50_000);
      expect(reloaded?.contextLimitTokens).toBe(128_000);
    } finally {
      repo.close();
    }
  });

  it('persists lastBackendId via updateJob and exposes it via getJob', async () => {
    const repo = makeRepo();
    try {
      const job = await repo.createJob({
        repo: 'acme/demo',
        issueNumber: 3,
        instruction: 'test',
        pieceName: 'general',
      });
      // Fresh jobs have lastBackendId=null (proxy hasn't responded yet, or direct worker)
      expect(job.lastBackendId).toBeNull();

      await repo.updateJob(job.id, { lastBackendId: 'gpu-rtx-a' });
      const reloaded = await repo.getJob(job.id);
      expect(reloaded?.lastBackendId).toBe('gpu-rtx-a');
    } finally {
      repo.close();
    }
  });
});

describe('Repository.updateMissionBrief', () => {
  let tempDir = '';

  afterEach(() => {
    if (tempDir) {
      rmSync(tempDir, { recursive: true, force: true });
      tempDir = '';
    }
  });

  function makeRepo(): Repository {
    tempDir = mkdtempSync(join(tmpdir(), 'maestro-mission-'));
    return new Repository(join(tempDir, 'orchestrator.db'));
  }

  it('persists a fresh brief and reads it back via getLocalTask', async () => {
    const repo = makeRepo();
    try {
      const task = await repo.createLocalTask({ title: 't', body: 'b' });
      const merged = await repo.updateMissionBrief(task.id, {
        goal: 'ship the thing',
        done: '- analyzed requirements',
        open: '- implement\n- test',
        clarifications: '',
      });
      expect(merged?.goal).toBe('ship the thing');
      expect(merged?.done).toBe('- analyzed requirements');
      expect(merged?.open).toBe('- implement\n- test');
      expect(merged?.clarifications).toBe('');

      const reloaded = await repo.getLocalTask(task.id);
      expect(reloaded?.missionBrief?.goal).toBe('ship the thing');
      expect(reloaded?.missionBrief?.done).toBe('- analyzed requirements');
    } finally {
      repo.close();
    }
  });

  it('partial-replace: undefined fields keep prior values', async () => {
    const repo = makeRepo();
    try {
      const task = await repo.createLocalTask({ title: 't', body: 'b' });
      await repo.updateMissionBrief(task.id, {
        goal: 'A',
        done: 'B',
        open: 'C',
        clarifications: 'D',
      });
      // Only overwrite `done`. goal/open/clarifications must survive.
      const merged = await repo.updateMissionBrief(task.id, { done: 'B-updated' });
      expect(merged?.goal).toBe('A');
      expect(merged?.done).toBe('B-updated');
      expect(merged?.open).toBe('C');
      expect(merged?.clarifications).toBe('D');
    } finally {
      repo.close();
    }
  });

  it('all-empty input clears the brief to NULL', async () => {
    const repo = makeRepo();
    try {
      const task = await repo.createLocalTask({ title: 't', body: 'b' });
      await repo.updateMissionBrief(task.id, { goal: 'X' });
      expect((await repo.getLocalTask(task.id))?.missionBrief?.goal).toBe('X');

      const cleared = await repo.updateMissionBrief(task.id, {
        goal: '',
        done: '',
        open: '',
        clarifications: '',
      });
      expect(cleared).toBeNull();
      expect((await repo.getLocalTask(task.id))?.missionBrief).toBeNull();
    } finally {
      repo.close();
    }
  });

  it('makeMissionBriefIO produces a sync read/update bound to the task', async () => {
    const repo = makeRepo();
    try {
      const task = await repo.createLocalTask({ title: 't', body: 'b' });
      const io = repo.makeMissionBriefIO(task.id);
      expect(io.read()).toBeNull();
      const merged = io.update({ goal: 'sync goal' });
      expect(merged?.goal).toBe('sync goal');
      expect(io.read()?.goal).toBe('sync goal');
    } finally {
      repo.close();
    }
  });
});

describe('resumeMcpWaitingJobs', () => {
  let tempDir = '';
  let repo: Repository;

  beforeEach(() => {
    tempDir = mkdtempSync(join(tmpdir(), 'maestro-resume-mcp-'));
    repo = new Repository(join(tempDir, 'orchestrator.db'));
  });

  afterEach(() => {
    repo.close();
    if (tempDir) {
      rmSync(tempDir, { recursive: true, force: true });
      tempDir = '';
    }
  });

  it('re-queues a matching waiting_human job and returns 1', async () => {
    const job = await repo.createJob({
      repo: 'acme/demo',
      issueNumber: 1,
      instruction: 'do something',
      ownerId: 'u1',
    });
    // Park the job as mcp_auth_required
    repo['db']
      .prepare(
        `UPDATE jobs SET status='waiting_human', wait_reason='mcp_auth_required' WHERE id = ?`,
      )
      .run(job.id);

    const count = repo.resumeMcpWaitingJobs('u1', 'canva');

    expect(count).toBe(1);
    const updated = repo['getJobSync'](job.id);
    expect(updated?.status).toBe('queued');
    expect(updated?.waitReason).toBeNull();
  });

  it('does not touch a waiting job belonging to a different owner', async () => {
    const job = await repo.createJob({
      repo: 'acme/demo',
      issueNumber: 2,
      instruction: 'do something',
      ownerId: 'u2',
    });
    repo['db']
      .prepare(
        `UPDATE jobs SET status='waiting_human', wait_reason='mcp_auth_required' WHERE id = ?`,
      )
      .run(job.id);

    const count = repo.resumeMcpWaitingJobs('u1', 'canva');

    expect(count).toBe(0);
    const unchanged = repo['getJobSync'](job.id);
    expect(unchanged?.status).toBe('waiting_human');
    expect(unchanged?.waitReason).toBe('mcp_auth_required');
  });

  it('does not touch a waiting_human job with a different wait_reason', async () => {
    const job = await repo.createJob({
      repo: 'acme/demo',
      issueNumber: 3,
      instruction: 'ask me',
      ownerId: 'u1',
    });
    repo['db']
      .prepare(`UPDATE jobs SET status='waiting_human', wait_reason='ask' WHERE id = ?`)
      .run(job.id);

    const count = repo.resumeMcpWaitingJobs('u1', 'canva');

    expect(count).toBe(0);
    const unchanged = repo['getJobSync'](job.id);
    expect(unchanged?.status).toBe('waiting_human');
    expect(unchanged?.waitReason).toBe('ask');
  });

  it('does not touch a queued job with wait_reason=mcp_auth_required (defensive)', async () => {
    const job = await repo.createJob({
      repo: 'acme/demo',
      issueNumber: 4,
      instruction: 'queued but has wait_reason',
      ownerId: 'u1',
    });
    // Manually set wait_reason without changing status (abnormal state)
    repo['db']
      .prepare(`UPDATE jobs SET wait_reason='mcp_auth_required' WHERE id = ?`)
      .run(job.id);

    const count = repo.resumeMcpWaitingJobs('u1', 'canva');

    expect(count).toBe(0);
    const unchanged = repo['getJobSync'](job.id);
    expect(unchanged?.status).toBe('queued');
  });
});

describe('Repository.getLatestResultComment', () => {
  let tempDir = '';

  afterEach(() => {
    if (tempDir) {
      rmSync(tempDir, { recursive: true, force: true });
      tempDir = '';
    }
  });

  function makeRepo(): Repository {
    tempDir = mkdtempSync(join(tmpdir(), 'maestro-latest-result-'));
    return new Repository(join(tempDir, 'orchestrator.db'));
  }

  it('getLatestResultComment returns the most recent agent result/ask comment', async () => {
    const repo = makeRepo();
    try {
      const task = await repo.createLocalTask({ title: 't', body: 'b', pieceName: 'chat' });
      await repo.addLocalTaskComment(task.id, 'user', 'q1', 'comment');
      await repo.addLocalTaskComment(task.id, 'agent', '✅ done A', 'result');
      await repo.addLocalTaskComment(task.id, 'user', 'q2', 'comment');
      await repo.addLocalTaskComment(task.id, 'agent', '❓ ask Q', 'ask');
      const got = await repo.getLatestResultComment(task.id);
      expect(got?.kind).toBe('ask');
      expect(got?.body).toBe('❓ ask Q');
    } finally {
      repo.close();
    }
  });

  it('getLatestResultComment returns null when no result/ask comments exist', async () => {
    const repo = makeRepo();
    try {
      const task = await repo.createLocalTask({ title: 't', body: 'b', pieceName: 'chat' });
      await repo.addLocalTaskComment(task.id, 'user', 'just a request', 'request');
      expect(await repo.getLatestResultComment(task.id)).toBeNull();
    } finally {
      repo.close();
    }
  });
});

describe('Repository browser notifications V2', () => {
  let tempDir = '';
  afterEach(() => {
    if (tempDir) {
      rmSync(tempDir, { recursive: true, force: true });
      tempDir = '';
    }
  });

  function makeRepo(): Repository {
    tempDir = mkdtempSync(join(tmpdir(), 'maestro-repo-push-'));
    return new Repository(join(tempDir, 'orchestrator.db'));
  }

  function createUser(repo: Repository, email: string) {
    return repo.createUser({
      email,
      name: email.split('@')[0]!,
      role: 'user',
      status: 'active',
    });
  }

  describe('push_subscriptions', () => {
    it('inserts a new subscription and lists it', () => {
      const repo = makeRepo();
      try {
        const user = createUser(repo, 'u1@example.com');
        const { id } = repo.upsertPushSubscription({
          userId: user.id,
          endpoint: 'https://fcm.googleapis.com/fcm/send/abc',
          p256dh: 'pubkey',
          auth: 'authsec',
          userAgent: 'Chrome on Pixel',
          vapidKeyId: 'v1-2026-05-28',
        });
        expect(typeof id).toBe('string');
        const subs = repo.listPushSubscriptionsForUser(user.id);
        expect(subs).toHaveLength(1);
        expect(subs[0]!.endpoint).toBe('https://fcm.googleapis.com/fcm/send/abc');
        expect(subs[0]!.failureCount).toBe(0);
        expect(subs[0]!.userAgent).toBe('Chrome on Pixel');
      } finally {
        repo.close();
      }
    });

    it('transfers ownership when the same endpoint is upserted by another user', () => {
      const repo = makeRepo();
      try {
        const userA = createUser(repo, 'a@example.com');
        const userB = createUser(repo, 'b@example.com');
        const endpoint = 'https://fcm.googleapis.com/fcm/send/shared-browser';
        const first = repo.upsertPushSubscription({
          userId: userA.id, endpoint, p256dh: 'ka', auth: 'aa', vapidKeyId: 'v1',
        });
        repo.markPushSubscriptionFailure(first.id);
        repo.markPushSubscriptionFailure(first.id);

        const second = repo.upsertPushSubscription({
          userId: userB.id, endpoint, p256dh: 'kb', auth: 'ab', vapidKeyId: 'v1',
        });
        expect(second.id).toBe(first.id);

        const aSubs = repo.listPushSubscriptionsForUser(userA.id);
        const bSubs = repo.listPushSubscriptionsForUser(userB.id);
        expect(aSubs).toHaveLength(0);
        expect(bSubs).toHaveLength(1);
        // failure_count was reset, new key material in place
        expect(bSubs[0]!.failureCount).toBe(0);
        expect(bSubs[0]!.p256dh).toBe('kb');
      } finally {
        repo.close();
      }
    });

    it('markSuccess resets failure_count and updates last_success_at', () => {
      const repo = makeRepo();
      try {
        const user = createUser(repo, 'u@example.com');
        const { id } = repo.upsertPushSubscription({
          userId: user.id, endpoint: 'https://x.example/y', p256dh: 'k', auth: 'a', vapidKeyId: 'v1',
        });
        repo.markPushSubscriptionFailure(id);
        repo.markPushSubscriptionFailure(id);
        expect(repo.getPushSubscriptionById(id)?.failureCount).toBe(2);
        repo.markPushSubscriptionSuccess(id);
        const sub = repo.getPushSubscriptionById(id);
        expect(sub?.failureCount).toBe(0);
        expect(sub?.lastSuccessAt).toBeTruthy();
      } finally {
        repo.close();
      }
    });

    it('deletePushSubscription removes the row', () => {
      const repo = makeRepo();
      try {
        const user = createUser(repo, 'u@example.com');
        const { id } = repo.upsertPushSubscription({
          userId: user.id, endpoint: 'https://x.example/y', p256dh: 'k', auth: 'a', vapidKeyId: 'v1',
        });
        repo.deletePushSubscription(id);
        expect(repo.getPushSubscriptionById(id)).toBeNull();
        expect(repo.listPushSubscriptionsForUser(user.id)).toHaveLength(0);
      } finally {
        repo.close();
      }
    });
  });

  describe('user_notification_prefs', () => {
    it('returns defaults for users with no row', () => {
      const repo = makeRepo();
      try {
        const user = createUser(repo, 'u@example.com');
        const prefs = repo.getUserNotificationPrefs(user.id);
        expect(prefs.enabled).toBe(true);
        expect(prefs.events).toEqual({
          running: true, succeeded: true, failed: true, waiting_human: true,
        });
        expect(prefs.includeDetails).toBe(false);
        expect(prefs.v1Migrated).toBe(false);
      } finally {
        repo.close();
      }
    });

    it('upsertUserNotificationPrefs applies partial updates', () => {
      const repo = makeRepo();
      try {
        const user = createUser(repo, 'u@example.com');
        repo.upsertUserNotificationPrefs(user.id, {
          enabled: false,
          events: { succeeded: false },
        } as never);
        const prefs = repo.getUserNotificationPrefs(user.id);
        expect(prefs.enabled).toBe(false);
        expect(prefs.events.succeeded).toBe(false);
        // unchanged events stay at default
        expect(prefs.events.running).toBe(true);
        expect(prefs.events.failed).toBe(true);
      } finally {
        repo.close();
      }
    });

    it('markV1MigrationComplete is idempotent (returns false on second call)', () => {
      const repo = makeRepo();
      try {
        const user = createUser(repo, 'u@example.com');
        expect(repo.markV1MigrationComplete(user.id)).toBe(true);
        expect(repo.markV1MigrationComplete(user.id)).toBe(false);
        expect(repo.getUserNotificationPrefs(user.id).v1Migrated).toBe(true);
      } finally {
        repo.close();
      }
    });
  });
});
