import fc from 'fast-check';
import { MFAService } from '../services/mfa.service';

// Feature: website-cloning-platform, Property 37: Multi-Factor Authentication Enforcement
describe('Multi-Factor Authentication Properties', () => {
  
  beforeEach(() => {
    // Clean up before each test
    MFAService.cleanupExpiredChallenges();
  });
  
  test('Property 37: MFA setup should generate valid secrets and codes', async () => {
    await fc.assert(
      fc.asyncProperty(
        fc.record({
          userId: fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0 && !['name', 'constructor', 'toString', '__proto__'].includes(s)),
          mfaType: fc.constantFrom('totp', 'sms', 'email'),
          phoneNumber: fc.string({ minLength: 10, maxLength: 15 }).map(s => '+1' + s.replace(/\D/g, '').slice(0, 10)),
          email: fc.emailAddress()
        }),
        async ({ userId, mfaType, phoneNumber, email }) => {
          const options = mfaType === 'sms' ? { phoneNumber } : mfaType === 'email' ? { email } : {};
          
          // Setup MFA
          const setup = MFAService.setupMFA(userId, mfaType, options);
          
          // Property: Setup should have correct structure
          const hasValidUserId = setup.userId === userId;
          const hasValidType = setup.type === mfaType;
          const hasBackupCodes = Array.isArray(setup.backupCodes) && setup.backupCodes.length === 10;
          const isInitiallyDisabled = setup.isEnabled === false;
          
          // Type-specific validations
          let typeSpecificValid = true;
          switch (mfaType) {
            case 'totp':
              typeSpecificValid = typeof setup.secret === 'string' && setup.secret.length > 0;
              break;
            case 'sms':
              typeSpecificValid = setup.phoneNumber === phoneNumber;
              break;
            case 'email':
              typeSpecificValid = setup.email === email;
              break;
          }
          
          // Property: All backup codes should be unique
          const uniqueBackupCodes = new Set(setup.backupCodes).size === setup.backupCodes.length;
          
          return hasValidUserId && hasValidType && hasBackupCodes && 
                 isInitiallyDisabled && typeSpecificValid && uniqueBackupCodes;
        }
      ),
      { 
        numRuns: 100,
        timeout: 5000
      }
    );
  });

  test('Property 37: MFA challenges should have proper lifecycle management', async () => {
    await fc.assert(
      fc.asyncProperty(
        fc.record({
          userId: fc.string({ minLength: 1, maxLength: 50 }),
          mfaType: fc.constantFrom('totp', 'sms', 'email')
        }),
        async ({ userId, mfaType }) => {
          // Setup and enable MFA
          MFAService.setupMFA(userId, mfaType);
          MFAService.enableMFA(userId);
          
          // Create challenge
          const challenge = MFAService.createChallenge(userId);
          
          // Property: Challenge should have valid structure
          const hasValidId = typeof challenge.challengeId === 'string' && challenge.challengeId.length > 0;
          const hasValidUserId = challenge.userId === userId;
          const hasValidType = challenge.type === mfaType;
          const hasValidCode = typeof challenge.code === 'string' && challenge.code.length === 6;
          const hasValidExpiry = challenge.expiresAt > new Date();
          const hasValidAttempts = challenge.attempts === 0 && challenge.maxAttempts === 3;
          const isNotUsed = challenge.isUsed === false;
          
          return hasValidId && hasValidUserId && hasValidType && hasValidCode && 
                 hasValidExpiry && hasValidAttempts && isNotUsed;
        }
      ),
      { 
        numRuns: 100,
        timeout: 5000
      }
    );
  });

  test('Property 37: MFA verification should enforce attempt limits', async () => {
    await fc.assert(
      fc.asyncProperty(
        fc.record({
          userId: fc.string({ minLength: 1, maxLength: 50 }),
          wrongCodes: fc.array(fc.string({ minLength: 6, maxLength: 6 }), { minLength: 1, maxLength: 5 })
        }),
        async ({ userId, wrongCodes }) => {
          // Setup and enable MFA
          MFAService.setupMFA(userId, 'email');
          MFAService.enableMFA(userId);
          
          // Create challenge
          const challenge = MFAService.createChallenge(userId);
          const correctCode = challenge.code;
          
          // Try wrong codes (but not the correct one)
          const filteredWrongCodes = wrongCodes.filter(code => code !== correctCode);
          let verificationResults: boolean[] = [];
          
          for (const wrongCode of filteredWrongCodes.slice(0, 4)) { // Max 4 attempts
            const result = MFAService.verifyChallenge(challenge.challengeId, wrongCode);
            verificationResults.push(result);
          }
          
          // Property: All wrong codes should fail verification
          const allWrongCodesFailed = verificationResults.every(result => result === false);
          
          // Property: After max attempts, even correct code should fail
          let correctCodeFailsAfterMaxAttempts = true;
          if (filteredWrongCodes.length >= 3) {
            const finalResult = MFAService.verifyChallenge(challenge.challengeId, correctCode);
            correctCodeFailsAfterMaxAttempts = finalResult === false;
          }
          
          return allWrongCodesFailed && correctCodeFailsAfterMaxAttempts;
        }
      ),
      { 
        numRuns: 100,
        timeout: 5000
      }
    );
  });

  test('Property 37: MFA backup codes should work correctly', async () => {
    await fc.assert(
      fc.asyncProperty(
        fc.record({
          userId: fc.string({ minLength: 1, maxLength: 50 }),
          codeIndex: fc.integer({ min: 0, max: 9 }) // Backup codes are 0-9
        }),
        async ({ userId, codeIndex }) => {
          // Setup and enable MFA
          const setup = MFAService.setupMFA(userId, 'totp');
          MFAService.enableMFA(userId);
          
          // Get a backup code
          const backupCode = setup.backupCodes[codeIndex];
          const originalCodeCount = setup.backupCodes.length;
          
          // Verify backup code
          const verificationResult = MFAService.verifyBackupCode(userId, backupCode);
          
          // Property: Backup code should verify successfully
          const backupCodeWorked = verificationResult === true;
          
          // Property: Used backup code should be removed
          const codeWasRemoved = setup.backupCodes.length === originalCodeCount - 1;
          const codeNoLongerExists = !setup.backupCodes.includes(backupCode);
          
          // Property: Same backup code should not work twice
          const secondVerification = MFAService.verifyBackupCode(userId, backupCode);
          const secondVerificationFailed = secondVerification === false;
          
          return backupCodeWorked && codeWasRemoved && codeNoLongerExists && secondVerificationFailed;
        }
      ),
      { 
        numRuns: 100,
        timeout: 5000
      }
    );
  });

  test('Property 37: MFA state transitions should be consistent', async () => {
    await fc.assert(
      fc.asyncProperty(
        fc.record({
          userId: fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
          operations: fc.array(
            fc.constantFrom('setup', 'enable', 'disable', 'check'),
            { minLength: 1, maxLength: 5 } // Reduced complexity
          )
        }),
        async ({ userId, operations }) => {
          // Always start with a clean state
          try {
            MFAService.disableMFA(userId);
          } catch (e) {
            // Ignore errors during cleanup
          }
          
          let hasSetup = false;
          let isEnabled = false;
          
          for (const operation of operations) {
            switch (operation) {
              case 'setup':
                MFAService.setupMFA(userId, 'email');
                hasSetup = true;
                isEnabled = false; // Setup always resets enabled state
                break;
                
              case 'enable':
                if (hasSetup) {
                  const result = MFAService.enableMFA(userId);
                  if (result) {
                    isEnabled = true;
                  }
                } else {
                  // Should fail if no setup
                  const result = MFAService.enableMFA(userId);
                  if (result) {
                    return false; // Should not succeed without setup
                  }
                }
                break;
                
              case 'disable':
                if (hasSetup) {
                  const result = MFAService.disableMFA(userId);
                  if (result) {
                    isEnabled = false;
                  }
                } else {
                  // Should fail if no setup
                  const result = MFAService.disableMFA(userId);
                  if (result) {
                    return false; // Should not succeed without setup
                  }
                }
                break;
                
              case 'check':
                const actualEnabled = MFAService.isMFAEnabled(userId);
                if (actualEnabled !== isEnabled) {
                  return false;
                }
                break;
            }
          }
          
          return true;
        }
      ),
      { 
        numRuns: 50, // Reduced number of runs
        timeout: 5000
      }
    );
  });

  test('Property 37: MFA validation should handle edge cases correctly', async () => {
    await fc.assert(
      fc.asyncProperty(
        fc.record({
          mfaType: fc.constantFrom('totp', 'sms', 'email'),
          phoneNumber: fc.option(fc.string({ minLength: 5, maxLength: 20 })),
          email: fc.option(fc.string({ minLength: 3, maxLength: 50 }))
        }),
        async ({ mfaType, phoneNumber, email }) => {
          const options = { phoneNumber, email };
          
          // Property: Validation should correctly identify valid/invalid setups
          const isValid = MFAService.validateMFASetup(mfaType, options);
          
          let expectedValid = false;
          switch (mfaType) {
            case 'totp':
              expectedValid = true; // TOTP doesn't need additional validation
              break;
            case 'sms':
              expectedValid = !!phoneNumber && /^\+[1-9]\d{1,14}$/.test(phoneNumber);
              break;
            case 'email':
              expectedValid = !!email && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
              break;
          }
          
          return isValid === expectedValid;
        }
      ),
      { 
        numRuns: 100,
        timeout: 5000
      }
    );
  });

  test('Property 37: Challenge cleanup should remove expired challenges', async () => {
    await fc.assert(
      fc.asyncProperty(
        fc.record({
          userId: fc.string({ minLength: 1, maxLength: 50 }),
          challengeCount: fc.integer({ min: 1, max: 5 })
        }),
        async ({ userId, challengeCount }) => {
          // Setup and enable MFA
          MFAService.setupMFA(userId, 'sms');
          MFAService.enableMFA(userId);
          
          // Create multiple challenges
          const challenges = [];
          for (let i = 0; i < challengeCount; i++) {
            const challenge = MFAService.createChallenge(userId);
            challenges.push(challenge);
          }
          
          // Check that all challenges exist
          const allExistBefore = challenges.every(challenge => {
            const status = MFAService.getChallengeStatus(challenge.challengeId);
            return status.exists;
          });
          
          // Simulate time passing (mock expired challenges by manipulating the expiry)
          // In a real test, we might wait or use a time mocking library
          
          // For this test, we'll just verify that cleanup doesn't break anything
          MFAService.cleanupExpiredChallenges();
          
          // Property: Cleanup should not crash and should be idempotent
          MFAService.cleanupExpiredChallenges();
          MFAService.cleanupExpiredChallenges();
          
          return allExistBefore; // Basic property: challenges existed before cleanup
        }
      ),
      { 
        numRuns: 50,
        timeout: 5000
      }
    );
  });
});