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

// New functional coverage for the job recovery / resume helpers, the audit-log
// writer, the comment-injection lifecycle, and the subtask-ASK requeue plumbing.
// All exercise the real Repository against a temp-file SQLite DB (schema +
// migrations applied by the constructor). See inventory gaps DB-003, DB-032,
// DB-027, DB-008, SVC-019.

describe('Repository job recovery / resume / comments', () => {
  let tempDir = '';

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

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

  // --- recoverOrphanedJobs ---------------------------------------------------

  describe('recoverOrphanedJobs', () => {
    it('requeues running and dispatching jobs and clears issue locks', async () => {
      const repo = makeRepo();
      try {
        const running = await repo.createJob({ repo: 'acme/a', issueNumber: 1, instruction: 'a' });
        const dispatching = await repo.createJob({ repo: 'acme/b', issueNumber: 2, instruction: 'b' });
        const queued = await repo.createJob({ repo: 'acme/c', issueNumber: 3, instruction: 'c' });
        await repo.updateJob(running.id, { status: 'running' });
        await repo.updateJob(dispatching.id, { status: 'dispatching' });
        await repo.lockIssue('acme/a', 1, running.id);

        const changes = await repo.recoverOrphanedJobs();

        expect(changes).toBe(2);
        expect((await repo.getJob(running.id))?.status).toBe('queued');
        expect((await repo.getJob(dispatching.id))?.status).toBe('queued');
        // queued job untouched
        expect((await repo.getJob(queued.id))?.status).toBe('queued');
        // worker_id cleared
        expect((await repo.getJob(running.id))?.workerId).toBeNull();
        // issue lock released, so re-locking succeeds
        expect(await repo.lockIssue('acme/a', 1, dispatching.id)).toBe(true);
      } finally {
        repo.close();
      }
    });

    it('is a no-op (returns 0) when nothing is running or dispatching', async () => {
      const repo = makeRepo();
      try {
        const queued = await repo.createJob({ repo: 'acme/a', issueNumber: 1, instruction: 'a' });
        expect(await repo.recoverOrphanedJobs()).toBe(0);
        expect((await repo.getJob(queued.id))?.status).toBe('queued');
      } finally {
        repo.close();
      }
    });

    it('requeues a waiting_subtasks parent once all its latest subtasks are terminal', async () => {
      const repo = makeRepo();
      try {
        const parent = await repo.createJob({ repo: 'acme/p', issueNumber: 10, instruction: 'parent' });
        await repo.updateJob(parent.id, { status: 'waiting_subtasks' });
        const sub = await repo.createJob({
          repo: 'subtask/' + parent.id, issueNumber: 100, instruction: 'child', parentJobId: parent.id,
        });
        await repo.updateJob(sub.id, { status: 'succeeded' });

        await repo.recoverOrphanedJobs();

        expect((await repo.getJob(parent.id))?.status).toBe('queued');
      } finally {
        repo.close();
      }
    });

    it('leaves a waiting_subtasks parent parked while a subtask is still in flight', async () => {
      const repo = makeRepo();
      try {
        const parent = await repo.createJob({ repo: 'acme/p', issueNumber: 11, instruction: 'parent' });
        await repo.updateJob(parent.id, { status: 'waiting_subtasks' });
        const sub = await repo.createJob({
          repo: 'subtask/' + parent.id, issueNumber: 101, instruction: 'child', parentJobId: parent.id,
        });
        await repo.updateJob(sub.id, { status: 'running' });

        await repo.recoverOrphanedJobs();

        expect((await repo.getJob(parent.id))?.status).toBe('waiting_subtasks');
      } finally {
        repo.close();
      }
    });
  });

  // --- recoverStuckRunningJobs ----------------------------------------------

  describe('recoverStuckRunningJobs', () => {
    it('requeues only running/dispatching jobs whose updated_at is older than staleMinutes', async () => {
      const repo = makeRepo();
      try {
        const stale = await repo.createJob({ repo: 'acme/a', issueNumber: 1, instruction: 'a' });
        const fresh = await repo.createJob({ repo: 'acme/b', issueNumber: 2, instruction: 'b' });
        await repo.updateJob(stale.id, { status: 'running' });
        await repo.updateJob(fresh.id, { status: 'running' });
        await repo.lockIssue('acme/a', 1, stale.id);

        // Backdate the stale job's updated_at by 30 minutes via the public db handle.
        repo.getDb()
          .prepare("UPDATE jobs SET updated_at = datetime('now', '-30 minutes') WHERE id = ?")
          .run(stale.id);

        const recovered = repo.recoverStuckRunningJobs(10);

        expect(recovered).toBe(1);
        const staleJob = await repo.getJob(stale.id);
        expect(staleJob?.status).toBe('queued');
        expect(staleJob?.workerId).toBeNull();
        expect(staleJob?.errorSummary).toContain('stuck in running');
        // fresh job is still running (updated_at is recent)
        expect((await repo.getJob(fresh.id))?.status).toBe('running');
        // the stuck job's issue lock was released
        expect(await repo.lockIssue('acme/a', 1, fresh.id)).toBe(true);
      } finally {
        repo.close();
      }
    });

    it('is a no-op (returns 0) when no job is stale', async () => {
      const repo = makeRepo();
      try {
        const running = await repo.createJob({ repo: 'acme/a', issueNumber: 1, instruction: 'a' });
        await repo.updateJob(running.id, { status: 'running' });
        // updated_at is now → not stale for any positive threshold
        expect(repo.recoverStuckRunningJobs(10)).toBe(0);
        expect((await repo.getJob(running.id))?.status).toBe('running');
      } finally {
        repo.close();
      }
    });
  });

  // --- resumeMcpWaitingJobs --------------------------------------------------

  describe('resumeMcpWaitingJobs', () => {
    it('requeues only the owner\'s jobs parked on mcp_auth_required', async () => {
      const repo = makeRepo();
      try {
        const mine = await repo.createJob({ repo: 'acme/a', issueNumber: 1, instruction: 'a', ownerId: 'u1' });
        const theirs = await repo.createJob({ repo: 'acme/b', issueNumber: 2, instruction: 'b', ownerId: 'u2' });
        const otherReason = await repo.createJob({ repo: 'acme/c', issueNumber: 3, instruction: 'c', ownerId: 'u1' });
        await repo.updateJob(mine.id, { status: 'waiting_human', waitReason: 'mcp_auth_required' });
        await repo.updateJob(theirs.id, { status: 'waiting_human', waitReason: 'mcp_auth_required' });
        await repo.updateJob(otherReason.id, { status: 'waiting_human', waitReason: 'tool_request' });

        const changes = repo.resumeMcpWaitingJobs('u1', 'server-x');

        expect(changes).toBe(1);
        const mineJob = await repo.getJob(mine.id);
        expect(mineJob?.status).toBe('queued');
        expect(mineJob?.waitReason).toBeNull();
        // other owner untouched
        expect((await repo.getJob(theirs.id))?.status).toBe('waiting_human');
        // different wait_reason untouched
        expect((await repo.getJob(otherReason.id))?.status).toBe('waiting_human');
      } finally {
        repo.close();
      }
    });

    it('is a no-op (returns 0) when the owner has no mcp-parked jobs', async () => {
      const repo = makeRepo();
      try {
        const j = await repo.createJob({ repo: 'acme/a', issueNumber: 1, instruction: 'a', ownerId: 'u1' });
        await repo.updateJob(j.id, { status: 'running' });
        expect(repo.resumeMcpWaitingJobs('u1', 'server-x')).toBe(0);
      } finally {
        repo.close();
      }
    });
  });

  // --- resumeToolRequestJob --------------------------------------------------

  describe('resumeToolRequestJob', () => {
    it('requeues a job parked on tool_request and clears wait_reason', async () => {
      const repo = makeRepo();
      try {
        const job = await repo.createJob({ repo: 'acme/a', issueNumber: 1, instruction: 'a' });
        await repo.updateJob(job.id, { status: 'waiting_human', waitReason: 'tool_request' });

        const changes = repo.resumeToolRequestJob(job.id);

        expect(changes).toBe(1);
        const resumed = await repo.getJob(job.id);
        expect(resumed?.status).toBe('queued');
        expect(resumed?.waitReason).toBeNull();
      } finally {
        repo.close();
      }
    });

    it('is a no-op (returns 0) for an unknown id or a job parked for another reason', async () => {
      const repo = makeRepo();
      try {
        expect(repo.resumeToolRequestJob('does-not-exist')).toBe(0);

        const job = await repo.createJob({ repo: 'acme/a', issueNumber: 1, instruction: 'a' });
        await repo.updateJob(job.id, { status: 'waiting_human', waitReason: 'mcp_auth_required' });
        expect(repo.resumeToolRequestJob(job.id)).toBe(0);
        expect((await repo.getJob(job.id))?.status).toBe('waiting_human');
      } finally {
        repo.close();
      }
    });
  });

  // --- requeueParentJobIfAllSubtasksDone ------------------------------------

  describe('requeueParentJobIfAllSubtasksDone', () => {
    it('requeues the parent and returns true once all latest subtasks are terminal', async () => {
      const repo = makeRepo();
      try {
        const parent = await repo.createJob({ repo: 'acme/p', issueNumber: 20, instruction: 'parent' });
        await repo.updateJob(parent.id, { status: 'waiting_subtasks' });
        const s1 = await repo.createJob({
          repo: 'subtask/' + parent.id, issueNumber: 200, instruction: 's1', parentJobId: parent.id,
        });
        const s2 = await repo.createJob({
          repo: 'subtask/' + parent.id, issueNumber: 201, instruction: 's2', parentJobId: parent.id,
        });
        await repo.updateJob(s1.id, { status: 'succeeded' });
        await repo.updateJob(s2.id, { status: 'failed' });

        const requeued = await repo.requeueParentJobIfAllSubtasksDone(parent.id);

        expect(requeued).toBe(true);
        expect((await repo.getJob(parent.id))?.status).toBe('queued');
      } finally {
        repo.close();
      }
    });

    it('returns false and leaves the parent parked while a subtask is still pending', async () => {
      const repo = makeRepo();
      try {
        const parent = await repo.createJob({ repo: 'acme/p', issueNumber: 21, instruction: 'parent' });
        await repo.updateJob(parent.id, { status: 'waiting_subtasks' });
        const s1 = await repo.createJob({
          repo: 'subtask/' + parent.id, issueNumber: 210, instruction: 's1', parentJobId: parent.id,
        });
        const s2 = await repo.createJob({
          repo: 'subtask/' + parent.id, issueNumber: 211, instruction: 's2', parentJobId: parent.id,
        });
        await repo.updateJob(s1.id, { status: 'succeeded' });
        await repo.updateJob(s2.id, { status: 'running' });

        const requeued = await repo.requeueParentJobIfAllSubtasksDone(parent.id);

        expect(requeued).toBe(false);
        expect((await repo.getJob(parent.id))?.status).toBe('waiting_subtasks');
      } finally {
        repo.close();
      }
    });

    it('returns false when the parent is not in waiting_subtasks', async () => {
      const repo = makeRepo();
      try {
        const parent = await repo.createJob({ repo: 'acme/p', issueNumber: 22, instruction: 'parent' });
        // parent is queued, not waiting_subtasks → guard rejects
        const s1 = await repo.createJob({
          repo: 'subtask/' + parent.id, issueNumber: 220, instruction: 's1', parentJobId: parent.id,
        });
        await repo.updateJob(s1.id, { status: 'succeeded' });

        expect(await repo.requeueParentJobIfAllSubtasksDone(parent.id)).toBe(false);
        expect((await repo.getJob(parent.id))?.status).toBe('queued');
      } finally {
        repo.close();
      }
    });

    it('uses only the latest job per subtask issue_number when judging completion', async () => {
      // A subtask issue that was retried: an OLD failed row + a NEWER running row.
      // ROW_NUMBER=1 (latest) is running → parent must stay parked.
      const repo = makeRepo();
      try {
        const parent = await repo.createJob({ repo: 'acme/p', issueNumber: 23, instruction: 'parent' });
        await repo.updateJob(parent.id, { status: 'waiting_subtasks' });
        const subRepo = 'subtask/' + parent.id;
        const old = await repo.createJob({ repo: subRepo, issueNumber: 230, instruction: 'old', parentJobId: parent.id });
        await repo.updateJob(old.id, { status: 'failed' });
        // newer row for the same issue_number
        const fresh = await repo.createJob({ repo: subRepo, issueNumber: 230, instruction: 'fresh', parentJobId: parent.id });
        await repo.updateJob(fresh.id, { status: 'running' });

        expect(await repo.requeueParentJobIfAllSubtasksDone(parent.id)).toBe(false);
        expect((await repo.getJob(parent.id))?.status).toBe('waiting_subtasks');
      } finally {
        repo.close();
      }
    });
  });

  // --- addAuditLog -----------------------------------------------------------

  describe('addAuditLog', () => {
    it('writes a queryable audit row with the expected fields', async () => {
      const repo = makeRepo();
      try {
        const job = await repo.createJob({ repo: 'acme/a', issueNumber: 1, instruction: 'a' });
        await repo.addAuditLog(job.id, 'tool.grant', 'user:u1', { tool: 'Bash', decision: 'approve' });

        const row = repo.getDb()
          .prepare('SELECT job_id, action, actor, detail FROM audit_log WHERE action = ?')
          .get('tool.grant') as { job_id: string; action: string; actor: string; detail: string };

        expect(row.job_id).toBe(job.id);
        expect(row.action).toBe('tool.grant');
        expect(row.actor).toBe('user:u1');
        expect(JSON.parse(row.detail)).toEqual({ tool: 'Bash', decision: 'approve' });
      } finally {
        repo.close();
      }
    });

    it('accepts a null job_id (system-level audit row)', async () => {
      const repo = makeRepo();
      try {
        await repo.addAuditLog(null, 'config.update', 'admin', { changed: ['concurrency'] });
        const row = repo.getDb()
          .prepare('SELECT job_id, action FROM audit_log WHERE action = ?')
          .get('config.update') as { job_id: string | null; action: string };
        expect(row.job_id).toBeNull();
        expect(row.action).toBe('config.update');
      } finally {
        repo.close();
      }
    });
  });

  // --- Comment injection lifecycle ------------------------------------------

  describe('comment injection lifecycle', () => {
    it('inject → getUninjectedComments → markCommentsInjected → none; latest result comment', async () => {
      const repo = makeRepo();
      try {
        const task = await repo.createLocalTask({ title: 't', body: 'b' });

        // Only user comments that are not yet injected should surface.
        const c1 = await repo.addLocalTaskComment(task.id, 'user', 'first question');
        await repo.addLocalTaskComment(task.id, 'agent', 'progress note', 'comment');
        const c2 = await repo.addLocalTaskComment(task.id, 'user', 'second question');

        const uninjected = await repo.getUninjectedComments(task.id);
        expect(uninjected.map((c) => c.id)).toEqual([c1.id, c2.id]);

        repo.markCommentsInjected([c1.id, c2.id]);
        expect(await repo.getUninjectedComments(task.id)).toHaveLength(0);

        // sinceId filter: only comments with id > sinceId returned.
        const c3 = await repo.addLocalTaskComment(task.id, 'user', 'third question');
        const afterC2 = await repo.getUninjectedComments(task.id, c2.id);
        expect(afterC2.map((c) => c.id)).toEqual([c3.id]);
      } finally {
        repo.close();
      }
    });

    it('getLatestResultComment returns the newest agent result/ask, null when none', async () => {
      const repo = makeRepo();
      try {
        const task = await repo.createLocalTask({ title: 't', body: 'b' });

        // No result/ask comment yet.
        expect(await repo.getLatestResultComment(task.id)).toBeNull();

        await repo.addLocalTaskComment(task.id, 'agent', 'plain comment', 'comment');
        await repo.addLocalTaskComment(task.id, 'user', 'ignored user comment', 'result');
        // Force a strictly later created_at so the ORDER BY DESC is deterministic.
        repo.getDb()
          .prepare(
            `INSERT INTO local_task_comments (task_id, author, kind, body, created_at)
             VALUES (?, 'agent', 'result', 'final answer', datetime('now', '+1 second'))`,
          )
          .run(task.id);

        const latest = await repo.getLatestResultComment(task.id);
        expect(latest?.kind).toBe('result');
        expect(latest?.body).toBe('final answer');
      } finally {
        repo.close();
      }
    });
  });
});
