package accountapiimp

import (
	"context"
	"errors"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/tech/sendico/pkg/db/repository/builder"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/model"
	"github.com/tech/sendico/server/interface/api/srequest"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.uber.org/zap"
)

// Helper function to create string pointers
func stringPtr(s string) *string {
	return &s
}

// TestTimezoneValidation tests timezone validation logic separately
func TestTimezoneValidation(t *testing.T) {
	t.Run("ValidTimezones", func(t *testing.T) {
		validTimezones := []string{
			"UTC",
			"America/New_York",
			"Europe/London",
			"Asia/Tokyo",
			"Australia/Sydney",
		}

		for _, tz := range validTimezones {
			t.Run(tz, func(t *testing.T) {
				_, err := time.LoadLocation(tz)
				assert.NoError(t, err, "Timezone %s should be valid", tz)
			})
		}
	})

	t.Run("InvalidTimezones", func(t *testing.T) {
		invalidTimezones := []string{
			"Invalid/Timezone",
			"Not/A/Timezone",
			"BadTimezone",
			"America/NotACity",
		}

		for _, tz := range invalidTimezones {
			t.Run(tz, func(t *testing.T) {
				_, err := time.LoadLocation(tz)
				assert.Error(t, err, "Timezone %s should be invalid", tz)
			})
		}
	})
}

// TestCreateValidSignupRequest tests the helper function for creating valid requests
func TestCreateValidSignupRequest(t *testing.T) {
	request := srequest.Signup{
		Account: model.AccountData{
			LoginData: model.LoginData{
				UserDataBase: model.UserDataBase{
					Login: "test@example.com",
				},
				Password: "TestPassword123!",
			},
			Describable: model.Describable{
				Name: "Test User",
			},
		},
		Organization: model.Describable{
			Name: "Test Organization",
		},
		OrganizationTimeZone: "UTC",
		OwnerRole: model.Describable{
			Name: "Owner",
		},
	}

	// Validate the request structure
	assert.Equal(t, "test@example.com", request.Account.Login)
	assert.Equal(t, "TestPassword123!", request.Account.Password)
	assert.Equal(t, "Test User", request.Account.Name)
	assert.Equal(t, "Test Organization", request.Organization.Name)
	assert.Equal(t, "UTC", request.OrganizationTimeZone)
}

// TestSignupRequestValidation tests various signup request validation scenarios
func TestSignupRequestValidation(t *testing.T) {
	t.Run("ValidRequest", func(t *testing.T) {
		request := srequest.Signup{
			Account: model.AccountData{
				LoginData: model.LoginData{
					UserDataBase: model.UserDataBase{
						Login: "test@example.com",
					},
					Password: "TestPassword123!",
				},
				Describable: model.Describable{
					Name: "Test User",
				},
			},
			Organization: model.Describable{
				Name: "Test Organization",
			},
			OrganizationTimeZone: "UTC",
		}

		// Basic validation - all required fields present
		assert.NotEmpty(t, request.Account.Login)
		assert.NotEmpty(t, request.Account.Password)
		assert.NotEmpty(t, request.Account.Name)
		assert.NotEmpty(t, request.Organization.Name)
		assert.NotEmpty(t, request.OrganizationTimeZone)
	})

	t.Run("EmailFormats", func(t *testing.T) {
		validEmails := []string{
			"test@example.com",
			"user.name@example.com",
			"user+tag@example.org",
			"test123@domain.co.uk",
		}

		for _, email := range validEmails {
			t.Run(email, func(t *testing.T) {
				request := srequest.Signup{
					Account: model.AccountData{
						LoginData: model.LoginData{
							UserDataBase: model.UserDataBase{
								Login: email,
							},
						},
					},
				}
				assert.Equal(t, email, request.Account.Login)
				assert.Contains(t, email, "@")
				assert.Contains(t, email, ".")
			})
		}
	})

	t.Run("PasswordComplexity", func(t *testing.T) {
		passwordTests := []struct {
			name     string
			password string
			valid    bool
		}{
			{"Strong", "TestPassword123!", true},
			{"WithNumbers", "MyPass123!", true},
			{"WithSymbols", "Complex@Pass1", true},
			{"TooShort", "Test1!", false},
			{"NoNumbers", "TestPassword!", false},
			{"NoSymbols", "TestPassword123", false},
			{"NoUppercase", "testpassword123!", false},
			{"NoLowercase", "TESTPASSWORD123!", false},
		}

		for _, tt := range passwordTests {
			t.Run(tt.name, func(t *testing.T) {
				request := srequest.Signup{
					Account: model.AccountData{
						LoginData: model.LoginData{
							Password: tt.password,
						},
					},
				}

				// Basic structure validation
				assert.Equal(t, tt.password, request.Account.Password)

				if tt.valid {
					assert.True(t, len(tt.password) >= 8, "Password should be at least 8 characters")
				} else {
					// For invalid passwords, at least one condition should fail
					hasDigit := false
					hasUpper := false
					hasLower := false
					hasSpecial := false

					for _, char := range tt.password {
						switch {
						case char >= '0' && char <= '9':
							hasDigit = true
						case char >= 'A' && char <= 'Z':
							hasUpper = true
						case char >= 'a' && char <= 'z':
							hasLower = true
						case char >= '!' && char <= '/' || char >= ':' && char <= '@':
							hasSpecial = true
						}
					}

					// At least one requirement should fail for invalid passwords
					if len(tt.password) >= 8 {
						assert.False(t, hasDigit && hasUpper && hasLower && hasSpecial,
							"Password %s should fail at least one requirement", tt.password)
					}
				}
			})
		}
	})
}

// TestAccountDataToAccount tests the ToAccount method
func TestAccountDataToAccount(t *testing.T) {
	accountData := model.AccountData{
		LoginData: model.LoginData{
			UserDataBase: model.UserDataBase{
				Login: "test@example.com",
			},
			Password: "TestPassword123!",
		},
		Describable: model.Describable{
			Name: "Test User",
		},
	}

	account := accountData.ToAccount()

	assert.Equal(t, accountData.Login, account.Login)
	assert.Equal(t, accountData.Password, account.Password)
	assert.Equal(t, accountData.Name, account.Name)

	// Verify the account has proper structure
	assert.NotNil(t, account)
	assert.IsType(t, &model.Account{}, account)
}

// TestColorValidation tests that colors are properly formatted
func TestColorValidation(t *testing.T) {
	validColors := []string{
		"#FF0000", // Red
		"#00FF00", // Green
		"#0000FF", // Blue
		"#FFFFFF", // White
		"#000000", // Black
		"#FF8000", // Orange
	}

	for _, color := range validColors {
		t.Run(color, func(t *testing.T) {
			colorPtr := stringPtr(color)
			assert.NotNil(t, colorPtr)
			assert.Equal(t, color, *colorPtr)
			assert.True(t, len(color) == 7, "Color should be 7 characters long")
			assert.True(t, color[0] == '#', "Color should start with #")
		})
	}
}

type stubAccountDB struct {
	result *model.Account
	err    error
}

func (s *stubAccountDB) GetByEmail(ctx context.Context, email string) (*model.Account, error) {
	return s.result, s.err
}

func (s *stubAccountDB) GetByToken(ctx context.Context, email string) (*model.Account, error) {
	return nil, merrors.NotImplemented("stub")
}

func (s *stubAccountDB) GetAccountsByRefs(ctx context.Context, orgRef bson.ObjectID, refs []bson.ObjectID) ([]model.Account, error) {
	return nil, merrors.NotImplemented("stub")
}

func (s *stubAccountDB) Create(ctx context.Context, object *model.Account) error {
	return merrors.NotImplemented("stub")
}

func (s *stubAccountDB) InsertMany(ctx context.Context, objects []*model.Account) error {
	return merrors.NotImplemented("stub")
}

func (s *stubAccountDB) Get(ctx context.Context, objectRef bson.ObjectID, result *model.Account) error {
	return merrors.NotImplemented("stub")
}

func (s *stubAccountDB) Update(ctx context.Context, object *model.Account) error {
	return merrors.NotImplemented("stub")
}

func (s *stubAccountDB) Patch(ctx context.Context, objectRef bson.ObjectID, patch builder.Patch) error {
	return merrors.NotImplemented("stub")
}

func (s *stubAccountDB) Delete(ctx context.Context, objectRef bson.ObjectID) error {
	return merrors.NotImplemented("stub")
}

func (s *stubAccountDB) DeleteMany(ctx context.Context, query builder.Query) error {
	return merrors.NotImplemented("stub")
}

func (s *stubAccountDB) DeleteCascade(ctx context.Context, objectRef bson.ObjectID) error {
	return merrors.NotImplemented("stub")
}

func (s *stubAccountDB) FindOne(ctx context.Context, query builder.Query, result *model.Account) error {
	return merrors.NotImplemented("stub")
}

func TestEnsureLoginAvailable(t *testing.T) {
	ctx := context.Background()
	logger := zap.NewNop()

	t.Run("available", func(t *testing.T) {
		api := &AccountAPI{
			logger: logger,
			db: &stubAccountDB{
				err: merrors.ErrNoData,
			},
		}
		assert.NoError(t, api.ensureLoginAvailable(ctx, "new@example.com"))
	})

	t.Run("taken", func(t *testing.T) {
		api := &AccountAPI{
			logger: logger,
			db: &stubAccountDB{
				result: &model.Account{},
			},
		}
		err := api.ensureLoginAvailable(ctx, "used@example.com")
		assert.Error(t, err)
		assert.True(t, errors.Is(err, merrors.ErrDataConflict))
	})

	t.Run("invalid login", func(t *testing.T) {
		api := &AccountAPI{
			logger: logger,
			db: &stubAccountDB{
				err: merrors.ErrNoData,
			},
		}
		err := api.ensureLoginAvailable(ctx, "  ")
		assert.Error(t, err)
		assert.True(t, errors.Is(err, merrors.ErrInvalidArg))
	})

	t.Run("db error", func(t *testing.T) {
		api := &AccountAPI{
			logger: logger,
			db: &stubAccountDB{
				err: errors.New("boom"),
			},
		}
		err := api.ensureLoginAvailable(ctx, "err@example.com")
		assert.EqualError(t, err, "boom")
	})
}
