import { describe, it, expect } from 'vitest';
import { revokeDelegationCascade } from './revoke.js';

function fakeRepo(over: Partial<ReturnType<typeof makeBase>> = {}) {
  const calls: { revoked: any[]; oidcRevoked: string[]; cancelled: string[]; audit: any[] } = {
    revoked: [],
    oidcRevoked: [],
    cancelled: [],
    audit: [],
  };
  const baseRow = {
    id: 'd1',
    userId: 'u1',
    clientId: 'c1',
    grantId: 'g1',
    revokedAt: null as string | null,
    grantedSpaceIds: [],
    grantedSkills: [],
    audience: null,
    expiresAt: null,
  };
  function makeBase() {
    return {
      getA2aDelegationById(id: string) {
        return id === 'd1' ? { ...baseRow } : undefined;
      },
      revokeA2aDelegation(id: string, iso: string) {
        calls.revoked.push([id, iso]);
      },
      oidcRevokeByGrantId(g: string) {
        calls.oidcRevoked.push(g);
      },
      listNonTerminalA2aTasksByGrant(_g: string) {
        return [
          { id: 't1', jobId: 'j1', payload: {} },
          { id: 't2', jobId: null, payload: {} },
        ];
      },
      cancelA2aLinkedJob(j: string) {
        calls.cancelled.push(j);
        return true;
      },
      async addAuditLog(jobId: string | null, action: string, actor: string, detail: object) {
        calls.audit.push({ jobId, action, actor, detail });
      },
    };
  }
  const base = makeBase();
  return { repo: { ...base, ...over }, calls };
}

const NOW = '2026-07-02T00:00:00.000Z';

describe('revokeDelegationCascade', () => {
  it('revokes, kills the grant, cancels non-terminal jobs, audits', async () => {
    const { repo, calls } = fakeRepo();
    const out = await revokeDelegationCascade(repo as any, 'd1', { userId: 'u1', isAdmin: false }, NOW);
    expect(out).toEqual({ status: 'revoked', delegationId: 'd1', grantId: 'g1', cancelledJobs: 1 });
    expect(calls.revoked).toEqual([['d1', NOW]]);
    expect(calls.oidcRevoked).toEqual(['g1']);
    expect(calls.cancelled).toEqual(['j1']); // t2 has no jobId
    expect(calls.audit).toHaveLength(1);
    expect(calls.audit[0]).toMatchObject({
      jobId: null,
      action: 'a2a.delegation.revoked',
      actor: 'u1',
      detail: { delegationId: 'd1', clientId: 'c1', cancelledJobs: 1, byAdmin: false },
    });
  });

  it('non-owner non-admin is forbidden and touches nothing', async () => {
    const { repo, calls } = fakeRepo();
    const out = await revokeDelegationCascade(repo as any, 'd1', { userId: 'someone-else', isAdmin: false }, NOW);
    expect(out).toEqual({ status: 'forbidden' });
    expect(calls.revoked).toEqual([]);
    expect(calls.oidcRevoked).toEqual([]);
  });

  it("admin may revoke another user's delegation", async () => {
    const { repo } = fakeRepo();
    const out = await revokeDelegationCascade(repo as any, 'd1', { userId: 'admin', isAdmin: true }, NOW);
    expect(out.status).toBe('revoked');
  });

  it('unknown id → not-found', async () => {
    const { repo } = fakeRepo();
    expect(
      await revokeDelegationCascade(repo as any, 'ghost', { userId: 'u1', isAdmin: false }, NOW),
    ).toEqual({ status: 'not-found' });
  });

  it('already revoked → idempotent, no job cancels', async () => {
    const { repo, calls } = fakeRepo({
      getA2aDelegationById: (id: string) =>
        id === 'd1'
          ? { id: 'd1', userId: 'u1', clientId: 'c1', grantId: 'g1', revokedAt: NOW }
          : undefined,
    });
    const out = await revokeDelegationCascade(repo as any, 'd1', { userId: 'u1', isAdmin: false }, NOW);
    expect(out).toEqual({ status: 'already-revoked', delegationId: 'd1' });
    expect(calls.cancelled).toEqual([]);
    expect(calls.audit).toEqual([]);
  });
});
