package auth

import (
	"testing"
	"time"

	"github.com/golang-jwt/jwt/v5"
	"github.com/nexuscrm/shared/pkg/constants"
)

func newTestSession() UserSession {
	return UserSession{
		ID:        "user-001",
		Name:      "Test User",
		Email:     "test@example.com",
		ProfileId: constants.ProfileStandardUser,
		RoleId:    nil,
	}
}

func ptrString(s string) *string { return &s }

// --- GenerateToken / ValidateToken round-trip ---

func TestGenerateToken_ValidRoundTrip(t *testing.T) {
	session := newTestSession()
	tokenStr, err := GenerateToken(session)
	if err != nil {
		t.Fatalf("GenerateToken() error = %v", err)
	}
	if tokenStr == "" {
		t.Fatal("GenerateToken() returned empty token")
	}

	claims, err := ValidateToken(tokenStr)
	if err != nil {
		t.Fatalf("ValidateToken() error = %v", err)
	}

	if claims.User.ID != session.ID {
		t.Errorf("claims.User.ID = %q, want %q", claims.User.ID, session.ID)
	}
	if claims.User.Name != session.Name {
		t.Errorf("claims.User.Name = %q, want %q", claims.User.Name, session.Name)
	}
	if claims.User.Email != session.Email {
		t.Errorf("claims.User.Email = %q, want %q", claims.User.Email, session.Email)
	}
	if claims.User.ProfileId != session.ProfileId {
		t.Errorf("claims.User.ProfileId = %q, want %q", claims.User.ProfileId, session.ProfileId)
	}
	if claims.Subject != session.ID {
		t.Errorf("claims.Subject = %q, want %q", claims.Subject, session.ID)
	}
}

func TestGenerateToken_HasExpiry(t *testing.T) {
	session := newTestSession()
	tokenStr, err := GenerateToken(session)
	if err != nil {
		t.Fatalf("GenerateToken() error = %v", err)
	}

	claims, err := ValidateToken(tokenStr)
	if err != nil {
		t.Fatalf("ValidateToken() error = %v", err)
	}

	if claims.ExpiresAt == nil {
		t.Fatal("claims.ExpiresAt is nil")
	}

	// Token should expire ~24h from now
	expectedExpiry := time.Now().Add(24 * time.Hour)
	diff := claims.ExpiresAt.Time.Sub(expectedExpiry)
	if diff < -5*time.Second || diff > 5*time.Second {
		t.Errorf("expiry is %v from expected 24h, want within 5s", diff)
	}
}

func TestGenerateToken_HasJTI(t *testing.T) {
	session := newTestSession()
	tokenStr, err := GenerateToken(session)
	if err != nil {
		t.Fatalf("GenerateToken() error = %v", err)
	}

	claims, err := ValidateToken(tokenStr)
	if err != nil {
		t.Fatalf("ValidateToken() error = %v", err)
	}

	if claims.ID == "" {
		t.Error("claims.ID (JTI) is empty")
	}
}

func TestGenerateToken_UniqueJTI(t *testing.T) {
	session := newTestSession()
	token1, _ := GenerateToken(session)
	token2, _ := GenerateToken(session)

	claims1, _ := ValidateToken(token1)
	claims2, _ := ValidateToken(token2)

	if claims1.ID == claims2.ID {
		t.Error("two tokens should have unique JTI values")
	}
}

func TestGenerateToken_WithRoleId(t *testing.T) {
	role := "role-admin"
	session := UserSession{
		ID:        "user-002",
		Name:      "Admin User",
		Email:     "admin@example.com",
		ProfileId: constants.ProfileSystemAdmin,
		RoleId:    &role,
	}

	tokenStr, err := GenerateToken(session)
	if err != nil {
		t.Fatalf("GenerateToken() error = %v", err)
	}

	claims, err := ValidateToken(tokenStr)
	if err != nil {
		t.Fatalf("ValidateToken() error = %v", err)
	}

	if claims.User.RoleId == nil {
		t.Fatal("claims.User.RoleId is nil, expected non-nil")
	}
	if *claims.User.RoleId != role {
		t.Errorf("claims.User.RoleId = %q, want %q", *claims.User.RoleId, role)
	}
}

// --- ValidateToken error cases ---

func TestValidateToken_InvalidToken(t *testing.T) {
	tests := []struct {
		name  string
		token string
	}{
		{name: "empty string", token: ""},
		{name: "garbage", token: "not.a.jwt"},
		{name: "truncated", token: "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjp7fX0"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			claims, err := ValidateToken(tt.token)
			if err == nil {
				t.Error("ValidateToken() expected error, got nil")
			}
			if claims != nil {
				t.Error("ValidateToken() expected nil claims on error")
			}
		})
	}
}

func TestValidateToken_WrongSigningMethod(t *testing.T) {
	// Create a token signed with a different method (none)
	claims := &Claims{
		User: newTestSession(),
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
		},
	}
	token := jwt.NewWithClaims(jwt.SigningMethodNone, claims)
	tokenStr, err := token.SignedString(jwt.UnsafeAllowNoneSignatureType)
	if err != nil {
		t.Fatalf("failed to create none-signed token: %v", err)
	}

	_, err = ValidateToken(tokenStr)
	if err == nil {
		t.Error("ValidateToken() should reject tokens with 'none' signing method")
	}
}

// --- DecodeToken ---

func TestDecodeToken_ValidToken(t *testing.T) {
	session := newTestSession()
	tokenStr, err := GenerateToken(session)
	if err != nil {
		t.Fatalf("GenerateToken() error = %v", err)
	}

	claims, err := DecodeToken(tokenStr)
	if err != nil {
		t.Fatalf("DecodeToken() error = %v", err)
	}

	if claims.User.ID != session.ID {
		t.Errorf("DecodeToken().User.ID = %q, want %q", claims.User.ID, session.ID)
	}
	if claims.ID == "" {
		t.Error("DecodeToken() should extract JTI")
	}
}

func TestDecodeToken_InvalidToken(t *testing.T) {
	_, err := DecodeToken("not.a.valid.jwt")
	if err == nil {
		t.Error("DecodeToken() expected error for invalid token")
	}
}

// --- UserSession methods ---

func TestUserSession_IsSuperUser(t *testing.T) {
	tests := []struct {
		name      string
		profileId string
		want      bool
	}{
		{name: "system admin is super user", profileId: constants.ProfileSystemAdmin, want: true},
		{name: "standard user is not super user", profileId: constants.ProfileStandardUser, want: false},
		{name: "empty profile is not super user", profileId: "", want: false},
		{name: "arbitrary profile is not super user", profileId: "custom_profile", want: false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			u := UserSession{ProfileId: tt.profileId}
			if got := u.IsSuperUser(); got != tt.want {
				t.Errorf("IsSuperUser() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestUserSession_DisplayName(t *testing.T) {
	u := UserSession{Name: "Jane Doe"}
	if got := u.DisplayName(); got != "Jane Doe" {
		t.Errorf("DisplayName() = %q, want %q", got, "Jane Doe")
	}
}

func TestUserSession_ToMap(t *testing.T) {
	role := "role-123"
	u := UserSession{
		ID:        "user-001",
		Name:      "Test User",
		Email:     "test@example.com",
		ProfileId: "standard_user",
		RoleId:    &role,
	}

	m := u.ToMap()

	if m[constants.FieldID] != "user-001" {
		t.Errorf("ToMap()[FieldID] = %v, want %q", m[constants.FieldID], "user-001")
	}
	if m[constants.FieldName] != "Test User" {
		t.Errorf("ToMap()[FieldName] = %v, want %q", m[constants.FieldName], "Test User")
	}
	if m["email"] != "test@example.com" {
		t.Errorf("ToMap()[email] = %v, want %q", m["email"], "test@example.com")
	}
	if m["profile_id"] != "standard_user" {
		t.Errorf("ToMap()[profile_id] = %v, want %q", m["profile_id"], "standard_user")
	}
	if m["role_id"] != "role-123" {
		t.Errorf("ToMap()[role_id] = %v, want %q", m["role_id"], "role-123")
	}
}

func TestUserSession_ToMap_NilRoleId(t *testing.T) {
	u := UserSession{
		ID:        "user-002",
		Name:      "No Role",
		Email:     "norole@example.com",
		ProfileId: "standard_user",
		RoleId:    nil,
	}

	m := u.ToMap()
	if m["role_id"] != "" {
		t.Errorf("ToMap()[role_id] = %v, want empty string for nil RoleId", m["role_id"])
	}
}

// --- ConstructDisplayName ---

func TestConstructDisplayName(t *testing.T) {
	tests := []struct {
		name      string
		username  string
		firstName string
		lastName  string
		want      string
	}{
		{name: "first and last", username: "jdoe", firstName: "Jane", lastName: "Doe", want: "Jane Doe"},
		{name: "first only", username: "jdoe", firstName: "Jane", lastName: "", want: "Jane"},
		{name: "last only", username: "jdoe", firstName: "", lastName: "Doe", want: "Doe"},
		{name: "fallback to username", username: "jdoe", firstName: "", lastName: "", want: "jdoe"},
		{name: "empty username fallback", username: "", firstName: "", lastName: "", want: ""},
	}

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