package auth

import (
	"errors"
	"testing"
	"time"

	"system-altrak/internal/domain"
	"system-altrak/pkg/utils"

	"github.com/glebarez/sqlite"
	"gorm.io/gorm"
)

type authRepoMock struct {
	revokedUserID        uint
	usersByID            map[uint]*domain.User
	usersByUsername      map[string]*domain.User
	refreshTokens        map[string]*domain.RefreshToken
	getUserByUsernameErr error
}

func resetAuthTokenState(t *testing.T) {
	t.Helper()
	utils.ResetTokenStateForTests()
	t.Cleanup(utils.ResetTokenStateForTests)
}

func (m *authRepoMock) GetUserByUsername(username string) (*domain.User, error) {
	if m.getUserByUsernameErr != nil {
		return nil, m.getUserByUsernameErr
	}
	if m.usersByUsername != nil {
		if user, ok := m.usersByUsername[username]; ok && user != nil {
			copyUser := *user
			return &copyUser, nil
		}
	}
	return nil, gorm.ErrRecordNotFound
}

func (m *authRepoMock) GetUserByID(id uint) (*domain.User, error) {
	if user, ok := m.usersByID[id]; ok {
		return user, nil
	}
	return nil, errors.New("user not found")
}

func (m *authRepoMock) SaveRefreshToken(token *domain.RefreshToken) error {
	if m.refreshTokens == nil {
		m.refreshTokens = make(map[string]*domain.RefreshToken)
	}
	m.refreshTokens[token.TokenHash] = token
	return nil
}

func (m *authRepoMock) GetRefreshToken(hash string) (*domain.RefreshToken, error) {
	if token, ok := m.refreshTokens[hash]; ok {
		return token, nil
	}
	return nil, errors.New("token not found")
}

func (m *authRepoMock) RevokeRefreshToken(userID uint) error {
	m.revokedUserID = userID
	return nil
}

func (m *authRepoMock) WithTransaction(fn func(repo AuthRepository) error) error {
	return fn(m)
}

func newAuthTestDB(t *testing.T) *gorm.DB {
	t.Helper()

	db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
	if err != nil {
		t.Fatalf("failed to open sqlite database: %v", err)
	}
	if err := db.AutoMigrate(&domain.User{}, &domain.RefreshToken{}); err != nil {
		t.Fatalf("failed to migrate auth schema: %v", err)
	}
	return db
}

func TestLogoutRevokesAccessAndRefresh(t *testing.T) {
	resetAuthTokenState(t)

	secret := "test-secret-with-minimum-32-characters-value"
	accessToken, _, err := utils.GenerateToken(123, "admin", "admin", 1, secret)
	if err != nil {
		t.Fatalf("failed to generate access token: %v", err)
	}

	repo := &authRepoMock{}
	svc := NewService(repo, secret)

	err = svc.Logout(accessToken, "")
	if err != nil {
		t.Fatalf("logout failed: %v", err)
	}

	if repo.revokedUserID != 123 {
		t.Fatalf("expected revoked user id 123, got %d", repo.revokedUserID)
	}

	_, err = utils.ValidateToken(accessToken, secret)
	if err == nil {
		t.Fatal("expected access token to be revoked")
	}
}

func TestLogoutRejectsWithoutSessionInfo(t *testing.T) {
	resetAuthTokenState(t)

	repo := &authRepoMock{}
	svc := NewService(repo, "test-secret-with-minimum-32-characters-value")

	err := svc.Logout("", "")
	if err == nil {
		t.Fatal("expected error when no session token is provided")
	}
}

func TestRefreshFlowPersistsNewToken(t *testing.T) {
	resetAuthTokenState(t)

	secret := "test-secret-with-minimum-32-characters-value"
	_, refreshToken, err := utils.GenerateToken(999, "admin", "admin", 1, secret)
	if err != nil {
		t.Fatalf("failed to generate refresh token: %v", err)
	}

	repo := &authRepoMock{
		usersByID: map[uint]*domain.User{
			999: {
				BaseModel: domain.BaseModel{ID: 999},
				Role:      "admin",
				BranchID:  1,
			},
		},
		refreshTokens: map[string]*domain.RefreshToken{
			utils.HashToken(refreshToken): {
				UserID:     999,
				TokenHash:  utils.HashToken(refreshToken),
				ExpiresAt:  time.Now().Add(1 * time.Hour),
				Persistent: true,
				Revoked:    false,
			},
		},
	}

	svc := NewService(repo, secret)
	access, newRefresh, persistent, err := svc.Refresh(refreshToken)
	if err != nil {
		t.Fatalf("refresh failed: %v", err)
	}

	if access == "" || newRefresh == "" {
		t.Fatal("expected refreshed access and refresh token")
	}

	if repo.revokedUserID != 999 {
		t.Fatalf("expected revoke user id 999, got %d", repo.revokedUserID)
	}
	if !persistent {
		t.Fatal("expected persistent refresh flag to be preserved")
	}
	if stored := repo.refreshTokens[utils.HashToken(newRefresh)]; stored == nil || !stored.Persistent {
		t.Fatalf("expected persisted refresh token to keep persistence flag, got %+v", stored)
	}
}

func TestLoginPersistsRememberMeFlag(t *testing.T) {
	resetAuthTokenState(t)

	secret := "test-secret-with-minimum-32-characters-value"
	passwordHash, err := utils.HashPassword("correct-horse-battery-staple")
	if err != nil {
		t.Fatalf("failed to hash password: %v", err)
	}

	repo := &authRepoMock{
		usersByUsername: map[string]*domain.User{
			"alice": {
				BaseModel:    domain.BaseModel{ID: 101},
				Username:     "alice",
				PasswordHash: passwordHash,
				Role:         "admin",
				BranchID:     3,
				IsActive:     true,
			},
		},
	}

	svc := NewService(repo, secret)
	user, access, refresh, err := svc.Login("alice", "correct-horse-battery-staple", true)
	if err != nil {
		t.Fatalf("login failed: %v", err)
	}

	if user == nil || user.PasswordHash != "" {
		t.Fatalf("expected sanitized user payload, got %+v", user)
	}
	if access == "" || refresh == "" {
		t.Fatal("expected access and refresh token values")
	}

	stored := repo.refreshTokens[utils.HashToken(refresh)]
	if stored == nil {
		t.Fatal("expected refresh token to be stored")
	}
	if !stored.Persistent {
		t.Fatal("expected remember-me flag to persist to the refresh token store")
	}
}

func TestLoginTrimsWhitespaceUsername(t *testing.T) {
	resetAuthTokenState(t)

	secret := "test-secret-with-minimum-32-characters-value"
	passwordHash, err := utils.HashPassword("secret")
	if err != nil {
		t.Fatalf("failed to hash password: %v", err)
	}

	repo := &authRepoMock{
		usersByUsername: map[string]*domain.User{
			"alice": {
				BaseModel:    domain.BaseModel{ID: 202},
				Username:     "alice",
				PasswordHash: passwordHash,
				Role:         "admin",
				BranchID:     3,
				IsActive:     true,
			},
		},
	}

	svc := NewService(repo, secret)
	user, _, _, err := svc.Login("  alice  ", "secret", false)
	if err != nil {
		t.Fatalf("login failed: %v", err)
	}
	if user == nil || user.Username != "alice" {
		t.Fatalf("expected trimmed username login, got %+v", user)
	}
}

func TestLoginReturnsInvalidCredentialsSentinel(t *testing.T) {
	resetAuthTokenState(t)

	repo := &authRepoMock{
		usersByUsername: map[string]*domain.User{
			"alice": {
				BaseModel:    domain.BaseModel{ID: 203},
				Username:     "alice",
				PasswordHash: "invalid-hash",
				Role:         "admin",
				BranchID:     3,
				IsActive:     true,
			},
		},
	}

	svc := NewService(repo, "test-secret-with-minimum-32-characters-value")
	_, _, _, err := svc.Login("alice", "wrong-secret", false)
	if !errors.Is(err, ErrInvalidCredentials) {
		t.Fatalf("expected invalid credentials error, got %v", err)
	}
}

func TestLoginRejectsInactiveUser(t *testing.T) {
	resetAuthTokenState(t)

	repo := &authRepoMock{
		usersByUsername: map[string]*domain.User{
			"alice": {
				BaseModel:    domain.BaseModel{ID: 204},
				Username:     "alice",
				PasswordHash: "secret-hash",
				Role:         "admin",
				BranchID:     3,
				IsActive:     false,
			},
		},
	}

	svc := NewService(repo, "test-secret-with-minimum-32-characters-value")
	_, _, _, err := svc.Login("alice", "anything", false)
	if !errors.Is(err, ErrInvalidCredentials) {
		t.Fatalf("expected inactive account to be rejected, got %v", err)
	}
}

func TestGetSessionReturnsSanitizedActiveUser(t *testing.T) {
	resetAuthTokenState(t)

	repo := &authRepoMock{
		usersByID: map[uint]*domain.User{
			404: {
				BaseModel:    domain.BaseModel{ID: 404},
				Username:     "alice",
				FullName:     "Alice Admin",
				PasswordHash: "secret-hash",
				Role:         "admin",
				BranchID:     8,
				IsActive:     true,
			},
		},
	}

	svc := NewService(repo, "test-secret-with-minimum-32-characters-value")
	user, err := svc.GetSession(404)
	if err != nil {
		t.Fatalf("get session failed: %v", err)
	}
	if user == nil {
		t.Fatal("expected session user")
	}
	if user.PasswordHash != "" {
		t.Fatalf("expected sanitized password hash, got %q", user.PasswordHash)
	}
	if user.Username != "alice" || user.Role != "admin" || user.BranchID != 8 {
		t.Fatalf("unexpected session user payload: %+v", user)
	}
	if repo.usersByID[404].PasswordHash != "secret-hash" {
		t.Fatalf("expected repo user payload to remain unchanged, got %+v", repo.usersByID[404])
	}
}

func TestGetSessionRejectsInactiveUser(t *testing.T) {
	resetAuthTokenState(t)

	repo := &authRepoMock{
		usersByID: map[uint]*domain.User{
			505: {
				BaseModel: domain.BaseModel{ID: 505},
				Username:  "inactive",
				Role:      "admin",
				BranchID:  1,
				IsActive:  false,
			},
		},
	}

	svc := NewService(repo, "test-secret-with-minimum-32-characters-value")
	_, err := svc.GetSession(505)
	if !errors.Is(err, ErrInvalidSession) {
		t.Fatalf("expected invalid session error, got %v", err)
	}
}

type rollbackAuthRepo struct {
	db       *gorm.DB
	failSave bool
}

func (r *rollbackAuthRepo) WithTransaction(fn func(repo AuthRepository) error) error {
	return r.db.Transaction(func(tx *gorm.DB) error {
		return fn(&rollbackAuthRepo{db: tx, failSave: r.failSave})
	})
}

func (r *rollbackAuthRepo) GetUserByUsername(username string) (*domain.User, error) {
	var user domain.User
	if err := r.db.Where("LOWER(username) = LOWER(?)", username).First(&user).Error; err != nil {
		return nil, err
	}
	return &user, nil
}

func (r *rollbackAuthRepo) GetUserByID(id uint) (*domain.User, error) {
	var user domain.User
	if err := r.db.First(&user, id).Error; err != nil {
		return nil, err
	}
	return &user, nil
}

func (r *rollbackAuthRepo) SaveRefreshToken(token *domain.RefreshToken) error {
	if r.failSave {
		return errors.New("forced save failure")
	}
	return r.db.Create(token).Error
}

func (r *rollbackAuthRepo) GetRefreshToken(hash string) (*domain.RefreshToken, error) {
	var token domain.RefreshToken
	if err := r.db.Where("token_hash = ? AND revoked = ?", hash, false).First(&token).Error; err != nil {
		return nil, err
	}
	return &token, nil
}

func (r *rollbackAuthRepo) RevokeRefreshToken(userID uint) error {
	return r.db.Model(&domain.RefreshToken{}).Where("user_id = ?", userID).Update("revoked", true).Error
}

func TestRefreshRollsBackOnSaveFailure(t *testing.T) {
	resetAuthTokenState(t)

	secret := "test-secret-with-minimum-32-characters-value"
	_, refreshToken, err := utils.GenerateToken(555, "admin", "admin", 1, secret)
	if err != nil {
		t.Fatalf("failed to generate refresh token: %v", err)
	}

	db := newAuthTestDB(t)
	if err := db.Create(&domain.User{BaseModel: domain.BaseModel{ID: 555}, Username: "bob", Role: "admin", BranchID: 1}).Error; err != nil {
		t.Fatalf("failed to seed user: %v", err)
	}
	if err := db.Create(&domain.RefreshToken{
		UserID:    555,
		TokenHash: utils.HashToken(refreshToken),
		ExpiresAt: time.Now().Add(1 * time.Hour),
		Revoked:   false,
	}).Error; err != nil {
		t.Fatalf("failed to seed refresh token: %v", err)
	}

	repo := &rollbackAuthRepo{db: db, failSave: true}
	svc := NewService(repo, secret)

	if _, _, _, err := svc.Refresh(refreshToken); err == nil {
		t.Fatal("expected refresh to fail when new token persistence fails")
	}

	var token domain.RefreshToken
	if err := db.Where("token_hash = ?", utils.HashToken(refreshToken)).First(&token).Error; err != nil {
		t.Fatalf("failed to reload refresh token: %v", err)
	}
	if token.Revoked {
		t.Fatal("expected refresh token revocation to roll back after persistence failure")
	}
}
