/**
 * Entra VerifiedID Client Tests
 */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { EntraVerifiedIDClient, VerifiableCredentialRequest, VerifiedCredential } from './entra-verifiedid';
import { EnhancedEntraVerifiedIDClient } from './entra-verifiedid-enhanced';
import fetch from 'node-fetch';

vi.mock('node-fetch');

describe('EntraVerifiedIDClient', () => {
  let client: EntraVerifiedIDClient;
  const config = {
    tenantId: 'test-tenant-id',
    clientId: 'test-client-id',
    clientSecret: 'test-client-secret',
    credentialManifestId: 'test-manifest-id',
  };

  beforeEach(() => {
    client = new EntraVerifiedIDClient(config);
    vi.clearAllMocks();
  });

  afterEach(() => {
    vi.restoreAllMocks();
  });

  describe('getAccessToken', () => {
    it('should cache access token until expiry', async () => {
      const mockTokenResponse = {
        access_token: 'test-token',
        expires_in: 3600,
      };

      (fetch as any).mockResolvedValueOnce({
        ok: true,
        json: async () => mockTokenResponse,
      });

      // First call
      const token1 = await (client as any).getAccessToken();
      expect(token1).toBe('test-token');

      // Second call should use cached token
      const token2 = await (client as any).getAccessToken();
      expect(token2).toBe('test-token');
      expect(fetch).toHaveBeenCalledTimes(1);
    });

    it('should refresh token when expired', async () => {
      const mockTokenResponse = {
        access_token: 'test-token',
        expires_in: 1, // 1 second expiry
      };

      (fetch as any).mockResolvedValue({
        ok: true,
        json: async () => mockTokenResponse,
      });

      await (client as any).getAccessToken();
      
      // Wait for token to expire
      await new Promise((resolve) => setTimeout(resolve, 1100));
      
      // Should fetch new token
      await (client as any).getAccessToken();
      expect(fetch).toHaveBeenCalledTimes(2);
    });
  });

  describe('validateCredentialRequest', () => {
    it('should validate request with valid claims', () => {
      const request: VerifiableCredentialRequest = {
        claims: { email: 'test@example.com', name: 'Test User' },
      };
      expect(() => (client as any).validateCredentialRequest(request)).not.toThrow();
    });

    it('should reject request with empty claims', () => {
      const request: VerifiableCredentialRequest = {
        claims: {},
      };
      expect(() => (client as any).validateCredentialRequest(request)).toThrow('At least one claim is required');
    });

    it('should validate PIN format', () => {
      const validRequest: VerifiableCredentialRequest = {
        claims: { email: 'test@example.com' },
        pin: '1234',
      };
      expect(() => (client as any).validateCredentialRequest(validRequest)).not.toThrow();

      const invalidRequest: VerifiableCredentialRequest = {
        claims: { email: 'test@example.com' },
        pin: 'abc',
      };
      expect(() => (client as any).validateCredentialRequest(invalidRequest)).toThrow('PIN must be between 4 and 8 characters');
    });
  });

  describe('issueCredential', () => {
    it('should issue credential successfully', async () => {
      const mockTokenResponse = {
        access_token: 'test-token',
        expires_in: 3600,
      };

      const mockIssuanceResponse = {
        requestId: 'test-request-id',
        url: 'https://verifiedid.did.msidentity.com/issuance',
        expiry: Date.now() + 3600000,
        qrCode: 'data:image/png;base64,test',
      };

      (fetch as any)
        .mockResolvedValueOnce({
          ok: true,
          json: async () => mockTokenResponse,
        })
        .mockResolvedValueOnce({
          ok: true,
          json: async () => mockIssuanceResponse,
        });

      const request: VerifiableCredentialRequest = {
        claims: { email: 'test@example.com', name: 'Test User' },
      };

      const result = await client.issueCredential(request);
      expect(result.requestId).toBe('test-request-id');
      expect(result.url).toBe('https://verifiedid.did.msidentity.com/issuance');
      expect(result.qrCode).toBe('data:image/png;base64,test');
    });

    it('should throw error when manifest ID is missing', async () => {
      const clientWithoutManifest = new EntraVerifiedIDClient({
        tenantId: 'test-tenant',
        clientId: 'test-client',
        clientSecret: 'test-secret',
      });

      const request: VerifiableCredentialRequest = {
        claims: { email: 'test@example.com' },
      };

      await expect(clientWithoutManifest.issueCredential(request)).rejects.toThrow('Credential manifest ID is required');
    });
  });

  describe('verifyCredential', () => {
    it('should verify credential successfully', async () => {
      const mockTokenResponse = {
        access_token: 'test-token',
        expires_in: 3600,
      };

      const mockVerifyResponse = {
        verified: true,
      };

      (fetch as any)
        .mockResolvedValueOnce({
          ok: true,
          json: async () => mockTokenResponse,
        })
        .mockResolvedValueOnce({
          ok: true,
          json: async () => mockVerifyResponse,
        });

      const credential: VerifiedCredential = {
        id: 'test-credential-id',
        type: ['VerifiableCredential'],
        issuer: 'did:web:test.verifiedid.msidentity.com',
        issuanceDate: new Date().toISOString(),
        credentialSubject: { email: 'test@example.com' },
        proof: {
          type: 'JsonWebSignature2020',
          created: new Date().toISOString(),
          proofPurpose: 'assertionMethod',
          verificationMethod: 'did:web:test#key',
          jws: 'test-jws',
        },
      };

      const result = await client.verifyCredential(credential);
      expect(result).toBe(true);
    });

    it('should reject invalid credential structure', async () => {
      const invalidCredential = {
        id: 'test-id',
      } as VerifiedCredential;

      await expect(client.verifyCredential(invalidCredential)).rejects.toThrow('Credential type is required');
    });
  });
});

describe('EnhancedEntraVerifiedIDClient', () => {
  let client: EnhancedEntraVerifiedIDClient;
  const config = {
    tenantId: 'test-tenant-id',
    clientId: 'test-client-id',
    clientSecret: 'test-client-secret',
    credentialManifestId: 'default-manifest-id',
    manifests: {
      default: 'default-manifest-id',
      diplomatic: 'diplomatic-manifest-id',
      judicial: 'judicial-manifest-id',
    },
  };

  beforeEach(() => {
    client = new EnhancedEntraVerifiedIDClient(config);
    vi.clearAllMocks();
  });

  describe('multi-manifest support', () => {
    it('should use default manifest when no manifest name provided', async () => {
      const mockTokenResponse = {
        access_token: 'test-token',
        expires_in: 3600,
      };

      const mockIssuanceResponse = {
        requestId: 'test-request-id',
        url: 'https://verifiedid.did.msidentity.com/issuance',
        expiry: Date.now() + 3600000,
      };

      (fetch as any)
        .mockResolvedValueOnce({
          ok: true,
          json: async () => mockTokenResponse,
        })
        .mockResolvedValueOnce({
          ok: true,
          json: async () => mockIssuanceResponse,
        });

      const request: VerifiableCredentialRequest = {
        claims: { email: 'test@example.com' },
      };

      await client.issueCredential(request);
      
      // Verify the request used default manifest
      const fetchCalls = (fetch as any).mock.calls;
      const issuanceCall = fetchCalls.find((call: any[]) => 
        call[0]?.includes('createIssuanceRequest')
      );
      expect(issuanceCall).toBeDefined();
    });

    it('should use specified manifest when manifest name provided', async () => {
      const mockTokenResponse = {
        access_token: 'test-token',
        expires_in: 3600,
      };

      const mockIssuanceResponse = {
        requestId: 'test-request-id',
        url: 'https://verifiedid.did.msidentity.com/issuance',
        expiry: Date.now() + 3600000,
      };

      (fetch as any)
        .mockResolvedValueOnce({
          ok: true,
          json: async () => mockTokenResponse,
        })
        .mockResolvedValueOnce({
          ok: true,
          json: async () => mockIssuanceResponse,
        });

      const request: VerifiableCredentialRequest & { manifestName?: string } = {
        claims: { email: 'test@example.com' },
        manifestName: 'diplomatic',
      };

      await client.issueCredential(request);
      
      // Verify the request used diplomatic manifest
      const fetchCalls = (fetch as any).mock.calls;
      const issuanceCall = fetchCalls.find((call: any[]) => 
        call[0]?.includes('createIssuanceRequest')
      );
      expect(issuanceCall).toBeDefined();
    });

    it('should allow registering new manifests', () => {
      client.registerManifest('financial', 'financial-manifest-id');
      const manifests = client.getManifests();
      expect(manifests.financial).toBe('financial-manifest-id');
    });
  });

  describe('retry logic', () => {
    it('should retry on retryable errors', async () => {
      const mockTokenResponse = {
        access_token: 'test-token',
        expires_in: 3600,
      };

      // First two calls fail with 500, third succeeds
      (fetch as any)
        .mockResolvedValueOnce({
          ok: true,
          json: async () => mockTokenResponse,
        })
        .mockResolvedValueOnce({
          ok: false,
          status: 500,
          text: async () => 'Internal Server Error',
        })
        .mockResolvedValueOnce({
          ok: true,
          json: async () => mockTokenResponse,
        })
        .mockResolvedValueOnce({
          ok: true,
          json: async () => ({
            requestId: 'test-request-id',
            url: 'https://verifiedid.did.msidentity.com/issuance',
            expiry: Date.now() + 3600000,
          }),
        });

      const request: VerifiableCredentialRequest = {
        claims: { email: 'test@example.com' },
      };

      const result = await client.issueCredential(request);
      expect(result.requestId).toBe('test-request-id');
      // Should have retried (more than 2 fetch calls)
      expect(fetch).toHaveBeenCalledTimes(4);
    });

    it('should not retry on non-retryable errors', async () => {
      const mockTokenResponse = {
        access_token: 'test-token',
        expires_in: 3600,
      };

      (fetch as any)
        .mockResolvedValueOnce({
          ok: true,
          json: async () => mockTokenResponse,
        })
        .mockResolvedValueOnce({
          ok: false,
          status: 400,
          text: async () => 'Bad Request',
        });

      const request: VerifiableCredentialRequest = {
        claims: { email: 'test@example.com' },
      };

      await expect(client.issueCredential(request)).rejects.toThrow();
      // Should not retry (only 2 fetch calls)
      expect(fetch).toHaveBeenCalledTimes(2);
    });
  });
});

