package user

import (
	"context"
	"errors"
	"testing"

	"tercul/internal/app/authz"
	"tercul/internal/domain"
	platform_auth "tercul/internal/platform/auth"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/suite"
)

type UserCommandsSuite struct {
	suite.Suite
	repo     *mockUserRepository
	authzSvc *authz.Service
	commands *UserCommands
}

func (s *UserCommandsSuite) SetupTest() {
	s.repo = new(mockUserRepository)
	// None of the repos are used by the authz checks in these command tests
	s.authzSvc = authz.NewService(nil, nil, nil, nil)
	s.commands = NewUserCommands(s.repo, s.authzSvc)
}

func TestUserCommandsSuite(t *testing.T) {
	suite.Run(t, new(UserCommandsSuite))
}

func (s *UserCommandsSuite) TestUpdateUser_Success_Self() {
	// Arrange
	ctx := platform_auth.ContextWithUserID(context.Background(), 1)
	input := UpdateUserInput{ID: 1, Username: strPtr("new_username")}

	s.repo.On("GetByID", ctx, uint(1)).Return(&domain.User{BaseModel: domain.BaseModel{ID: 1}}, nil)
	s.repo.On("Update", ctx, mock.AnythingOfType("*domain.User")).Return(nil)

	// Act
	updatedUser, err := s.commands.UpdateUser(ctx, input)

	// Assert
	assert.NoError(s.T(), err)
	assert.NotNil(s.T(), updatedUser)
	assert.Equal(s.T(), "new_username", updatedUser.Username)
	s.repo.AssertExpectations(s.T())
}

func (s *UserCommandsSuite) TestUpdateUser_Success_Admin() {
	// Arrange
	ctx := platform_auth.ContextWithAdminUser(context.Background(), 99) // Admin user
	input := UpdateUserInput{ID: 1, Username: strPtr("new_username_by_admin")}

	s.repo.On("GetByID", ctx, uint(1)).Return(&domain.User{BaseModel: domain.BaseModel{ID: 1}}, nil)
	s.repo.On("Update", ctx, mock.AnythingOfType("*domain.User")).Return(nil)

	// Act
	updatedUser, err := s.commands.UpdateUser(ctx, input)

	// Assert
	assert.NoError(s.T(), err)
	assert.NotNil(s.T(), updatedUser)
	assert.Equal(s.T(), "new_username_by_admin", updatedUser.Username)
	s.repo.AssertExpectations(s.T())
}

func (s *UserCommandsSuite) TestUpdateUser_Forbidden() {
	// Arrange
	ctx := platform_auth.ContextWithUserID(context.Background(), 2) // Different user
	input := UpdateUserInput{ID: 1, Username: strPtr("forbidden_username")}

	// No need to mock GetByID, as the auth check happens first.

	// Act
	_, err := s.commands.UpdateUser(ctx, input)

	// Assert
	assert.Error(s.T(), err)
	assert.ErrorIs(s.T(), err, domain.ErrForbidden)
	s.repo.AssertNotCalled(s.T(), "GetByID", mock.Anything, mock.Anything)
}

func (s *UserCommandsSuite) TestUpdateUser_Unauthorized() {
	// Arrange
	ctx := context.Background() // No user in context
	input := UpdateUserInput{ID: 1, Username: strPtr("unauthorized_username")}

	// Act
	_, err := s.commands.UpdateUser(ctx, input)

	// Assert
	assert.Error(s.T(), err)
	assert.ErrorIs(s.T(), err, domain.ErrUnauthorized)
	s.repo.AssertNotCalled(s.T(), "GetByID", mock.Anything, mock.Anything)
}

// Helper to get a pointer to a string
func strPtr(s string) *string {
	return &s
}

func (s *UserCommandsSuite) TestCreateUser() {
	// Arrange
	ctx := context.Background()
	input := CreateUserInput{
		Username: "newuser",
		Email:    "new@example.com",
		Password: "password",
	}
	s.repo.On("Create", ctx, mock.AnythingOfType("*domain.User")).Return(nil)

	// Act
	user, err := s.commands.CreateUser(ctx, input)

	// Assert
	assert.NoError(s.T(), err)
	assert.NotNil(s.T(), user)
	assert.Equal(s.T(), "newuser", user.Username)
	s.repo.AssertExpectations(s.T())
}

func (s *UserCommandsSuite) TestDeleteUser_Success() {
	// Arrange
	ctx := platform_auth.ContextWithAdminUser(context.Background(), 99)
	s.repo.On("Delete", ctx, uint(1)).Return(nil)

	// Act
	err := s.commands.DeleteUser(ctx, 1)

	// Assert
	assert.NoError(s.T(), err)
	s.repo.AssertExpectations(s.T())
}

func (s *UserCommandsSuite) TestDeleteUser_Forbidden() {
	// Arrange
	ctx := platform_auth.ContextWithUserID(context.Background(), 2) // Non-admin user

	// Act
	err := s.commands.DeleteUser(ctx, 1)

	// Assert
	assert.Error(s.T(), err)
	assert.ErrorIs(s.T(), err, domain.ErrForbidden)
	s.repo.AssertNotCalled(s.T(), "Delete", mock.Anything, mock.Anything)
}

func (s *UserCommandsSuite) TestUpdateUser_NotFound() {
	// Arrange
	ctx := platform_auth.ContextWithUserID(context.Background(), 1)
	input := UpdateUserInput{ID: 1, Username: strPtr("new_username")}

	s.repo.On("GetByID", ctx, uint(1)).Return(nil, domain.ErrEntityNotFound)

	// Act
	_, err := s.commands.UpdateUser(ctx, input)

	// Assert
	assert.Error(s.T(), err)
	assert.ErrorIs(s.T(), err, domain.ErrEntityNotFound)
	s.repo.AssertExpectations(s.T())
}

func (s *UserCommandsSuite) TestCreateUser_Fails() {
	// Arrange
	ctx := context.Background()
	input := CreateUserInput{
		Username: "newuser",
		Email:    "new@example.com",
		Password: "password",
	}
	s.repo.On("Create", ctx, mock.AnythingOfType("*domain.User")).Return(errors.New("db error"))

	// Act
	_, err := s.commands.CreateUser(ctx, input)

	// Assert
	assert.Error(s.T(), err)
	assert.EqualError(s.T(), err, "db error")
	s.repo.AssertExpectations(s.T())
}

func (s *UserCommandsSuite) TestDeleteUser_Unauthorized() {
	// Arrange
	ctx := context.Background() // No user in context

	// Act
	err := s.commands.DeleteUser(ctx, 1)

	// Assert
	assert.Error(s.T(), err)
	assert.ErrorIs(s.T(), err, domain.ErrUnauthorized)
	s.repo.AssertNotCalled(s.T(), "Delete", mock.Anything, mock.Anything)
}

func (s *UserCommandsSuite) TestDeleteUser_AuthzFails() {
	// Arrange
	// This test requires a mock for the authz service, which is not currently mocked.
	// For now, this highlights a gap. To properly test this, we would need to
	// inject a mockable authz service.
	// Since the current authz service is a concrete implementation, we can't easily
	// simulate an error from `CanUpdateUser`. We will skip this test for now
	// as it requires a larger refactoring of the authz service dependency.
	s.T().Skip("Skipping test for authz failure as it requires mockable authz service")
}

func (s *UserCommandsSuite) TestUpdateUser_UpdateFails() {
	// Arrange
	ctx := platform_auth.ContextWithUserID(context.Background(), 1)
	input := UpdateUserInput{ID: 1, Username: strPtr("new_username")}
	testUser := &domain.User{BaseModel: domain.BaseModel{ID: 1}}

	s.repo.On("GetByID", ctx, uint(1)).Return(testUser, nil)
	s.repo.On("Update", ctx, mock.AnythingOfType("*domain.User")).Return(errors.New("db error"))

	// Act
	_, err := s.commands.UpdateUser(ctx, input)

	// Assert
	assert.Error(s.T(), err)
	assert.EqualError(s.T(), err, "db error")
	s.repo.AssertExpectations(s.T())
}

func (s *UserCommandsSuite) TestUpdateUser_SetPasswordFails() {
	// Arrange
	ctx := platform_auth.ContextWithUserID(context.Background(), 1)
	emptyPassword := ""
	input := UpdateUserInput{ID: 1, Password: &emptyPassword}
	testUser := &domain.User{BaseModel: domain.BaseModel{ID: 1}}

	s.repo.On("GetByID", ctx, uint(1)).Return(testUser, nil)

	// Act
	_, err := s.commands.UpdateUser(ctx, input)

	// Assert
	assert.Error(s.T(), err)
	assert.EqualError(s.T(), err, "password cannot be empty")
	s.repo.AssertExpectations(s.T())
}

func (s *UserCommandsSuite) TestUpdateUser_AllFields() {
	// Arrange
	ctx := platform_auth.ContextWithUserID(context.Background(), 1)
	countryID := uint(10)
	cityID := uint(20)
	addressID := uint(30)
	newRole := domain.UserRoleEditor
	verified := true
	active := false

	input := UpdateUserInput{
		ID:          1,
		Username:    strPtr("all_fields"),
		Email:       strPtr("all@fields.com"),
		Password:    strPtr("new_password"),
		FirstName:   strPtr("First"),
		LastName:    strPtr("Last"),
		DisplayName: strPtr("Display"),
		Bio:         strPtr("Bio"),
		AvatarURL:   strPtr("http://avatar.url"),
		Role:        &newRole,
		Verified:    &verified,
		Active:      &active,
		CountryID:   &countryID,
		CityID:      &cityID,
		AddressID:   &addressID,
	}

	s.repo.On("GetByID", ctx, uint(1)).Return(&domain.User{BaseModel: domain.BaseModel{ID: 1}}, nil)
	s.repo.On("Update", ctx, mock.AnythingOfType("*domain.User")).Run(func(args mock.Arguments) {
		userArg := args.Get(1).(*domain.User)
		assert.Equal(s.T(), "all_fields", userArg.Username)
		assert.Equal(s.T(), "all@fields.com", userArg.Email)
		assert.True(s.T(), userArg.CheckPassword("new_password"))
		assert.Equal(s.T(), "First", userArg.FirstName)
		assert.Equal(s.T(), "Last", userArg.LastName)
		assert.Equal(s.T(), "Display", userArg.DisplayName)
		assert.Equal(s.T(), "Bio", userArg.Bio)
		assert.Equal(s.T(), "http://avatar.url", userArg.AvatarURL)
		assert.Equal(s.T(), newRole, userArg.Role)
		assert.Equal(s.T(), verified, userArg.Verified)
		assert.Equal(s.T(), active, userArg.Active)
		assert.Equal(s.T(), &countryID, userArg.CountryID)
		assert.Equal(s.T(), &cityID, userArg.CityID)
		assert.Equal(s.T(), &addressID, userArg.AddressID)
	}).Return(nil)

	// Act
	_, err := s.commands.UpdateUser(ctx, input)

	// Assert
	assert.NoError(s.T(), err)
	s.repo.AssertExpectations(s.T())
}

func (s *UserCommandsSuite) TestDeleteUser_NotFound() {
	// Arrange
	ctx := platform_auth.ContextWithAdminUser(context.Background(), 99)
	s.repo.On("Delete", ctx, uint(1)).Return(domain.ErrEntityNotFound)

	// Act
	err := s.commands.DeleteUser(ctx, 1)

	// Assert
	assert.Error(s.T(), err)
	assert.ErrorIs(s.T(), err, domain.ErrEntityNotFound)
	s.repo.AssertExpectations(s.T())
}