package mysql

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"testing"

	"github.com/fleetdm/fleet/v4/server"
	"github.com/fleetdm/fleet/v4/server/test"

	"github.com/fleetdm/fleet/v4/server/fleet"
	"github.com/fleetdm/fleet/v4/server/ptr"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestUsers(t *testing.T) {
	ds := CreateMySQLDS(t)

	cases := []struct {
		name string
		fn   func(t *testing.T, ds *Datastore)
	}{
		{"Create", testUsersCreate},
		{"ByID", testUsersByID},
		{"Save", testUsersSave},
		{"List", testUsersList},
		{"Teams", testUsersTeams},
		{"CreateWithTeams", testUsersCreateWithTeams},
		{"SaveMany", testUsersSaveMany},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			defer TruncateTables(t, ds)
			c.fn(t, ds)
		})
	}
}

func testUsersCreate(t *testing.T, ds *Datastore) {
	createTests := []struct {
		password, email             string
		isAdmin, passwordReset, sso bool
		resultingPasswordReset      bool
	}{
		{"foobar", "mike@fleet.co", true, false, true, false},
		{"foobar", "jason@fleet.co", true, false, false, false},
		{"foobar", "jason2@fleet.co", true, true, true, false},
		{"foobar", "jason3@fleet.co", true, true, false, true},
	}

	for _, tt := range createTests {
		u := &fleet.User{
			Password:                 []byte(tt.password),
			AdminForcedPasswordReset: tt.passwordReset,
			Email:                    tt.email,
			SSOEnabled:               tt.sso,
			GlobalRole:               ptr.String(fleet.RoleObserver),
		}
		user, err := ds.NewUser(context.Background(), u)
		assert.Nil(t, err)

		verify, err := ds.UserByEmail(context.Background(), tt.email)
		assert.Nil(t, err)

		assert.Equal(t, user.ID, verify.ID)
		assert.Equal(t, tt.email, verify.Email)
		assert.Equal(t, tt.email, verify.Email)
		assert.Equal(t, tt.sso, verify.SSOEnabled)
		assert.Equal(t, tt.resultingPasswordReset, verify.AdminForcedPasswordReset)
	}
}

func testUsersByID(t *testing.T, ds *Datastore) {
	users := createTestUsers(t, ds)
	for _, tt := range users {
		returned, err := ds.UserByID(context.Background(), tt.ID)
		assert.Nil(t, err)
		assert.Equal(t, tt.ID, returned.ID)
	}

	// test missing user
	_, err := ds.UserByID(context.Background(), 10000000000)
	assert.NotNil(t, err)
}

func createTestUsers(t *testing.T, ds fleet.Datastore) []*fleet.User {
	createTests := []struct {
		password, email        string
		isAdmin, passwordReset bool
	}{
		{"foobar", "mike@fleet.co", true, false},
		{"foobar", "jason@fleet.co", false, false},
	}

	var users []*fleet.User
	for _, tt := range createTests {
		u := &fleet.User{
			Name:                     tt.email,
			Password:                 []byte(tt.password),
			AdminForcedPasswordReset: tt.passwordReset,
			Email:                    tt.email,
			GlobalRole:               ptr.String(fleet.RoleObserver),
		}

		user, err := ds.NewUser(context.Background(), u)
		assert.Nil(t, err)

		users = append(users, user)
	}
	assert.NotEmpty(t, users)
	return users
}

func testUsersSave(t *testing.T, ds *Datastore) {
	users := createTestUsers(t, ds)
	testUserGlobalRole(t, ds, users)
	testEmailAttribute(t, ds, users)
	testPasswordAttribute(t, ds, users)
}

func testPasswordAttribute(t *testing.T, ds fleet.Datastore, users []*fleet.User) {
	for _, user := range users {
		randomText, err := server.GenerateRandomText(8) // GenerateRandomText(8)
		assert.Nil(t, err)
		user.Password = []byte(randomText)
		err = ds.SaveUser(context.Background(), user)
		assert.Nil(t, err)

		verify, err := ds.UserByID(context.Background(), user.ID)
		assert.Nil(t, err)
		assert.Equal(t, user.Password, verify.Password)
	}
}

func testEmailAttribute(t *testing.T, ds fleet.Datastore, users []*fleet.User) {
	for _, user := range users {
		user.Email = fmt.Sprintf("test.%s", user.Email)
		err := ds.SaveUser(context.Background(), user)
		assert.Nil(t, err)

		verify, err := ds.UserByID(context.Background(), user.ID)
		assert.Nil(t, err)
		assert.Equal(t, user.Email, verify.Email)
	}
}

func testUserGlobalRole(t *testing.T, ds fleet.Datastore, users []*fleet.User) {
	for _, user := range users {
		user.GlobalRole = ptr.String("admin")
		err := ds.SaveUser(context.Background(), user)
		assert.Nil(t, err)

		verify, err := ds.UserByID(context.Background(), user.ID)
		assert.Nil(t, err)
		assert.Equal(t, user.GlobalRole, verify.GlobalRole)
	}
	err := ds.SaveUser(context.Background(), &fleet.User{
		Name:       "some@email.asd",
		Password:   []byte("asdasd"),
		Email:      "some@email.asd",
		GlobalRole: ptr.String(fleet.RoleObserver),
		Teams:      []fleet.UserTeam{{Role: fleet.RoleMaintainer}},
	})
	var ferr *fleet.Error
	require.True(t, errors.As(err, &ferr))
	assert.Equal(t, "Cannot specify both Global Role and Team Roles", ferr.Message)
}

func testUsersList(t *testing.T, ds *Datastore) {
	createTestUsers(t, ds)

	users, err := ds.ListUsers(context.Background(), fleet.UserListOptions{})
	assert.NoError(t, err)
	require.Len(t, users, 2)

	users, err = ds.ListUsers(context.Background(), fleet.UserListOptions{ListOptions: fleet.ListOptions{MatchQuery: "jason"}})
	assert.NoError(t, err)
	require.Len(t, users, 1)
	assert.Equal(t, "jason@fleet.co", users[0].Email)

	users, err = ds.ListUsers(context.Background(), fleet.UserListOptions{ListOptions: fleet.ListOptions{MatchQuery: "ike"}})
	assert.NoError(t, err)
	require.Len(t, users, 1)
	assert.Equal(t, "mike@fleet.co", users[0].Email)
}

func testUsersTeams(t *testing.T, ds *Datastore) {
	for i := 0; i < 10; i++ {
		_, err := ds.NewTeam(context.Background(), &fleet.Team{Name: fmt.Sprintf("%d", i)})
		require.NoError(t, err)
	}

	users := createTestUsers(t, ds)

	assert.Len(t, users[0].Teams, 0)
	assert.Len(t, users[1].Teams, 0)

	// Add invalid team should fail
	users[0].Teams = []fleet.UserTeam{
		{
			Team: fleet.Team{ID: 13},
			Role: "foobar",
		},
	}
	err := ds.SaveUser(context.Background(), users[0])
	require.Error(t, err)

	// Add valid team should succeed
	users[0].Teams = []fleet.UserTeam{
		{
			Team: fleet.Team{ID: 3},
			Role: fleet.RoleObserver,
		},
	}
	users[0].GlobalRole = nil
	err = ds.SaveUser(context.Background(), users[0])
	require.NoError(t, err)

	users, err = ds.ListUsers(
		context.Background(),
		fleet.UserListOptions{
			ListOptions: fleet.ListOptions{OrderKey: "name", OrderDirection: fleet.OrderDescending},
		},
	)
	require.NoError(t, err)

	assert.Len(t, users[0].Teams, 1)
	// For user with a global role, Teams should be empty
	assert.Len(t, users[1].Teams, 0)

	users[1].Teams = []fleet.UserTeam{
		{
			Team: fleet.Team{ID: 1},
			Role: fleet.RoleObserver,
		},
		{
			Team: fleet.Team{ID: 2},
			Role: fleet.RoleObserver,
		},
		{
			Team: fleet.Team{ID: 3},
			Role: fleet.RoleObserver,
		},
	}
	users[1].GlobalRole = nil
	err = ds.SaveUser(context.Background(), users[1])
	require.NoError(t, err)

	users, err = ds.ListUsers(
		context.Background(),
		fleet.UserListOptions{
			ListOptions: fleet.ListOptions{OrderKey: "name", OrderDirection: fleet.OrderDescending},
		},
	)
	require.NoError(t, err)

	assert.Len(t, users[0].Teams, 1)
	assert.Len(t, users[1].Teams, 3)

	// Clear teams
	users[1].Teams = []fleet.UserTeam{}
	users[1].GlobalRole = ptr.String(fleet.RoleObserver)
	err = ds.SaveUser(context.Background(), users[1])
	require.NoError(t, err)

	users, err = ds.ListUsers(
		context.Background(),
		fleet.UserListOptions{
			ListOptions: fleet.ListOptions{OrderKey: "name", OrderDirection: fleet.OrderDescending},
		},
	)
	require.NoError(t, err)

	assert.Len(t, users[0].Teams, 1)
	assert.Len(t, users[1].Teams, 0)
}

func testUsersCreateWithTeams(t *testing.T, ds *Datastore) {
	for i := 0; i < 10; i++ {
		_, err := ds.NewTeam(context.Background(), &fleet.Team{Name: fmt.Sprintf("%d", i)})
		require.NoError(t, err)
	}

	u := &fleet.User{
		Password: []byte("foo"),
		Teams: []fleet.UserTeam{
			{
				Team: fleet.Team{ID: 6},
				Role: fleet.RoleObserver,
			},
			{
				Team: fleet.Team{ID: 3},
				Role: fleet.RoleObserver,
			},
			{
				Team: fleet.Team{ID: 9},
				Role: fleet.RoleMaintainer,
			},
		},
	}
	user, err := ds.NewUser(context.Background(), u)
	assert.Nil(t, err)
	assert.Len(t, user.Teams, 3)

	user, err = ds.UserByID(context.Background(), user.ID)
	require.NoError(t, err)
	assert.Len(t, user.Teams, 3)

	assert.Equal(t, uint(3), user.Teams[0].ID)
	assert.Equal(t, "observer", user.Teams[0].Role)
	assert.Equal(t, uint(6), user.Teams[1].ID)
	assert.Equal(t, "observer", user.Teams[1].Role)
	assert.Equal(t, uint(9), user.Teams[2].ID)
	assert.Equal(t, "maintainer", user.Teams[2].Role)
}

func testUsersSaveMany(t *testing.T, ds *Datastore) {
	u1 := test.NewUser(t, ds, t.Name()+"Admin1", t.Name()+"admin1@fleet.co", true)
	u2 := test.NewUser(t, ds, t.Name()+"Admin2", t.Name()+"admin2@fleet.co", true)
	u3 := test.NewUser(t, ds, t.Name()+"Admin3", t.Name()+"admin3@fleet.co", true)

	u1.Email += "m"
	u2.Email += "m"
	u3.Email += "m"

	require.NoError(t, ds.SaveUsers(context.Background(), []*fleet.User{u1, u2, u3}))

	gotU1, err := ds.UserByID(context.Background(), u1.ID)
	require.NoError(t, err)
	assert.True(t, strings.HasSuffix(gotU1.Email, "fleet.com"))

	gotU2, err := ds.UserByID(context.Background(), u3.ID)
	require.NoError(t, err)
	assert.True(t, strings.HasSuffix(gotU2.Email, "fleet.com"))

	gotU3, err := ds.UserByID(context.Background(), u3.ID)
	require.NoError(t, err)
	assert.True(t, strings.HasSuffix(gotU3.Email, "fleet.com"))
}
