package services

import (
	"testing"

	"github.com/stretchr/testify/assert"
	"go.uber.org/zap"
	"golang.org/x/crypto/bcrypt"
)

// createTestPasswordService creates a minimal PasswordService for testing Hash and Compare
func createTestPasswordService() *PasswordService {
	logger, _ := zap.NewDevelopment()
	return &PasswordService{
		logger: logger,
	}
}

func TestPasswordService_Hash(t *testing.T) {
	service := createTestPasswordService()

	tests := []struct {
		name     string
		password string
		wantErr  bool
	}{
		{
			name:     "hash simple password",
			password: "testpassword123",
			wantErr:  false,
		},
		{
			name:     "hash complex password",
			password: "SecurePass123!@#",
			wantErr:  false,
		},
		{
			name:     "hash password with special chars",
			password: "Test@123#Pass$",
			wantErr:  false,
		},
		{
			name:     "hash empty password",
			password: "",
			wantErr:  false,
		},
		{
			name:     "hash long password",
			password: "VeryLongPassword123456789!@#$%^&*()",
			wantErr:  false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			hash, err := service.Hash(tt.password)
			if tt.wantErr {
				assert.Error(t, err)
				assert.Empty(t, hash)
			} else {
				assert.NoError(t, err)
				assert.NotEmpty(t, hash)
				// Verify it's a valid bcrypt hash (starts with $2a$ or $2b$)
				assert.Contains(t, []string{"$2a$", "$2b$"}, hash[:4])
			}
		})
	}
}

func TestPasswordService_Hash_DifferentResults(t *testing.T) {
	service := createTestPasswordService()
	password := "testpassword123"

	// Hash the same password twice - should produce different hashes (due to salt)
	hash1, err1 := service.Hash(password)
	hash2, err2 := service.Hash(password)

	assert.NoError(t, err1)
	assert.NoError(t, err2)
	assert.NotEqual(t, hash1, hash2, "Two hashes of the same password should be different (due to salt)")
}

func TestPasswordService_Hash_ValidBcryptFormat(t *testing.T) {
	service := createTestPasswordService()
	password := "testpassword123"

	hash, err := service.Hash(password)
	assert.NoError(t, err)

	// Verify the hash is valid by trying to parse it
	cost, err := bcrypt.Cost([]byte(hash))
	assert.NoError(t, err)
	assert.Equal(t, bcryptCost, cost, "Hash should have bcrypt cost 12")
}

func TestPasswordService_Compare_ValidPassword(t *testing.T) {
	service := createTestPasswordService()

	tests := []struct {
		name     string
		password string
	}{
		{
			name:     "compare valid password",
			password: "testpassword123",
		},
		{
			name:     "compare valid password with special chars",
			password: "SecurePass123!@#",
		},
		{
			name:     "compare empty password",
			password: "",
		},
		{
			name:     "compare long password",
			password: "VeryLongPassword123456789!@#$%^&*()",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			hash, err := service.Hash(tt.password)
			assert.NoError(t, err)

			result := service.Compare(hash, tt.password)
			assert.True(t, result, "Password should match the hash")
		})
	}
}

func TestPasswordService_Compare_InvalidPassword(t *testing.T) {
	service := createTestPasswordService()
	password := "testpassword123"
	wrongPassword := "wrongpassword123"

	hash, err := service.Hash(password)
	assert.NoError(t, err)

	result := service.Compare(hash, wrongPassword)
	assert.False(t, result, "Wrong password should not match the hash")
}

func TestPasswordService_Compare_EmptyHash(t *testing.T) {
	service := createTestPasswordService()

	result := service.Compare("", "testpassword123")
	assert.False(t, result, "Empty hash should not match any password")
}

func TestPasswordService_Compare_EmptyPassword(t *testing.T) {
	service := createTestPasswordService()

	hash, err := service.Hash("testpassword123")
	assert.NoError(t, err)

	result := service.Compare(hash, "")
	assert.False(t, result, "Empty password should not match the hash")
}

func TestPasswordService_Compare_InvalidHash(t *testing.T) {
	service := createTestPasswordService()

	tests := []struct {
		name           string
		hash           string
		password       string
		expectedResult bool
	}{
		{
			name:           "invalid hash format",
			hash:           "invalidhash",
			password:       "testpassword123",
			expectedResult: false,
		},
		{
			name:           "malformed bcrypt hash",
			hash:           "$2a$12$invalid",
			password:       "testpassword123",
			expectedResult: false,
		},
		{
			name:           "hash with wrong cost",
			hash:           "$2a$10$invalidhashformat",
			password:       "testpassword123",
			expectedResult: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := service.Compare(tt.hash, tt.password)
			assert.Equal(t, tt.expectedResult, result)
		})
	}
}

func TestPasswordService_HashAndCompare_Integration(t *testing.T) {
	service := createTestPasswordService()

	testCases := []struct {
		name     string
		password string
	}{
		{
			name:     "simple password",
			password: "password123",
		},
		{
			name:     "password with uppercase",
			password: "Password123",
		},
		{
			name:     "password with special chars",
			password: "Pass@123!",
		},
		{
			name:     "password with spaces",
			password: "Pass 123!",
		},
		{
			name:     "password with unicode",
			password: "Passé123!",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			// Hash the password
			hash, err := service.Hash(tc.password)
			assert.NoError(t, err)
			assert.NotEmpty(t, hash)

			// Compare with correct password - should match
			result := service.Compare(hash, tc.password)
			assert.True(t, result, "Password should match its hash")

			// Compare with wrong password - should not match
			wrongResult := service.Compare(hash, "wrongpassword")
			assert.False(t, wrongResult, "Wrong password should not match")
		})
	}
}

func TestPasswordService_Hash_ConsistentCost(t *testing.T) {
	service := createTestPasswordService()
	password := "testpassword123"

	hash, err := service.Hash(password)
	assert.NoError(t, err)

	// Verify the cost is 12
	cost, err := bcrypt.Cost([]byte(hash))
	assert.NoError(t, err)
	assert.Equal(t, bcryptCost, cost)
}

func TestPasswordService_Hash_ErrorHandling(t *testing.T) {
	service := createTestPasswordService()

	// Test with extremely long password (bcrypt has a limit of 72 bytes)
	// This should still work as bcrypt truncates, but we test the error path
	veryLongPassword := make([]byte, 1000)
	for i := range veryLongPassword {
		veryLongPassword[i] = 'a'
	}

	// This should still succeed as bcrypt handles long passwords
	hash, err := service.Hash(string(veryLongPassword))
	assert.NoError(t, err)
	assert.NotEmpty(t, hash)

	// Verify we can still compare it (bcrypt truncates to 72 bytes)
	result := service.Compare(hash, string(veryLongPassword))
	assert.True(t, result, "Long password should still work (truncated by bcrypt)")
}

func TestPasswordService_Compare_CaseSensitive(t *testing.T) {
	service := createTestPasswordService()
	password := "TestPassword123"
	upperPassword := "TESTPASSWORD123"
	lowerPassword := "testpassword123"

	hash, err := service.Hash(password)
	assert.NoError(t, err)

	// Exact match should work
	assert.True(t, service.Compare(hash, password))

	// Case variations should not match
	assert.False(t, service.Compare(hash, upperPassword))
	assert.False(t, service.Compare(hash, lowerPassword))
}
