/**
 * Integration tests for A2A delegation endpoints.
 * Tests POST /oauth2/token/delegate, POST /oauth2/token/verify-delegation,
 * DELETE /oauth2/token/delegate/:chainId
 *
 * Requires a running PostgreSQL instance and valid JWT environment variables.
 * Uses supertest to test the full Express request pipeline.
 */

import request from 'supertest';
import express from 'express';
import { DelegationController } from '../../src/controllers/DelegationController';
import { DelegationService } from '../../src/services/DelegationService';
import { createDelegationRouter } from '../../src/routes/delegation';
import { Pool } from 'pg';
import { AuditService } from '../../src/services/AuditService';

// Mock heavy dependencies for integration-level tests
jest.mock('../../src/services/AuditService');
const MockAuditService = AuditService as jest.MockedClass<typeof AuditService>;

const mockQuery = jest.fn();
const mockPool = { query: mockQuery } as unknown as Pool;

// Auth middleware that injects a test user
const testAuth = (req: express.Request, _res: express.Response, next: express.NextFunction) => {
  req.user = {
    sub: 'delegator-agent-uuid',
    client_id: 'delegator-agent-uuid',
    scope: 'agents:read tokens:read audit:read',
    jti: 'test-jti',
    iat: Math.floor(Date.now() / 1000),
    exp: Math.floor(Date.now() / 1000) + 3600,
    organization_id: 'org_system',
  };
  next();
};

const DELEGATEE_ID = 'delegatee-agent-uuid';
const CHAIN_ID = 'chain-uuid-1234';
const DELEGATION_TOKEN = 'delegation-token-uuid';

const MOCK_CHAIN = {
  id: CHAIN_ID,
  tenant_id: 'org_system',
  delegator_agent_id: 'delegator-agent-uuid',
  delegatee_agent_id: DELEGATEE_ID,
  scopes: ['agents:read'],
  delegation_token: DELEGATION_TOKEN,
  signature: 'mock-sig',
  ttl_seconds: 3600,
  issued_at: new Date(),
  expires_at: new Date(Date.now() + 3600_000),
  revoked_at: null,
  created_at: new Date(),
};

function buildApp() {
  const app = express();
  app.use(express.json());

  const auditService = new MockAuditService({} as never) as jest.Mocked<AuditService>;
  auditService.logEvent = jest.fn().mockResolvedValue({});

  process.env['DELEGATION_SECRET'] = 'test-secret';
  const delegationService = new DelegationService(mockPool, auditService);
  const controller = new DelegationController(delegationService);
  app.use('/api/v1', createDelegationRouter(controller, testAuth));

  // Error handler
  app.use(
    (
      err: { httpStatus?: number; code?: string; message?: string },
      _req: express.Request,
      res: express.Response,
      _next: express.NextFunction,
    ) => {
      res.status(err.httpStatus ?? 500).json({ code: err.code ?? 'ERROR', message: err.message });
    },
  );

  return app;
}

describe('Delegation Endpoints', () => {
  let app: express.Application;

  beforeEach(() => {
    jest.clearAllMocks();
    app = buildApp();
  });

  afterEach(() => {
    delete process.env['DELEGATION_SECRET'];
  });

  // ────────────────────────────────────────────────────────────────
  // POST /api/v1/oauth2/token/delegate
  // ────────────────────────────────────────────────────────────────

  describe('POST /api/v1/oauth2/token/delegate', () => {
    it('returns 201 with delegation token on success', async () => {
      // delegatee exists in same tenant
      mockQuery.mockResolvedValueOnce({ rows: [{ agent_id: DELEGATEE_ID }] });
      // insert returns chain row
      mockQuery.mockResolvedValueOnce({ rows: [MOCK_CHAIN] });

      const res = await request(app)
        .post('/api/v1/oauth2/token/delegate')
        .send({ delegateeAgentId: DELEGATEE_ID, scopes: ['agents:read'], ttlSeconds: 3600 });

      expect(res.status).toBe(201);
      expect(res.body).toHaveProperty('delegationToken');
      expect(res.body).toHaveProperty('chainId', CHAIN_ID);
      expect(res.body).toHaveProperty('scopes');
    });

    it('returns 400 when scopes exceed delegator permissions', async () => {
      const res = await request(app)
        .post('/api/v1/oauth2/token/delegate')
        .send({ delegateeAgentId: DELEGATEE_ID, scopes: ['admin:orgs'], ttlSeconds: 3600 });

      expect(res.status).toBe(400);
    });

    it('returns 422 on self-delegation', async () => {
      const res = await request(app)
        .post('/api/v1/oauth2/token/delegate')
        .send({
          delegateeAgentId: 'delegator-agent-uuid', // same as req.user.sub
          scopes: ['agents:read'],
          ttlSeconds: 3600,
        });

      expect(res.status).toBe(400);
    });

    it('returns 404 when delegatee does not exist', async () => {
      mockQuery.mockResolvedValueOnce({ rows: [] });

      const res = await request(app)
        .post('/api/v1/oauth2/token/delegate')
        .send({
          delegateeAgentId: 'nonexistent-agent',
          scopes: ['agents:read'],
          ttlSeconds: 3600,
        });

      expect(res.status).toBe(404);
    });

    it('returns 400 when delegateeAgentId is missing', async () => {
      const res = await request(app)
        .post('/api/v1/oauth2/token/delegate')
        .send({ scopes: ['agents:read'], ttlSeconds: 3600 });

      expect(res.status).toBe(400);
    });
  });

  // ────────────────────────────────────────────────────────────────
  // POST /api/v1/oauth2/token/verify-delegation
  // ────────────────────────────────────────────────────────────────

  describe('POST /api/v1/oauth2/token/verify-delegation', () => {
    it('returns 200 with valid: true for an active chain', async () => {
      const { signDelegationPayload } = await import('../../src/utils/delegationCrypto');
      const payload = {
        chainId: MOCK_CHAIN.id,
        tenantId: MOCK_CHAIN.tenant_id,
        delegatorAgentId: MOCK_CHAIN.delegator_agent_id,
        delegateeAgentId: MOCK_CHAIN.delegatee_agent_id,
        scopes: MOCK_CHAIN.scopes,
        issuedAt: MOCK_CHAIN.issued_at.toISOString(),
        expiresAt: MOCK_CHAIN.expires_at.toISOString(),
      };
      const realSignature = signDelegationPayload(payload, 'test-secret');

      mockQuery.mockResolvedValueOnce({
        rows: [{ ...MOCK_CHAIN, signature: realSignature }],
      });

      const res = await request(app)
        .post('/api/v1/oauth2/token/verify-delegation')
        .send({ delegationToken: DELEGATION_TOKEN });

      expect(res.status).toBe(200);
      expect(res.body.valid).toBe(true);
      expect(res.body).toHaveProperty('chainId');
      expect(res.body).toHaveProperty('scopes');
    });

    it('returns 200 with valid: false for an expired chain', async () => {
      const expiredChain = {
        ...MOCK_CHAIN,
        expires_at: new Date(Date.now() - 1000),
      };
      mockQuery.mockResolvedValueOnce({ rows: [expiredChain] });

      const res = await request(app)
        .post('/api/v1/oauth2/token/verify-delegation')
        .send({ delegationToken: DELEGATION_TOKEN });

      expect(res.status).toBe(200);
      expect(res.body.valid).toBe(false);
    });

    it('returns 200 with valid: false for a revoked chain', async () => {
      const revokedChain = { ...MOCK_CHAIN, revoked_at: new Date() };
      mockQuery.mockResolvedValueOnce({ rows: [revokedChain] });

      const res = await request(app)
        .post('/api/v1/oauth2/token/verify-delegation')
        .send({ delegationToken: DELEGATION_TOKEN });

      expect(res.status).toBe(200);
      expect(res.body.valid).toBe(false);
      expect(res.body.revokedAt).toBeTruthy();
    });

    it('returns 404 when no chain found for the token', async () => {
      mockQuery.mockResolvedValueOnce({ rows: [] });

      const res = await request(app)
        .post('/api/v1/oauth2/token/verify-delegation')
        .send({ delegationToken: 'nonexistent-token' });

      expect(res.status).toBe(404);
    });
  });

  // ────────────────────────────────────────────────────────────────
  // DELETE /api/v1/oauth2/token/delegate/:chainId
  // ────────────────────────────────────────────────────────────────

  describe('DELETE /api/v1/oauth2/token/delegate/:chainId', () => {
    it('returns 204 when delegator revokes their own chain', async () => {
      mockQuery.mockResolvedValueOnce({ rows: [MOCK_CHAIN] }); // fetch
      mockQuery.mockResolvedValueOnce({ rows: [] }); // update

      const res = await request(app).delete(`/api/v1/oauth2/token/delegate/${CHAIN_ID}`);

      expect(res.status).toBe(204);
    });

    it('returns 403 when non-delegator attempts revocation', async () => {
      const chainWithDifferentDelegator = {
        ...MOCK_CHAIN,
        delegator_agent_id: 'some-other-agent',
      };
      mockQuery.mockResolvedValueOnce({ rows: [chainWithDifferentDelegator] });

      const res = await request(app).delete(`/api/v1/oauth2/token/delegate/${CHAIN_ID}`);

      expect(res.status).toBe(403);
    });

    it('returns 409 when chain is already revoked', async () => {
      const alreadyRevoked = { ...MOCK_CHAIN, revoked_at: new Date() };
      mockQuery.mockResolvedValueOnce({ rows: [alreadyRevoked] });

      const res = await request(app).delete(`/api/v1/oauth2/token/delegate/${CHAIN_ID}`);

      expect(res.status).toBe(409);
    });

    it('returns 404 when chain does not exist', async () => {
      mockQuery.mockResolvedValueOnce({ rows: [] });

      const res = await request(app).delete(`/api/v1/oauth2/token/delegate/nonexistent-chain`);

      expect(res.status).toBe(404);
    });
  });
});
