package auth

import (
	"testing"

	"golang.org/x/crypto/bcrypt"
)

func TestHashPassword(t *testing.T) {
	tests := []struct {
		name     string
		password string
		wantErr  bool
	}{
		{name: "valid password", password: "SecureP@ss1", wantErr: false},
		{name: "empty password", password: "", wantErr: false},
		{name: "long password at bcrypt 72-byte limit", password: "Aa1!aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", wantErr: false},
		{name: "password exceeding bcrypt 72-byte limit", password: "Aa1!aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", wantErr: true},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			hash, err := HashPassword(tt.password)
			if (err != nil) != tt.wantErr {
				t.Errorf("HashPassword() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if !tt.wantErr && hash == "" {
				t.Error("HashPassword() returned empty hash")
			}
			if !tt.wantErr && hash == tt.password {
				t.Error("HashPassword() returned unhashed password")
			}
		})
	}
}

func TestHashPassword_UniqueHashes(t *testing.T) {
	password := "SameP@ss1"
	hash1, err1 := HashPassword(password)
	hash2, err2 := HashPassword(password)

	if err1 != nil || err2 != nil {
		t.Fatalf("HashPassword() unexpected errors: %v, %v", err1, err2)
	}
	if hash1 == hash2 {
		t.Error("HashPassword() should produce different hashes for the same password (bcrypt uses random salt)")
	}
}

func TestVerifyPassword(t *testing.T) {
	correctPassword := "SecureP@ss1"
	hash, err := HashPassword(correctPassword)
	if err != nil {
		t.Fatalf("HashPassword() failed: %v", err)
	}

	tests := []struct {
		name     string
		password string
		hash     string
		want     bool
	}{
		{name: "correct password", password: correctPassword, hash: hash, want: true},
		{name: "wrong password", password: "WrongP@ss1", hash: hash, want: false},
		{name: "empty password against hash", password: "", hash: hash, want: false},
		{name: "correct password against invalid hash", password: correctPassword, hash: "not-a-bcrypt-hash", want: false},
		{name: "empty password against empty hash", password: "", hash: "", want: false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := VerifyPassword(tt.password, tt.hash)
			if got != tt.want {
				t.Errorf("VerifyPassword() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestValidatePasswordStrength(t *testing.T) {
	tests := []struct {
		name    string
		password string
		wantErr bool
		errMsg  string
	}{
		{
			name:     "valid strong password",
			password: "SecureP@ss1",
			wantErr:  false,
		},
		{
			name:     "too short",
			password: "Aa1!xyz",
			wantErr:  true,
			errMsg:   "must be at least 8 characters long",
		},
		{
			name:     "exactly 8 characters valid",
			password: "Aa1!xxxx",
			wantErr:  false,
		},
		{
			name:     "too long (129 characters)",
			password: "Aa1!" + string(make([]byte, 125)),
			wantErr:  true,
		},
		{
			name:     "missing uppercase",
			password: "securep@ss1",
			wantErr:  true,
			errMsg:   "must contain at least one uppercase letter",
		},
		{
			name:     "missing lowercase",
			password: "SECUREP@SS1",
			wantErr:  true,
			errMsg:   "must contain at least one lowercase letter",
		},
		{
			name:     "missing digit",
			password: "SecureP@ss",
			wantErr:  true,
			errMsg:   "must contain at least one number",
		},
		{
			name:     "missing special character",
			password: "SecurePass1",
			wantErr:  true,
			errMsg:   "must contain at least one special character",
		},
		{
			name:     "all special characters accepted",
			password: "Aa1@Bb2$Cc3!Dd4%Ee5*Ff6?Gg7&",
			wantErr:  false,
		},
		{
			name:     "exactly 128 characters valid",
			password: "Aa1!" + stringOfLength('x', 124),
			wantErr:  false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := ValidatePasswordStrength(tt.password)
			if (err != nil) != tt.wantErr {
				t.Errorf("ValidatePasswordStrength() error = %v, wantErr %v", err, tt.wantErr)
			}
			if tt.wantErr && tt.errMsg != "" && err != nil {
				if got := err.Error(); !contains(got, tt.errMsg) {
					t.Errorf("ValidatePasswordStrength() error message = %q, want it to contain %q", got, tt.errMsg)
				}
			}
		})
	}
}

func TestValidateBcryptCost(t *testing.T) {
	// Generate a hash with default cost (10)
	defaultCostHash, err := HashPassword("SecureP@ss1")
	if err != nil {
		t.Fatalf("HashPassword() failed: %v", err)
	}

	// Generate a hash with low cost (4) — this is the attack vector
	lowCostBytes, err := bcrypt.GenerateFromPassword([]byte("SecureP@ss1"), 4)
	if err != nil {
		t.Fatalf("bcrypt.GenerateFromPassword() with cost 4 failed: %v", err)
	}
	lowCostHash := string(lowCostBytes)

	// Generate a hash with higher cost (12)
	highCostBytes, err := bcrypt.GenerateFromPassword([]byte("SecureP@ss1"), 12)
	if err != nil {
		t.Fatalf("bcrypt.GenerateFromPassword() with cost 12 failed: %v", err)
	}
	highCostHash := string(highCostBytes)

	tests := []struct {
		name    string
		hash    string
		wantErr bool
	}{
		{name: "default cost (10) accepted", hash: defaultCostHash, wantErr: false},
		{name: "low cost (4) rejected", hash: lowCostHash, wantErr: true},
		{name: "high cost (12) accepted", hash: highCostHash, wantErr: false},
		{name: "malformed hash rejected", hash: "not-a-bcrypt-hash", wantErr: true},
		{name: "empty string rejected", hash: "", wantErr: true},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := ValidateBcryptCost(tt.hash)
			if (err != nil) != tt.wantErr {
				t.Errorf("ValidateBcryptCost() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}

func TestIsValidEmail(t *testing.T) {
	tests := []struct {
		name  string
		email string
		want  bool
	}{
		{name: "valid simple email", email: "user@example.com", want: true},
		{name: "valid email with dots", email: "first.last@example.com", want: true},
		{name: "valid email with plus", email: "user+tag@example.com", want: true},
		{name: "valid email with subdomain", email: "user@mail.example.com", want: true},
		{name: "valid email with percent", email: "user%name@example.com", want: true},
		{name: "missing @", email: "userexample.com", want: false},
		{name: "missing domain", email: "user@", want: false},
		{name: "missing local part", email: "@example.com", want: false},
		{name: "missing TLD", email: "user@example", want: false},
		{name: "single char TLD", email: "user@example.c", want: false},
		{name: "empty string", email: "", want: false},
		{name: "too short (2 chars)", email: "a@", want: false},
		{name: "spaces only", email: "   ", want: false},
		{name: "with leading space (trimmed)", email: " user@example.com", want: true},
		{name: "with trailing space (trimmed)", email: "user@example.com ", want: true},
		{name: "double @", email: "user@@example.com", want: false},
		{name: "valid two-char TLD", email: "user@example.co", want: true},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := IsValidEmail(tt.email)
			if got != tt.want {
				t.Errorf("IsValidEmail(%q) = %v, want %v", tt.email, got, tt.want)
			}
		})
	}
}

// helpers

func stringOfLength(ch byte, n int) string {
	b := make([]byte, n)
	for i := range b {
		b[i] = ch
	}
	return string(b)
}

func contains(s, substr string) bool {
	return len(s) >= len(substr) && searchString(s, substr)
}

func searchString(s, substr string) bool {
	for i := 0; i <= len(s)-len(substr); i++ {
		if s[i:i+len(substr)] == substr {
			return true
		}
	}
	return false
}
