package invite

import (
	"os"
	"strings"
	"testing"
	"time"

	"geeks-accelerator/oss/saas-starter-kit/internal/account"
	"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
	"geeks-accelerator/oss/saas-starter-kit/internal/platform/notify"
	"geeks-accelerator/oss/saas-starter-kit/internal/platform/tests"
	"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
	"geeks-accelerator/oss/saas-starter-kit/internal/user"
	"geeks-accelerator/oss/saas-starter-kit/internal/user_account"
	"github.com/dgrijalva/jwt-go"
	"github.com/pborman/uuid"
	"github.com/pkg/errors"
)

var (
	test *tests.Test
	repo *Repository
)

// TestMain is the entry point for testing.
func TestMain(m *testing.M) {
	os.Exit(testMain(m))
}

func testMain(m *testing.M) int {
	test = tests.New()
	defer test.TearDown()

	userRepo := user.MockRepository(test.MasterDB)
	userAccRepo := user_account.NewRepository(test.MasterDB)
	accRepo := account.NewRepository(test.MasterDB)

	// Mock the methods needed to make an invite.
	resetUrl := func(string) string {
		return ""
	}
	notify := &notify.MockEmail{}
	secretKey := "6368616e676520746869732070613434"

	repo = NewRepository(test.MasterDB, userRepo, userAccRepo, accRepo, resetUrl, notify, secretKey)

	return m.Run()
}

// TestSendUserInvites validates that invite users works.
func TestSendUserInvites(t *testing.T) {

	t.Log("Given the need ensure a user an invite users to their account.")
	{
		ctx := tests.Context()

		now := time.Date(2018, time.October, 1, 0, 0, 0, 0, time.UTC)

		// Create a new user for testing.
		initPass := uuid.NewRandom().String()
		u, err := repo.User.Create(ctx, auth.Claims{}, user.UserCreateRequest{
			FirstName:       "Lee",
			LastName:        "Brown",
			Email:           uuid.NewRandom().String() + "@geeksinthewoods.com",
			Password:        initPass,
			PasswordConfirm: initPass,
		}, now)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tCreate user failed.", tests.Failed)
		}

		a, err := repo.Account.Create(ctx, auth.Claims{}, account.AccountCreateRequest{
			Name:     uuid.NewRandom().String(),
			Address1: "101 E Main",
			City:     "Valdez",
			Region:   "AK",
			Country:  "US",
			Zipcode:  "99686",
		}, now)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tCreate account failed.", tests.Failed)
		}

		uRoles := []user_account.UserAccountRole{user_account.UserAccountRole_Admin}
		_, err = repo.UserAccount.Create(ctx, auth.Claims{}, user_account.UserAccountCreateRequest{
			UserID:    u.ID,
			AccountID: a.ID,
			Roles:     uRoles,
		}, now)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tCreate account failed.", tests.Failed)
		}

		claims := auth.Claims{
			AccountIDs: []string{a.ID},
			StandardClaims: jwt.StandardClaims{
				Subject:   u.ID,
				Audience:  a.ID,
				IssuedAt:  now.Unix(),
				ExpiresAt: now.Add(time.Hour).Unix(),
			},
		}
		for _, r := range uRoles {
			claims.Roles = append(claims.Roles, r.String())
		}

		// Ensure validation is working by trying ResetPassword with an empty request.
		{
			expectedErr := errors.New("Key: 'SendUserInvitesRequest.account_id' Error:Field validation for 'account_id' failed on the 'required' tag\n" +
				"Key: 'SendUserInvitesRequest.user_id' Error:Field validation for 'user_id' failed on the 'required' tag\n" +
				"Key: 'SendUserInvitesRequest.emails' Error:Field validation for 'emails' failed on the 'required' tag\n" +
				"Key: 'SendUserInvitesRequest.roles' Error:Field validation for 'roles' failed on the 'required' tag")
			_, err = repo.SendUserInvites(ctx, claims, SendUserInvitesRequest{}, now)
			if err == nil {
				t.Logf("\t\tWant: %+v", expectedErr)
				t.Fatalf("\t%s\tInviteUsers failed.", tests.Failed)
			}

			errStr := strings.Replace(err.Error(), "{{", "", -1)
			errStr = strings.Replace(errStr, "}}", "", -1)

			if errStr != expectedErr.Error() {
				t.Logf("\t\tGot : %+v", errStr)
				t.Logf("\t\tWant: %+v", expectedErr)
				t.Fatalf("\t%s\tInviteUsers Validation failed.", tests.Failed)
			}
			t.Logf("\t%s\tInviteUsers Validation ok.", tests.Success)
		}

		ttl := time.Hour

		inviteEmails := []string{
			uuid.NewRandom().String() + "@geeksinthewoods.com",
		}

		// Make the reset password request.
		inviteHashes, err := repo.SendUserInvites(ctx, claims, SendUserInvitesRequest{
			UserID:    u.ID,
			AccountID: a.ID,
			Emails:    inviteEmails,
			Roles:     []user_account.UserAccountRole{user_account.UserAccountRole_User},
			TTL:       ttl,
		}, now)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tInviteUsers failed.", tests.Failed)
		} else if len(inviteHashes) != len(inviteEmails) {
			t.Logf("\t\tGot : %+v", len(inviteHashes))
			t.Logf("\t\tWant: %+v", len(inviteEmails))
			t.Fatalf("\t%s\tInviteUsers failed.", tests.Failed)
		}
		t.Logf("\t%s\tInviteUsers ok.", tests.Success)

		// Ensure validation is working by trying ResetConfirm with an empty request.
		{
			expectedErr := errors.New("Key: 'AcceptInviteUserRequest.invite_hash' Error:Field validation for 'invite_hash' failed on the 'required' tag\n" +
				"Key: 'AcceptInviteUserRequest.email' Error:Field validation for 'email' failed on the 'required' tag\n" +
				"Key: 'AcceptInviteUserRequest.first_name' Error:Field validation for 'first_name' failed on the 'required' tag\n" +
				"Key: 'AcceptInviteUserRequest.last_name' Error:Field validation for 'last_name' failed on the 'required' tag\n" +
				"Key: 'AcceptInviteUserRequest.password' Error:Field validation for 'password' failed on the 'required' tag\n" +
				"Key: 'AcceptInviteUserRequest.password_confirm' Error:Field validation for 'password_confirm' failed on the 'required' tag")
			_, err = repo.AcceptInviteUser(ctx, AcceptInviteUserRequest{}, now)
			if err == nil {
				t.Logf("\t\tWant: %+v", expectedErr)
				t.Fatalf("\t%s\tResetConfirm failed.", tests.Failed)
			}

			errStr := strings.Replace(err.Error(), "{{", "", -1)
			errStr = strings.Replace(errStr, "}}", "", -1)

			if errStr != expectedErr.Error() {
				t.Logf("\t\tGot : %+v", errStr)
				t.Logf("\t\tWant: %+v", expectedErr)
				t.Fatalf("\t%s\tResetConfirm Validation failed.", tests.Failed)
			}
			t.Logf("\t%s\tResetConfirm Validation ok.", tests.Success)
		}

		// Ensure the TTL is enforced.
		{
			newPass := uuid.NewRandom().String()
			_, err = repo.AcceptInviteUser(ctx, AcceptInviteUserRequest{
				InviteHash:      inviteHashes[0],
				Email:           inviteEmails[0],
				FirstName:       "Foo",
				LastName:        "Bar",
				Password:        newPass,
				PasswordConfirm: newPass,
			}, now.UTC().Add(ttl*2))
			if errors.Cause(err) != ErrInviteExpired {
				t.Logf("\t\tGot : %+v", errors.Cause(err))
				t.Logf("\t\tWant: %+v", ErrInviteExpired)
				t.Fatalf("\t%s\tInviteAccept enforce TTL failed.", tests.Failed)
			}
			t.Logf("\t%s\tInviteAccept enforce TTL ok.", tests.Success)
		}

		// Assuming we have received the email and clicked the link, we now can ensure accept works.
		for idx, inviteHash := range inviteHashes {

			newPass := uuid.NewRandom().String()
			hash, err := repo.AcceptInviteUser(ctx, AcceptInviteUserRequest{
				InviteHash:      inviteHash,
				Email:           inviteEmails[idx],
				FirstName:       "Foo",
				LastName:        "Bar",
				Password:        newPass,
				PasswordConfirm: newPass,
			}, now)
			if err != nil {
				t.Log("\t\tGot :", err)
				t.Fatalf("\t%s\tInviteAccept failed.", tests.Failed)
			}

			// Validate the result.
			var res = struct {
				UserID    string `validate:"required,uuid"`
				AccountID string `validate:"required,uuid"`
			}{
				UserID:    hash.UserID,
				AccountID: hash.AccountID,
			}
			err = webcontext.Validator().StructCtx(ctx, res)
			if err != nil {
				t.Log("\t\tGot :", err)
				t.Fatalf("\t%s\tInviteAccept failed.", tests.Failed)
			}

			t.Logf("\t%s\tInviteAccept ok.", tests.Success)
		}

		// Ensure the reset hash does not work after its used.
		{
			newPass := uuid.NewRandom().String()
			_, err = repo.AcceptInviteUser(ctx, AcceptInviteUserRequest{
				InviteHash:      inviteHashes[0],
				Email:           inviteEmails[0],
				FirstName:       "Foo",
				LastName:        "Bar",
				Password:        newPass,
				PasswordConfirm: newPass,
			}, now)
			if errors.Cause(err) != ErrUserAccountActive {
				t.Logf("\t\tGot : %+v", errors.Cause(err))
				t.Logf("\t\tWant: %+v", ErrUserAccountActive)
				t.Fatalf("\t%s\tInviteAccept verify reuse failed.", tests.Failed)
			}
			t.Logf("\t%s\tInviteAccept verify reuse disabled ok.", tests.Success)
		}
	}
}
