package translation_test

import (
	"context"
	"testing"

	"gorm.io/gorm"
	"tercul/internal/app/authz"
	"tercul/internal/app/translation"
	"tercul/internal/domain"
	platform_auth "tercul/internal/platform/auth"
	"tercul/internal/testutil"

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

// MockAuthorRepository is a mock implementation of the AuthorRepository interface.
type mockAuthorRepository struct{ mock.Mock }

func (m *mockAuthorRepository) Create(ctx context.Context, entity *domain.Author) error {
	args := m.Called(ctx, entity)
	return args.Error(0)
}
func (m *mockAuthorRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Author) error {
	args := m.Called(ctx, tx, entity)
	return args.Error(0)
}
func (m *mockAuthorRepository) GetByID(ctx context.Context, id uint) (*domain.Author, error) {
	args := m.Called(ctx, id)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*domain.Author), args.Error(1)
}
func (m *mockAuthorRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error) {
	args := m.Called(ctx, id, options)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*domain.Author), args.Error(1)
}
func (m *mockAuthorRepository) Update(ctx context.Context, entity *domain.Author) error {
	args := m.Called(ctx, entity)
	return args.Error(0)
}
func (m *mockAuthorRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Author) error {
	args := m.Called(ctx, tx, entity)
	return args.Error(0)
}
func (m *mockAuthorRepository) Delete(ctx context.Context, id uint) error {
	args := m.Called(ctx, id)
	return args.Error(0)
}
func (m *mockAuthorRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error {
	args := m.Called(ctx, tx, id)
	return args.Error(0)
}
func (m *mockAuthorRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Author], error) {
	args := m.Called(ctx, page, pageSize)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*domain.PaginatedResult[domain.Author]), args.Error(1)
}
func (m *mockAuthorRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.Author, error) {
	args := m.Called(ctx, options)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]domain.Author), args.Error(1)
}
func (m *mockAuthorRepository) ListAll(ctx context.Context) ([]domain.Author, error) {
	args := m.Called(ctx)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]domain.Author), args.Error(1)
}
func (m *mockAuthorRepository) Count(ctx context.Context) (int64, error) {
	args := m.Called(ctx)
	return args.Get(0).(int64), args.Error(1)
}
func (m *mockAuthorRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) {
	args := m.Called(ctx, options)
	return args.Get(0).(int64), args.Error(1)
}
func (m *mockAuthorRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Author, error) {
	args := m.Called(ctx, preloads, id)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*domain.Author), args.Error(1)
}
func (m *mockAuthorRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Author, error) {
	args := m.Called(ctx, batchSize, offset)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]domain.Author), args.Error(1)
}
func (m *mockAuthorRepository) Exists(ctx context.Context, id uint) (bool, error) {
	args := m.Called(ctx, id)
	return args.Bool(0), args.Error(1)
}
func (m *mockAuthorRepository) BeginTx(ctx context.Context) (*gorm.DB, error) {
	args := m.Called(ctx)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*gorm.DB), args.Error(1)
}
func (m *mockAuthorRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
	return fn(nil)
}
func (m *mockAuthorRepository) FindByName(ctx context.Context, name string) (*domain.Author, error) {
	args := m.Called(ctx, name)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*domain.Author), args.Error(1)
}
func (m *mockAuthorRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Author, error) {
	args := m.Called(ctx, workID)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]domain.Author), args.Error(1)
}
func (m *mockAuthorRepository) ListByBookID(ctx context.Context, bookID uint) ([]domain.Author, error) {
	args := m.Called(ctx, bookID)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]domain.Author), args.Error(1)
}
func (m *mockAuthorRepository) ListByCountryID(ctx context.Context, countryID uint) ([]domain.Author, error) {
	args := m.Called(ctx, countryID)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]domain.Author), args.Error(1)
}
func (m *mockAuthorRepository) GetWithTranslations(ctx context.Context, id uint) (*domain.Author, error) {
	args := m.Called(ctx, id)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*domain.Author), args.Error(1)
}

type TranslationCommandsTestSuite struct {
	suite.Suite
	mockWorkRepo        *testutil.MockWorkRepository
	mockTranslationRepo *testutil.MockTranslationRepository
	mockAuthorRepo      *mockAuthorRepository
	mockUserRepo        *testutil.MockUserRepository
	authzSvc            *authz.Service
	cmd                 *translation.TranslationCommands
	adminCtx            context.Context
	userCtx             context.Context
	adminUser           *domain.User
	regularUser         *domain.User
}

func (s *TranslationCommandsTestSuite) SetupTest() {
	s.mockWorkRepo = new(testutil.MockWorkRepository)
	s.mockTranslationRepo = new(testutil.MockTranslationRepository)
	s.mockAuthorRepo = new(mockAuthorRepository)
	s.mockUserRepo = new(testutil.MockUserRepository)
	s.authzSvc = authz.NewService(s.mockWorkRepo, s.mockAuthorRepo, s.mockUserRepo, s.mockTranslationRepo)
	s.cmd = translation.NewTranslationCommands(s.mockTranslationRepo, s.authzSvc)

	s.adminUser = &domain.User{BaseModel: domain.BaseModel{ID: 1}, Role: domain.UserRoleAdmin, Username: "admin"}
	s.regularUser = &domain.User{BaseModel: domain.BaseModel{ID: 2}, Role: domain.UserRoleContributor, Username: "contributor"}

	s.adminCtx = context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{
		UserID: s.adminUser.ID,
		Role:   string(s.adminUser.Role),
	})
	s.userCtx = context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{
		UserID: s.regularUser.ID,
		Role:   string(s.regularUser.Role),
	})
}

func (s *TranslationCommandsTestSuite) TestCreateOrUpdateTranslation() {
	testWork := &domain.Work{
		TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: 1}},
	}
	testAuthor := &domain.Author{
		TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: 1}},
		Name:              s.regularUser.Username,
	}
	baseInput := translation.CreateOrUpdateTranslationInput{
		Title:            "Test Title",
		Content:          "Test content",
		Language:         "es",
		TranslatableID:   testWork.ID,
		TranslatableType: "works",
	}

	s.Run("should create translation for admin", func() {
		s.SetupTest()
		input := baseInput
		// Arrange
		s.mockWorkRepo.On("GetByID", mock.Anything, testWork.ID).Return(testWork, nil).Once()
		s.mockTranslationRepo.On("Upsert", mock.Anything, mock.AnythingOfType("*domain.Translation")).Return(nil).Once()

		// Act
		result, err := s.cmd.CreateOrUpdateTranslation(s.adminCtx, input)

		// Assert
		s.NoError(err)
		s.NotNil(result)
		s.Equal(input.Title, result.Title)
		s.Equal(s.adminUser.ID, *result.TranslatorID)
		s.mockWorkRepo.AssertExpectations(s.T())
		s.mockTranslationRepo.AssertExpectations(s.T())
	})

	s.Run("should create translation for author", func() {
		s.SetupTest()
		input := baseInput
		// Arrange
		s.mockUserRepo.On("GetByID", mock.Anything, s.regularUser.ID).Return(s.regularUser, nil).Once()
		s.mockAuthorRepo.On("FindByName", mock.Anything, s.regularUser.Username).Return(testAuthor, nil).Once()
		s.mockWorkRepo.On("GetByID", mock.Anything, testWork.ID).Return(testWork, nil).Once()
		s.mockWorkRepo.On("IsAuthor", mock.Anything, testWork.ID, testAuthor.ID).Return(true, nil).Once()
		s.mockTranslationRepo.On("Upsert", mock.Anything, mock.AnythingOfType("*domain.Translation")).Return(nil).Once()

		// Act
		result, err := s.cmd.CreateOrUpdateTranslation(s.userCtx, input)

		// Assert
		s.NoError(err)
		s.NotNil(result)
		s.Equal(input.Title, result.Title)
		s.Equal(s.regularUser.ID, *result.TranslatorID)
		s.mockUserRepo.AssertExpectations(s.T())
		s.mockAuthorRepo.AssertExpectations(s.T())
		s.mockWorkRepo.AssertExpectations(s.T())
		s.mockTranslationRepo.AssertExpectations(s.T())
	})

	s.Run("should fail if user is not authorized", func() {
		s.SetupTest()
		input := baseInput
		// Arrange
		s.mockUserRepo.On("GetByID", mock.Anything, s.regularUser.ID).Return(s.regularUser, nil).Once()
		s.mockAuthorRepo.On("FindByName", mock.Anything, s.regularUser.Username).Return(testAuthor, nil).Once()
		s.mockWorkRepo.On("GetByID", mock.Anything, testWork.ID).Return(testWork, nil).Once()
		s.mockWorkRepo.On("IsAuthor", mock.Anything, testWork.ID, testAuthor.ID).Return(false, nil).Once()

		// Act
		_, err := s.cmd.CreateOrUpdateTranslation(s.userCtx, input)

		// Assert
		s.Error(err)
		s.ErrorIs(err, domain.ErrForbidden)
		s.mockUserRepo.AssertExpectations(s.T())
		s.mockAuthorRepo.AssertExpectations(s.T())
		s.mockWorkRepo.AssertExpectations(s.T())
	})

	s.Run("should fail on validation error for empty language", func() {
		s.SetupTest()
		// Arrange
		input := baseInput
		input.Language = ""

		// Act
		_, err := s.cmd.CreateOrUpdateTranslation(s.userCtx, input)

		// Assert
		s.Error(err)
		assert.ErrorContains(s.T(), err, "language cannot be empty")
	})
}

func TestTranslationCommands(t *testing.T) {
	suite.Run(t, new(TranslationCommandsTestSuite))
}