package repo

import (
	"fmt"
	"os"
	"reflect"
	"strings"
	"testing"
	"time"

	"github.com/go-gorp/gorp"
	"github.com/kylelemons/godebug/pretty"

	"github.com/coreos/dex/db"
	"github.com/coreos/dex/user"
)

var (
	testUsers = []user.UserWithRemoteIdentities{
		{
			User: user.User{
				ID:        "ID-1",
				Email:     "Email-1@example.com",
				CreatedAt: time.Now().Truncate(time.Second),
			},
			RemoteIdentities: []user.RemoteIdentity{
				{
					ConnectorID: "IDPC-1",
					ID:          "RID-1",
				},
			},
		},
		{
			User: user.User{
				ID:        "ID-2",
				Email:     "Email-2@example.com",
				CreatedAt: time.Now(),
				Disabled:  true,
			},
			RemoteIdentities: []user.RemoteIdentity{
				{
					ConnectorID: "IDPC-2",
					ID:          "RID-2",
				},
			},
		},
	}
)

func newUserRepo(t *testing.T, users []user.UserWithRemoteIdentities) user.UserRepo {
	if users == nil {
		users = []user.UserWithRemoteIdentities{}
	}
	var dbMap *gorp.DbMap
	if os.Getenv("DEX_TEST_DSN") == "" {
		dbMap = db.NewMemDB()
	} else {
		dbMap = connect(t)
	}
	repo, err := db.NewUserRepoFromUsers(dbMap, users)
	if err != nil {
		t.Fatalf("Unable to add users: %v", err)
	}
	return repo
}

func TestNewUser(t *testing.T) {
	now := time.Now().UTC().Truncate(time.Second)
	tests := []struct {
		user user.User
		err  error
	}{
		{
			user: user.User{
				ID:        "ID-bob",
				Email:     "bob@example.com",
				CreatedAt: now,
			},
			err: nil,
		},
		{
			user: user.User{
				ID:        "ID-admin",
				Email:     "admin@example.com",
				Admin:     true,
				CreatedAt: now,
			},
			err: nil,
		},
		{
			user: user.User{
				ID:            "ID-verified",
				Email:         "verified@example.com",
				EmailVerified: true,
				CreatedAt:     now,
			},
			err: nil,
		},
		{
			user: user.User{
				ID:          "ID-same",
				Email:       "Email-1@example.com",
				DisplayName: "Oops Same Email",
				CreatedAt:   now,
			},
			err: user.ErrorDuplicateEmail,
		},
		{
			user: user.User{
				ID:          "ID-same",
				Email:       "email-1@example.com",
				DisplayName: "A lower case version of the original email",
				CreatedAt:   now,
			},
			err: user.ErrorDuplicateEmail,
		},
		{
			user: user.User{
				Email:       "AnotherEmail@example.com",
				DisplayName: "Can't set your own ID!",
				CreatedAt:   now,
			},
			err: user.ErrorInvalidID,
		},
		{
			user: user.User{
				ID:          "ID-noemail",
				DisplayName: "No Email",
				CreatedAt:   now,
			},
			err: user.ErrorInvalidEmail,
		},
	}

	for i, tt := range tests {
		repo := newUserRepo(t, testUsers)
		err := repo.Create(nil, tt.user)
		if tt.err != nil {
			if err != tt.err {
				t.Errorf("case %d: want=%v, got=%v", i, tt.err, err)
			}
		} else {
			if err != nil {
				t.Errorf("case %d: want nil err, got %v", i, err)
			}

			gotUser, err := repo.Get(nil, tt.user.ID)
			if err != nil {
				t.Errorf("case %d: want nil err, got %v", i, err)
			}

			if diff := pretty.Compare(tt.user, gotUser); diff != "" {
				t.Errorf("case %d: Compare(want, got) = %v", i,
					diff)
			}
		}
	}
}

func TestUpdateUser(t *testing.T) {
	tests := []struct {
		user user.User
		err  error
	}{
		{
			// Update the email.
			user: user.User{
				ID:    "ID-1",
				Email: "Email-1.1@example.com",
			},
			err: nil,
		},
		{
			// No-op.
			user: user.User{
				ID:    "ID-1",
				Email: "Email-1@example.com",
			},
			err: nil,
		},
		{
			// No email.
			user: user.User{
				ID:    "ID-1",
				Email: "",
			},
			err: user.ErrorInvalidEmail,
		},
		{
			// Try Update on non-existent user.
			user: user.User{
				ID:    "NonExistent",
				Email: "GoodEmail@email.com",
			},
			err: user.ErrorNotFound,
		},
		{
			// Try update to someone else's email.
			user: user.User{
				ID:    "ID-2",
				Email: "Email-1@example.com",
			},
			err: user.ErrorDuplicateEmail,
		},
	}

	for i, tt := range tests {
		repo := newUserRepo(t, testUsers)
		err := repo.Update(nil, tt.user)
		if tt.err != nil {
			if err != tt.err {
				t.Errorf("case %d: want=%q, got=%q", i, tt.err, err)
			}
		} else {
			if err != nil {
				t.Errorf("case %d: want nil err, got %q", i, err)
			}

			gotUser, err := repo.Get(nil, tt.user.ID)
			if err != nil {
				t.Errorf("case %d: want nil err, got %q", i, err)
			}

			tt.user.Email = strings.ToLower(tt.user.Email)
			if diff := pretty.Compare(tt.user, gotUser); diff != "" {
				t.Errorf("case %d: Compare(want, got) = %v", i,
					diff)
			}
		}
	}
}

func TestDisableUser(t *testing.T) {
	tests := []struct {
		id      string
		disable bool
		err     error
	}{
		{
			id: "ID-1",
		},
		{
			id:      "ID-1",
			disable: true,
		},
		{
			id: "ID-2",
		},
		{
			id:      "ID-2",
			disable: true,
		},
		{
			id:  "NO SUCH ID",
			err: user.ErrorNotFound,
		},
		{
			id:      "NO SUCH ID",
			err:     user.ErrorNotFound,
			disable: true,
		},
		{
			id:  "",
			err: user.ErrorInvalidID,
		},
	}

	for i, tt := range tests {
		repo := newUserRepo(t, testUsers)
		err := repo.Disable(nil, tt.id, tt.disable)
		switch {
		case err != tt.err:
			t.Errorf("case %d: want=%q, got=%q", i, tt.err, err)
		case tt.err == nil:
			gotUser, err := repo.Get(nil, tt.id)
			if err != nil {
				t.Fatalf("case %d: want nil err, got %q", i, err)
			}

			if gotUser.Disabled != tt.disable {
				t.Errorf("case %d: disabled status want=%v got=%v",
					i, tt.disable, gotUser.Disabled)
			}
		}
	}
}

func TestAttachRemoteIdentity(t *testing.T) {
	tests := []struct {
		id  string
		rid user.RemoteIdentity
		err error
	}{
		{
			id: "ID-1",
			rid: user.RemoteIdentity{
				ConnectorID: "IDPC-1",
				ID:          "RID-1.1",
			},
		},
		{
			id: "ID-1",
			rid: user.RemoteIdentity{
				ConnectorID: "IDPC-2",
				ID:          "RID-2",
			},
			err: user.ErrorDuplicateRemoteIdentity,
		},
		{
			id: "NoSuchUser",
			rid: user.RemoteIdentity{
				ConnectorID: "IDPC-3",
				ID:          "RID-3",
			},
			err: user.ErrorNotFound,
		},
	}

	for i, tt := range tests {
		repo := newUserRepo(t, testUsers)
		err := repo.AddRemoteIdentity(nil, tt.id, tt.rid)
		if tt.err != nil {
			if err != tt.err {
				t.Errorf("case %d: want=%q, got=%q", i, tt.err, err)
			}
		} else {
			if err != nil {
				t.Errorf("case %d: want nil err, got %q", i, err)
			}

			gotUser, err := repo.GetByRemoteIdentity(nil, tt.rid)
			if err != nil {
				t.Errorf("case %d: want nil err, got %q", i, err)
			}

			wantUser, err := repo.Get(nil, tt.id)
			if err != nil {
				t.Errorf("case %d: want nil err, got %q", i, err)
			}

			gotRIDs, err := repo.GetRemoteIdentities(nil, tt.id)
			if err != nil {
				t.Errorf("case %d: want nil err, got %q", i, err)
			}

			if findRemoteIdentity(gotRIDs, tt.rid) == -1 {
				t.Errorf("case %d: user.RemoteIdentity not found", i)
			}

			if !reflect.DeepEqual(wantUser, gotUser) {
				t.Errorf("case %d: want=%#v, got=%#v", i,
					wantUser, gotUser)
			}
		}
	}
}

func TestRemoveRemoteIdentity(t *testing.T) {
	tests := []struct {
		id  string
		rid user.RemoteIdentity
		err error
	}{
		{
			id: "ID-1",
			rid: user.RemoteIdentity{
				ConnectorID: "IDPC-1",
				ID:          "RID-1",
			},
		},
		{
			id: "ID-1",
			rid: user.RemoteIdentity{
				ConnectorID: "IDPC-2",
				ID:          "RID-2",
			},
			err: user.ErrorNotFound,
		},
		{
			id: "NoSuchUser",
			rid: user.RemoteIdentity{
				ConnectorID: "IDPC-3",
				ID:          "RID-3",
			},
			err: user.ErrorNotFound,
		},
	}

	for i, tt := range tests {
		repo := newUserRepo(t, testUsers)
		err := repo.RemoveRemoteIdentity(nil, tt.id, tt.rid)
		if tt.err != nil {
			if err != tt.err {
				t.Errorf("case %d: want=%q, got=%q", i, tt.err, err)
			}
		} else {
			if err != nil {
				t.Errorf("case %d: want nil err, got %q", i, err)
			}

			gotUser, err := repo.GetByRemoteIdentity(nil, tt.rid)
			if err == nil {
				if gotUser.ID == tt.id {
					t.Errorf("case %d: user found.", i)

				}
			} else if err != user.ErrorNotFound {
				t.Errorf("case %d: want %q err, got %q err", i, user.ErrorNotFound, err)
			}

			gotRIDs, err := repo.GetRemoteIdentities(nil, tt.id)
			if err != nil {
				t.Errorf("case %d: want nil err, got %q", i, err)
			}

			if findRemoteIdentity(gotRIDs, tt.rid) != -1 {
				t.Errorf("case %d: user.RemoteIdentity found", i)
			}

		}
	}
}

func findRemoteIdentity(rids []user.RemoteIdentity, rid user.RemoteIdentity) int {
	for i, curRID := range rids {
		if curRID == rid {
			return i
		}
	}
	return -1
}

func TestGetByEmail(t *testing.T) {
	tests := []struct {
		email     string
		wantEmail string
		wantErr   error
	}{
		{
			email:     "Email-1@example.com",
			wantEmail: "email-1@example.com",
			wantErr:   nil,
		},
		{
			email:     "EMAIL-1@example.com", // Emails should be case insensitive.
			wantEmail: "email-1@example.com",
			wantErr:   nil,
		},
		{
			email:   "NoSuchEmail@example.com",
			wantErr: user.ErrorNotFound,
		},
	}

	for i, tt := range tests {
		repo := newUserRepo(t, testUsers)
		gotUser, gotErr := repo.GetByEmail(nil, tt.email)
		if tt.wantErr != nil {
			if tt.wantErr != gotErr {
				t.Errorf("case %d: wantErr=%q, gotErr=%q", i, tt.wantErr, gotErr)
			}
			continue
		}

		if gotErr != nil {
			t.Errorf("case %d: want nil err:% q", i, gotErr)
			continue
		}

		if tt.wantEmail != gotUser.Email {
			t.Errorf("case %d: want=%q, got=%q", i, tt.email, gotUser.Email)
		}
	}
}

func TestGetAdminCount(t *testing.T) {
	tests := []struct {
		addUsers []user.User
		want     int
	}{
		{
			addUsers: []user.User{
				user.User{
					ID:    "ID-admin",
					Email: "Admin@example.com",
					Admin: true,
				},
			},
			want: 1,
		},
		{
			want: 0,
		},
		{
			addUsers: []user.User{
				user.User{
					ID:    "ID-admin",
					Email: "NotAdmin@example.com",
				},
			},
			want: 0,
		},
		{
			addUsers: []user.User{
				user.User{
					ID:    "ID-admin",
					Email: "Admin@example.com",
					Admin: true,
				},
				user.User{
					ID:    "ID-admin2",
					Email: "AnotherAdmin@example.com",
					Admin: true,
				},
			},
			want: 2,
		},
	}

	for i, tt := range tests {
		repo := newUserRepo(t, testUsers)
		for _, addUser := range tt.addUsers {
			err := repo.Create(nil, addUser)
			if err != nil {
				t.Fatalf("case %d: couldn't add user: %q", i, err)
			}
		}

		got, err := repo.GetAdminCount(nil)
		if err != nil {
			t.Errorf("case %d: couldn't get admin count: %q", i, err)
			continue
		}

		if tt.want != got {
			t.Errorf("case %d: want=%d, got=%d", i, tt.want, got)
		}
	}
}

func TestList(t *testing.T) {
	repoUsers := []user.UserWithRemoteIdentities{}
	for i := 0; i < 10; i++ {
		repoUsers = append(repoUsers, user.UserWithRemoteIdentities{
			User: user.User{
				ID:    fmt.Sprintf("%d", i),
				Email: fmt.Sprintf("%d@example.com", i),
			},
		})

	}
	tests := []struct {
		filter      user.UserFilter
		maxResults  int
		expectedIDs [][]string
	}{
		{
			maxResults:  5,
			expectedIDs: [][]string{{"0", "1", "2", "3", "4"}, {"5", "6", "7", "8", "9"}},
		},
		{
			maxResults:  3,
			expectedIDs: [][]string{{"0", "1", "2"}, {"3", "4", "5"}, {"6", "7", "8"}, {"9"}},
		},
		{
			maxResults:  9,
			expectedIDs: [][]string{{"0", "1", "2", "3", "4", "5", "6", "7", "8"}, {"9"}},
		},
		{
			maxResults:  10,
			expectedIDs: [][]string{{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"}},
		},
	}

	for i, tt := range tests {
		repo := newUserRepo(t, repoUsers)
		var tok string
		gotIDs := [][]string{}
		done := false
		for !done {
			var users []user.User
			var err error
			users, tok, err = repo.List(nil, tt.filter, tt.maxResults, tok)
			if err != nil {
				t.Errorf("case %d: unexpected err: %v", i, err)
				done = true
				continue
			}
			ids := []string{}
			for _, user := range users {
				ids = append(ids, user.ID)
			}
			gotIDs = append(gotIDs, ids)
			if tok == "" {
				done = true
			}
		}
		if diff := pretty.Compare(tt.expectedIDs, gotIDs); diff != "" {
			t.Errorf("case %d: Compare(want, got) = %v", i,
				diff)
		}
	}
}

func TestListErrorNotFound(t *testing.T) {
	repo := newUserRepo(t, nil)
	_, _, err := repo.List(nil, user.UserFilter{}, 10, "")
	if err != user.ErrorNotFound {
		t.Errorf("want=%q, got=%q", user.ErrorNotFound, err)
	}
}
