package delivery_address

import (
	"context"
	"database/sql/driver"
	"errors"
	"fmt"
	"github.com/DATA-DOG/go-sqlmock"
	"github.com/UpmeshLTD/urus-aio/infrastructure/db/entities"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/stretchr/testify/suite"
	"gorm.io/gorm"
	"regexp"
	"strings"
	"testing"
	"time"

	tt "github.com/UpmeshLTD/urus-aio/infrastructure/testing"
)

type RepositoryTestSuite struct {
	suite.Suite
	db   *gorm.DB
	ctx  context.Context
	mock sqlmock.Sqlmock
	repo Repository
}

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

	s.ctx = context.TODO()
	s.repo = NewRepository()
}

func (s *RepositoryTestSuite) AfterTest(_, _ string) {
	require.NoError(s.T(), s.mock.ExpectationsWereMet())
}

func (s *RepositoryTestSuite) Test_GetByCartID_Success() {
	sample := NewSampleDeliveryAddress()

	s.mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "delivery_addresses" WHERE cart_id = $1`)).
		WithArgs(sample.CartID).
		WillReturnRows(sqlmock.NewRows([]string{"address_id", "cart_id", "owner", "recipient_name", "recipient_phone_number", "version"}).
			AddRow(
				sample.AddressID,
				sample.CartID,
				sample.Owner,
				sample.RecipientName,
				sample.RecipientPhoneNumber,
				sample.Version,
			))

	s.mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "addresses" WHERE "addresses"."id" = $1 AND "addresses"."deleted_at" IS NULL`)).
		WithArgs(sample.AddressID).
		WillReturnRows(sqlmock.NewRows([]string{"id", "address_1", "address_2", "postal_code", "country_id", "deactivate", "unit_number", "created_at", "updated_at"}).
			AddRow(
				sample.AddressID,
				sample.Address.Address1,
				sample.Address.Address2,
				sample.Address.PostalCode,
				sample.Address.CountryID,
				sample.Address.Deactivate,
				sample.Address.UnitNumber,
				sample.Address.CreatedAt,
				sample.Address.UpdatedAt,
			))

	s.mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "countries" WHERE "countries"."id" = $1 AND "countries"."deleted_at" IS NULL`)).
		WithArgs(sample.Address.CountryID).
		WillReturnRows(sqlmock.NewRows([]string{"id", "code", "localized_name", "international_name", "created_at", "updated_at"}).
			AddRow(
				sample.Address.Country.ID.ID,
				sample.Address.Country.Code,
				sample.Address.Country.LocalizedName,
				sample.Address.Country.InternationalName,
				sample.Address.Country.CreatedAt,
				sample.Address.Country.UpdatedAt,
			))

	s.mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "address_sub_levels" WHERE "address_sub_levels"."address_id" = $1`)).
		WithArgs(sample.AddressID).
		WillReturnRows(func() (rows *sqlmock.Rows) {
			rows = sqlmock.NewRows([]string{"address_id", "location_id"})
			for _, v := range sample.Address.SubLevelLocations {
				rows.AddRow(sample.Address.ID.ID, v.ID.ID)
			}
			return rows
		}())

	s.mock.ExpectQuery(regexp.QuoteMeta(func() string {
		var arr []string
		for i := 1; i <= len(sample.Address.SubLevelLocations); i++ {
			arr = append(arr, fmt.Sprintf("$%d", i))
		}
		return fmt.Sprintf(`SELECT * FROM "locations" WHERE "locations"."id" IN (%s) AND "locations"."deleted_at" IS NULL`, strings.Join(arr, ","))
	}())).
		WithArgs(func() (ids []driver.Value) {
			for _, v := range sample.Address.SubLevelLocations {
				ids = append(ids, v.ID.ID)
			}
			return ids
		}()...).
		WillReturnRows(func() (rows *sqlmock.Rows) {
			rows = sqlmock.NewRows([]string{"id", "localized_name", "international_name", "country_id", "parent_id", "created_at", "updated_at"})
			for _, v := range sample.Address.SubLevelLocations {
				rows.AddRow(
					v.ID.ID,
					v.LocalizedName,
					v.InternationalName,
					v.CountryID,
					v.ParentID,
					v.CreatedAt,
					v.UpdatedAt,
				)
			}
			return rows
		}())

	expectedResult := &sample
	actualResult, err := s.repo.GetByCartID(s.ctx, s.db, sample.CartID)
	require.NoError(s.T(), err)
	assert.Equal(s.T(), expectedResult, actualResult, "they should be equal")
}

func (s *RepositoryTestSuite) Test_GetByCartID_NotFound() {
	expected := errors.New("record not found")
	s.mock.ExpectQuery(`SELECT (.+) FROM "delivery_addresses"`).
		WillReturnRows(sqlmock.NewRows([]string{"address_id", "cart_id", "owner", "recipient_name", "recipient_phone_number", "version"}))

	sample := NewSampleDeliveryAddress()
	result, err := s.repo.GetByCartID(s.ctx, s.db, sample.CartID)
	assert.Nil(s.T(), result, "result should be nil")
	assert.Equal(s.T(), expected, err, "they should be equal")
}

func (s *RepositoryTestSuite) Test_GetByCartID_Failed() {
	expected := errors.New("failed to query delivery_addresses")
	s.mock.ExpectQuery(`SELECT (.+) FROM "delivery_addresses"`).
		WillReturnError(expected)

	sample := NewSampleDeliveryAddress()
	result, err := s.repo.GetByCartID(s.ctx, s.db, sample.CartID)
	assert.Nil(s.T(), result, "result should be nil")
	assert.Equal(s.T(), expected, err, "they should be equal")
}

func (s *RepositoryTestSuite) Test_GetByCartIDs_Success() {
	sample := NewSampleDeliveryAddress()

	s.mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "delivery_addresses" WHERE cart_id IN ($1)`)).
		WithArgs(sample.CartID).
		WillReturnRows(sqlmock.NewRows([]string{"address_id", "cart_id", "owner", "recipient_name", "recipient_phone_number", "version"}).
			AddRow(
				sample.AddressID,
				sample.CartID,
				sample.Owner,
				sample.RecipientName,
				sample.RecipientPhoneNumber,
				sample.Version,
			))

	s.mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "addresses" WHERE "addresses"."id" = $1 AND "addresses"."deleted_at" IS NULL`)).
		WithArgs(sample.AddressID).
		WillReturnRows(sqlmock.NewRows([]string{"id", "address_1", "address_2", "postal_code", "country_id", "deactivate", "unit_number", "created_at", "updated_at"}).
			AddRow(
				sample.AddressID,
				sample.Address.Address1,
				sample.Address.Address2,
				sample.Address.PostalCode,
				sample.Address.CountryID,
				sample.Address.Deactivate,
				sample.Address.UnitNumber,
				sample.Address.CreatedAt,
				sample.Address.UpdatedAt,
			))

	s.mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "countries" WHERE "countries"."id" = $1 AND "countries"."deleted_at" IS NULL`)).
		WithArgs(sample.Address.CountryID).
		WillReturnRows(sqlmock.NewRows([]string{"id", "code", "localized_name", "international_name", "created_at", "updated_at"}).
			AddRow(
				sample.Address.Country.ID.ID,
				sample.Address.Country.Code,
				sample.Address.Country.LocalizedName,
				sample.Address.Country.InternationalName,
				sample.Address.Country.CreatedAt,
				sample.Address.Country.UpdatedAt,
			))

	s.mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "address_sub_levels" WHERE "address_sub_levels"."address_id" = $1`)).
		WithArgs(sample.AddressID).
		WillReturnRows(func() (rows *sqlmock.Rows) {
			rows = sqlmock.NewRows([]string{"address_id", "location_id"})
			for _, v := range sample.Address.SubLevelLocations {
				rows.AddRow(sample.Address.ID.ID, v.ID.ID)
			}
			return rows
		}())

	s.mock.ExpectQuery(regexp.QuoteMeta(func() string {
		var arr []string
		for i := 1; i <= len(sample.Address.SubLevelLocations); i++ {
			arr = append(arr, fmt.Sprintf("$%d", i))
		}
		return fmt.Sprintf(`SELECT * FROM "locations" WHERE "locations"."id" IN (%s) AND "locations"."deleted_at" IS NULL`, strings.Join(arr, ","))
	}())).
		WithArgs(func() (ids []driver.Value) {
			for _, v := range sample.Address.SubLevelLocations {
				ids = append(ids, v.ID.ID)
			}
			return ids
		}()...).
		WillReturnRows(func() (rows *sqlmock.Rows) {
			rows = sqlmock.NewRows([]string{"id", "localized_name", "international_name", "country_id", "parent_id", "created_at", "updated_at"})
			for _, v := range sample.Address.SubLevelLocations {
				rows.AddRow(
					v.ID.ID,
					v.LocalizedName,
					v.InternationalName,
					v.CountryID,
					v.ParentID,
					v.CreatedAt,
					v.UpdatedAt,
				)
			}
			return rows
		}())

	expectedResult := []*entities.DeliveryAddress{&sample}
	actualResult, err := s.repo.GetByCartIDs(s.ctx, s.db, []string{sample.CartID})
	require.NoError(s.T(), err)
	assert.Equal(s.T(), expectedResult, actualResult, "they should be equal")
}

func (s *RepositoryTestSuite) Test_GetByCartIDs_NotFound() {
	expected := make([]*entities.DeliveryAddress, 0)
	s.mock.ExpectQuery(`SELECT (.+) FROM "delivery_addresses"`).
		WillReturnRows(sqlmock.NewRows([]string{"address_id", "cart_id", "owner", "recipient_name", "recipient_phone_number", "version"}))

	sample := NewSampleDeliveryAddress()
	actual, err := s.repo.GetByCartIDs(s.ctx, s.db, []string{sample.CartID})
	require.NoError(s.T(), err)
	assert.Equal(s.T(), expected, actual, "they should be equal")
}

func (s *RepositoryTestSuite) Test_GetByCartIDs_Failed() {
	expected := errors.New("failed to query delivery_addresses")
	s.mock.ExpectQuery(`SELECT (.+) FROM "delivery_addresses"`).WillReturnError(expected)

	sample := NewSampleDeliveryAddress()
	actual, err := s.repo.GetByCartIDs(s.ctx, s.db, []string{sample.CartID})
	require.Error(s.T(), err)
	assert.Equal(s.T(), expected, err, "they should be equal")
	assert.Nil(s.T(), actual, "result should be nil")
}

func (s *RepositoryTestSuite) Test_Create_Success() {
	s.mock.ExpectBegin()
	s.mock.ExpectQuery(`INSERT INTO "countries"`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1))
	s.mock.ExpectQuery(`INSERT INTO "addresses"`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1))
	s.mock.ExpectQuery(`INSERT INTO "locations"`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1))
	s.mock.ExpectExec(`INSERT INTO "address_sub_levels"`).WillReturnResult(sqlmock.NewResult(0, 0))
	s.mock.ExpectExec(`INSERT INTO "delivery_addresses"`).WillReturnResult(sqlmock.NewResult(0, 0))
	s.mock.ExpectCommit()

	sample := NewSampleDeliveryAddress()
	err := s.repo.Create(s.ctx, s.db, &sample)
	require.NoError(s.T(), err)
	assert.Equal(s.T(), int64(1), sample.AddressID, "they should be equal")
}

func (s *RepositoryTestSuite) Test_Create_Failed() {
	expected := errors.New("failed to query delivery_addresses")
	s.mock.ExpectBegin()
	s.mock.ExpectQuery(`INSERT INTO "countries"`).WillReturnError(expected)
	s.mock.ExpectRollback()

	sample := NewSampleDeliveryAddress()
	err := s.repo.Create(s.ctx, s.db, &sample)
	require.Error(s.T(), err)
	assert.Equal(s.T(), expected, err, "they should be equal")
}

func (s *RepositoryTestSuite) Test_Update_Success() {
	s.mock.ExpectBegin()
	s.mock.ExpectExec(`UPDATE "delivery_addresses"`).WillReturnResult(sqlmock.NewResult(0, 1))
	s.mock.ExpectCommit()

	sample := NewSampleDeliveryAddress()
	err := s.repo.Update(s.ctx, s.db, sample.CartID, map[string]interface{}{
		"AddressID":            sample.AddressID,
		"Owner":                sample.Owner,
		"RecipientName":        sample.RecipientName,
		"RecipientPhoneNumber": sample.RecipientPhoneNumber,
		"Version":              sample.Version,
	})
	require.NoError(s.T(), err)
}

func (s *RepositoryTestSuite) Test_Update_Failed() {
	expected := errors.New("failed to update delivery_addresses")
	s.mock.ExpectBegin()
	s.mock.ExpectExec(`UPDATE "delivery_addresses"`).WillReturnError(expected)
	s.mock.ExpectRollback()

	sample := NewSampleDeliveryAddress()
	err := s.repo.Update(s.ctx, s.db, sample.CartID, map[string]interface{}{
		"AddressID":            sample.AddressID,
		"Owner":                sample.Owner,
		"RecipientName":        sample.RecipientName,
		"RecipientPhoneNumber": sample.RecipientPhoneNumber,
		"Version":              sample.Version,
	})
	require.Error(s.T(), err)
	assert.Equal(s.T(), expected, err, "they should be equal")
}

func TestDeliveryAddressRepository(t *testing.T) {
	suite.Run(t, new(RepositoryTestSuite))
}

func BaseEntityFromInt64(val int64) entities.BaseEntity {
	return entities.BaseEntity{
		ID: entities.IDFromInt64(val),
		AuditTime: entities.AuditTime{
			CreatedAt: time.Now(),
			UpdatedAt: time.Now(),
		},
	}
}

func NewSampleDeliveryAddress() entities.DeliveryAddress {
	addressID := int64(1327)
	countryID := int64(3)
	return entities.DeliveryAddress{
		AddressID: addressID,
		Address: entities.Address{
			BaseEntity: BaseEntityFromInt64(addressID),
			CountryID:  countryID,
			Address1:   "600 Nguyen Luong Bang",
			Address2:   "Phu My",
			PostalCode: "1010",
			UnitNumber: "B1",
			Deactivate: false,
			Country: entities.Country{
				BaseEntity:        BaseEntityFromInt64(countryID),
				Code:              "PH",
				LocalizedName:     "Pilipinas",
				InternationalName: "Philippines",
			},
			SubLevelLocations: []entities.Location{
				{
					BaseEntity:        BaseEntityFromInt64(288),
					CountryID:         countryID,
					ParentID:          0,
					InternationalName: "Metro Manila",
					LocalizedName:     "Metro Manila",
				},
				{
					BaseEntity:        BaseEntityFromInt64(1660),
					CountryID:         countryID,
					ParentID:          288,
					InternationalName: "San Nicolas",
					LocalizedName:     "San Nicolas",
				},
				{
					BaseEntity:        BaseEntityFromInt64(37595),
					CountryID:         countryID,
					ParentID:          1660,
					InternationalName: "Barangay 281",
					LocalizedName:     "Barangay 281",
				},
			},
		},
		CartID:               "778ce8c4-6278-4e7d-90d0-6b465c5c7f5e",
		Owner:                "facebook|5146483818773603",
		RecipientName:        "Poogky",
		RecipientPhoneNumber: "6312345678",
		Version:              1,
	}
}
