package m365

import (
	"context"
	"runtime/trace"
	"sync"
	"testing"
	"time"

	"github.com/alcionai/clues"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/stretchr/testify/suite"

	"github.com/alcionai/corso/src/internal/common/dttm"
	"github.com/alcionai/corso/src/internal/common/idname"
	inMock "github.com/alcionai/corso/src/internal/common/idname/mock"
	"github.com/alcionai/corso/src/internal/data"
	dataMock "github.com/alcionai/corso/src/internal/data/mock"
	exchMock "github.com/alcionai/corso/src/internal/m365/exchange/mock"
	"github.com/alcionai/corso/src/internal/m365/graph"
	"github.com/alcionai/corso/src/internal/m365/mock"
	"github.com/alcionai/corso/src/internal/m365/resource"
	"github.com/alcionai/corso/src/internal/m365/stub"
	"github.com/alcionai/corso/src/internal/m365/support"
	"github.com/alcionai/corso/src/internal/operations/inject"
	"github.com/alcionai/corso/src/internal/tester"
	"github.com/alcionai/corso/src/internal/tester/tconfig"
	"github.com/alcionai/corso/src/internal/version"
	"github.com/alcionai/corso/src/pkg/backup/details"
	"github.com/alcionai/corso/src/pkg/control"
	"github.com/alcionai/corso/src/pkg/control/testdata"
	"github.com/alcionai/corso/src/pkg/count"
	"github.com/alcionai/corso/src/pkg/fault"
	"github.com/alcionai/corso/src/pkg/path"
	"github.com/alcionai/corso/src/pkg/selectors"
	selTD "github.com/alcionai/corso/src/pkg/selectors/testdata"
	"github.com/alcionai/corso/src/pkg/services/m365/api"
)

// ---------------------------------------------------------------------------
// Unit tests
// ---------------------------------------------------------------------------

type ControllerUnitSuite struct {
	tester.Suite
}

func TestControllerUnitSuite(t *testing.T) {
	suite.Run(t, &ControllerUnitSuite{Suite: tester.NewUnitSuite(t)})
}

func (suite *ControllerUnitSuite) TestPopulateOwnerIDAndNamesFrom() {
	const (
		id   = "owner-id"
		name = "owner-name"
	)

	var (
		itn    = map[string]string{id: name}
		nti    = map[string]string{name: id}
		lookup = &resourceClient{
			enum:   resource.Users,
			getter: &mock.IDNameGetter{ID: id, Name: name},
		}
		noLookup = &resourceClient{enum: resource.Users, getter: &mock.IDNameGetter{}}
	)

	table := []struct {
		name       string
		owner      string
		ins        inMock.Cache
		rc         *resourceClient
		expectID   string
		expectName string
		expectErr  require.ErrorAssertionFunc
	}{
		{
			name:       "nil ins",
			owner:      id,
			rc:         lookup,
			expectID:   id,
			expectName: name,
			expectErr:  require.NoError,
		},
		{
			name:       "nil ins no lookup",
			owner:      id,
			rc:         noLookup,
			expectID:   "",
			expectName: "",
			expectErr:  require.Error,
		},
		{
			name:       "only id map with owner id",
			owner:      id,
			ins:        inMock.NewCache(itn, nil),
			rc:         noLookup,
			expectID:   id,
			expectName: name,
			expectErr:  require.NoError,
		},
		{
			name:       "only name map with owner id",
			owner:      id,
			ins:        inMock.NewCache(nil, nti),
			rc:         noLookup,
			expectID:   "",
			expectName: "",
			expectErr:  require.Error,
		},
		{
			name:       "only name map with owner id and lookup",
			owner:      id,
			ins:        inMock.NewCache(nil, nti),
			rc:         lookup,
			expectID:   id,
			expectName: name,
			expectErr:  require.NoError,
		},
		{
			name:       "only id map with owner name",
			owner:      name,
			ins:        inMock.NewCache(itn, nil),
			rc:         lookup,
			expectID:   id,
			expectName: name,
			expectErr:  require.NoError,
		},
		{
			name:       "only name map with owner name",
			owner:      name,
			ins:        inMock.NewCache(nil, nti),
			rc:         noLookup,
			expectID:   id,
			expectName: name,
			expectErr:  require.NoError,
		},
		{
			name:       "only id map with owner name",
			owner:      name,
			ins:        inMock.NewCache(itn, nil),
			rc:         noLookup,
			expectID:   "",
			expectName: "",
			expectErr:  require.Error,
		},
		{
			name:       "only id map with owner name and lookup",
			owner:      name,
			ins:        inMock.NewCache(itn, nil),
			rc:         lookup,
			expectID:   id,
			expectName: name,
			expectErr:  require.NoError,
		},
		{
			name:       "both maps with owner id",
			owner:      id,
			ins:        inMock.NewCache(itn, nti),
			rc:         noLookup,
			expectID:   id,
			expectName: name,
			expectErr:  require.NoError,
		},
		{
			name:       "both maps with owner name",
			owner:      name,
			ins:        inMock.NewCache(itn, nti),
			rc:         noLookup,
			expectID:   id,
			expectName: name,
			expectErr:  require.NoError,
		},
		{
			name:  "non-matching maps with owner id",
			owner: id,
			ins: inMock.NewCache(
				map[string]string{"foo": "bar"},
				map[string]string{"fnords": "smarf"}),
			rc:         noLookup,
			expectID:   "",
			expectName: "",
			expectErr:  require.Error,
		},
		{
			name:  "non-matching with owner name",
			owner: name,
			ins: inMock.NewCache(
				map[string]string{"foo": "bar"},
				map[string]string{"fnords": "smarf"}),
			rc:         noLookup,
			expectID:   "",
			expectName: "",
			expectErr:  require.Error,
		},
		{
			name:  "non-matching maps with owner id and lookup",
			owner: id,
			ins: inMock.NewCache(
				map[string]string{"foo": "bar"},
				map[string]string{"fnords": "smarf"}),
			rc:         lookup,
			expectID:   id,
			expectName: name,
			expectErr:  require.NoError,
		},
		{
			name:  "non-matching with owner name and lookup",
			owner: name,
			ins: inMock.NewCache(
				map[string]string{"foo": "bar"},
				map[string]string{"fnords": "smarf"}),
			rc:         lookup,
			expectID:   id,
			expectName: name,
			expectErr:  require.NoError,
		},
	}
	for _, test := range table {
		suite.Run(test.name, func() {
			t := suite.T()

			ctx, flush := tester.NewContext(t)
			defer flush()

			ctrl := &Controller{ownerLookup: test.rc}

			rID, rName, err := ctrl.PopulateProtectedResourceIDAndName(ctx, test.owner, test.ins)
			test.expectErr(t, err, clues.ToCore(err))
			assert.Equal(t, test.expectID, rID, "id")
			assert.Equal(t, test.expectName, rName, "name")
		})
	}
}

func (suite *ControllerUnitSuite) TestController_Wait() {
	t := suite.T()

	ctx, flush := tester.NewContext(t)
	defer flush()

	var (
		ctrl = &Controller{
			wg:     &sync.WaitGroup{},
			region: &trace.Region{},
		}
		metrics = support.CollectionMetrics{
			Objects:   2,
			Successes: 3,
			Bytes:     4,
		}
		status = support.CreateStatus(ctx, support.Backup, 1, metrics, "details")
	)

	ctrl.wg.Add(1)
	ctrl.UpdateStatus(status)

	result := ctrl.Wait()
	require.NotNil(t, result)
	assert.Nil(t, ctrl.region, "region")
	assert.Empty(t, ctrl.status, "status")
	assert.Equal(t, 1, result.Folders)
	assert.Equal(t, 2, result.Objects)
	assert.Equal(t, 3, result.Successes)
	assert.Equal(t, int64(4), result.Bytes)
}

func (suite *ControllerUnitSuite) TestController_CacheItemInfo() {
	var (
		odid   = "od-id"
		odname = "od-name"
		spid   = "sp-id"
		spname = "sp-name"
		// intentionally declared outside the test loop
		ctrl = &Controller{
			wg:                 &sync.WaitGroup{},
			region:             &trace.Region{},
			backupDriveIDNames: idname.NewCache(nil),
		}
	)

	table := []struct {
		name       string
		service    path.ServiceType
		cat        path.CategoryType
		dii        details.ItemInfo
		expectID   string
		expectName string
	}{
		{
			name: "exchange",
			dii: details.ItemInfo{
				Exchange: &details.ExchangeInfo{},
			},
			expectID:   "",
			expectName: "",
		},
		{
			name: "folder",
			dii: details.ItemInfo{
				Folder: &details.FolderInfo{},
			},
			expectID:   "",
			expectName: "",
		},
		{
			name: "onedrive",
			dii: details.ItemInfo{
				OneDrive: &details.OneDriveInfo{
					DriveID:   odid,
					DriveName: odname,
				},
			},
			expectID:   odid,
			expectName: odname,
		},
		{
			name: "sharepoint",
			dii: details.ItemInfo{
				SharePoint: &details.SharePointInfo{
					DriveID:   spid,
					DriveName: spname,
				},
			},
			expectID:   spid,
			expectName: spname,
		},
	}
	for _, test := range table {
		suite.Run(test.name, func() {
			t := suite.T()

			ctrl.CacheItemInfo(test.dii)

			name, _ := ctrl.backupDriveIDNames.NameOf(test.expectID)
			assert.Equal(t, test.expectName, name)

			id, _ := ctrl.backupDriveIDNames.IDOf(test.expectName)
			assert.Equal(t, test.expectID, id)
		})
	}
}

// ---------------------------------------------------------------------------
// Integration tests
// ---------------------------------------------------------------------------

type ControllerIntegrationSuite struct {
	tester.Suite
	ctrl          *Controller
	user          string
	secondaryUser string
}

func TestControllerIntegrationSuite(t *testing.T) {
	suite.Run(t, &ControllerIntegrationSuite{
		Suite: tester.NewIntegrationSuite(
			t,
			[][]string{tconfig.M365AcctCredEnvs},
		),
	})
}

func (suite *ControllerIntegrationSuite) SetupSuite() {
	t := suite.T()

	ctx, flush := tester.NewContext(t)
	defer flush()

	suite.ctrl = newController(ctx, t, resource.Users, path.ExchangeService)
	suite.user = tconfig.M365UserID(t)
	suite.secondaryUser = tconfig.SecondaryM365UserID(t)

	tester.LogTimeOfTest(t)
}

func (suite *ControllerIntegrationSuite) TestRestoreFailsBadService() {
	t := suite.T()

	ctx, flush := tester.NewContext(t)
	defer flush()

	var (
		restoreCfg = testdata.DefaultRestoreConfig("")
		sel        = selectors.Selector{
			Service: selectors.ServiceUnknown,
		}
	)

	restoreCfg.IncludePermissions = true

	rcc := inject.RestoreConsumerConfig{
		BackupVersion:     version.Backup,
		Options:           control.DefaultOptions(),
		ProtectedResource: sel,
		RestoreConfig:     restoreCfg,
		Selector:          sel,
	}

	deets, err := suite.ctrl.ConsumeRestoreCollections(
		ctx,
		rcc,
		[]data.RestoreCollection{&dataMock.Collection{}},
		fault.New(true),
		count.New())
	assert.Error(t, err, graph.ErrServiceNotEnabled, clues.ToCore(err))
	assert.Nil(t, deets)

	status := suite.ctrl.Wait()
	assert.Equal(t, 0, status.Objects)
	assert.Equal(t, 0, status.Folders)
	assert.Equal(t, 0, status.Successes)
}

func (suite *ControllerIntegrationSuite) TestEmptyCollections() {
	restoreCfg := testdata.DefaultRestoreConfig("")
	restoreCfg.IncludePermissions = true

	table := []struct {
		name string
		col  []data.RestoreCollection
		sel  selectors.Selector
	}{
		{
			name: "ExchangeNil",
			col:  nil,
			sel: selectors.Selector{
				Service: selectors.ServiceExchange,
			},
		},
		{
			name: "ExchangeEmpty",
			col:  []data.RestoreCollection{},
			sel: selectors.Selector{
				Service: selectors.ServiceExchange,
			},
		},
		{
			name: "OneDriveNil",
			col:  nil,
			sel: selectors.Selector{
				Service: selectors.ServiceOneDrive,
			},
		},
		{
			name: "OneDriveEmpty",
			col:  []data.RestoreCollection{},
			sel: selectors.Selector{
				Service: selectors.ServiceOneDrive,
			},
		},
		{
			name: "SharePointNil",
			col:  nil,
			sel: selectors.Selector{
				Service: selectors.ServiceSharePoint,
			},
		},
		{
			name: "SharePointEmpty",
			col:  []data.RestoreCollection{},
			sel: selectors.Selector{
				Service: selectors.ServiceSharePoint,
			},
		},
	}

	for _, test := range table {
		suite.Run(test.name, func() {
			t := suite.T()

			ctx, flush := tester.NewContext(t)
			defer flush()

			rcc := inject.RestoreConsumerConfig{
				BackupVersion:     version.Backup,
				Options:           control.DefaultOptions(),
				ProtectedResource: test.sel,
				RestoreConfig:     restoreCfg,
				Selector:          test.sel,
			}

			deets, err := suite.ctrl.ConsumeRestoreCollections(
				ctx,
				rcc,
				test.col,
				fault.New(true),
				count.New())
			require.Error(t, err, clues.ToCore(err))
			assert.Nil(t, deets)
		})
	}
}

//-------------------------------------------------------------
// Exchange Functions
//-------------------------------------------------------------

func runRestore(
	t *testing.T,
	ctx context.Context, //revive:disable-line:context-as-argument
	sci stub.ConfigInfo,
	backupVersion int,
	collections []data.RestoreCollection,
	numRestoreItems int,
) {
	t.Logf(
		"Restoring collections to %s for resourceOwners(s) %v\n",
		sci.RestoreCfg.Location,
		sci.ResourceOwners)

	start := time.Now()

	restoreCtrl := newController(ctx, t, sci.Resource, path.ExchangeService)
	restoreSel := getSelectorWith(t, sci.Service, sci.ResourceOwners, true)

	rcc := inject.RestoreConsumerConfig{
		BackupVersion:     backupVersion,
		Options:           control.DefaultOptions(),
		ProtectedResource: restoreSel,
		RestoreConfig:     sci.RestoreCfg,
		Selector:          restoreSel,
	}

	deets, err := restoreCtrl.ConsumeRestoreCollections(
		ctx,
		rcc,
		collections,
		fault.New(true),
		count.New())
	require.NoError(t, err, clues.ToCore(err))
	assert.NotNil(t, deets)

	status := restoreCtrl.Wait()
	runTime := time.Since(start)

	assert.Equal(t, numRestoreItems, status.Objects, "restored status.Objects")
	assert.Equal(t, numRestoreItems, status.Successes, "restored status.Successes")
	assert.Len(
		t,
		// Don't check folders as those are now added to details.
		deets.Items(),
		numRestoreItems,
		"details entries contains same item count as total successful items restored")

	t.Logf("Restore complete in %v\n", runTime)
}

func runBackupAndCompare(
	t *testing.T,
	ctx context.Context, //revive:disable-line:context-as-argument
	sci stub.ConfigInfo,
	expectedData map[string]map[string][]byte,
	totalItems int,
	totalKopiaItems int,
	inputCollections []stub.ColInfo,
) {
	t.Helper()

	// Run a backup and compare its output with what we put in.
	cats := make(map[path.CategoryType]struct{}, len(inputCollections))
	for _, c := range inputCollections {
		cats[c.Category] = struct{}{}
	}

	var (
		expectedDests = make([]destAndCats, 0, len(sci.ResourceOwners))
		idToName      = map[string]string{}
		nameToID      = map[string]string{}
	)

	for _, ro := range sci.ResourceOwners {
		expectedDests = append(expectedDests, destAndCats{
			resourceOwner: ro,
			dest:          sci.RestoreCfg.Location,
			cats:          cats,
		})

		idToName[ro] = ro
		nameToID[ro] = ro
	}

	backupCtrl := newController(ctx, t, sci.Resource, path.ExchangeService)
	backupCtrl.IDNameLookup = inMock.NewCache(idToName, nameToID)

	backupSel := backupSelectorForExpected(t, sci.Service, expectedDests)
	t.Logf("Selective backup of %s\n", backupSel)

	bpc := inject.BackupProducerConfig{
		LastBackupVersion: version.NoBackup,
		Options:           sci.Opts,
		ProtectedResource: backupSel,
		Selector:          backupSel,
	}

	start := time.Now()
	dcs, excludes, canUsePreviousBackup, err := backupCtrl.ProduceBackupCollections(
		ctx,
		bpc,
		fault.New(true))
	require.NoError(t, err, clues.ToCore(err))
	assert.True(t, canUsePreviousBackup, "can use previous backup")
	// No excludes yet because this isn't an incremental backup.
	assert.True(t, excludes.Empty())

	t.Logf("Backup enumeration complete in %v\n", time.Since(start))

	// Pull the data prior to waiting for the status as otherwise it will
	// deadlock.
	skipped := checkCollections(
		t,
		ctx,
		totalKopiaItems,
		expectedData,
		dcs,
		sci)

	status := backupCtrl.Wait()

	assert.Equalf(t, totalItems+skipped, status.Objects,
		"backup status.Objects; wanted %d items + %d skipped", totalItems, skipped)
	assert.Equalf(t, totalItems+skipped, status.Successes,
		"backup status.Successes; wanted %d items + %d skipped", totalItems, skipped)
}

func runRestoreBackupTest(
	t *testing.T,
	test restoreBackupInfo,
	tenant string,
	resourceOwners []string,
	opts control.Options,
	restoreCfg control.RestoreConfig,
) {
	ctx, flush := tester.NewContext(t)
	defer flush()

	cfg := stub.ConfigInfo{
		Opts:           opts,
		Resource:       test.resourceCat,
		Service:        test.service,
		Tenant:         tenant,
		ResourceOwners: resourceOwners,
		RestoreCfg:     restoreCfg,
	}

	totalItems, totalKopiaItems, collections, expectedData, err := stub.GetCollectionsAndExpected(
		cfg,
		test.collections,
		version.Backup)

	require.NoError(t, err)

	runRestore(
		t,
		ctx,
		cfg,
		version.Backup,
		collections,
		totalItems)

	runBackupAndCompare(
		t,
		ctx,
		cfg,
		expectedData,
		totalItems,
		totalKopiaItems,
		test.collections)
}

// runRestoreTest restores with data using the test's backup version
func runRestoreTestWithVersion(
	t *testing.T,
	test restoreBackupInfoMultiVersion,
	tenant string,
	resourceOwners []string,
	opts control.Options,
	restoreCfg control.RestoreConfig,
) {
	ctx, flush := tester.NewContext(t)
	defer flush()

	cfg := stub.ConfigInfo{
		Opts:           opts,
		Resource:       test.resourceCat,
		Service:        test.service,
		Tenant:         tenant,
		ResourceOwners: resourceOwners,
		RestoreCfg:     restoreCfg,
	}

	totalItems, _, collections, _, err := stub.GetCollectionsAndExpected(
		cfg,
		test.collectionsPrevious,
		test.backupVersion)
	require.NoError(t, err)

	runRestore(
		t,
		ctx,
		cfg,
		test.backupVersion,
		collections,
		totalItems)
}

// runRestoreBackupTestVersions restores with data from an older
// version of the backup and check the restored data against the
// something that would be in the form of a newer backup.
func runRestoreBackupTestVersions(
	t *testing.T,
	test restoreBackupInfoMultiVersion,
	tenant string,
	resourceOwners []string,
	opts control.Options,
	restoreCfg control.RestoreConfig,
) {
	ctx, flush := tester.NewContext(t)
	defer flush()

	cfg := stub.ConfigInfo{
		Opts:           opts,
		Resource:       test.resourceCat,
		Service:        test.service,
		Tenant:         tenant,
		ResourceOwners: resourceOwners,
		RestoreCfg:     restoreCfg,
	}

	totalItems, _, collections, _, err := stub.GetCollectionsAndExpected(
		cfg,
		test.collectionsPrevious,
		test.backupVersion)
	require.NoError(t, err)

	runRestore(
		t,
		ctx,
		cfg,
		test.backupVersion,
		collections,
		totalItems)

	// Get expected output for new version.
	totalItems, totalKopiaItems, _, expectedData, err := stub.GetCollectionsAndExpected(
		cfg,
		test.collectionsLatest,
		version.Backup)
	require.NoError(t, err)

	runBackupAndCompare(
		t,
		ctx,
		cfg,
		expectedData,
		totalItems,
		totalKopiaItems,
		test.collectionsLatest)
}

func (suite *ControllerIntegrationSuite) TestRestoreAndBackup_core() {
	bodyText := "This email has some text. However, all the text is on the same line."
	subjectText := "Test message for restore"

	table := []restoreBackupInfo{
		{
			name:        "EmailsWithAttachments",
			service:     path.ExchangeService,
			resourceCat: resource.Users,
			collections: []stub.ColInfo{
				{
					PathElements: []string{api.MailInbox},
					Category:     path.EmailCategory,
					Items: []stub.ItemInfo{
						{
							Name: "someencodeditemID",
							Data: exchMock.MessageWithDirectAttachment(
								subjectText + "-1",
							),
							LookupKey: subjectText + "-1",
						},
						{
							Name: "someencodeditemID2",
							Data: exchMock.MessageWithTwoAttachments(
								subjectText + "-2",
							),
							LookupKey: subjectText + "-2",
						},
					},
				},
			},
		},
		{
			name:        "MultipleEmailsMultipleFolders",
			service:     path.ExchangeService,
			resourceCat: resource.Users,
			collections: []stub.ColInfo{
				{
					PathElements: []string{api.MailInbox},
					Category:     path.EmailCategory,
					Items: []stub.ItemInfo{
						{
							Name: "someencodeditemID",
							Data: exchMock.MessageWithBodyBytes(
								subjectText+"-1",
								bodyText+" 1.",
								bodyText+" 1.",
							),
							LookupKey: subjectText + "-1",
						},
					},
				},
				{
					PathElements: []string{"Work"},
					Category:     path.EmailCategory,
					Items: []stub.ItemInfo{
						{
							Name: "someencodeditemID2",
							Data: exchMock.MessageWithBodyBytes(
								subjectText+"-2",
								bodyText+" 2.",
								bodyText+" 2.",
							),
							LookupKey: subjectText + "-2",
						},
						{
							Name: "someencodeditemID3",
							Data: exchMock.MessageWithBodyBytes(
								subjectText+"-3",
								bodyText+" 3.",
								bodyText+" 3.",
							),
							LookupKey: subjectText + "-3",
						},
					},
				},
				{
					PathElements: []string{"Work", api.MailInbox},
					Category:     path.EmailCategory,
					Items: []stub.ItemInfo{
						{
							Name: "someencodeditemID4",
							Data: exchMock.MessageWithBodyBytes(
								subjectText+"-4",
								bodyText+" 4.",
								bodyText+" 4.",
							),
							LookupKey: subjectText + "-4",
						},
					},
				},
				{
					PathElements: []string{"Work", api.MailInbox, "Work"},
					Category:     path.EmailCategory,
					Items: []stub.ItemInfo{
						{
							Name: "someencodeditemID5",
							Data: exchMock.MessageWithBodyBytes(
								subjectText+"-5",
								bodyText+" 5.",
								bodyText+" 5.",
							),
							LookupKey: subjectText + "-5",
						},
					},
				},
			},
		},
		{
			name:        "MultipleContactsSingleFolder",
			service:     path.ExchangeService,
			resourceCat: resource.Users,
			collections: []stub.ColInfo{
				{
					PathElements: []string{"Contacts"},
					Category:     path.ContactsCategory,
					Items: []stub.ItemInfo{
						{
							Name:      "someencodeditemID",
							Data:      exchMock.ContactBytes("Ghimley"),
							LookupKey: "Ghimley",
						},
						{
							Name:      "someencodeditemID2",
							Data:      exchMock.ContactBytes("Irgot"),
							LookupKey: "Irgot",
						},
						{
							Name:      "someencodeditemID3",
							Data:      exchMock.ContactBytes("Jannes"),
							LookupKey: "Jannes",
						},
					},
				},
			},
		},
		{
			name:        "MultipleContactsMultipleFolders",
			service:     path.ExchangeService,
			resourceCat: resource.Users,
			collections: []stub.ColInfo{
				{
					PathElements: []string{"Work"},
					Category:     path.ContactsCategory,
					Items: []stub.ItemInfo{
						{
							Name:      "someencodeditemID",
							Data:      exchMock.ContactBytes("Ghimley"),
							LookupKey: "Ghimley",
						},
						{
							Name:      "someencodeditemID2",
							Data:      exchMock.ContactBytes("Irgot"),
							LookupKey: "Irgot",
						},
						{
							Name:      "someencodeditemID3",
							Data:      exchMock.ContactBytes("Jannes"),
							LookupKey: "Jannes",
						},
					},
				},
				{
					PathElements: []string{"Personal"},
					Category:     path.ContactsCategory,
					Items: []stub.ItemInfo{
						{
							Name:      "someencodeditemID4",
							Data:      exchMock.ContactBytes("Argon"),
							LookupKey: "Argon",
						},
						{
							Name:      "someencodeditemID5",
							Data:      exchMock.ContactBytes("Bernard"),
							LookupKey: "Bernard",
						},
					},
				},
			},
		},
		// {
		// 	name:    "MultipleEventsSingleCalendar",
		// 	service: path.ExchangeService,
		// 	collections: []colInfo{
		// 		{
		// 			pathElements: []string{"Work"},
		// 			category:     path.EventsCategory,
		// 			items: []itemInfo{
		// 				{
		// 					name:      "someencodeditemID",
		// 					data:      exchMock.EventWithSubjectBytes("Ghimley"),
		// 					lookupKey: "Ghimley",
		// 				},
		// 				{
		// 					name:      "someencodeditemID2",
		// 					data:      exchMock.EventWithSubjectBytes("Irgot"),
		// 					lookupKey: "Irgot",
		// 				},
		// 				{
		// 					name:      "someencodeditemID3",
		// 					data:      exchMock.EventWithSubjectBytes("Jannes"),
		// 					lookupKey: "Jannes",
		// 				},
		// 			},
		// 		},
		// 	},
		// },
		// {
		// 	name:    "MultipleEventsMultipleCalendars",
		// 	service: path.ExchangeService,
		// 	collections: []colInfo{
		// 		{
		// 			pathElements: []string{"Work"},
		// 			category:     path.EventsCategory,
		// 			items: []itemInfo{
		// 				{
		// 					name:      "someencodeditemID",
		// 					data:      exchMock.EventWithSubjectBytes("Ghimley"),
		// 					lookupKey: "Ghimley",
		// 				},
		// 				{
		// 					name:      "someencodeditemID2",
		// 					data:      exchMock.EventWithSubjectBytes("Irgot"),
		// 					lookupKey: "Irgot",
		// 				},
		// 				{
		// 					name:      "someencodeditemID3",
		// 					data:      exchMock.EventWithSubjectBytes("Jannes"),
		// 					lookupKey: "Jannes",
		// 				},
		// 			},
		// 		},
		// 		{
		// 			pathElements: []string{"Personal"},
		// 			category:     path.EventsCategory,
		// 			items: []itemInfo{
		// 				{
		// 					name:      "someencodeditemID4",
		// 					data:      exchMock.EventWithSubjectBytes("Argon"),
		// 					lookupKey: "Argon",
		// 				},
		// 				{
		// 					name:      "someencodeditemID5",
		// 					data:      exchMock.EventWithSubjectBytes("Bernard"),
		// 					lookupKey: "Bernard",
		// 				},
		// 			},
		// 		},
		// 	},
		// },
	}

	for _, test := range table {
		suite.Run(test.name, func() {
			runRestoreBackupTest(
				suite.T(),
				test,
				suite.ctrl.tenant,
				[]string{suite.user},
				control.DefaultOptions(),
				control.DefaultRestoreConfig(dttm.HumanReadableDriveItem))
		})
	}
}

func (suite *ControllerIntegrationSuite) TestMultiFolderBackupDifferentNames() {
	table := []restoreBackupInfo{
		{
			name:        "Contacts",
			service:     path.ExchangeService,
			resourceCat: resource.Users,
			collections: []stub.ColInfo{
				{
					PathElements: []string{"Work"},
					Category:     path.ContactsCategory,
					Items: []stub.ItemInfo{
						{
							Name:      "someencodeditemID",
							Data:      exchMock.ContactBytes("Ghimley"),
							LookupKey: "Ghimley",
						},
					},
				},
				{
					PathElements: []string{"Personal"},
					Category:     path.ContactsCategory,
					Items: []stub.ItemInfo{
						{
							Name:      "someencodeditemID2",
							Data:      exchMock.ContactBytes("Irgot"),
							LookupKey: "Irgot",
						},
					},
				},
			},
		},
		// {
		// 	name:    "Events",
		// 	service: path.ExchangeService,
		// 	collections: []colInfo{
		// 		{
		// 			pathElements: []string{"Work"},
		// 			category:     path.EventsCategory,
		// 			items: []itemInfo{
		// 				{
		// 					name:      "someencodeditemID",
		// 					data:      exchMock.EventWithSubjectBytes("Ghimley"),
		// 					lookupKey: "Ghimley",
		// 				},
		// 			},
		// 		},
		// 		{
		// 			PathElements: []string{"Personal"},
		// 			Category:     path.EventsCategory,
		// 			Items: []ItemInfo{
		// 				{
		// 					name:      "someencodeditemID2",
		// 					data:      exchMock.EventWithSubjectBytes("Irgot"),
		// 					lookupKey: "Irgot",
		// 				},
		// 			},
		// 		},
		// 	},
		// },
	}

	for _, test := range table {
		suite.Run(test.name, func() {
			t := suite.T()

			ctx, flush := tester.NewContext(t)
			defer flush()

			restoreSel := getSelectorWith(t, test.service, []string{suite.user}, true)
			expectedDests := make([]destAndCats, 0, len(test.collections))
			allItems := 0
			allExpectedData := map[string]map[string][]byte{}

			for i, collection := range test.collections {
				// Get a restoreCfg per collection so they're independent.
				restoreCfg := testdata.DefaultRestoreConfig("")
				restoreCfg.IncludePermissions = true

				expectedDests = append(expectedDests, destAndCats{
					resourceOwner: suite.user,
					dest:          restoreCfg.Location,
					cats: map[path.CategoryType]struct{}{
						collection.Category: {},
					},
				})

				totalItems, _, collections, expectedData, err := stub.CollectionsForInfo(
					test.service,
					suite.ctrl.tenant,
					suite.user,
					restoreCfg,
					[]stub.ColInfo{collection},
					version.Backup,
				)
				require.NoError(t, err)

				allItems += totalItems

				for k, v := range expectedData {
					allExpectedData[k] = v
				}

				t.Logf(
					"Restoring %v/%v collections to %s\n",
					i+1,
					len(test.collections),
					restoreCfg.Location,
				)

				restoreCtrl := newController(ctx, t, test.resourceCat, path.ExchangeService)

				rcc := inject.RestoreConsumerConfig{
					BackupVersion:     version.Backup,
					Options:           control.DefaultOptions(),
					ProtectedResource: restoreSel,
					RestoreConfig:     restoreCfg,
					Selector:          restoreSel,
				}

				deets, err := restoreCtrl.ConsumeRestoreCollections(
					ctx,
					rcc,
					collections,
					fault.New(true),
					count.New())
				require.NoError(t, err, clues.ToCore(err))
				require.NotNil(t, deets)

				status := restoreCtrl.Wait()
				// Always just 1 because it's just 1 collection.
				assert.Equal(t, totalItems, status.Objects, "status.Objects")
				assert.Equal(t, totalItems, status.Successes, "status.Successes")
				assert.Len(
					t,
					deets.Items(),
					totalItems,
					"details entries contains same item count as total successful items restored")

				t.Log("Restore complete")
			}

			// Run a backup and compare its output with what we put in.

			backupCtrl := newController(ctx, t, test.resourceCat, path.ExchangeService)
			backupSel := backupSelectorForExpected(t, test.service, expectedDests)
			t.Log("Selective backup of", backupSel)

			bpc := inject.BackupProducerConfig{
				LastBackupVersion: version.NoBackup,
				Options:           control.DefaultOptions(),
				ProtectedResource: backupSel,
				Selector:          backupSel,
			}

			dcs, excludes, canUsePreviousBackup, err := backupCtrl.ProduceBackupCollections(
				ctx,
				bpc,
				fault.New(true))
			require.NoError(t, err, clues.ToCore(err))
			assert.True(t, canUsePreviousBackup, "can use previous backup")
			// No excludes yet because this isn't an incremental backup.
			assert.True(t, excludes.Empty())

			t.Log("Backup enumeration complete")

			restoreCfg := control.DefaultRestoreConfig(dttm.HumanReadableDriveItem)
			restoreCfg.IncludePermissions = true

			ci := stub.ConfigInfo{
				Opts: control.DefaultOptions(),
				// Alright to be empty, needed for OneDrive.
				RestoreCfg: restoreCfg,
			}

			// Pull the data prior to waiting for the status as otherwise it will
			// deadlock.
			skipped := checkCollections(t, ctx, allItems, allExpectedData, dcs, ci)

			status := backupCtrl.Wait()
			assert.Equal(t, allItems+skipped, status.Objects, "status.Objects")
			assert.Equal(t, allItems+skipped, status.Successes, "status.Successes")
		})
	}
}

// TODO: this should only be run during smoke tests, not part of the standard CI.
// That's why it's set aside instead of being included in the other test set.
func (suite *ControllerIntegrationSuite) TestRestoreAndBackup_largeMailAttachment() {
	subjectText := "Test message for restore with large attachment"

	test := restoreBackupInfo{
		name:        "EmailsWithLargeAttachments",
		service:     path.ExchangeService,
		resourceCat: resource.Users,
		collections: []stub.ColInfo{
			{
				PathElements: []string{api.MailInbox},
				Category:     path.EmailCategory,
				Items: []stub.ItemInfo{
					{
						Name:      "35mbAttachment",
						Data:      exchMock.MessageWithSizedAttachment(subjectText, 35),
						LookupKey: subjectText,
					},
				},
			},
		},
	}

	restoreCfg := control.DefaultRestoreConfig(dttm.HumanReadableDriveItem)
	restoreCfg.IncludePermissions = true

	runRestoreBackupTest(
		suite.T(),
		test,
		suite.ctrl.tenant,
		[]string{suite.user},
		control.DefaultOptions(),
		restoreCfg)
}

func (suite *ControllerIntegrationSuite) TestBackup_CreatesPrefixCollections() {
	table := []struct {
		name         string
		resourceCat  resource.Category
		selectorFunc func(t *testing.T) selectors.Selector
		service      path.ServiceType
		categories   []string
	}{
		{
			name:        "Exchange",
			resourceCat: resource.Users,
			selectorFunc: func(t *testing.T) selectors.Selector {
				sel := selectors.NewExchangeBackup([]string{suite.user})
				sel.Include(
					sel.ContactFolders([]string{selectors.NoneTgt}),
					sel.EventCalendars([]string{selectors.NoneTgt}),
					sel.MailFolders([]string{selectors.NoneTgt}))

				return sel.Selector
			},
			service: path.ExchangeService,
			categories: []string{
				path.EmailCategory.String(),
				path.ContactsCategory.String(),
				path.EventsCategory.String(),
			},
		},
		{
			name:        "OneDrive",
			resourceCat: resource.Users,
			selectorFunc: func(t *testing.T) selectors.Selector {
				sel := selectors.NewOneDriveBackup([]string{suite.user})
				sel.Include(sel.Folders([]string{selectors.NoneTgt}))

				return sel.Selector
			},
			service: path.OneDriveService,
			categories: []string{
				path.FilesCategory.String(),
			},
		},
		{
			name:        "SharePoint",
			resourceCat: resource.Sites,
			selectorFunc: func(t *testing.T) selectors.Selector {
				sel := selectors.NewSharePointBackup([]string{tconfig.M365SiteID(t)})
				sel.Include(
					sel.LibraryFolders([]string{selectors.NoneTgt}),
					// not yet in use
					//  sel.Pages([]string{selectors.NoneTgt}),
					//  sel.Lists([]string{selectors.NoneTgt}),
				)

				return sel.Selector
			},
			service: path.SharePointService,
			categories: []string{
				path.LibrariesCategory.String(),
				// not yet in use
				// path.PagesCategory.String(),
				// path.ListsCategory.String(),
			},
		},
	}

	for _, test := range table {
		suite.Run(test.name, func() {
			t := suite.T()

			ctx, flush := tester.NewContext(t)
			defer flush()

			var (
				backupCtrl = newController(ctx, t, test.resourceCat, path.ExchangeService)
				backupSel  = test.selectorFunc(t)
				errs       = fault.New(true)
				start      = time.Now()
			)

			id, name, err := backupCtrl.PopulateProtectedResourceIDAndName(ctx, backupSel.DiscreteOwner, nil)
			require.NoError(t, err, clues.ToCore(err))

			backupSel.SetDiscreteOwnerIDName(id, name)

			bpc := inject.BackupProducerConfig{
				LastBackupVersion: version.NoBackup,
				Options:           control.DefaultOptions(),
				ProtectedResource: inMock.NewProvider(id, name),
				Selector:          backupSel,
			}

			dcs, excludes, canUsePreviousBackup, err := backupCtrl.ProduceBackupCollections(
				ctx,
				bpc,
				fault.New(true))
			require.NoError(t, err, clues.ToCore(err))
			assert.True(t, canUsePreviousBackup, "can use previous backup")
			// No excludes yet because this isn't an incremental backup.
			assert.True(t, excludes.Empty())

			t.Logf("Backup enumeration complete in %v\n", time.Since(start))

			// Use a map to find duplicates.
			foundCategories := []string{}
			for _, col := range dcs {
				// TODO(ashmrtn): We should be able to remove the below if we change how
				// status updates are done. Ideally we shouldn't have to fetch items in
				// these collections to avoid deadlocking.
				var found int

				// Need to iterate through this before the continue below else we'll
				// hang checking the status.
				for range col.Items(ctx, errs) {
					found++
				}

				// Ignore metadata collections.
				fullPath := col.FullPath()
				if fullPath.Service() != test.service {
					continue
				}

				assert.Empty(t, fullPath.Folders(), "non-prefix collection")
				assert.NotEqual(t, col.State(), data.NewState, "prefix collection marked as new")
				foundCategories = append(foundCategories, fullPath.Category().String())

				assert.Zero(t, found, "non-empty collection")
			}

			assert.ElementsMatch(t, test.categories, foundCategories)

			backupCtrl.Wait()

			assert.NoError(t, errs.Failure())
		})
	}
}

type DisconnectedUnitSuite struct {
	tester.Suite
}

func TestDisconnectedUnitSuite(t *testing.T) {
	s := &DisconnectedUnitSuite{
		Suite: tester.NewUnitSuite(t),
	}

	suite.Run(t, s)
}

func statusTestTask(
	t *testing.T,
	ctrl *Controller,
	objects, success, folder int,
) {
	ctx, flush := tester.NewContext(t)
	defer flush()

	status := support.CreateStatus(
		ctx,
		support.Restore, folder,
		support.CollectionMetrics{
			Objects:   objects,
			Successes: success,
			Bytes:     0,
		},
		"statusTestTask")
	ctrl.UpdateStatus(status)
}

func (suite *DisconnectedUnitSuite) TestController_Status() {
	t := suite.T()
	ctrl := Controller{wg: &sync.WaitGroup{}}

	// Two tasks
	ctrl.incrementAwaitingMessages()
	ctrl.incrementAwaitingMessages()

	// Each helper task processes 4 objects, 1 success, 3 errors, 1 folders
	go statusTestTask(t, &ctrl, 4, 1, 1)
	go statusTestTask(t, &ctrl, 4, 1, 1)

	stats := ctrl.Wait()

	assert.NotEmpty(t, ctrl.PrintableStatus())
	// Expect 8 objects
	assert.Equal(t, 8, stats.Objects)
	// Expect 2 success
	assert.Equal(t, 2, stats.Successes)
	// Expect 2 folders
	assert.Equal(t, 2, stats.Folders)
}

func (suite *DisconnectedUnitSuite) TestVerifyBackupInputs_allServices() {
	sites := []string{"abc.site.foo", "bar.site.baz"}

	tests := []struct {
		name       string
		excludes   func(t *testing.T) selectors.Selector
		filters    func(t *testing.T) selectors.Selector
		includes   func(t *testing.T) selectors.Selector
		checkError assert.ErrorAssertionFunc
	}{
		{
			name:       "Valid User",
			checkError: assert.NoError,
			excludes: func(t *testing.T) selectors.Selector {
				sel := selectors.NewOneDriveBackup([]string{"elliotReid@someHospital.org", "foo@SomeCompany.org"})
				sel.Exclude(selTD.OneDriveBackupFolderScope(sel))
				sel.DiscreteOwner = "elliotReid@someHospital.org"
				return sel.Selector
			},
			filters: func(t *testing.T) selectors.Selector {
				sel := selectors.NewOneDriveBackup([]string{"elliotReid@someHospital.org", "foo@SomeCompany.org"})
				sel.Filter(selTD.OneDriveBackupFolderScope(sel))
				sel.DiscreteOwner = "elliotReid@someHospital.org"
				return sel.Selector
			},
			includes: func(t *testing.T) selectors.Selector {
				sel := selectors.NewOneDriveBackup([]string{"elliotReid@someHospital.org", "foo@SomeCompany.org"})
				sel.Include(selTD.OneDriveBackupFolderScope(sel))
				sel.DiscreteOwner = "elliotReid@someHospital.org"
				return sel.Selector
			},
		},
		{
			name:       "Invalid User",
			checkError: assert.NoError,
			excludes: func(t *testing.T) selectors.Selector {
				sel := selectors.NewOneDriveBackup([]string{"foo@SomeCompany.org"})
				sel.Exclude(selTD.OneDriveBackupFolderScope(sel))
				return sel.Selector
			},
			filters: func(t *testing.T) selectors.Selector {
				sel := selectors.NewOneDriveBackup([]string{"foo@SomeCompany.org"})
				sel.Filter(selTD.OneDriveBackupFolderScope(sel))
				return sel.Selector
			},
			includes: func(t *testing.T) selectors.Selector {
				sel := selectors.NewOneDriveBackup([]string{"foo@SomeCompany.org"})
				sel.Include(selTD.OneDriveBackupFolderScope(sel))
				return sel.Selector
			},
		},
		{
			name:       "valid sites",
			checkError: assert.NoError,
			excludes: func(t *testing.T) selectors.Selector {
				sel := selectors.NewSharePointBackup([]string{"abc.site.foo", "bar.site.baz"})
				sel.DiscreteOwner = "abc.site.foo"
				sel.Exclude(sel.AllData())
				return sel.Selector
			},
			filters: func(t *testing.T) selectors.Selector {
				sel := selectors.NewSharePointBackup([]string{"abc.site.foo", "bar.site.baz"})
				sel.DiscreteOwner = "abc.site.foo"
				sel.Filter(sel.AllData())
				return sel.Selector
			},
			includes: func(t *testing.T) selectors.Selector {
				sel := selectors.NewSharePointBackup([]string{"abc.site.foo", "bar.site.baz"})
				sel.DiscreteOwner = "abc.site.foo"
				sel.Include(sel.AllData())
				return sel.Selector
			},
		},
		{
			name:       "invalid sites",
			checkError: assert.Error,
			excludes: func(t *testing.T) selectors.Selector {
				sel := selectors.NewSharePointBackup([]string{"fnords.smarfs.brawnhilda"})
				sel.Exclude(sel.AllData())
				return sel.Selector
			},
			filters: func(t *testing.T) selectors.Selector {
				sel := selectors.NewSharePointBackup([]string{"fnords.smarfs.brawnhilda"})
				sel.Filter(sel.AllData())
				return sel.Selector
			},
			includes: func(t *testing.T) selectors.Selector {
				sel := selectors.NewSharePointBackup([]string{"fnords.smarfs.brawnhilda"})
				sel.Include(sel.AllData())
				return sel.Selector
			},
		},
	}

	for _, test := range tests {
		suite.Run(test.name, func() {
			t := suite.T()

			err := verifyBackupInputs(test.excludes(t), sites)
			test.checkError(t, err, clues.ToCore(err))
			err = verifyBackupInputs(test.filters(t), sites)
			test.checkError(t, err, clues.ToCore(err))
			err = verifyBackupInputs(test.includes(t), sites)
			test.checkError(t, err, clues.ToCore(err))
		})
	}
}
