/**
 * Unit tests for src/services/DelegationService.ts
 */

import { Pool } from 'pg';
import { DelegationService } from '../../../src/services/DelegationService';
import { AuditService } from '../../../src/services/AuditService';
import {
  ValidationError,
  AgentNotFoundError,
  AuthorizationError,
  CredentialAlreadyRevokedError,
} from '../../../src/utils/errors';

jest.mock('../../../src/services/AuditService');
const MockAuditService = AuditService as jest.MockedClass<typeof AuditService>;

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

const TENANT_ID = 'org_system';
const DELEGATOR_ID = 'delegator-uuid-1234';
const DELEGATEE_ID = 'delegatee-uuid-5678';
const DELEGATOR_SCOPES = ['agents:read', 'tokens:read', 'audit:read'];
const IP = '127.0.0.1';
const UA = 'test-agent/1.0';

const MOCK_CHAIN_ROW = {
  id: 'chain-uuid-1234',
  tenant_id: TENANT_ID,
  delegator_agent_id: DELEGATOR_ID,
  delegatee_agent_id: DELEGATEE_ID,
  scopes: ['agents:read'],
  delegation_token: 'token-uuid-abcd',
  signature: 'mock-signature',
  ttl_seconds: 3600,
  issued_at: new Date('2026-04-03T00:00:00Z'),
  expires_at: new Date(Date.now() + 3600_000),
  revoked_at: null,
  created_at: new Date('2026-04-03T00:00:00Z'),
};

describe('DelegationService', () => {
  let service: DelegationService;
  let auditService: jest.Mocked<AuditService>;

  beforeEach(() => {
    jest.clearAllMocks();
    // Set delegation secret for tests
    process.env['DELEGATION_SECRET'] = 'test-secret';
    auditService = new MockAuditService({} as never) as jest.Mocked<AuditService>;
    auditService.logEvent = jest.fn().mockResolvedValue({});
    service = new DelegationService(mockPool, auditService);
  });

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

  // ────────────────────────────────────────────────────────────────
  // createDelegation
  // ────────────────────────────────────────────────────────────────

  describe('createDelegation', () => {
    it('creates a delegation chain successfully', async () => {
      // delegatee exists
      mockQuery.mockResolvedValueOnce({ rows: [{ agent_id: DELEGATEE_ID }] });
      // insert returns row
      mockQuery.mockResolvedValueOnce({ rows: [MOCK_CHAIN_ROW] });

      const result = await service.createDelegation(
        TENANT_ID,
        DELEGATOR_ID,
        DELEGATOR_SCOPES,
        { delegateeAgentId: DELEGATEE_ID, scopes: ['agents:read'], ttlSeconds: 3600 },
        IP,
        UA,
      );

      expect(result.delegatorAgentId).toBe(DELEGATOR_ID);
      expect(result.delegateeAgentId).toBe(DELEGATEE_ID);
      expect(auditService.logEvent).toHaveBeenCalledWith(
        DELEGATOR_ID,
        'delegation.created',
        'success',
        IP,
        UA,
        expect.objectContaining({ chainId: expect.any(String), delegateeAgentId: DELEGATEE_ID }),
        TENANT_ID,
      );
    });

    it('throws ValidationError when scope escalation is attempted', async () => {
      await expect(
        service.createDelegation(
          TENANT_ID,
          DELEGATOR_ID,
          DELEGATOR_SCOPES,
          { delegateeAgentId: DELEGATEE_ID, scopes: ['admin:orgs'], ttlSeconds: 3600 },
          IP,
          UA,
        ),
      ).rejects.toThrow(ValidationError);
    });

    it('throws ValidationError on self-delegation', async () => {
      await expect(
        service.createDelegation(
          TENANT_ID,
          DELEGATOR_ID,
          DELEGATOR_SCOPES,
          { delegateeAgentId: DELEGATOR_ID, scopes: ['agents:read'], ttlSeconds: 3600 },
          IP,
          UA,
        ),
      ).rejects.toThrow(ValidationError);
    });

    it('throws ValidationError when ttlSeconds is below minimum', async () => {
      await expect(
        service.createDelegation(
          TENANT_ID,
          DELEGATOR_ID,
          DELEGATOR_SCOPES,
          { delegateeAgentId: DELEGATEE_ID, scopes: ['agents:read'], ttlSeconds: 30 },
          IP,
          UA,
        ),
      ).rejects.toThrow(ValidationError);
    });

    it('throws ValidationError when ttlSeconds exceeds maximum', async () => {
      await expect(
        service.createDelegation(
          TENANT_ID,
          DELEGATOR_ID,
          DELEGATOR_SCOPES,
          { delegateeAgentId: DELEGATEE_ID, scopes: ['agents:read'], ttlSeconds: 99999 },
          IP,
          UA,
        ),
      ).rejects.toThrow(ValidationError);
    });

    it('throws AgentNotFoundError when delegatee is in a different tenant', async () => {
      // delegatee not found in this tenant
      mockQuery.mockResolvedValueOnce({ rows: [] });

      await expect(
        service.createDelegation(
          TENANT_ID,
          DELEGATOR_ID,
          DELEGATOR_SCOPES,
          { delegateeAgentId: 'other-tenant-agent', scopes: ['agents:read'], ttlSeconds: 3600 },
          IP,
          UA,
        ),
      ).rejects.toThrow(AgentNotFoundError);
    });
  });

  // ────────────────────────────────────────────────────────────────
  // verifyDelegation
  // ────────────────────────────────────────────────────────────────

  describe('verifyDelegation', () => {
    it('returns valid: true for an active, non-expired chain', async () => {
      // Need a real signature to pass verification
      const { signDelegationPayload } = await import('../../../src/utils/delegationCrypto');
      const payload = {
        chainId: MOCK_CHAIN_ROW.id,
        tenantId: MOCK_CHAIN_ROW.tenant_id,
        delegatorAgentId: MOCK_CHAIN_ROW.delegator_agent_id,
        delegateeAgentId: MOCK_CHAIN_ROW.delegatee_agent_id,
        scopes: MOCK_CHAIN_ROW.scopes,
        issuedAt: MOCK_CHAIN_ROW.issued_at.toISOString(),
        expiresAt: MOCK_CHAIN_ROW.expires_at.toISOString(),
      };
      const realSignature = signDelegationPayload(payload, 'test-secret');

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

      const result = await service.verifyDelegation('token-uuid-abcd', DELEGATEE_ID, IP, UA);

      expect(result.valid).toBe(true);
      expect(result.chainId).toBe(MOCK_CHAIN_ROW.id);
      expect(auditService.logEvent).toHaveBeenCalledWith(
        DELEGATEE_ID,
        'delegation.verified',
        'success',
        IP,
        UA,
        expect.objectContaining({ valid: true }),
        TENANT_ID,
      );
    });

    it('returns valid: false (not throws) for an expired chain', async () => {
      const expiredRow = {
        ...MOCK_CHAIN_ROW,
        expires_at: new Date(Date.now() - 1000), // past
        signature: 'any-sig',
      };
      mockQuery.mockResolvedValueOnce({ rows: [expiredRow] });

      const result = await service.verifyDelegation('token-uuid-abcd', DELEGATEE_ID, IP, UA);

      expect(result.valid).toBe(false);
    });

    it('returns valid: false (not throws) for a revoked chain', async () => {
      const revokedRow = {
        ...MOCK_CHAIN_ROW,
        revoked_at: new Date(),
        signature: 'any-sig',
      };
      mockQuery.mockResolvedValueOnce({ rows: [revokedRow] });

      const result = await service.verifyDelegation('token-uuid-abcd', DELEGATEE_ID, IP, UA);

      expect(result.valid).toBe(false);
      expect(result.revokedAt).toBeDefined();
    });

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

      await expect(
        service.verifyDelegation('nonexistent-token', DELEGATEE_ID, IP, UA),
      ).rejects.toThrow(AgentNotFoundError);
    });
  });

  // ────────────────────────────────────────────────────────────────
  // revokeDelegation
  // ────────────────────────────────────────────────────────────────

  describe('revokeDelegation', () => {
    it('revokes a delegation chain when called by the delegator', async () => {
      mockQuery.mockResolvedValueOnce({ rows: [MOCK_CHAIN_ROW] }); // fetch chain
      mockQuery.mockResolvedValueOnce({ rows: [] }); // update revoked_at

      await service.revokeDelegation(MOCK_CHAIN_ROW.id, DELEGATOR_ID, IP, UA);

      expect(auditService.logEvent).toHaveBeenCalledWith(
        DELEGATOR_ID,
        'delegation.revoked',
        'success',
        IP,
        UA,
        expect.objectContaining({ chainId: MOCK_CHAIN_ROW.id }),
        TENANT_ID,
      );
    });

    it('throws AuthorizationError when a non-delegator attempts revocation', async () => {
      mockQuery.mockResolvedValueOnce({ rows: [MOCK_CHAIN_ROW] });

      await expect(
        service.revokeDelegation(MOCK_CHAIN_ROW.id, 'other-agent-id', IP, UA),
      ).rejects.toThrow(AuthorizationError);
    });

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

      await expect(
        service.revokeDelegation(MOCK_CHAIN_ROW.id, DELEGATOR_ID, IP, UA),
      ).rejects.toThrow(CredentialAlreadyRevokedError);
    });

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

      await expect(
        service.revokeDelegation('nonexistent-chain-id', DELEGATOR_ID, IP, UA),
      ).rejects.toThrow(AgentNotFoundError);
    });
  });
});
