package de.effigenix.domain.usermanagement;

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 PasswordHash Value Object.
 * Tests BCrypt validation, immutability, factory methods, and equality.
 */
@DisplayName("PasswordHash Value Object")
class PasswordHashTest {

    // Valid BCrypt hashes with different versions
    private static final String VALID_BCRYPT_2A = "$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW";
    private static final String VALID_BCRYPT_2B = "$2b$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW";
    private static final String VALID_BCRYPT_2Y = "$2y$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW";

    @Test
    @DisplayName("should_CreatePasswordHash_When_ValidBcryptHashProvided")
    void should_CreatePasswordHash_When_ValidBcryptHashProvided() {
        // Act
        PasswordHash hash = new PasswordHash(VALID_BCRYPT_2A);

        // Assert
        assertThat(hash.value()).isEqualTo(VALID_BCRYPT_2A);
    }

    @Test
    @DisplayName("should_AcceptMultipleBcryptVersions_When_ValidHashesProvided")
    void should_AcceptMultipleBcryptVersions_When_ValidHashesProvided() {
        // Act & Assert
        assertThatCode(() -> {
            new PasswordHash(VALID_BCRYPT_2A);
            new PasswordHash(VALID_BCRYPT_2B);
            new PasswordHash(VALID_BCRYPT_2Y);
        }).doesNotThrowAnyException();
    }

    @Test
    @DisplayName("should_ThrowException_When_NullHashProvided")
    void should_ThrowException_When_NullHashProvided() {
        // Act & Assert
        assertThatThrownBy(() -> new PasswordHash(null))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("PasswordHash cannot be null or empty");
    }

    @Test
    @DisplayName("should_ThrowException_When_EmptyHashProvided")
    void should_ThrowException_When_EmptyHashProvided() {
        // Act & Assert
        assertThatThrownBy(() -> new PasswordHash(""))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("PasswordHash cannot be null or empty");
    }

    @Test
    @DisplayName("should_ThrowException_When_BlankHashProvided")
    void should_ThrowException_When_BlankHashProvided() {
        // Act & Assert
        assertThatThrownBy(() -> new PasswordHash("   "))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("PasswordHash cannot be null or empty");
    }

    @ParameterizedTest
    @ValueSource(strings = {
            "notabcrypthash",                      // Plain text
            "$1$salt$hash",                         // MD5 crypt format
            "$2c$12$invalid",                       // Invalid version ($2c$)
            "$2a$12$short",                         // Too short
            "$2a$12$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",  // Wrong length
            "password123",                          // Plain password
            "$2a$12$",                             // Incomplete hash
    })
    @DisplayName("should_ThrowException_When_InvalidBcryptFormatProvided")
    void should_ThrowException_When_InvalidBcryptFormatProvided(String invalidHash) {
        // Act & Assert
        assertThatThrownBy(() -> new PasswordHash(invalidHash))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("Invalid BCrypt hash format");
    }

    @Test
    @DisplayName("should_CreateHashFromStaticFactory_When_OfMethodCalled")
    void should_CreateHashFromStaticFactory_When_OfMethodCalled() {
        // Act
        PasswordHash hash = PasswordHash.of(VALID_BCRYPT_2B);

        // Assert
        assertThat(hash.value()).isEqualTo(VALID_BCRYPT_2B);
    }

    @Test
    @DisplayName("should_BeImmutable_When_RecordCreated")
    void should_BeImmutable_When_RecordCreated() {
        // Arrange
        PasswordHash hash = new PasswordHash(VALID_BCRYPT_2A);

        // Act & Assert - Records are immutable by design
        assertThat(hash).isNotNull();
        // Can't modify value as it's a record
    }

    @Test
    @DisplayName("should_BeEqual_When_SameHashProvided")
    void should_BeEqual_When_SameHashProvided() {
        // Arrange
        PasswordHash hash1 = new PasswordHash(VALID_BCRYPT_2A);
        PasswordHash hash2 = new PasswordHash(VALID_BCRYPT_2A);

        // Act & Assert
        assertThat(hash1).isEqualTo(hash2);
        assertThat(hash1.hashCode()).isEqualTo(hash2.hashCode());
    }

    @Test
    @DisplayName("should_NotBeEqual_When_DifferentHashesProvided")
    void should_NotBeEqual_When_DifferentHashesProvided() {
        // Arrange
        PasswordHash hash1 = new PasswordHash(VALID_BCRYPT_2A);
        PasswordHash hash2 = new PasswordHash(VALID_BCRYPT_2B);

        // Act & Assert
        assertThat(hash1).isNotEqualTo(hash2);
    }

    @Test
    @DisplayName("should_ValidateBcryptLength_When_TooShort")
    void should_ValidateBcryptLength_When_TooShort() {
        // Arrange
        String tooShort = "$2a$12$" + "x".repeat(40); // Less than 56 chars after prefix

        // Act & Assert
        assertThatThrownBy(() -> new PasswordHash(tooShort))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("Invalid BCrypt hash format");
    }

    @Test
    @DisplayName("should_ValidateBcryptLength_When_TooLong")
    void should_ValidateBcryptLength_When_TooLong() {
        // Arrange
        String tooLong = "$2a$12$" + "x".repeat(60); // More than 56 chars after prefix

        // Act & Assert
        assertThatThrownBy(() -> new PasswordHash(tooLong))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("Invalid BCrypt hash format");
    }
}
