// @vitest-environment node
import { describe, it, expect, beforeEach } from 'vitest';
import { Repository } from './repository.js';

function makeRepo(): Repository {
  // new Repository(':memory:') calls initSchema() in the constructor — no separate call needed.
  return new Repository(':memory:');
}

describe('A2A revocation repo primitives', () => {
  let repo: Repository;
  beforeEach(() => { repo = makeRepo(); });

  it('getA2aDelegationById returns the row or undefined', () => {
    repo.createA2aDelegation({
      id: 'd1', userId: 'u1', clientId: 'c1', grantId: 'g1',
      grantedSpaceIds: ['s1'], grantedSkills: ['chat'],
      audience: 'aud', expiresAt: null, revokedAt: null,
    });
    expect(repo.getA2aDelegationById('d1')?.grantId).toBe('g1');
    expect(repo.getA2aDelegationById('nope')).toBeUndefined();
  });

  it('listNonTerminalA2aTasksByGrant returns only non-terminal tasks for the grant', async () => {
    // createJob is async and returns Promise<Job>; use job.id for the jobId link.
    const job = await repo.createJob({ repo: 'local/task-1', issueNumber: 1, instruction: 'test' });
    const jobRunning = job.id;
    repo.saveA2aTask({ id: 't-working', contextId: null, jobId: jobRunning, localTaskId: null,
      payload: { status: { state: 'working' } }, grantId: 'g1' });
    repo.saveA2aTask({ id: 't-done', contextId: null, jobId: null, localTaskId: null,
      payload: { status: { state: 'completed' } }, grantId: 'g1' });
    repo.saveA2aTask({ id: 't-other', contextId: null, jobId: null, localTaskId: null,
      payload: { status: { state: 'working' } }, grantId: 'g2' });
    const rows = repo.listNonTerminalA2aTasksByGrant('g1');
    expect(rows.map(r => r.id)).toEqual(['t-working']);
  });

  it('cancelA2aLinkedJob moves a queued job to cancelled and is idempotent', async () => {
    const job = await repo.createJob({ repo: 'local/task-1', issueNumber: 1, instruction: 'test' });
    const jid = job.id; // starts as 'queued'
    expect(repo.cancelA2aLinkedJob(jid)).toBe(true);
    expect((await repo.getJob(jid))?.status).toBe('cancelled');
    expect(repo.cancelA2aLinkedJob(jid)).toBe(false); // already terminal — no change
  });

  it('cancelA2aLinkedJob cancels a retry-status job (non-terminal, waiting for requeue)', async () => {
    const job = await repo.createJob({ repo: 'local/task-1', issueNumber: 1, instruction: 'test' });
    const jid = job.id;
    // Force the job into 'retry' status directly — this is the state set by scheduleRetryOrFail()
    // before the worker requeues it. Cancelling it must prevent the requeue from running the job.
    (repo as unknown as { db: { prepare: (sql: string) => { run: (...args: unknown[]) => void } } })
      .db.prepare("UPDATE jobs SET status = 'retry' WHERE id = ?").run(jid);
    expect((await repo.getJob(jid))?.status).toBe('retry');
    // cancelA2aLinkedJob must now reach 'retry' jobs
    expect(repo.cancelA2aLinkedJob(jid)).toBe(true);
    expect((await repo.getJob(jid))?.status).toBe('cancelled');
    // idempotent — already terminal
    expect(repo.cancelA2aLinkedJob(jid)).toBe(false);
  });

  it('listLiveA2aDelegationsForClient excludes revoked and expired', () => {
    const now = '2026-07-02T00:00:00.000Z';
    repo.createA2aDelegation({ id: 'live', userId: 'u1', clientId: 'c1', grantId: 'g-live',
      grantedSpaceIds: [], grantedSkills: [], audience: null, expiresAt: null, revokedAt: null });
    repo.createA2aDelegation({ id: 'revoked', userId: 'u1', clientId: 'c1', grantId: 'g-rev',
      grantedSpaceIds: [], grantedSkills: [], audience: null, expiresAt: null, revokedAt: now });
    repo.createA2aDelegation({ id: 'expired', userId: 'u1', clientId: 'c1', grantId: 'g-exp',
      grantedSpaceIds: [], grantedSkills: [], audience: null,
      expiresAt: '2020-01-01T00:00:00.000Z', revokedAt: null });
    expect(repo.listLiveA2aDelegationsForClient('c1', now).map(d => d.id)).toEqual(['live']);
  });

  it('countNonTerminalA2aTasksByGrant counts only non-terminal tasks for the grant', async () => {
    // Seed: working + completed + rejected + input-required for g1, plus working for g2
    repo.saveA2aTask({ id: 'cnt-working', contextId: null, jobId: null, localTaskId: null,
      payload: { status: { state: 'working' } }, grantId: 'g1' });
    repo.saveA2aTask({ id: 'cnt-completed', contextId: null, jobId: null, localTaskId: null,
      payload: { status: { state: 'completed' } }, grantId: 'g1' });
    repo.saveA2aTask({ id: 'cnt-rejected', contextId: null, jobId: null, localTaskId: null,
      payload: { status: { state: 'rejected' } }, grantId: 'g1' });
    repo.saveA2aTask({ id: 'cnt-input-required', contextId: null, jobId: null, localTaskId: null,
      payload: { status: { state: 'input-required' } }, grantId: 'g1' });
    repo.saveA2aTask({ id: 'cnt-g2-working', contextId: null, jobId: null, localTaskId: null,
      payload: { status: { state: 'working' } }, grantId: 'g2' });
    // g1: only the 'working' task counts — completed, rejected, and input-required are excluded.
    // input-required is parked (no live worker/LLM) so must not block new task creation.
    expect(repo.countNonTerminalA2aTasksByGrant('g1')).toBe(1);
    // g2: one non-terminal task
    expect(repo.countNonTerminalA2aTasksByGrant('g2')).toBe(1);
    // unknown grant: zero
    expect(repo.countNonTerminalA2aTasksByGrant('nope')).toBe(0);

    // listNonTerminalA2aTasksByGrant must still include input-required for revocation coverage.
    const listed = repo.listNonTerminalA2aTasksByGrant('g1').map(r => r.id);
    expect(listed).toContain('cnt-working');
    expect(listed).toContain('cnt-input-required');
    expect(listed).not.toContain('cnt-completed');
    expect(listed).not.toContain('cnt-rejected');
  });

  it('countNonTerminalA2aTasksByGrant counts an input-required task once its job resumes (no ASK-park bypass)', async () => {
    // A parked input-required task (job still waiting_human) must NOT count — holds no capacity.
    const parked = await repo.createJob({ repo: 'local/task-1', issueNumber: 1, instruction: 'p' });
    (repo as unknown as { db: { prepare: (s: string) => { run: (...a: unknown[]) => void } } })
      .db.prepare("UPDATE jobs SET status = 'waiting_human' WHERE id = ?").run(parked.id);
    repo.saveA2aTask({ id: 'ir-parked', contextId: null, jobId: parked.id, localTaskId: null,
      payload: { status: { state: 'input-required' } }, grantId: 'gx' });
    expect(repo.countNonTerminalA2aTasksByGrant('gx')).toBe(0);

    // The human answers → job resumes to 'running' while the a2a_tasks payload still reads
    // 'input-required'. That resumed task consumes capacity again and MUST count.
    (repo as unknown as { db: { prepare: (s: string) => { run: (...a: unknown[]) => void } } })
      .db.prepare("UPDATE jobs SET status = 'running' WHERE id = ?").run(parked.id);
    expect(repo.countNonTerminalA2aTasksByGrant('gx')).toBe(1);

    // An input-required task whose job is gone (jobId null) stays excluded — nothing running.
    repo.saveA2aTask({ id: 'ir-nojob', contextId: null, jobId: null, localTaskId: null,
      payload: { status: { state: 'input-required' } }, grantId: 'gx' });
    expect(repo.countNonTerminalA2aTasksByGrant('gx')).toBe(1);
  });

  // sonnet review Finding 2: 二重失効で元の revoked_at を上書きしない
  it('revokeA2aDelegation does not overwrite an existing revoked_at', () => {
    const first = '2026-07-02T00:00:00.000Z';
    const second = '2026-07-03T00:00:00.000Z';
    repo.createA2aDelegation({ id: 'd1', userId: 'u1', clientId: 'c1', grantId: 'g1',
      grantedSpaceIds: [], grantedSkills: [], audience: null, expiresAt: null, revokedAt: null });
    repo.revokeA2aDelegation('d1', first);
    repo.revokeA2aDelegation('d1', second); // 2 回目は no-op であるべき
    expect(repo.getA2aDelegationById('d1')?.revokedAt).toBe(first);
  });
});
