package de.effigenix.infrastructure.security;

import de.effigenix.domain.usermanagement.PasswordHash;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import static org.assertj.core.api.Assertions.*;

/**
 * Unit tests for BCryptPasswordHasher Implementation.
 * Tests hashing, verification, and validation logic for password security.
 */
@DisplayName("BCryptPasswordHasher Implementation")
class BCryptPasswordHasherTest {

    private BCryptPasswordHasher hasher;

    @BeforeEach
    void setUp() {
        hasher = new BCryptPasswordHasher();
    }

    @Test
    @DisplayName("should_HashPassword_When_ValidPasswordProvided")
    void should_HashPassword_When_ValidPasswordProvided() {
        // Act
        PasswordHash hash = hasher.hash("ValidPassword123!");

        // Assert
        assertThat(hash).isNotNull();
        assertThat(hash.value()).startsWith("$2a$12$")
                .hasSize(60);
    }

    @Test
    @DisplayName("should_GenerateUniqueBcryptHash_When_SamePasswordHashedTwice")
    void should_GenerateUniqueBcryptHash_When_SamePasswordHashedTwice() {
        // Act
        PasswordHash hash1 = hasher.hash("SamePassword123!");
        PasswordHash hash2 = hasher.hash("SamePassword123!");

        // Assert
        assertThat(hash1.value()).isNotEqualTo(hash2.value());
    }

    @Test
    @DisplayName("should_ThrowException_When_NullPasswordProvided")
    void should_ThrowException_When_NullPasswordProvided() {
        // Act & Assert
        assertThatThrownBy(() -> hasher.hash(null))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("Password cannot be null or empty");
    }

    @Test
    @DisplayName("should_ThrowException_When_EmptyPasswordProvided")
    void should_ThrowException_When_EmptyPasswordProvided() {
        // Act & Assert
        assertThatThrownBy(() -> hasher.hash(""))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("Password cannot be null or empty");
    }

    @Test
    @DisplayName("should_ThrowException_When_BlankPasswordProvided")
    void should_ThrowException_When_BlankPasswordProvided() {
        // Act & Assert
        assertThatThrownBy(() -> hasher.hash("   "))
                .isInstanceOf(IllegalArgumentException.class);
    }

    @Test
    @DisplayName("should_ThrowException_When_WeakPasswordProvidedToHash")
    void should_ThrowException_When_WeakPasswordProvidedToHash() {
        // Act & Assert
        assertThatThrownBy(() -> hasher.hash("weak"))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("Password does not meet minimum requirements");
    }

    @Test
    @DisplayName("should_VerifyPassword_When_CorrectPasswordProvided")
    void should_VerifyPassword_When_CorrectPasswordProvided() {
        // Arrange
        String plainPassword = "MySecurePass123!";
        PasswordHash hash = hasher.hash(plainPassword);

        // Act
        boolean isValid = hasher.verify(plainPassword, hash);

        // Assert
        assertThat(isValid).isTrue();
    }

    @Test
    @DisplayName("should_RejectPassword_When_IncorrectPasswordProvided")
    void should_RejectPassword_When_IncorrectPasswordProvided() {
        // Arrange
        PasswordHash hash = hasher.hash("CorrectPass123!");

        // Act
        boolean isValid = hasher.verify("WrongPassword123!", hash);

        // Assert
        assertThat(isValid).isFalse();
    }

    @Test
    @DisplayName("should_ReturnFalse_When_NullPasswordProvidedToVerify")
    void should_ReturnFalse_When_NullPasswordProvidedToVerify() {
        // Arrange
        PasswordHash hash = hasher.hash("ValidPassword123!");

        // Act
        boolean isValid = hasher.verify(null, hash);

        // Assert
        assertThat(isValid).isFalse();
    }

    @Test
    @DisplayName("should_ReturnFalse_When_NullHashProvidedToVerify")
    void should_ReturnFalse_When_NullHashProvidedToVerify() {
        // Act
        boolean isValid = hasher.verify("ValidPassword123!", null);

        // Assert
        assertThat(isValid).isFalse();
    }

    @Test
    @DisplayName("should_ReturnFalse_When_BothNullProvidedToVerify")
    void should_ReturnFalse_When_BothNullProvidedToVerify() {
        // Act
        boolean isValid = hasher.verify(null, null);

        // Assert
        assertThat(isValid).isFalse();
    }

    @Test
    @DisplayName("should_ValidatePasswordLength_When_LessThan8Characters")
    void should_ValidatePasswordLength_When_LessThan8Characters() {
        // Act
        boolean isValid = hasher.isValidPassword("Short1!");

        // Assert
        assertThat(isValid).isFalse();
    }

    @Test
    @DisplayName("should_ValidatePasswordLength_When_Exactly8Characters")
    void should_ValidatePasswordLength_When_Exactly8Characters() {
        // Act
        boolean isValid = hasher.isValidPassword("Exactly1!");

        // Assert
        assertThat(isValid).isTrue();
    }

    @Test
    @DisplayName("should_ValidateRequiresUpperCase_When_PasswordMissingUpperCase")
    void should_ValidateRequiresUpperCase_When_PasswordMissingUpperCase() {
        // Act
        boolean isValid = hasher.isValidPassword("lowercase123!");

        // Assert
        assertThat(isValid).isFalse();
    }

    @Test
    @DisplayName("should_ValidateRequiresLowerCase_When_PasswordMissingLowerCase")
    void should_ValidateRequiresLowerCase_When_PasswordMissingLowerCase() {
        // Act
        boolean isValid = hasher.isValidPassword("UPPERCASE123!");

        // Assert
        assertThat(isValid).isFalse();
    }

    @Test
    @DisplayName("should_ValidateRequiresDigit_When_PasswordMissingDigit")
    void should_ValidateRequiresDigit_When_PasswordMissingDigit() {
        // Act
        boolean isValid = hasher.isValidPassword("NoDigitPass!");

        // Assert
        assertThat(isValid).isFalse();
    }

    @Test
    @DisplayName("should_ValidateRequiresSpecialChar_When_PasswordMissingSpecialChar")
    void should_ValidateRequiresSpecialChar_When_PasswordMissingSpecialChar() {
        // Act
        boolean isValid = hasher.isValidPassword("NoSpecial123");

        // Assert
        assertThat(isValid).isFalse();
    }

    @Test
    @DisplayName("should_AcceptValidPassword_When_AllRequirementsMet")
    void should_AcceptValidPassword_When_AllRequirementsMet() {
        // Act
        boolean isValid = hasher.isValidPassword("ValidPass123!");

        // Assert
        assertThat(isValid).isTrue();
    }

    @Test
    @DisplayName("should_ReturnFalse_When_NullPasswordProvidedToValidate")
    void should_ReturnFalse_When_NullPasswordProvidedToValidate() {
        // Act
        boolean isValid = hasher.isValidPassword(null);

        // Assert
        assertThat(isValid).isFalse();
    }

    @ParameterizedTest
    @ValueSource(strings = {
            "ValidPass123!",
            "Secure#Pass456",
            "Complex@Pass789",
            "MyP@ssw0rd",
            "Str0ng!Pass"
    })
    @DisplayName("should_AcceptAllValidPasswords_When_RequirementsMetWithDifferentFormats")
    void should_AcceptAllValidPasswords_When_RequirementsMetWithDifferentFormats(String password) {
        // Act
        boolean isValid = hasher.isValidPassword(password);

        // Assert
        assertThat(isValid).isTrue();
    }

    @Test
    @DisplayName("should_ProduceCorrectBcryptFormat_When_PasswordHashed")
    void should_ProduceCorrectBcryptFormat_When_PasswordHashed() {
        // Act
        PasswordHash hash = hasher.hash("ValidPassword123!");

        // Assert
        assertThat(hash.value())
                .matches("\\$2[aby]\\$\\d{2}\\$.*")  // Matches BCrypt format
                .hasSize(60);                         // BCrypt hash is 60 chars
    }

    @Test
    @DisplayName("should_UseStrength12_When_CreatingHasher")
    void should_UseStrength12_When_CreatingHasher() {
        // Arrange
        String password = "TestPassword123!";

        // Act
        PasswordHash hash = hasher.hash(password);

        // Assert
        // BCrypt with strength 12 produces format $2a$12$...
        assertThat(hash.value()).matches("\\$2[aby]\\$12\\$.*");
    }

    @Test
    @DisplayName("should_ResistTimingAttacks_When_VerifyingPassword")
    void should_ResistTimingAttacks_When_VerifyingPassword() {
        // Arrange
        PasswordHash hash = hasher.hash("ValidPassword123!");

        // Act - Verify with correct and incorrect passwords
        long startCorrect = System.nanoTime();
        boolean resultCorrect = hasher.verify("ValidPassword123!", hash);
        long timeCorrect = System.nanoTime() - startCorrect;

        long startIncorrect = System.nanoTime();
        boolean resultIncorrect = hasher.verify("WrongPassword123!", hash);
        long timeIncorrect = System.nanoTime() - startIncorrect;

        // Assert
        assertThat(resultCorrect).isTrue();
        assertThat(resultIncorrect).isFalse();
        // BCrypt uses constant-time comparison, so times should be similar
        // (Allow for some variance due to system load)
        long timeDifference = Math.abs(timeCorrect - timeIncorrect);
        long maxAllowedDifference = Math.max(timeCorrect, timeIncorrect) / 2;
        assertThat(timeDifference).isLessThan(maxAllowedDifference);
    }

    @Test
    @DisplayName("should_HandleInvalidHashGracefully_When_VerifyingWithMalformedHash")
    void should_HandleInvalidHashGracefully_When_VerifyingWithMalformedHash() {
        // Arrange
        PasswordHash malformedHash = new PasswordHash("$2a$" + "x".repeat(56));

        // Act
        boolean result = hasher.verify("SomePassword123!", malformedHash);

        // Assert
        assertThat(result).isFalse();
    }

    @Test
    @DisplayName("should_AcceptMultipleValidFormats_When_VerifyingDifferentBcryptVersions")
    void should_AcceptMultipleValidFormats_When_VerifyingDifferentBcryptVersions() {
        // Arrange
        String password = "ValidPassword123!";
        PasswordHash hash1 = hasher.hash(password);
        PasswordHash hash2 = hasher.hash(password);

        // Act & Assert
        assertThat(hasher.verify(password, hash1)).isTrue();
        assertThat(hasher.verify(password, hash2)).isTrue();
    }

    @Test
    @DisplayName("should_HashAndVerifyWithLongPassword_When_PasswordExceedsMinimum")
    void should_HashAndVerifyWithLongPassword_When_PasswordExceedsMinimum() {
        // Arrange
        String longPassword = "VeryLongPasswordWith123!ExtraCharactersForSecurity";

        // Act
        PasswordHash hash = hasher.hash(longPassword);
        boolean verified = hasher.verify(longPassword, hash);

        // Assert
        assertThat(verified).isTrue();
    }

    @Test
    @DisplayName("should_DifferentiateBetweenSimilarPasswords_When_VerifyingTypos")
    void should_DifferentiateBetweenSimilarPasswords_When_VerifyingTypos() {
        // Arrange
        PasswordHash hash = hasher.hash("CorrectPass123!");

        // Act
        boolean result1 = hasher.verify("CorrectPass123!", hash);  // Correct
        boolean result2 = hasher.verify("CorrectPass124!", hash);  // One character different

        // Assert
        assertThat(result1).isTrue();
        assertThat(result2).isFalse();
    }
}
