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

describe('Repository tool_requests', () => {
  let tmpDir: string;
  let repo: Repository;

  beforeEach(() => {
    tmpDir = mkdtempSync(join(tmpdir(), 'tool-req-test-'));
    repo = new Repository(join(tmpDir, 'test.db'));
  });
  afterEach(() => {
    rmSync(tmpDir, { recursive: true, force: true });
  });

  it('records and lists tool requests for a task (newest first)', () => {
    repo.recordToolRequest({ taskId: 't1', jobId: 'j1', pieceName: 'chat', movementName: 'respond', toolName: 'WebSearch', reason: 'need search', category: 'requested' });
    repo.recordToolRequest({ taskId: 't1', jobId: 'j2', pieceName: 'chat', movementName: 'respond', toolName: 'Bash', reason: 'run cmd', category: 'blocked' });
    repo.recordToolRequest({ taskId: 'other', jobId: 'j3', pieceName: 'chat', movementName: 'respond', toolName: 'X', category: 'requested' });

    const reqs = repo.listToolRequestsByTask('t1');
    expect(reqs).toHaveLength(2);
    expect(reqs.map((r) => r.toolName).sort()).toEqual(['Bash', 'WebSearch']);
    const ws = reqs.find((r) => r.toolName === 'WebSearch')!;
    expect(ws.category).toBe('requested');
    expect(ws.status).toBe('pending');
    expect(ws.reason).toBe('need search');
  });

  it('de-dups identical pending (job, movement, tool) — retries reuse the row', () => {
    const id1 = repo.recordToolRequest({ jobId: 'j1', pieceName: 'p', movementName: 'm', toolName: 'Bash', category: 'blocked' });
    const id2 = repo.recordToolRequest({ jobId: 'j1', pieceName: 'p', movementName: 'm', toolName: 'Bash', category: 'blocked' });
    expect(id2).toBe(id1);
    // different movement → new row
    const id3 = repo.recordToolRequest({ jobId: 'j1', pieceName: 'p', movementName: 'm2', toolName: 'Bash', category: 'blocked' });
    expect(id3).not.toBe(id1);
  });

  it('de-dups by task when job_id is null, and by piece when both are null (no unbounded growth)', () => {
    // No job binding, same task → de-dup by task_id.
    const a1 = repo.recordToolRequest({ taskId: 't9', pieceName: 'p', movementName: 'm', toolName: 'Bash', category: 'blocked' });
    const a2 = repo.recordToolRequest({ taskId: 't9', pieceName: 'p', movementName: 'm', toolName: 'Bash', category: 'blocked' });
    expect(a2).toBe(a1);
    // No job and no task → de-dup by piece.
    const b1 = repo.recordToolRequest({ pieceName: 'lonely', movementName: 'm', toolName: 'X', category: 'requested' });
    const b2 = repo.recordToolRequest({ pieceName: 'lonely', movementName: 'm', toolName: 'X', category: 'requested' });
    expect(b2).toBe(b1);
    expect(repo.aggregateToolRequestsByPiece('lonely').find((r) => r.toolName === 'X')!.count).toBe(1);
  });

  it('decideToolRequest applies once and is idempotent (race-safe)', () => {
    const id = repo.recordToolRequest({ jobId: 'jx', pieceName: 'p', movementName: 'm', toolName: 'Bash', category: 'requested' });
    expect(repo.decideToolRequest(id, { status: 'approved', grantScope: 'task', decidedBy: 'u1' })).toBe(true);
    const after = repo.getToolRequest(id)!;
    expect(after.status).toBe('approved');
    expect(after.grantScope).toBe('task');
    expect(after.decidedBy).toBe('u1');
    // second decide is a no-op (already decided)
    expect(repo.decideToolRequest(id, { status: 'denied' })).toBe(false);
    expect(repo.getToolRequest(id)!.status).toBe('approved');
  });

  it('grant overlay: getGrantedTools/addGrantedTool round-trip + de-dup', async () => {
    const task = await repo.createLocalTask({ title: 't', body: '', spaceId: null, ownerId: 'u1' });
    expect(repo.getGrantedTools(String(task.id))).toEqual([]);
    repo.addGrantedTool(String(task.id), 'WebSearch');
    repo.addGrantedTool(String(task.id), 'WebSearch'); // de-dup
    repo.addGrantedTool(String(task.id), 'Bash');
    expect(repo.getGrantedTools(String(task.id)).sort()).toEqual(['Bash', 'WebSearch']);
  });

  it('aggregates per piece by (tool, category) with counts', () => {
    repo.recordToolRequest({ jobId: 'a', pieceName: 'research', movementName: 'm1', toolName: 'WebSearch', category: 'requested', reason: 'r1' });
    repo.recordToolRequest({ jobId: 'b', pieceName: 'research', movementName: 'm2', toolName: 'WebSearch', category: 'requested', reason: 'r2' });
    repo.recordToolRequest({ jobId: 'c', pieceName: 'research', movementName: 'm1', toolName: 'Bash', category: 'blocked' });
    repo.recordToolRequest({ jobId: 'd', pieceName: 'other', movementName: 'm1', toolName: 'Bash', category: 'blocked' });

    const agg = repo.aggregateToolRequestsByPiece('research');
    const ws = agg.find((a) => a.toolName === 'WebSearch')!;
    expect(ws.count).toBe(2);
    expect(ws.category).toBe('requested');
    expect(agg.find((a) => a.toolName === 'Bash')!.count).toBe(1);
    expect(agg.some((a) => a.toolName === 'X')).toBe(false);
  });
});
