// @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 { createUserDelegationsRouter, createAdminDelegationsRouter } from './delegations-api.js';

// ── Stub principals ───────────────────────────────────────────────────────────
const USER1 = { id: 'user1', role: 'user', orgIds: [] };
const USER2 = { id: 'user2', role: 'user', orgIds: [] };
const ADMIN = { id: 'admin1', role: 'admin', orgIds: [] };

// ── App factories ─────────────────────────────────────────────────────────────

function makeUserApp(repo: Repository, actingUser: object) {
  const app = express();
  // authActive=true → viewerOf() reads req.user
  app.use((req, _res, next) => { (req as any).user = actingUser; next(); });
  app.use('/api/local/a2a/delegations', express.json(), createUserDelegationsRouter(repo, true));
  return app;
}

function makeAdminApp(repo: Repository, actingAdmin: object) {
  const app = express();
  // authActive=true → viewerOf() reads req.user (same pattern as user router)
  app.use((req, _res, next) => { (req as any).user = actingAdmin; next(); });
  app.use('/api/admin/a2a/delegations', express.json(), createAdminDelegationsRouter(repo, true));
  return app;
}

// ── Seed helper ───────────────────────────────────────────────────────────────

interface SeedOpts {
  clientId?: string;
  grantId?: string | null;
  grantedSpaceIds?: string[];
  grantedSkills?: string[];
  audience?: string | null;
  expiresAt?: string | null;
  revokedAt?: string | null;
}

function seedDelegation(repo: Repository, userId: string, opts: SeedOpts = {}): string {
  const id = randomUUID();
  repo.createA2aDelegation({
    id,
    userId,
    clientId: opts.clientId ?? 'client-x',
    grantId: opts.grantId ?? null,
    grantedSpaceIds: opts.grantedSpaceIds ?? ['space-1'],
    grantedSkills: opts.grantedSkills ?? ['skill-a'],
    audience: opts.audience ?? null,
    expiresAt: opts.expiresAt ?? null,
    revokedAt: opts.revokedAt ?? null,
  });
  return id;
}

// ── User router tests ─────────────────────────────────────────────────────────

describe('createUserDelegationsRouter', () => {
  let repo: Repository;

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

  it('GET / returns only the viewer\'s own delegations, not other users\'', async () => {
    const d1 = seedDelegation(repo, 'user1');
    const d2 = seedDelegation(repo, 'user2');
    const res = await request(makeUserApp(repo, USER1)).get('/api/local/a2a/delegations');
    expect(res.status).toBe(200);
    const ids: string[] = res.body.delegations.map((d: any) => d.id);
    expect(ids).toContain(d1);
    expect(ids).not.toContain(d2);
  });

  it('GET / response shape includes required fields', async () => {
    seedDelegation(repo, 'user1', { grantedSpaceIds: ['s1'], grantedSkills: ['sk1'] });
    const res = await request(makeUserApp(repo, USER1)).get('/api/local/a2a/delegations');
    expect(res.status).toBe(200);
    const d = res.body.delegations[0];
    expect(d).toHaveProperty('id');
    expect(d).toHaveProperty('clientId');
    expect(d).toHaveProperty('clientName');
    expect(d).toHaveProperty('grantedSpaceIds');
    expect(d).toHaveProperty('grantedSkills');
    expect(d).toHaveProperty('expiresAt');
    expect(d).toHaveProperty('revokedAt');
    expect(d).toHaveProperty('createdAt');
    expect(d).toHaveProperty('live');
  });

  it('GET / live=true for active delegation', async () => {
    const id = seedDelegation(repo, 'user1');
    const res = await request(makeUserApp(repo, USER1)).get('/api/local/a2a/delegations');
    expect(res.status).toBe(200);
    const d = res.body.delegations.find((x: any) => x.id === id);
    expect(d.live).toBe(true);
  });

  it('GET / live=false for revoked delegation', async () => {
    const id = seedDelegation(repo, 'user1', { revokedAt: '2020-01-01T00:00:00.000Z' });
    const res = await request(makeUserApp(repo, USER1)).get('/api/local/a2a/delegations');
    expect(res.status).toBe(200);
    const d = res.body.delegations.find((x: any) => x.id === id);
    expect(d.live).toBe(false);
  });

  it('GET / live=false for expired delegation', async () => {
    const id = seedDelegation(repo, 'user1', { expiresAt: '2020-01-01T00:00:00.000Z' });
    const res = await request(makeUserApp(repo, USER1)).get('/api/local/a2a/delegations');
    const d = res.body.delegations.find((x: any) => x.id === id);
    expect(d.live).toBe(false);
  });

  it('POST /:id/revoke with owner → 200 and row is now revoked', async () => {
    const id = seedDelegation(repo, 'user1');
    const res = await request(makeUserApp(repo, USER1))
      .post(`/api/local/a2a/delegations/${id}/revoke`);
    expect(res.status).toBe(200);
    expect(res.body.status).toBe('revoked');
    expect(repo.getA2aDelegationById(id)?.revokedAt).toBeTruthy();
  });

  it('POST /:id/revoke by non-owner → 404 and row untouched', async () => {
    const id = seedDelegation(repo, 'user1');
    const res = await request(makeUserApp(repo, USER2))
      .post(`/api/local/a2a/delegations/${id}/revoke`);
    expect(res.status).toBe(404);
    expect(res.body.error).toBe('not_found');
    // row must be untouched
    expect(repo.getA2aDelegationById(id)?.revokedAt).toBeNull();
  });

  it('POST /:id/revoke with unknown id → 404 (same body as forbidden)', async () => {
    const res = await request(makeUserApp(repo, USER1))
      .post('/api/local/a2a/delegations/no-such-id/revoke');
    expect(res.status).toBe(404);
    expect(res.body.error).toBe('not_found');
  });

  it('POST /:id/revoke on already-revoked delegation → 200 with already-revoked status', async () => {
    const id = seedDelegation(repo, 'user1', { revokedAt: '2020-01-01T00:00:00.000Z' });
    const res = await request(makeUserApp(repo, USER1))
      .post(`/api/local/a2a/delegations/${id}/revoke`);
    expect(res.status).toBe(200);
    expect(res.body.status).toBe('already-revoked');
  });
});

// ── Admin router tests ────────────────────────────────────────────────────────

describe('createAdminDelegationsRouter', () => {
  let repo: Repository;

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

  it('GET / returns all delegations from all users', async () => {
    const d1 = seedDelegation(repo, 'user1');
    const d2 = seedDelegation(repo, 'user2');
    const res = await request(makeAdminApp(repo, ADMIN)).get('/api/admin/a2a/delegations');
    expect(res.status).toBe(200);
    const ids: string[] = res.body.delegations.map((d: any) => d.id);
    expect(ids).toContain(d1);
    expect(ids).toContain(d2);
  });

  it('GET / response includes userId per row', async () => {
    seedDelegation(repo, 'user1');
    const res = await request(makeAdminApp(repo, ADMIN)).get('/api/admin/a2a/delegations');
    expect(res.body.delegations[0]).toHaveProperty('userId');
  });

  it('POST /:id/revoke admin can revoke another user\'s delegation → 200', async () => {
    const id = seedDelegation(repo, 'user1');
    const res = await request(makeAdminApp(repo, ADMIN))
      .post(`/api/admin/a2a/delegations/${id}/revoke`);
    expect(res.status).toBe(200);
    expect(res.body.status).toBe('revoked');
    expect(repo.getA2aDelegationById(id)?.revokedAt).toBeTruthy();
  });

  it('POST /:id/revoke unknown id → 404', async () => {
    const res = await request(makeAdminApp(repo, ADMIN))
      .post('/api/admin/a2a/delegations/unknown-id/revoke');
    expect(res.status).toBe(404);
    expect(res.body.error).toBe('not_found');
  });

  it('POST /:id/revoke already-revoked → 200', async () => {
    const id = seedDelegation(repo, 'user2', { revokedAt: '2020-01-01T00:00:00.000Z' });
    const res = await request(makeAdminApp(repo, ADMIN))
      .post(`/api/admin/a2a/delegations/${id}/revoke`);
    expect(res.status).toBe(200);
    expect(res.body.status).toBe('already-revoked');
  });
});

// ── Error handling — repo/cascade throws → 500 ────────────────────────────────

describe('error handling: repo throws → 500', () => {
  it('user GET / returns 500 when repo throws', async () => {
    const repo = new Repository(':memory:');
    (repo as any).listA2aDelegationsForUserWithClient = () => { throw new Error('simulated db error'); };
    const res = await request(makeUserApp(repo, USER1)).get('/api/local/a2a/delegations');
    expect(res.status).toBe(500);
    expect(res.body.error).toBe('internal');
  });

  it('user POST /:id/revoke returns 500 when cascade throws (repo.getA2aDelegationById throws)', async () => {
    const repo = new Repository(':memory:');
    // revokeDelegationCascade calls repo.getA2aDelegationById first — make it throw
    (repo as any).getA2aDelegationById = () => { throw new Error('simulated db error'); };
    const res = await request(makeUserApp(repo, USER1))
      .post('/api/local/a2a/delegations/any-id/revoke');
    expect(res.status).toBe(500);
    expect(res.body.error).toBe('internal');
  });

  it('admin GET / returns 500 when repo throws', async () => {
    const repo = new Repository(':memory:');
    (repo as any).listA2aDelegationsAllWithClient = () => { throw new Error('simulated db error'); };
    const res = await request(makeAdminApp(repo, ADMIN)).get('/api/admin/a2a/delegations');
    expect(res.status).toBe(500);
    expect(res.body.error).toBe('internal');
  });

  it('admin POST /:id/revoke returns 500 when cascade throws (repo.getA2aDelegationById throws)', async () => {
    const repo = new Repository(':memory:');
    // revokeDelegationCascade calls repo.getA2aDelegationById first — make it throw
    (repo as any).getA2aDelegationById = () => { throw new Error('simulated db error'); };
    const res = await request(makeAdminApp(repo, ADMIN))
      .post('/api/admin/a2a/delegations/any-id/revoke');
    expect(res.status).toBe(500);
    expect(res.body.error).toBe('internal');
  });
});
