package graphql

import (
	"context"
	"fmt"
	"testing"
	"tercul/internal/app"
	"tercul/internal/app/authz"
	"tercul/internal/app/user"
	"tercul/internal/domain"
	"tercul/internal/adapters/graphql/model"
	platform_auth "tercul/internal/platform/auth"

	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/suite"
	"gorm.io/gorm"
)

// mockUserRepositoryForUserResolver is a mock for the user repository.
type mockUserRepositoryForUserResolver struct{ mock.Mock }

// Implement domain.UserRepository
func (m *mockUserRepositoryForUserResolver) GetByID(ctx context.Context, id uint) (*domain.User, error) {
	args := m.Called(ctx, id)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*domain.User), args.Error(1)
}
func (m *mockUserRepositoryForUserResolver) FindByUsername(ctx context.Context, username string) (*domain.User, error) {
	args := m.Called(ctx, username)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*domain.User), args.Error(1)
}
func (m *mockUserRepositoryForUserResolver) FindByEmail(ctx context.Context, email string) (*domain.User, error) {
	args := m.Called(ctx, email)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*domain.User), args.Error(1)
}
func (m *mockUserRepositoryForUserResolver) ListByRole(ctx context.Context, role domain.UserRole) ([]domain.User, error) {
	args := m.Called(ctx, role)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]domain.User), args.Error(1)
}
func (m *mockUserRepositoryForUserResolver) Create(ctx context.Context, entity *domain.User) error {
	return m.Called(ctx, entity).Error(0)
}
func (m *mockUserRepositoryForUserResolver) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.User) error {
	return nil
}
func (m *mockUserRepositoryForUserResolver) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.User, error) {
	return nil, nil
}
func (m *mockUserRepositoryForUserResolver) Update(ctx context.Context, entity *domain.User) error {
	return m.Called(ctx, entity).Error(0)
}
func (m *mockUserRepositoryForUserResolver) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.User) error {
	return nil
}
func (m *mockUserRepositoryForUserResolver) Delete(ctx context.Context, id uint) error {
	return m.Called(ctx, id).Error(0)
}
func (m *mockUserRepositoryForUserResolver) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error {
	return nil
}
func (m *mockUserRepositoryForUserResolver) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.User], error) {
	return nil, nil
}
func (m *mockUserRepositoryForUserResolver) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.User, error) {
	return nil, nil
}
func (m *mockUserRepositoryForUserResolver) ListAll(ctx context.Context) ([]domain.User, error) {
	args := m.Called(ctx)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]domain.User), args.Error(1)
}
func (m *mockUserRepositoryForUserResolver) Count(ctx context.Context) (int64, error) { return 0, nil }
func (m *mockUserRepositoryForUserResolver) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) {
	return 0, nil
}
func (m *mockUserRepositoryForUserResolver) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.User, error) {
	return nil, nil
}
func (m *mockUserRepositoryForUserResolver) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.User, error) {
	return nil, nil
}
func (m *mockUserRepositoryForUserResolver) Exists(ctx context.Context, id uint) (bool, error) {
	return false, nil
}
func (m *mockUserRepositoryForUserResolver) BeginTx(ctx context.Context) (*gorm.DB, error) {
	return nil, nil
}
func (m *mockUserRepositoryForUserResolver) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
	return nil
}

// mockUserProfileRepository is a mock for the user profile repository.
type mockUserProfileRepository struct{ mock.Mock }

// Implement domain.UserProfileRepository
func (m *mockUserProfileRepository) GetByUserID(ctx context.Context, userID uint) (*domain.UserProfile, error) {
	args := m.Called(ctx, userID)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*domain.UserProfile), args.Error(1)
}

// Implement BaseRepository methods for UserProfile
func (m *mockUserProfileRepository) Create(ctx context.Context, entity *domain.UserProfile) error {
	return nil
}
func (m *mockUserProfileRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.UserProfile) error {
	return nil
}
func (m *mockUserProfileRepository) GetByID(ctx context.Context, id uint) (*domain.UserProfile, error) {
	return nil, nil
}
func (m *mockUserProfileRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.UserProfile, error) {
	return nil, nil
}
func (m *mockUserProfileRepository) Update(ctx context.Context, entity *domain.UserProfile) error {
	return nil
}
func (m *mockUserProfileRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.UserProfile) error {
	return nil
}
func (m *mockUserProfileRepository) Delete(ctx context.Context, id uint) error { return nil }
func (m *mockUserProfileRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error {
	return nil
}
func (m *mockUserProfileRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.UserProfile], error) {
	return nil, nil
}
func (m *mockUserProfileRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.UserProfile, error) {
	return nil, nil
}
func (m *mockUserProfileRepository) ListAll(ctx context.Context) ([]domain.UserProfile, error) {
	return nil, nil
}
func (m *mockUserProfileRepository) Count(ctx context.Context) (int64, error) { return 0, nil }
func (m *mockUserProfileRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) {
	return 0, nil
}
func (m *mockUserProfileRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.UserProfile, error) {
	return nil, nil
}
func (m *mockUserProfileRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.UserProfile, error) {
	return nil, nil
}
func (m *mockUserProfileRepository) Exists(ctx context.Context, id uint) (bool, error) {
	return false, nil
}
func (m *mockUserProfileRepository) BeginTx(ctx context.Context) (*gorm.DB, error) { return nil, nil }
func (m *mockUserProfileRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
	return nil
}

// UserResolversUnitSuite is a unit test suite for the user resolvers.
type UserResolversUnitSuite struct {
	suite.Suite
	resolver            *Resolver
	mockUserRepo        *mockUserRepositoryForUserResolver
	mockUserProfileRepo *mockUserProfileRepository
}

// SetupTest sets up the test suite
func (s *UserResolversUnitSuite) SetupTest() {
	s.mockUserRepo = new(mockUserRepositoryForUserResolver)
	s.mockUserProfileRepo = new(mockUserProfileRepository)

	// The authz service dependencies are not needed for the user commands being tested.
	authzSvc := authz.NewService(nil, nil, s.mockUserRepo, nil)

	userCommands := user.NewUserCommands(s.mockUserRepo, authzSvc)
	userQueries := user.NewUserQueries(s.mockUserRepo, s.mockUserProfileRepo)

	userService := &user.Service{
		Commands: userCommands,
		Queries:  userQueries,
	}

	s.resolver = &Resolver{
		App: &app.Application{
			User: userService,
		},
	}
}

// TestUserResolversUnitSuite runs the test suite
func TestUserResolversUnitSuite(t *testing.T) {
	suite.Run(t, new(UserResolversUnitSuite))
}

func (s *UserResolversUnitSuite) TestUserQuery() {
	s.Run("Success", func() {
		s.SetupTest()
		userID := uint(1)
		userIDStr := "1"
		ctx := context.Background()

		expectedUser := &domain.User{
			Username: "testuser",
			Email:    "test@test.com",
			Role:     domain.UserRoleReader,
		}
		expectedUser.ID = userID

		s.mockUserRepo.On("GetByID", mock.Anything, userID).Return(expectedUser, nil).Once()

		gqlUser, err := s.resolver.Query().User(ctx, userIDStr)

		s.Require().NoError(err)
		s.Require().NotNil(gqlUser)
		s.Equal(userIDStr, gqlUser.ID)
		s.Equal(expectedUser.Username, gqlUser.Username)
		s.mockUserRepo.AssertExpectations(s.T())
	})

	s.Run("Not Found", func() {
		s.SetupTest()
		userID := uint(99)
		userIDStr := "99"
		ctx := context.Background()

		s.mockUserRepo.On("GetByID", mock.Anything, userID).Return(nil, domain.ErrEntityNotFound).Once()

		gqlUser, err := s.resolver.Query().User(ctx, userIDStr)

		s.Require().Error(err) // The resolver should propagate the error
		s.Require().Nil(gqlUser)
		s.mockUserRepo.AssertExpectations(s.T())
	})

	s.Run("Invalid ID", func() {
		s.SetupTest()
		ctx := context.Background()
		_, err := s.resolver.Query().User(ctx, "invalid")
		s.Require().Error(err)
	})
}

func (s *UserResolversUnitSuite) TestUserProfileQuery() {
	s.Run("Success", func() {
		s.SetupTest()
		userID := uint(1)
		userIDStr := "1"
		ctx := context.Background()

		expectedProfile := &domain.UserProfile{
			UserID:      userID,
			PhoneNumber: "12345",
		}
		expectedProfile.ID = 1

		expectedUser := &domain.User{
			Username: "testuser",
		}
		expectedUser.ID = userID

		s.mockUserProfileRepo.On("GetByUserID", mock.Anything, userID).Return(expectedProfile, nil).Once()
		s.mockUserRepo.On("GetByID", mock.Anything, userID).Return(expectedUser, nil).Once()

		gqlProfile, err := s.resolver.Query().UserProfile(ctx, userIDStr)

		s.Require().NoError(err)
		s.Require().NotNil(gqlProfile)
		s.Equal(userIDStr, gqlProfile.UserID)
		s.Equal(&expectedProfile.PhoneNumber, gqlProfile.PhoneNumber)
		s.mockUserProfileRepo.AssertExpectations(s.T())
		s.mockUserRepo.AssertExpectations(s.T())
	})

	s.Run("Profile Not Found", func() {
		s.SetupTest()
		userID := uint(99)
		userIDStr := "99"
		ctx := context.Background()

		s.mockUserProfileRepo.On("GetByUserID", mock.Anything, userID).Return(nil, domain.ErrEntityNotFound).Once()

		gqlProfile, err := s.resolver.Query().UserProfile(ctx, userIDStr)

		s.Require().Error(err)
		s.Require().Nil(gqlProfile)
		s.mockUserProfileRepo.AssertExpectations(s.T())
	})

	s.Run("User Not Found for profile", func() {
		s.SetupTest()
		userID := uint(1)
		userIDStr := "1"
		ctx := context.Background()

		expectedProfile := &domain.UserProfile{
			UserID: userID,
		}
		expectedProfile.ID = 1

		s.mockUserProfileRepo.On("GetByUserID", mock.Anything, userID).Return(expectedProfile, nil).Once()
		s.mockUserRepo.On("GetByID", mock.Anything, userID).Return(nil, domain.ErrEntityNotFound).Once()

		_, err := s.resolver.Query().UserProfile(ctx, userIDStr)

		s.Require().Error(err)
		s.mockUserProfileRepo.AssertExpectations(s.T())
		s.mockUserRepo.AssertExpectations(s.T())
	})
}

func (s *UserResolversUnitSuite) TestUpdateProfileMutation() {
	s.Run("Success", func() {
		s.SetupTest()
		actorID := uint(1)

		ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{
			UserID: actorID,
			Role:   string(domain.UserRoleReader),
		})

		displayName := "New Name"
		input := model.UserInput{DisplayName: &displayName}

		originalUser := &domain.User{DisplayName: "Old Name"}
		originalUser.ID = actorID

		s.mockUserRepo.On("GetByID", mock.Anything, actorID).Return(originalUser, nil).Once()
		s.mockUserRepo.On("Update", mock.Anything, mock.MatchedBy(func(u *domain.User) bool {
			return u.ID == actorID && u.DisplayName == displayName
		})).Return(nil).Once()

		updatedUser, err := s.resolver.Mutation().UpdateProfile(ctx, input)

		s.Require().NoError(err)
		s.Require().NotNil(updatedUser)
		s.Equal(displayName, *updatedUser.DisplayName)
		s.mockUserRepo.AssertExpectations(s.T())
	})

	s.Run("Unauthorized", func() {
		s.SetupTest()
		ctx := context.Background() // no user
		displayName := "New Name"
		input := model.UserInput{DisplayName: &displayName}

		_, err := s.resolver.Mutation().UpdateProfile(ctx, input)
		s.Require().Error(err)
		s.ErrorIs(err, domain.ErrUnauthorized)
	})
}

func (s *UserResolversUnitSuite) TestUpdateUserMutation() {
	s.Run("Success as self", func() {
		s.SetupTest()
		actorID := uint(1)
		targetID := uint(1)
		targetIDStr := "1"
		username := "new_username"
		ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{
			UserID: actorID,
			Role:   string(domain.UserRoleReader),
		})
		input := model.UserInput{Username: &username}

		originalUser := &domain.User{Username: "old_username"}
		originalUser.ID = targetID

		s.mockUserRepo.On("GetByID", mock.Anything, targetID).Return(originalUser, nil).Once()
		s.mockUserRepo.On("Update", mock.Anything, mock.MatchedBy(func(u *domain.User) bool {
			return u.ID == targetID && u.Username == username
		})).Return(nil).Once()

		updatedUser, err := s.resolver.Mutation().UpdateUser(ctx, targetIDStr, input)

		s.Require().NoError(err)
		s.Require().NotNil(updatedUser)
		s.Equal(username, updatedUser.Username)
		s.mockUserRepo.AssertExpectations(s.T())
	})

	s.Run("Success as admin", func() {
		s.SetupTest()
		actorID := uint(99) // Admin
		targetID := uint(1)
		targetIDStr := "1"
		username := "new_username"
		ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{
			UserID: actorID,
			Role:   string(domain.UserRoleAdmin),
		})
		input := model.UserInput{Username: &username}

		originalUser := &domain.User{Username: "old_username"}
		originalUser.ID = targetID

		s.mockUserRepo.On("GetByID", mock.Anything, targetID).Return(originalUser, nil).Once()
		s.mockUserRepo.On("Update", mock.Anything, mock.MatchedBy(func(u *domain.User) bool {
			return u.ID == targetID && u.Username == username
		})).Return(nil).Once()

		updatedUser, err := s.resolver.Mutation().UpdateUser(ctx, targetIDStr, input)

		s.Require().NoError(err)
		s.Require().NotNil(updatedUser)
		s.Equal(username, updatedUser.Username)
		s.mockUserRepo.AssertExpectations(s.T())
	})

	s.Run("Forbidden", func() {
		s.SetupTest()
		actorID := uint(2)
		targetIDStr := "1"
		username := "new_username"
		ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{
			UserID: actorID,
			Role:   string(domain.UserRoleReader),
		})
		input := model.UserInput{Username: &username}

		_, err := s.resolver.Mutation().UpdateUser(ctx, targetIDStr, input)

		s.Require().Error(err)
		s.ErrorIs(err, domain.ErrForbidden)
		s.mockUserRepo.AssertNotCalled(s.T(), "GetByID")
		s.mockUserRepo.AssertNotCalled(s.T(), "Update")
	})

	s.Run("User not found", func() {
		s.SetupTest()
		actorID := uint(1)
		targetID := uint(1)
		targetIDStr := "1"
		username := "new_username"
		ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{
			UserID: actorID,
			Role:   string(domain.UserRoleReader),
		})
		input := model.UserInput{Username: &username}

		s.mockUserRepo.On("GetByID", mock.Anything, targetID).Return(nil, domain.ErrEntityNotFound).Once()

		_, err := s.resolver.Mutation().UpdateUser(ctx, targetIDStr, input)

		s.Require().Error(err)
		s.ErrorIs(err, domain.ErrEntityNotFound)
		s.mockUserRepo.AssertExpectations(s.T())
		s.mockUserRepo.AssertNotCalled(s.T(), "Update")
	})
}

func (s *UserResolversUnitSuite) TestUsersQuery() {
	s.Run("Success without role", func() {
		s.SetupTest()
		ctx := context.Background()
		expectedUsers := []domain.User{
			{Username: "user1"},
			{Username: "user2"},
		}
		s.mockUserRepo.On("ListAll", mock.Anything).Return(expectedUsers, nil).Once()

		users, err := s.resolver.Query().Users(ctx, nil, nil, nil)

		s.Require().NoError(err)
		s.Len(users, 2)
		s.mockUserRepo.AssertExpectations(s.T())
	})

	s.Run("Success with role", func() {
		s.SetupTest()
		ctx := context.Background()
		role := domain.UserRoleAdmin
		modelRole := model.UserRoleAdmin
		expectedUsers := []domain.User{
			{Username: "admin1", Role: role},
		}
		s.mockUserRepo.On("ListByRole", mock.Anything, role).Return(expectedUsers, nil).Once()

		users, err := s.resolver.Query().Users(ctx, nil, nil, &modelRole)

		s.Require().NoError(err)
		s.Len(users, 1)
		s.Equal(model.UserRoleAdmin, users[0].Role)
		s.mockUserRepo.AssertExpectations(s.T())
	})
}

func (s *UserResolversUnitSuite) TestUserByEmailQuery() {
	s.Run("Success", func() {
		s.SetupTest()
		email := "test@test.com"
		ctx := context.Background()

		expectedUser := &domain.User{
			Username: "testuser",
			Email:    email,
			Role:     domain.UserRoleReader,
		}
		expectedUser.ID = 1

		s.mockUserRepo.On("FindByEmail", mock.Anything, email).Return(expectedUser, nil).Once()

		gqlUser, err := s.resolver.Query().UserByEmail(ctx, email)

		s.Require().NoError(err)
		s.Require().NotNil(gqlUser)
		s.Equal(email, gqlUser.Email)
		s.mockUserRepo.AssertExpectations(s.T())
	})
}

func (s *UserResolversUnitSuite) TestUserByUsernameQuery() {
	s.Run("Success", func() {
		s.SetupTest()
		username := "testuser"
		ctx := context.Background()

		expectedUser := &domain.User{
			Username: username,
			Email:    "test@test.com",
			Role:     domain.UserRoleReader,
		}
		expectedUser.ID = 1

		s.mockUserRepo.On("FindByUsername", mock.Anything, username).Return(expectedUser, nil).Once()

		gqlUser, err := s.resolver.Query().UserByUsername(ctx, username)

		s.Require().NoError(err)
		s.Require().NotNil(gqlUser)
		s.Equal(username, gqlUser.Username)
		s.mockUserRepo.AssertExpectations(s.T())
	})
}

func (s *UserResolversUnitSuite) TestMeQuery() {
	s.Run("Success", func() {
		s.SetupTest()
		userID := uint(1)
		ctx := platform_auth.ContextWithUserID(context.Background(), userID)

		expectedUser := &domain.User{
			Username: "testuser",
			Email:    "test@test.com",
			Role:     domain.UserRoleReader,
		}
		expectedUser.ID = userID

		s.mockUserRepo.On("GetByID", mock.Anything, userID).Return(expectedUser, nil).Once()

		gqlUser, err := s.resolver.Query().Me(ctx)

		s.Require().NoError(err)
		s.Require().NotNil(gqlUser)
		s.Equal(fmt.Sprintf("%d", userID), gqlUser.ID)
		s.mockUserRepo.AssertExpectations(s.T())
	})

	s.Run("Unauthorized", func() {
		s.SetupTest()
		ctx := context.Background() // No user in context
		_, err := s.resolver.Query().Me(ctx)
		s.Require().Error(err)
		s.Equal(domain.ErrUnauthorized, err)
	})
}

func (s *UserResolversUnitSuite) TestDeleteUserMutation() {
	s.Run("Success as self", func() {
		s.SetupTest()
		actorID := uint(1)
		targetID := uint(1)
		targetIDStr := "1"
		ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{
			UserID: actorID,
			Role:   string(domain.UserRoleReader),
		})

		s.mockUserRepo.On("Delete", mock.Anything, targetID).Return(nil).Once()

		ok, err := s.resolver.Mutation().DeleteUser(ctx, targetIDStr)

		s.Require().NoError(err)
		s.True(ok)
		s.mockUserRepo.AssertExpectations(s.T())
	})

	s.Run("Success as admin", func() {
		s.SetupTest()
		actorID := uint(99) // Admin
		targetID := uint(1)
		targetIDStr := "1"
		ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{
			UserID: actorID,
			Role:   string(domain.UserRoleAdmin),
		})

		s.mockUserRepo.On("Delete", mock.Anything, targetID).Return(nil).Once()

		ok, err := s.resolver.Mutation().DeleteUser(ctx, targetIDStr)

		s.Require().NoError(err)
		s.True(ok)
		s.mockUserRepo.AssertExpectations(s.T())
	})

	s.Run("Forbidden", func() {
		s.SetupTest()
		actorID := uint(2)
		targetIDStr := "1"
		ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{
			UserID: actorID,
			Role:   string(domain.UserRoleReader),
		})

		ok, err := s.resolver.Mutation().DeleteUser(ctx, targetIDStr)

		s.Require().Error(err)
		s.ErrorIs(err, domain.ErrForbidden)
		s.False(ok)
		s.mockUserRepo.AssertNotCalled(s.T(), "Delete")
	})

	s.Run("Invalid ID", func() {
		s.SetupTest()
		ctx := context.Background()
		_, err := s.resolver.Mutation().DeleteUser(ctx, "invalid")
		s.Require().Error(err)
	})
}