package delivery_address

import (
	"context"
	"github.com/DATA-DOG/go-sqlmock"
	"github.com/UpmeshLTD/urus-aio/infrastructure/db/entities"
	"github.com/UpmeshLTD/urus-aio/infrastructure/utils"
	"github.com/UpmeshLTD/urus-aio/paid-order/db/gormdb"
	"github.com/pkg/errors"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
	"github.com/stretchr/testify/suite"
	"gorm.io/gorm"
	"testing"

	tt "github.com/UpmeshLTD/urus-aio/infrastructure/testing"
	addressMocks "github.com/UpmeshLTD/urus-aio/paid-order/domain/mock/address"
	deliveryAddressMocks "github.com/UpmeshLTD/urus-aio/paid-order/domain/mock/delivery_address"
	deliveryAddressLogMocks "github.com/UpmeshLTD/urus-aio/paid-order/domain/mock/delivery_address_log"
)

type ServiceTestSuite struct {
	suite.Suite
	service Service
	mock    sqlmock.Sqlmock
	ctx     context.Context
	gormDB  gormdb.GormDB
	db      *gorm.DB
	repo    struct {
		address            *addressMocks.Repository
		deliveryAddress    *deliveryAddressMocks.Repository
		deliveryAddressLog *deliveryAddressLogMocks.Repository
	}
}

func (s *ServiceTestSuite) SetupSuite() {
	var err error
	s.db, s.mock, err = tt.MockGormSqlmock()
	require.NoError(s.T(), err)

	s.ctx = context.TODO()
	s.gormDB = gormdb.Wrap(s.db)
}

func (s *ServiceTestSuite) SetupTest() {
	s.repo.address = new(addressMocks.Repository)
	s.repo.deliveryAddress = new(deliveryAddressMocks.Repository)
	s.repo.deliveryAddressLog = new(deliveryAddressLogMocks.Repository)
	s.service = NewService(
		s.gormDB,
		s.repo.deliveryAddress,
		s.repo.address,
		s.repo.deliveryAddressLog,
	)
}

func (s *ServiceTestSuite) Test_Get() {
	// Prepare mock data
	expectedResult := NewSampleDeliveryAddress()

	// Mock repository, return the delivery address
	s.repo.deliveryAddress.On("GetByCartID", s.ctx, mock.Anything, expectedResult.CartID).Return(&expectedResult, nil)

	// Service must return the delivery address on success
	actualResult, err := s.service.Get(s.ctx, expectedResult.CartID)
	require.NoError(s.T(), err)

	// And they must be equal
	require.Equal(s.T(), &expectedResult, actualResult, "they should be equal")
}

func (s *ServiceTestSuite) Test_GetList() {
	// Prepare mock data
	// Address 1
	address1 := NewSampleDeliveryAddress()
	address1.AddressID = 1
	address1.Address.ID.ID = 1
	address1.CartID = "778ce8c4-6278-4e7d-90d0-6b465c5c7f5e"

	// Address 2
	address2 := NewSampleDeliveryAddress()
	address2.AddressID = 2
	address2.Address.ID.ID = 2
	address2.CartID = "778ce8c4-6278-4e7d-90d0-6b465c5c7f5f"

	cartIDs := []string{address1.CartID, address2.CartID}
	expectedResult := []*entities.DeliveryAddress{&address1, &address2}

	// Mock repository, return the list of delivery addresses
	s.repo.deliveryAddress.On("GetByCartIDs", s.ctx, mock.Anything, cartIDs).Return(expectedResult, nil)

	// Service must return the list of delivery addresses on success
	actualResult, err := s.service.GetList(s.ctx, cartIDs)
	require.NoError(s.T(), err)

	// And they must be equal
	require.Equal(s.T(), expectedResult, actualResult, "they should be equal")
}

func (s *ServiceTestSuite) Test_Create_Success() {
	// Prepare mock data
	expectedResult := NewSampleDeliveryAddress()
	actualResult := NewSampleDeliveryAddress()
	actualResult.AddressID = 0 // Will be updated to a new ID after creating success

	// Mock repository, return the address
	s.repo.address.On("Create", s.ctx, mock.Anything, &actualResult.Address).Return(&expectedResult.Address, nil)
	s.repo.deliveryAddress.On("Create", s.ctx, mock.Anything, &actualResult).Return(nil)

	// Mock transaction
	s.mock.ExpectBegin()
	s.mock.ExpectCommit()

	// Service must return nil on success
	err := s.service.Create(s.ctx, &actualResult)
	require.NoError(s.T(), err)

	// And the AddressID must be updated
	require.Equal(s.T(), expectedResult.AddressID, actualResult.AddressID, "they should be equal")
}

func (s *ServiceTestSuite) Test_Create_Failed_Address() {
	expected := errors.New("failed to create address")

	// Prepare mock data
	emptyDeliveryAddress := entities.DeliveryAddress{}

	// Mock repository, return error
	s.repo.address.On("Create", s.ctx, mock.Anything, &emptyDeliveryAddress.Address).Return(nil, expected)

	// Mock transaction
	s.mock.ExpectBegin()
	s.mock.ExpectRollback()

	// Service must return error on failure
	err := s.service.Create(s.ctx, &emptyDeliveryAddress)
	require.Error(s.T(), err)
	assert.Equal(s.T(), expected, err, "they should be equal")
}

func (s *ServiceTestSuite) Test_Create_Failed_DeliveryAddress() {
	expectedErr := errors.New("failed to create delivery address")

	// Prepare mock data
	expectedResult := entities.DeliveryAddress{}
	actualResult := NewSampleDeliveryAddress()
	actualResult.AddressID = 0 // Will be updated to a new ID after creating success

	// Mock repository, return the address
	s.repo.address.On("Create", s.ctx, mock.Anything, &actualResult.Address).Return(&expectedResult.Address, nil)

	// Mock repository, return error
	s.repo.deliveryAddress.On("Create", s.ctx, mock.Anything, &actualResult).Return(expectedErr)

	// Mock transaction
	s.mock.ExpectBegin()
	s.mock.ExpectRollback()

	// Service must return error on failure
	err := s.service.Create(s.ctx, &actualResult)
	require.Error(s.T(), err)
	assert.Equal(s.T(), expectedErr, err, "they should be equal")
}

func (s *ServiceTestSuite) Test_Update_Success() {
	// Prepare mock data
	// Current delivery address and the new values will be updated
	sample := NewSampleDeliveryAddress()
	sample.RecipientName = "New nane"
	sample.RecipientPhoneNumber = "New phone"

	// Mock repository, both method will return nil on success
	s.repo.address.On("Update", s.ctx, mock.Anything, sample.Address.ID, mock.Anything).Return(nil)
	s.repo.deliveryAddress.On("Update", s.ctx, mock.Anything, sample.CartID, mock.Anything).Return(nil)

	// Mock transaction
	s.mock.ExpectBegin()
	s.mock.ExpectCommit()

	// Service must return nil on success
	err := s.service.Update(s.ctx, &sample)
	require.NoError(s.T(), err)
}

func (s *ServiceTestSuite) Test_Update_Failed_Address() {
	expected := errors.New("failed to update address")

	// Prepare mock data
	// Current delivery address and the new values will be updated
	sample := NewSampleDeliveryAddress()
	sample.RecipientName = "New nane"
	sample.RecipientPhoneNumber = "New phone"

	// Mock repository, return error
	s.repo.address.On("Update", s.ctx, mock.Anything, sample.Address.ID, mock.Anything).Return(expected)

	// Mock transaction
	s.mock.ExpectBegin()
	s.mock.ExpectRollback()

	// Service must return error on failure
	err := s.service.Update(s.ctx, &sample)
	require.Error(s.T(), err)
	assert.Equal(s.T(), expected, err, "they should be equal")
}

func (s *ServiceTestSuite) Test_Update_Failed_DeliveryAddress() {
	expected := errors.New("failed to update delivery")

	// Prepare mock data
	// Current delivery address and the new values will be updated
	sample := NewSampleDeliveryAddress()
	sample.RecipientName = "New nane"
	sample.RecipientPhoneNumber = "New phone"

	// Mock repository, address repo return nil on success
	s.repo.address.On("Update", s.ctx, mock.Anything, sample.Address.ID, mock.Anything).Return(nil)

	// Mock repository, delivery address repo return error
	s.repo.deliveryAddress.On("Update", s.ctx, mock.Anything, sample.CartID, mock.Anything).Return(expected)

	// Mock transaction
	s.mock.ExpectBegin()
	s.mock.ExpectRollback()

	// Service must return error on failure
	err := s.service.Update(s.ctx, &sample)
	require.Error(s.T(), err)
	assert.Equal(s.T(), expected, err, "they should be equal")
}

func (s *ServiceTestSuite) Test_UpdatePatch_Success() {
	// Prepare mock data
	// Current delivery address and the new values will be updated
	sample := NewSampleDeliveryAddress()

	// Mock repository, both method will return nil on success
	s.repo.address.On("Update", s.ctx, mock.Anything, sample.Address.ID, mock.Anything).Return(nil)
	s.repo.deliveryAddress.On("Update", s.ctx, mock.Anything, sample.CartID, mock.Anything).Return(nil)

	// Mock transaction
	s.mock.ExpectBegin()
	s.mock.ExpectCommit()

	// Service must return nil on success
	err := s.service.UpdatePatch(s.ctx, &sample, &entities.SaveDeliveryAddressParams{
		Address1:   utils.String("Address1"),
		Address2:   utils.String("Address2"),
		PostalCode: utils.String("PostalCode"),
		UnitNumber: utils.String("UnitNumber"),
	})
	require.NoError(s.T(), err)
}

func (s *ServiceTestSuite) Test_UpdatePatch_Failed_Address() {
	expected := errors.New("failed to update")
	sample := NewSampleDeliveryAddress()

	// Mock repository, both method will return error
	s.repo.address.On("Update", s.ctx, mock.Anything, sample.Address.ID, mock.Anything).Return(expected)

	// Mock transaction
	s.mock.ExpectBegin()
	s.mock.ExpectRollback()

	// Service must return error on failure
	err := s.service.UpdatePatch(s.ctx, &sample, &entities.SaveDeliveryAddressParams{
		Address1:   utils.String("Address1"),
		Address2:   utils.String("Address2"),
		PostalCode: utils.String("PostalCode"),
		UnitNumber: utils.String("UnitNumber"),
	})
	require.Error(s.T(), err)
	assert.Equal(s.T(), expected, err, "they should be equal")
}

func (s *ServiceTestSuite) Test_UpdatePatch_Failed_DeliveryAddress() {
	expected := errors.New("failed to update delivery address")
	sample := NewSampleDeliveryAddress()

	// Mock repository, address repo return nil on success
	s.repo.address.On("Update", s.ctx, mock.Anything, sample.Address.ID, mock.Anything).Return(nil)

	// Mock repository, delivery address repo return error
	s.repo.deliveryAddress.On("Update", s.ctx, mock.Anything, sample.CartID, mock.Anything).Return(expected)

	// Mock transaction
	s.mock.ExpectBegin()
	s.mock.ExpectRollback()

	// Service must return error on failure
	err := s.service.UpdatePatch(s.ctx, &sample, &entities.SaveDeliveryAddressParams{
		Address1:   utils.String("Address1"),
		Address2:   utils.String("Address2"),
		PostalCode: utils.String("PostalCode"),
		UnitNumber: utils.String("UnitNumber"),
	})
	require.Error(s.T(), err)
	assert.Equal(s.T(), expected, err, "they should be equal")
}

func TestDeliveryAddressService(t *testing.T) {
	suite.Run(t, new(ServiceTestSuite))
}
