// @vitest-environment node
import { describe, it, expect, beforeEach } from 'vitest';
import express from 'express';
import request from 'supertest';
import { randomUUID } from 'crypto';
import { Repository } from '../../db/repository.js';
import { createA2aClientsAdminRouter } from './a2a-clients-admin-api.js';

const ADMIN = { id: 'admin1', role: 'admin', orgIds: [] };

function makeApp(repo: Repository) {
  const app = express();
  app.use(express.json());
  // authActive=true → viewerOf reads req.user (same pattern as delegations-api.test.ts)
  app.use((req, _res, next) => { (req as any).user = ADMIN; next(); });
  app.use('/api/admin/a2a/clients', createA2aClientsAdminRouter(repo, true));
  return app;
}

// ── Seed helpers ──────────────────────────────────────────────────────────────

function seedClient(repo: Repository): string {
  const clientId = `a2a_${randomUUID()}`;
  repo.createA2aClient({
    clientId, name: 'Rogue Agent',
    redirectUris: ['https://rogue.example/cb'],
    secretHash: null, tokenEndpointAuthMethod: 'none',
    agentCardUrl: null, issuer: null, keyFingerprint: null,
    status: 'active', createdBy: ADMIN.id,
  });
  return clientId;
}

async function seedRunningJob(repo: Repository, issueNumber: number): Promise<string> {
  const job = await repo.createJob({ repo: 'test/repo', issueNumber, instruction: `task ${issueNumber}` });
  // createJob inserts 'queued'; cancelA2aLinkedJob matches queued/running/etc.
  return job.id;
}

function seedDelegationWithJob(
  repo: Repository,
  clientId: string,
  userId: string,
  grantId: string,
  jobId: string,
): string {
  const delId = randomUUID();
  repo.createA2aDelegation({
    id: delId, userId, clientId,
    grantId,
    grantedSpaceIds: ['space-1'], grantedSkills: [],
    audience: null, expiresAt: null, revokedAt: null,
  });
  // Link a non-terminal a2a_task (payload={} → status.state IS NULL → non-terminal)
  repo.saveA2aTask({
    id: randomUUID(), contextId: null,
    jobId, localTaskId: null,
    payload: {},
    delegationId: delId,
    grantId,
  });
  return delId;
}

// ── Tests ─────────────────────────────────────────────────────────────────────

describe('POST /:id/disable — cascade revocation', () => {
  let repo: Repository;

  beforeEach(() => { repo = new Repository(':memory:'); });

  it('disables the client, revokes two live delegations, cancels two linked jobs, and leaves the pre-revoked delegation untouched', async () => {
    const clientId = seedClient(repo);

    // Two live delegations, each linked to a queued (cancellable) job
    const job1Id = await seedRunningJob(repo, 1);
    const job2Id = await seedRunningJob(repo, 2);
    const delId1 = seedDelegationWithJob(repo, clientId, 'user1', 'grant-1', job1Id);
    const delId2 = seedDelegationWithJob(repo, clientId, 'user2', 'grant-2', job2Id);

    // One already-revoked delegation for the same client
    const preRevokedAt = '2024-01-01T00:00:00.000Z';
    const delId3 = randomUUID();
    repo.createA2aDelegation({
      id: delId3, userId: 'user3', clientId,
      grantId: null,
      grantedSpaceIds: [], grantedSkills: [],
      audience: null, expiresAt: null,
      revokedAt: preRevokedAt,
    });

    const res = await request(makeApp(repo))
      .post(`/api/admin/a2a/clients/${clientId}/disable`);

    // ── Response shape ────────────────────────────────────────────────────────
    expect(res.status).toBe(200);
    expect(res.body.status).toBe('disabled');
    expect(res.body.revokedDelegations).toBe(2);
    expect(res.body.cancelledJobs).toBe(2);

    // ── Client status ─────────────────────────────────────────────────────────
    expect(repo.getA2aClient(clientId)?.status).toBe('disabled');

    // ── Live delegations are now revoked ──────────────────────────────────────
    expect(repo.getA2aDelegationById(delId1)?.revokedAt).toBeTruthy();
    expect(repo.getA2aDelegationById(delId2)?.revokedAt).toBeTruthy();

    // ── Pre-revoked delegation is untouched (revokedAt unchanged) ─────────────
    expect(repo.getA2aDelegationById(delId3)?.revokedAt).toBe(preRevokedAt);

    // ── Linked jobs are now cancelled ─────────────────────────────────────────
    const db = (repo as any).db;
    const j1 = db.prepare('SELECT status FROM jobs WHERE id = ?').get(job1Id) as { status: string };
    const j2 = db.prepare('SELECT status FROM jobs WHERE id = ?').get(job2Id) as { status: string };
    expect(j1.status).toBe('cancelled');
    expect(j2.status).toBe('cancelled');
  });

  it('returns 404 when disabling a nonexistent client', async () => {
    const res = await request(makeApp(repo))
      .post('/api/admin/a2a/clients/no-such-client/disable');
    expect(res.status).toBe(404);
  });

  it('returns { status:disabled, revokedDelegations:0, cancelledJobs:0 } when the client has no live delegations', async () => {
    const clientId = seedClient(repo);
    const res = await request(makeApp(repo))
      .post(`/api/admin/a2a/clients/${clientId}/disable`);
    expect(res.status).toBe(200);
    expect(res.body.status).toBe('disabled');
    expect(res.body.revokedDelegations).toBe(0);
    expect(res.body.cancelledJobs).toBe(0);
  });

  it('returns 500 when the repo throws during delegation enumeration', async () => {
    const clientId = seedClient(repo);
    (repo as any).listLiveA2aDelegationsForClient = () => { throw new Error('db error'); };
    const res = await request(makeApp(repo))
      .post(`/api/admin/a2a/clients/${clientId}/disable`);
    expect(res.status).toBe(500);
    expect(res.body.error).toBe('internal');
  });
});
