package exchange

import (
	"bytes"
	"context"
	"sync"
	"testing"

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

	inMock "github.com/alcionai/corso/src/internal/common/idname/mock"
	"github.com/alcionai/corso/src/internal/common/ptr"
	"github.com/alcionai/corso/src/internal/data"
	"github.com/alcionai/corso/src/internal/m365/graph"
	"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/account"
	"github.com/alcionai/corso/src/pkg/control"
	"github.com/alcionai/corso/src/pkg/fault"
	"github.com/alcionai/corso/src/pkg/path"
	"github.com/alcionai/corso/src/pkg/selectors"
	"github.com/alcionai/corso/src/pkg/services/m365/api"
)

// ---------------------------------------------------------------------------
// mocks
// ---------------------------------------------------------------------------

var _ backupHandler = &mockBackupHandler{}

type mockBackupHandler struct {
	mg       mockGetter
	category path.CategoryType
	ac       api.Client
	userID   string
}

func (bh mockBackupHandler) itemEnumerator() addedAndRemovedItemGetter { return bh.mg }
func (bh mockBackupHandler) itemHandler() itemGetterSerializer         { return nil }

func (bh mockBackupHandler) NewContainerCache(
	userID string,
) (string, graph.ContainerResolver) {
	return BackupHandlers(bh.ac)[bh.category].NewContainerCache(bh.userID)
}

var _ addedAndRemovedItemGetter = &mockGetter{}

type (
	mockGetter struct {
		noReturnDelta bool
		results       map[string]mockGetterResults
	}
	mockGetterResults struct {
		added    []string
		removed  []string
		newDelta api.DeltaUpdate
		err      error
	}
)

func (mg mockGetter) GetAddedAndRemovedItemIDs(
	ctx context.Context,
	userID, cID, prevDelta string,
	_ bool,
	_ bool,
) (
	[]string,
	[]string,
	api.DeltaUpdate,
	error,
) {
	results, ok := mg.results[cID]
	if !ok {
		return nil, nil, api.DeltaUpdate{}, clues.New("mock not found for " + cID)
	}

	delta := results.newDelta
	if mg.noReturnDelta {
		delta.URL = ""
	}

	return results.added, results.removed, delta, results.err
}

var _ graph.ContainerResolver = &mockResolver{}

type (
	mockResolver struct {
		items []graph.CachedContainer
		added map[string]string
	}
)

func newMockResolver(items ...mockContainer) mockResolver {
	is := make([]graph.CachedContainer, 0, len(items))

	for _, i := range items {
		is = append(is, i)
	}

	return mockResolver{items: is}
}

func (m mockResolver) Items() []graph.CachedContainer {
	return m.items
}

func (m mockResolver) AddToCache(ctx context.Context, ctrl graph.Container) error {
	if len(m.added) == 0 {
		m.added = map[string]string{}
	}

	m.added[ptr.Val(ctrl.GetDisplayName())] = ptr.Val(ctrl.GetId())

	return nil
}
func (m mockResolver) DestinationNameToID(dest string) string { return m.added[dest] }
func (m mockResolver) IDToPath(context.Context, string) (*path.Builder, *path.Builder, error) {
	return nil, nil, nil
}
func (m mockResolver) PathInCache(string) (string, bool)                             { return "", false }
func (m mockResolver) LocationInCache(string) (string, bool)                         { return "", false }
func (m mockResolver) Populate(context.Context, *fault.Bus, string, ...string) error { return nil }

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

type DataCollectionsUnitSuite struct {
	tester.Suite
}

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

func (suite *DataCollectionsUnitSuite) TestParseMetadataCollections() {
	type fileValues struct {
		fileName string
		value    string
	}

	table := []struct {
		name                 string
		data                 []fileValues
		expect               map[string]DeltaPath
		canUsePreviousBackup bool
		expectError          assert.ErrorAssertionFunc
	}{
		{
			name: "delta urls only",
			data: []fileValues{
				{graph.DeltaURLsFileName, "delta-link"},
			},
			expect:               map[string]DeltaPath{},
			canUsePreviousBackup: true,
			expectError:          assert.NoError,
		},
		{
			name: "multiple delta urls",
			data: []fileValues{
				{graph.DeltaURLsFileName, "delta-link"},
				{graph.DeltaURLsFileName, "delta-link-2"},
			},
			canUsePreviousBackup: false,
			expectError:          assert.Error,
		},
		{
			name: "previous path only",
			data: []fileValues{
				{graph.PreviousPathFileName, "prev-path"},
			},
			expect: map[string]DeltaPath{
				"key": {
					Delta: "delta-link",
					Path:  "prev-path",
				},
			},
			canUsePreviousBackup: true,
			expectError:          assert.NoError,
		},
		{
			name: "multiple previous paths",
			data: []fileValues{
				{graph.PreviousPathFileName, "prev-path"},
				{graph.PreviousPathFileName, "prev-path-2"},
			},
			canUsePreviousBackup: false,
			expectError:          assert.Error,
		},
		{
			name: "delta urls and previous paths",
			data: []fileValues{
				{graph.DeltaURLsFileName, "delta-link"},
				{graph.PreviousPathFileName, "prev-path"},
			},
			expect: map[string]DeltaPath{
				"key": {
					Delta: "delta-link",
					Path:  "prev-path",
				},
			},
			canUsePreviousBackup: true,
			expectError:          assert.NoError,
		},
		{
			name: "delta urls and empty previous paths",
			data: []fileValues{
				{graph.DeltaURLsFileName, "delta-link"},
				{graph.PreviousPathFileName, ""},
			},
			expect:               map[string]DeltaPath{},
			canUsePreviousBackup: true,
			expectError:          assert.NoError,
		},
		{
			name: "empty delta urls and previous paths",
			data: []fileValues{
				{graph.DeltaURLsFileName, ""},
				{graph.PreviousPathFileName, "prev-path"},
			},
			expect: map[string]DeltaPath{
				"key": {
					Delta: "delta-link",
					Path:  "prev-path",
				},
			},
			canUsePreviousBackup: true,
			expectError:          assert.NoError,
		},
		{
			name: "delta urls with special chars",
			data: []fileValues{
				{graph.DeltaURLsFileName, "`!@#$%^&*()_[]{}/\"\\"},
				{graph.PreviousPathFileName, "prev-path"},
			},
			expect: map[string]DeltaPath{
				"key": {
					Delta: "`!@#$%^&*()_[]{}/\"\\",
					Path:  "prev-path",
				},
			},
			canUsePreviousBackup: true,
			expectError:          assert.NoError,
		},
		{
			name: "delta urls with escaped chars",
			data: []fileValues{
				{graph.DeltaURLsFileName, `\n\r\t\b\f\v\0\\`},
				{graph.PreviousPathFileName, "prev-path"},
			},
			expect: map[string]DeltaPath{
				"key": {
					Delta: "\\n\\r\\t\\b\\f\\v\\0\\\\",
					Path:  "prev-path",
				},
			},
			canUsePreviousBackup: true,
			expectError:          assert.NoError,
		},
		{
			name: "delta urls with newline char runes",
			data: []fileValues{
				// rune(92) = \, rune(110) = n.  Ensuring it's not possible to
				// error in serializing/deserializing and produce a single newline
				// character from those two runes.
				{graph.DeltaURLsFileName, string([]rune{rune(92), rune(110)})},
				{graph.PreviousPathFileName, "prev-path"},
			},
			expect: map[string]DeltaPath{
				"key": {
					Delta: "\\n",
					Path:  "prev-path",
				},
			},
			canUsePreviousBackup: true,
			expectError:          assert.NoError,
		},
	}
	for _, test := range table {
		suite.Run(test.name, func() {
			t := suite.T()

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

			entries := []graph.MetadataCollectionEntry{}

			for _, d := range test.data {
				entries = append(
					entries,
					graph.NewMetadataEntry(d.fileName, map[string]string{"key": d.value}))
			}

			coll, err := graph.MakeMetadataCollection(
				"t", "u",
				path.ExchangeService,
				path.EmailCategory,
				entries,
				func(cos *support.ControllerOperationStatus) {},
			)
			require.NoError(t, err, clues.ToCore(err))

			cdps, canUsePreviousBackup, err := parseMetadataCollections(ctx, []data.RestoreCollection{
				data.NoFetchRestoreCollection{Collection: coll},
			})
			test.expectError(t, err, clues.ToCore(err))

			assert.Equal(t, test.canUsePreviousBackup, canUsePreviousBackup, "can use previous backup")

			emails := cdps[path.EmailCategory]

			assert.Len(t, emails, len(test.expect))

			for k, v := range emails {
				assert.Equal(t, v.Delta, emails[k].Delta, "delta")
				assert.Equal(t, v.Path, emails[k].Path, "path")
			}
		})
	}
}

type failingColl struct {
	t *testing.T
}

func (f failingColl) Items(ctx context.Context, errs *fault.Bus) <-chan data.Stream {
	ic := make(chan data.Stream)
	defer close(ic)

	errs.AddRecoverable(ctx, assert.AnError)

	return ic
}

func (f failingColl) FullPath() path.Path {
	tmp, err := path.Build(
		"tenant",
		"user",
		path.ExchangeService,
		path.EmailCategory,
		false,
		"inbox")
	require.NoError(f.t, err, clues.ToCore(err))

	return tmp
}

func (f failingColl) FetchItemByName(context.Context, string) (data.Stream, error) {
	// no fetch calls will be made
	return nil, nil
}

// This check is to ensure that we don't error out, but still return
// canUsePreviousBackup as false on read errors
func (suite *DataCollectionsUnitSuite) TestParseMetadataCollections_ReadFailure() {
	t := suite.T()

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

	fc := failingColl{t}

	_, canUsePreviousBackup, err := parseMetadataCollections(ctx, []data.RestoreCollection{fc})
	require.NoError(t, err)
	require.False(t, canUsePreviousBackup)
}

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

func newStatusUpdater(t *testing.T, wg *sync.WaitGroup) func(status *support.ControllerOperationStatus) {
	updater := func(status *support.ControllerOperationStatus) {
		defer wg.Done()
	}

	return updater
}

type BackupIntgSuite struct {
	tester.Suite
	user     string
	site     string
	tenantID string
	ac       api.Client
}

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

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

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

	graph.InitializeConcurrencyLimiter(ctx, true, 4)

	suite.user = tconfig.M365UserID(t)
	suite.site = tconfig.M365SiteID(t)

	acct := tconfig.NewM365Account(t)
	creds, err := acct.M365Config()
	require.NoError(t, err, clues.ToCore(err))

	suite.ac, err = api.NewClient(creds, control.DefaultOptions())
	require.NoError(t, err, clues.ToCore(err))

	suite.tenantID = creds.AzureTenantID

	tester.LogTimeOfTest(t)
}

func (suite *BackupIntgSuite) TestMailFetch() {
	var (
		userID   = tconfig.M365UserID(suite.T())
		users    = []string{userID}
		handlers = BackupHandlers(suite.ac)
	)

	tests := []struct {
		name                string
		scope               selectors.ExchangeScope
		folderNames         map[string]struct{}
		canMakeDeltaQueries bool
	}{
		{
			name: "Folder Iterative Check Mail",
			scope: selectors.NewExchangeBackup(users).MailFolders(
				[]string{api.MailInbox},
				selectors.PrefixMatch(),
			)[0],
			folderNames: map[string]struct{}{
				api.MailInbox: {},
			},
			canMakeDeltaQueries: true,
		},
		{
			name: "Folder Iterative Check Mail Non-Delta",
			scope: selectors.NewExchangeBackup(users).MailFolders(
				[]string{api.MailInbox},
				selectors.PrefixMatch(),
			)[0],
			folderNames: map[string]struct{}{
				api.MailInbox: {},
			},
			canMakeDeltaQueries: false,
		},
	}

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

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

			ctrlOpts := control.DefaultOptions()
			ctrlOpts.ToggleFeatures.DisableDelta = !test.canMakeDeltaQueries

			bpc := inject.BackupProducerConfig{
				LastBackupVersion: version.NoBackup,
				Options:           ctrlOpts,
				ProtectedResource: inMock.NewProvider(userID, userID),
			}

			collections, err := createCollections(
				ctx,
				bpc,
				handlers,
				suite.tenantID,
				test.scope,
				DeltaPaths{},
				func(status *support.ControllerOperationStatus) {},
				fault.New(true))
			require.NoError(t, err, clues.ToCore(err))

			for _, c := range collections {
				if c.FullPath().Service() == path.ExchangeMetadataService {
					continue
				}

				require.NotEmpty(t, c.FullPath().Folder(false))

				// TODO(ashmrtn): Remove when LocationPath is made part of BackupCollection
				// interface.
				if !assert.Implements(t, (*data.LocationPather)(nil), c) {
					continue
				}

				loc := c.(data.LocationPather).LocationPath().String()

				require.NotEmpty(t, loc)

				delete(test.folderNames, loc)
			}

			assert.Empty(t, test.folderNames)
		})
	}
}

func (suite *BackupIntgSuite) TestDelta() {
	var (
		userID   = tconfig.M365UserID(suite.T())
		users    = []string{userID}
		handlers = BackupHandlers(suite.ac)
	)

	tests := []struct {
		name  string
		scope selectors.ExchangeScope
	}{
		{
			name: "Mail",
			scope: selectors.NewExchangeBackup(users).MailFolders(
				[]string{api.MailInbox},
				selectors.PrefixMatch(),
			)[0],
		},
		{
			name: "Contacts",
			scope: selectors.NewExchangeBackup(users).ContactFolders(
				[]string{api.DefaultContacts},
				selectors.PrefixMatch(),
			)[0],
		},
		{
			name: "Events",
			scope: selectors.NewExchangeBackup(users).EventCalendars(
				[]string{api.DefaultCalendar},
				selectors.PrefixMatch(),
			)[0],
		},
	}
	for _, test := range tests {
		suite.Run(test.name, func() {
			t := suite.T()

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

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

			// get collections without providing any delta history (ie: full backup)
			collections, err := createCollections(
				ctx,
				bpc,
				handlers,
				suite.tenantID,
				test.scope,
				DeltaPaths{},
				func(status *support.ControllerOperationStatus) {},
				fault.New(true))
			require.NoError(t, err, clues.ToCore(err))
			assert.Less(t, 1, len(collections), "retrieved metadata and data collections")

			var metadata data.BackupCollection

			for _, coll := range collections {
				if coll.FullPath().Service() == path.ExchangeMetadataService {
					metadata = coll
				}
			}

			require.NotNil(t, metadata, "collections contains a metadata collection")

			cdps, canUsePreviousBackup, err := parseMetadataCollections(ctx, []data.RestoreCollection{
				data.NoFetchRestoreCollection{Collection: metadata},
			})
			require.NoError(t, err, clues.ToCore(err))
			assert.True(t, canUsePreviousBackup, "can use previous backup")

			dps := cdps[test.scope.Category().PathType()]

			// now do another backup with the previous delta tokens,
			// which should only contain the difference.
			collections, err = createCollections(
				ctx,
				bpc,
				handlers,
				suite.tenantID,
				test.scope,
				dps,
				func(status *support.ControllerOperationStatus) {},
				fault.New(true))
			require.NoError(t, err, clues.ToCore(err))

			// TODO(keepers): this isn't a very useful test at the moment.  It needs to
			// investigate the items in the original and delta collections to at least
			// assert some minimum assumptions, such as "deltas should retrieve fewer items".
			// Delta usage is commented out at the moment, anyway.  So this is currently
			// a sanity check that the minimum behavior won't break.
			for _, coll := range collections {
				if coll.FullPath().Service() != path.ExchangeMetadataService {
					ec, ok := coll.(*Collection)
					require.True(t, ok, "collection is *Collection")
					assert.NotNil(t, ec)
				}
			}
		})
	}
}

// TestMailSerializationRegression verifies that all mail data stored in the
// test account can be successfully downloaded into bytes and restored into
// M365 mail objects
func (suite *BackupIntgSuite) TestMailSerializationRegression() {
	t := suite.T()

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

	var (
		wg       sync.WaitGroup
		users    = []string{suite.user}
		handlers = BackupHandlers(suite.ac)
	)

	sel := selectors.NewExchangeBackup(users)
	sel.Include(sel.MailFolders([]string{api.MailInbox}, selectors.PrefixMatch()))

	bpc := inject.BackupProducerConfig{
		LastBackupVersion: version.NoBackup,
		Options:           control.DefaultOptions(),
		ProtectedResource: inMock.NewProvider(suite.user, suite.user),
		Selector:          sel.Selector,
	}

	collections, err := createCollections(
		ctx,
		bpc,
		handlers,
		suite.tenantID,
		sel.Scopes()[0],
		DeltaPaths{},
		newStatusUpdater(t, &wg),
		fault.New(true))
	require.NoError(t, err, clues.ToCore(err))

	wg.Add(len(collections))

	for _, edc := range collections {
		suite.Run(edc.FullPath().String(), func() {
			t := suite.T()

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

			isMetadata := edc.FullPath().Service() == path.ExchangeMetadataService
			streamChannel := edc.Items(ctx, fault.New(true))

			// Verify that each message can be restored
			for stream := range streamChannel {
				buf := &bytes.Buffer{}

				read, err := buf.ReadFrom(stream.ToReader())
				assert.NoError(t, err, clues.ToCore(err))
				assert.NotZero(t, read)

				if isMetadata {
					continue
				}

				message, err := api.BytesToMessageable(buf.Bytes())
				assert.NotNil(t, message)
				assert.NoError(t, err, clues.ToCore(err))
			}
		})
	}

	wg.Wait()
}

// TestContactSerializationRegression verifies ability to query contact items
// and to store contact within Collection. Downloaded contacts are run through
// a regression test to ensure that downloaded items can be uploaded.
func (suite *BackupIntgSuite) TestContactSerializationRegression() {
	var (
		users    = []string{suite.user}
		handlers = BackupHandlers(suite.ac)
	)

	tests := []struct {
		name  string
		scope selectors.ExchangeScope
	}{
		{
			name: "Default Contact Folder",
			scope: selectors.NewExchangeBackup(users).ContactFolders(
				[]string{api.DefaultContacts},
				selectors.PrefixMatch())[0],
		},
	}

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

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

			var wg sync.WaitGroup

			bpc := inject.BackupProducerConfig{
				LastBackupVersion: version.NoBackup,
				Options:           control.DefaultOptions(),
				ProtectedResource: inMock.NewProvider(suite.user, suite.user),
			}

			edcs, err := createCollections(
				ctx,
				bpc,
				handlers,
				suite.tenantID,
				test.scope,
				DeltaPaths{},
				newStatusUpdater(t, &wg),
				fault.New(true))
			require.NoError(t, err, clues.ToCore(err))

			wg.Add(len(edcs))

			require.GreaterOrEqual(t, len(edcs), 1, "expected 1 <= num collections <= 2")
			require.GreaterOrEqual(t, 2, len(edcs), "expected 1 <= num collections <= 2")

			for _, edc := range edcs {
				isMetadata := edc.FullPath().Service() == path.ExchangeMetadataService
				count := 0

				for stream := range edc.Items(ctx, fault.New(true)) {
					buf := &bytes.Buffer{}
					read, err := buf.ReadFrom(stream.ToReader())
					assert.NoError(t, err, clues.ToCore(err))
					assert.NotZero(t, read)

					if isMetadata {
						continue
					}

					contact, err := api.BytesToContactable(buf.Bytes())
					assert.NotNil(t, contact)
					assert.NoError(t, err, "converting contact bytes: "+buf.String(), clues.ToCore(err))
					count++
				}

				if isMetadata {
					continue
				}

				// TODO(ashmrtn): Remove when LocationPath is made part of BackupCollection
				// interface.
				if !assert.Implements(t, (*data.LocationPather)(nil), edc) {
					continue
				}

				assert.Equal(
					t,
					edc.(data.LocationPather).LocationPath().String(),
					api.DefaultContacts)
				assert.NotZero(t, count)
			}

			wg.Wait()
		})
	}
}

// TestEventsSerializationRegression ensures functionality of createCollections
// to be able to successfully query, download and restore event objects
func (suite *BackupIntgSuite) TestEventsSerializationRegression() {
	t := suite.T()

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

	var (
		users    = []string{suite.user}
		handlers = BackupHandlers(suite.ac)
		calID    string
		bdayID   string
	)

	fn := func(gcc graph.CachedContainer) error {
		if ptr.Val(gcc.GetDisplayName()) == api.DefaultCalendar {
			calID = ptr.Val(gcc.GetId())
		}

		if ptr.Val(gcc.GetDisplayName()) == "Birthdays" {
			bdayID = ptr.Val(gcc.GetId())
		}

		return nil
	}

	err := suite.ac.Events().EnumerateContainers(
		ctx,
		suite.user,
		api.DefaultCalendar,
		fn,
		fault.New(true))
	require.NoError(t, err, clues.ToCore(err))

	tests := []struct {
		name, expected string
		scope          selectors.ExchangeScope
	}{
		{
			name:     "Default Event Calendar",
			expected: calID,
			scope: selectors.NewExchangeBackup(users).EventCalendars(
				[]string{api.DefaultCalendar},
				selectors.PrefixMatch(),
			)[0],
		},
		{
			name:     "Birthday Calendar",
			expected: bdayID,
			scope: selectors.NewExchangeBackup(users).EventCalendars(
				[]string{"Birthdays"},
				selectors.PrefixMatch(),
			)[0],
		},
	}

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

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

			var wg sync.WaitGroup

			bpc := inject.BackupProducerConfig{
				LastBackupVersion: version.NoBackup,
				Options:           control.DefaultOptions(),
				ProtectedResource: inMock.NewProvider(suite.user, suite.user),
			}

			collections, err := createCollections(
				ctx,
				bpc,
				handlers,
				suite.tenantID,
				test.scope,
				DeltaPaths{},
				newStatusUpdater(t, &wg),
				fault.New(true))
			require.NoError(t, err, clues.ToCore(err))
			require.Len(t, collections, 2)

			wg.Add(len(collections))

			for _, edc := range collections {
				var isMetadata bool

				if edc.FullPath().Service() != path.ExchangeMetadataService {
					isMetadata = true
					assert.Equal(t, test.expected, edc.FullPath().Folder(false))
				} else {
					assert.Equal(t, "", edc.FullPath().Folder(false))
				}

				for item := range edc.Items(ctx, fault.New(true)) {
					buf := &bytes.Buffer{}

					read, err := buf.ReadFrom(item.ToReader())
					assert.NoError(t, err, clues.ToCore(err))
					assert.NotZero(t, read)

					if isMetadata {
						continue
					}

					event, err := api.BytesToEventable(buf.Bytes())
					assert.NotNil(t, event)
					assert.NoError(t, err, "creating event from bytes: "+buf.String(), clues.ToCore(err))
				}
			}

			wg.Wait()
		})
	}
}

type CollectionPopulationSuite struct {
	tester.Suite
	creds account.M365Config
}

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

func (suite *CollectionPopulationSuite) SetupSuite() {
	a := tconfig.NewFakeM365Account(suite.T())
	m365, err := a.M365Config()
	require.NoError(suite.T(), err, clues.ToCore(err))
	suite.creds = m365
}

func (suite *CollectionPopulationSuite) TestPopulateCollections() {
	var (
		qp = graph.QueryParams{
			Category:          path.EmailCategory, // doesn't matter which one we use.
			ProtectedResource: inMock.NewProvider("user_id", "user_name"),
			TenantID:          suite.creds.AzureTenantID,
		}
		statusUpdater = func(*support.ControllerOperationStatus) {}
		allScope      = selectors.NewExchangeBackup(nil).MailFolders(selectors.Any())[0]
		dps           = DeltaPaths{} // incrementals are tested separately
		commonResult  = mockGetterResults{
			added:    []string{"a1", "a2", "a3"},
			removed:  []string{"r1", "r2", "r3"},
			newDelta: api.DeltaUpdate{URL: "delta_url"},
		}
		errorResult = mockGetterResults{
			added:    []string{"a1", "a2", "a3"},
			removed:  []string{"r1", "r2", "r3"},
			newDelta: api.DeltaUpdate{URL: "delta_url"},
			err:      assert.AnError,
		}
		deletedInFlightResult = mockGetterResults{
			added:    []string{"a1", "a2", "a3"},
			removed:  []string{"r1", "r2", "r3"},
			newDelta: api.DeltaUpdate{URL: "delta_url"},
			err:      graph.ErrDeletedInFlight,
		}
		container1 = mockContainer{
			id:          strPtr("1"),
			displayName: strPtr("display_name_1"),
			p:           path.Builder{}.Append("1"),
			l:           path.Builder{}.Append("display_name_1"),
		}
		container2 = mockContainer{
			id:          strPtr("2"),
			displayName: strPtr("display_name_2"),
			p:           path.Builder{}.Append("2"),
			l:           path.Builder{}.Append("display_name_2"),
		}
	)

	table := []struct {
		name                  string
		getter                mockGetter
		resolver              graph.ContainerResolver
		scope                 selectors.ExchangeScope
		failFast              control.FailurePolicy
		expectErr             assert.ErrorAssertionFunc
		expectNewColls        int
		expectMetadataColls   int
		expectDoNotMergeColls int
	}{
		{
			name: "happy path, one container",
			getter: mockGetter{
				results: map[string]mockGetterResults{
					"1": commonResult,
				},
			},
			resolver:            newMockResolver(container1),
			scope:               allScope,
			expectErr:           assert.NoError,
			expectNewColls:      1,
			expectMetadataColls: 1,
		},
		{
			name: "happy path, many containers",
			getter: mockGetter{
				results: map[string]mockGetterResults{
					"1": commonResult,
					"2": commonResult,
				},
			},
			resolver:            newMockResolver(container1, container2),
			scope:               allScope,
			expectErr:           assert.NoError,
			expectNewColls:      2,
			expectMetadataColls: 1,
		},
		{
			name: "no containers pass scope",
			getter: mockGetter{
				results: map[string]mockGetterResults{
					"1": commonResult,
					"2": commonResult,
				},
			},
			resolver:            newMockResolver(container1, container2),
			scope:               selectors.NewExchangeBackup(nil).MailFolders(selectors.None())[0],
			expectErr:           assert.NoError,
			expectNewColls:      0,
			expectMetadataColls: 1,
		},
		{
			name: "err: deleted in flight",
			getter: mockGetter{
				results: map[string]mockGetterResults{
					"1": deletedInFlightResult,
				},
			},
			resolver:              newMockResolver(container1),
			scope:                 allScope,
			expectErr:             assert.NoError,
			expectNewColls:        1,
			expectMetadataColls:   1,
			expectDoNotMergeColls: 1,
		},
		{
			name: "err: other error",
			getter: mockGetter{
				results: map[string]mockGetterResults{
					"1": errorResult,
				},
			},
			resolver:            newMockResolver(container1),
			scope:               allScope,
			expectErr:           assert.NoError,
			expectNewColls:      0,
			expectMetadataColls: 1,
		},
		{
			name: "half collections error: deleted in flight",
			getter: mockGetter{
				results: map[string]mockGetterResults{
					"1": deletedInFlightResult,
					"2": commonResult,
				},
			},
			resolver:              newMockResolver(container1, container2),
			scope:                 allScope,
			expectErr:             assert.NoError,
			expectNewColls:        2,
			expectMetadataColls:   1,
			expectDoNotMergeColls: 1,
		},
		{
			name: "half collections error: other error",
			getter: mockGetter{
				results: map[string]mockGetterResults{
					"1": errorResult,
					"2": commonResult,
				},
			},
			resolver:            newMockResolver(container1, container2),
			scope:               allScope,
			expectErr:           assert.NoError,
			expectNewColls:      1,
			expectMetadataColls: 1,
		},
		{
			name: "half collections error: deleted in flight, fail fast",
			getter: mockGetter{
				results: map[string]mockGetterResults{
					"1": deletedInFlightResult,
					"2": commonResult,
				},
			},
			resolver:              newMockResolver(container1, container2),
			scope:                 allScope,
			failFast:              control.FailFast,
			expectErr:             assert.NoError,
			expectNewColls:        2,
			expectMetadataColls:   1,
			expectDoNotMergeColls: 1,
		},
		{
			name: "half collections error: other error, fail fast",
			getter: mockGetter{
				results: map[string]mockGetterResults{
					"1": errorResult,
					"2": commonResult,
				},
			},
			resolver:            newMockResolver(container1, container2),
			scope:               allScope,
			failFast:            control.FailFast,
			expectErr:           assert.Error,
			expectNewColls:      0,
			expectMetadataColls: 0,
		},
	}
	for _, test := range table {
		for _, canMakeDeltaQueries := range []bool{true, false} {
			name := test.name

			if canMakeDeltaQueries {
				name += "-delta"
			} else {
				name += "-non-delta"
			}

			suite.Run(name, func() {
				t := suite.T()

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

				ctrlOpts := control.Options{FailureHandling: test.failFast}
				ctrlOpts.ToggleFeatures.DisableDelta = !canMakeDeltaQueries

				mbh := mockBackupHandler{
					mg:       test.getter,
					category: qp.Category,
				}

				collections, err := populateCollections(
					ctx,
					qp,
					mbh,
					statusUpdater,
					test.resolver,
					test.scope,
					dps,
					ctrlOpts,
					fault.New(test.failFast == control.FailFast))
				test.expectErr(t, err, clues.ToCore(err))

				// collection assertions

				deleteds, news, metadatas, doNotMerges := 0, 0, 0, 0
				for _, c := range collections {
					if c.FullPath().Service() == path.ExchangeMetadataService {
						metadatas++
						continue
					}

					if c.State() == data.DeletedState {
						deleteds++
					}

					if c.State() == data.NewState {
						news++
					}

					if c.DoNotMergeItems() {
						doNotMerges++
					}
				}

				assert.Zero(t, deleteds, "deleted collections")
				assert.Equal(t, test.expectNewColls, news, "new collections")
				assert.Equal(t, test.expectMetadataColls, metadatas, "metadata collections")
				assert.Equal(t, test.expectDoNotMergeColls, doNotMerges, "doNotMerge collections")

				// items in collections assertions
				for k, expect := range test.getter.results {
					coll := collections[k]

					if coll == nil {
						continue
					}

					exColl, ok := coll.(*Collection)
					require.True(t, ok, "collection is an *exchange.Collection")

					ids := [][]string{
						make([]string, 0, len(exColl.added)),
						make([]string, 0, len(exColl.removed)),
					}

					for i, cIDs := range []map[string]struct{}{exColl.added, exColl.removed} {
						for id := range cIDs {
							ids[i] = append(ids[i], id)
						}
					}

					assert.ElementsMatch(t, expect.added, ids[0], "added items")
					assert.ElementsMatch(t, expect.removed, ids[1], "removed items")
				}
			})
		}
	}
}

func checkMetadata(
	t *testing.T,
	ctx context.Context, //revive:disable-line:context-as-argument
	cat path.CategoryType,
	expect DeltaPaths,
	c data.BackupCollection,
) {
	catPaths, _, err := parseMetadataCollections(
		ctx,
		[]data.RestoreCollection{data.NoFetchRestoreCollection{Collection: c}})
	if !assert.NoError(t, err, "getting metadata", clues.ToCore(err)) {
		return
	}

	assert.Equal(t, expect, catPaths[cat])
}

func (suite *CollectionPopulationSuite) TestFilterContainersAndFillCollections_DuplicateFolders() {
	type scopeCat struct {
		scope selectors.ExchangeScope
		cat   path.CategoryType
	}

	var (
		qp = graph.QueryParams{
			ProtectedResource: inMock.NewProvider("user_id", "user_name"),
			TenantID:          suite.creds.AzureTenantID,
		}

		statusUpdater = func(*support.ControllerOperationStatus) {}

		dataTypes = []scopeCat{
			{
				scope: selectors.NewExchangeBackup(nil).MailFolders(selectors.Any())[0],
				cat:   path.EmailCategory,
			},
			{
				scope: selectors.NewExchangeBackup(nil).ContactFolders(selectors.Any())[0],
				cat:   path.ContactsCategory,
			},
			{
				scope: selectors.NewExchangeBackup(nil).EventCalendars(selectors.Any())[0],
				cat:   path.EventsCategory,
			},
		}

		location = path.Builder{}.Append("foo", "bar")

		result1 = mockGetterResults{
			added:    []string{"a1", "a2", "a3"},
			removed:  []string{"r1", "r2", "r3"},
			newDelta: api.DeltaUpdate{URL: "delta_url"},
		}
		result2 = mockGetterResults{
			added:    []string{"a4", "a5", "a6"},
			removed:  []string{"r4", "r5", "r6"},
			newDelta: api.DeltaUpdate{URL: "delta_url2"},
		}

		container1 = mockContainer{
			id:          strPtr("1"),
			displayName: strPtr("bar"),
			p:           path.Builder{}.Append("1"),
			l:           location,
		}
		container2 = mockContainer{
			id:          strPtr("2"),
			displayName: strPtr("bar"),
			p:           path.Builder{}.Append("2"),
			l:           location,
		}
	)

	oldPath1 := func(t *testing.T, cat path.CategoryType) path.Path {
		res, err := location.Append("1").ToDataLayerPath(
			suite.creds.AzureTenantID,
			qp.ProtectedResource.ID(),
			path.ExchangeService,
			cat,
			false)
		require.NoError(t, err, clues.ToCore(err))

		return res
	}

	oldPath2 := func(t *testing.T, cat path.CategoryType) path.Path {
		res, err := location.Append("2").ToDataLayerPath(
			suite.creds.AzureTenantID,
			qp.ProtectedResource.ID(),
			path.ExchangeService,
			cat,
			false)
		require.NoError(t, err, clues.ToCore(err))

		return res
	}

	idPath1 := func(t *testing.T, cat path.CategoryType) path.Path {
		res, err := path.Builder{}.Append("1").ToDataLayerPath(
			suite.creds.AzureTenantID,
			qp.ProtectedResource.ID(),
			path.ExchangeService,
			cat,
			false)
		require.NoError(t, err, clues.ToCore(err))

		return res
	}

	idPath2 := func(t *testing.T, cat path.CategoryType) path.Path {
		res, err := path.Builder{}.Append("2").ToDataLayerPath(
			suite.creds.AzureTenantID,
			qp.ProtectedResource.ID(),
			path.ExchangeService,
			cat,
			false)
		require.NoError(t, err, clues.ToCore(err))

		return res
	}

	table := []struct {
		name           string
		getter         mockGetter
		resolver       graph.ContainerResolver
		inputMetadata  func(t *testing.T, cat path.CategoryType) DeltaPaths
		expectNewColls int
		expectDeleted  int
		expectMetadata func(t *testing.T, cat path.CategoryType) DeltaPaths
	}{
		{
			name: "1 moved to duplicate",
			getter: mockGetter{
				results: map[string]mockGetterResults{
					"1": result1,
					"2": result2,
				},
			},
			resolver: newMockResolver(container1, container2),
			inputMetadata: func(t *testing.T, cat path.CategoryType) DeltaPaths {
				return DeltaPaths{
					"1": DeltaPath{
						Delta: "old_delta",
						Path:  oldPath1(t, cat).String(),
					},
					"2": DeltaPath{
						Delta: "old_delta",
						Path:  idPath2(t, cat).String(),
					},
				}
			},
			expectMetadata: func(t *testing.T, cat path.CategoryType) DeltaPaths {
				return DeltaPaths{
					"1": DeltaPath{
						Delta: "delta_url",
						Path:  idPath1(t, cat).String(),
					},
					"2": DeltaPath{
						Delta: "delta_url2",
						Path:  idPath2(t, cat).String(),
					},
				}
			},
		},
		{
			name: "both move to duplicate",
			getter: mockGetter{
				results: map[string]mockGetterResults{
					"1": result1,
					"2": result2,
				},
			},
			resolver: newMockResolver(container1, container2),
			inputMetadata: func(t *testing.T, cat path.CategoryType) DeltaPaths {
				return DeltaPaths{
					"1": DeltaPath{
						Delta: "old_delta",
						Path:  oldPath1(t, cat).String(),
					},
					"2": DeltaPath{
						Delta: "old_delta",
						Path:  oldPath2(t, cat).String(),
					},
				}
			},
			expectMetadata: func(t *testing.T, cat path.CategoryType) DeltaPaths {
				return DeltaPaths{
					"1": DeltaPath{
						Delta: "delta_url",
						Path:  idPath1(t, cat).String(),
					},
					"2": DeltaPath{
						Delta: "delta_url2",
						Path:  idPath2(t, cat).String(),
					},
				}
			},
		},
		{
			name: "both new",
			getter: mockGetter{
				results: map[string]mockGetterResults{
					"1": result1,
					"2": result2,
				},
			},
			resolver: newMockResolver(container1, container2),
			inputMetadata: func(t *testing.T, cat path.CategoryType) DeltaPaths {
				return DeltaPaths{}
			},
			expectNewColls: 2,
			expectMetadata: func(t *testing.T, cat path.CategoryType) DeltaPaths {
				return DeltaPaths{
					"1": DeltaPath{
						Delta: "delta_url",
						Path:  idPath1(t, cat).String(),
					},
					"2": DeltaPath{
						Delta: "delta_url2",
						Path:  idPath2(t, cat).String(),
					},
				}
			},
		},
		{
			name: "add 1 remove 2",
			getter: mockGetter{
				results: map[string]mockGetterResults{
					"1": result1,
				},
			},
			resolver: newMockResolver(container1),
			inputMetadata: func(t *testing.T, cat path.CategoryType) DeltaPaths {
				return DeltaPaths{
					"2": DeltaPath{
						Delta: "old_delta",
						Path:  idPath2(t, cat).String(),
					},
				}
			},
			expectNewColls: 1,
			expectDeleted:  1,
			expectMetadata: func(t *testing.T, cat path.CategoryType) DeltaPaths {
				return DeltaPaths{
					"1": DeltaPath{
						Delta: "delta_url",
						Path:  idPath1(t, cat).String(),
					},
				}
			},
		},
	}

	for _, sc := range dataTypes {
		suite.Run(sc.cat.String(), func() {
			qp.Category = sc.cat

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

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

					mbh := mockBackupHandler{
						mg:       test.getter,
						category: qp.Category,
					}

					collections, err := populateCollections(
						ctx,
						qp,
						mbh,
						statusUpdater,
						test.resolver,
						sc.scope,
						test.inputMetadata(t, qp.Category),
						control.Options{FailureHandling: control.FailFast},
						fault.New(true))
					require.NoError(t, err, "getting collections", clues.ToCore(err))

					// collection assertions

					deleteds, news, metadatas := 0, 0, 0
					for _, c := range collections {
						if c.State() == data.DeletedState {
							deleteds++
							continue
						}

						if c.FullPath().Service() == path.ExchangeMetadataService {
							metadatas++
							checkMetadata(t, ctx, qp.Category, test.expectMetadata(t, qp.Category), c)
							continue
						}

						if c.State() == data.NewState {
							news++
						}
					}

					assert.Equal(t, test.expectDeleted, deleteds, "deleted collections")
					assert.Equal(t, test.expectNewColls, news, "new collections")
					assert.Equal(t, 1, metadatas, "metadata collections")

					// items in collections assertions
					for k, expect := range test.getter.results {
						coll := collections[k]

						if coll == nil {
							continue
						}

						exColl, ok := coll.(*Collection)
						require.True(t, ok, "collection is an *exchange.Collection")

						ids := [][]string{
							make([]string, 0, len(exColl.added)),
							make([]string, 0, len(exColl.removed)),
						}

						for i, cIDs := range []map[string]struct{}{exColl.added, exColl.removed} {
							for id := range cIDs {
								ids[i] = append(ids[i], id)
							}
						}

						assert.ElementsMatch(t, expect.added, ids[0], "added items")
						assert.ElementsMatch(t, expect.removed, ids[1], "removed items")
					}
				})
			}
		})
	}
}

func (suite *CollectionPopulationSuite) TestFilterContainersAndFillCollections_repeatedItems() {
	newDelta := api.DeltaUpdate{URL: "delta_url"}

	table := []struct {
		name          string
		getter        mockGetter
		expectAdded   map[string]struct{}
		expectRemoved map[string]struct{}
	}{
		{
			name: "repeated adds",
			getter: mockGetter{
				results: map[string]mockGetterResults{
					"1": {
						added:    []string{"a1", "a2", "a3", "a1"},
						newDelta: newDelta,
					},
				},
			},
			expectAdded: map[string]struct{}{
				"a1": {},
				"a2": {},
				"a3": {},
			},
			expectRemoved: map[string]struct{}{},
		},
		{
			name: "repeated removes",
			getter: mockGetter{
				results: map[string]mockGetterResults{
					"1": {
						removed:  []string{"r1", "r2", "r3", "r1"},
						newDelta: newDelta,
					},
				},
			},
			expectAdded: map[string]struct{}{},
			expectRemoved: map[string]struct{}{
				"r1": {},
				"r2": {},
				"r3": {},
			},
		},
		{
			name: "remove for same item wins",
			getter: mockGetter{
				results: map[string]mockGetterResults{
					"1": {
						added:    []string{"i1", "a2", "a3"},
						removed:  []string{"i1", "r2", "r3"},
						newDelta: newDelta,
					},
				},
			},
			expectAdded: map[string]struct{}{
				"a2": {},
				"a3": {},
			},
			expectRemoved: map[string]struct{}{
				"i1": {},
				"r2": {},
				"r3": {},
			},
		},
	}
	for _, test := range table {
		suite.Run(test.name, func() {
			t := suite.T()

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

			var (
				qp = graph.QueryParams{
					Category:          path.EmailCategory, // doesn't matter which one we use.
					ProtectedResource: inMock.NewProvider("user_id", "user_name"),
					TenantID:          suite.creds.AzureTenantID,
				}
				statusUpdater = func(*support.ControllerOperationStatus) {}
				allScope      = selectors.NewExchangeBackup(nil).MailFolders(selectors.Any())[0]
				dps           = DeltaPaths{} // incrementals are tested separately
				container1    = mockContainer{
					id:          strPtr("1"),
					displayName: strPtr("display_name_1"),
					p:           path.Builder{}.Append("1"),
					l:           path.Builder{}.Append("display_name_1"),
				}
				resolver = newMockResolver(container1)
				mbh      = mockBackupHandler{
					mg:       test.getter,
					category: qp.Category,
				}
			)

			require.Equal(t, "user_id", qp.ProtectedResource.ID(), qp.ProtectedResource)
			require.Equal(t, "user_name", qp.ProtectedResource.Name(), qp.ProtectedResource)

			collections, err := populateCollections(
				ctx,
				qp,
				mbh,
				statusUpdater,
				resolver,
				allScope,
				dps,
				control.Options{FailureHandling: control.FailFast},
				fault.New(true))
			require.NoError(t, err, clues.ToCore(err))

			// collection assertions

			deleteds, news, metadatas, doNotMerges := 0, 0, 0, 0
			for _, c := range collections {
				if c.FullPath().Service() == path.ExchangeMetadataService {
					metadatas++
					continue
				}

				if c.State() == data.DeletedState {
					deleteds++
				}

				if c.State() == data.NewState {
					news++
				}

				if c.DoNotMergeItems() {
					doNotMerges++
				}
			}

			assert.Zero(t, deleteds, "deleted collections")
			assert.Equal(t, 1, news, "new collections")
			assert.Equal(t, 1, metadatas, "metadata collections")
			assert.Zero(t, doNotMerges, "doNotMerge collections")

			// items in collections assertions
			for k := range test.getter.results {
				coll := collections[k]
				if !assert.NotNilf(t, coll, "missing collection for path %s", k) {
					continue
				}

				exColl, ok := coll.(*Collection)
				require.True(t, ok, "collection is an *exchange.Collection")

				assert.Equal(t, test.expectAdded, exColl.added, "added items")
				assert.Equal(t, test.expectRemoved, exColl.removed, "removed items")
			}
		})
	}
}

func (suite *CollectionPopulationSuite) TestFilterContainersAndFillCollections_incrementals_nondelta() {
	var (
		userID   = "user_id"
		tenantID = suite.creds.AzureTenantID
		cat      = path.EmailCategory // doesn't matter which one we use,
		qp       = graph.QueryParams{
			Category:          cat,
			ProtectedResource: inMock.NewProvider("user_id", "user_name"),
			TenantID:          suite.creds.AzureTenantID,
		}
		statusUpdater = func(*support.ControllerOperationStatus) {}
		allScope      = selectors.NewExchangeBackup(nil).MailFolders(selectors.Any())[0]
		commonResults = mockGetterResults{
			added:    []string{"added"},
			newDelta: api.DeltaUpdate{URL: "new_delta_url"},
		}
		expiredResults = mockGetterResults{
			added: []string{"added"},
			newDelta: api.DeltaUpdate{
				URL:   "new_delta_url",
				Reset: true,
			},
		}
	)

	prevPath := func(t *testing.T, at ...string) path.Path {
		p, err := path.Build(tenantID, userID, path.ExchangeService, cat, false, at...)
		require.NoError(t, err, clues.ToCore(err))

		return p
	}

	type endState struct {
		state      data.CollectionState
		doNotMerge bool
	}

	table := []struct {
		name                  string
		getter                mockGetter
		resolver              graph.ContainerResolver
		dps                   DeltaPaths
		expect                map[string]endState
		skipWhenForcedNoDelta bool
	}{
		{
			name: "new container",
			getter: mockGetter{
				results: map[string]mockGetterResults{
					"1": commonResults,
				},
			},
			resolver: newMockResolver(mockContainer{
				id:          strPtr("1"),
				displayName: strPtr("new"),
				p:           path.Builder{}.Append("1", "new"),
				l:           path.Builder{}.Append("1", "new"),
			}),
			dps: DeltaPaths{},
			expect: map[string]endState{
				"1": {data.NewState, false},
			},
		},
		{
			name: "not moved container",
			getter: mockGetter{
				results: map[string]mockGetterResults{
					"1": commonResults,
				},
			},
			resolver: newMockResolver(mockContainer{
				id:          strPtr("1"),
				displayName: strPtr("not_moved"),
				p:           path.Builder{}.Append("1", "not_moved"),
				l:           path.Builder{}.Append("1", "not_moved"),
			}),
			dps: DeltaPaths{
				"1": DeltaPath{
					Delta: "old_delta_url",
					Path:  prevPath(suite.T(), "1", "not_moved").String(),
				},
			},
			expect: map[string]endState{
				"1": {data.NotMovedState, false},
			},
		},
		{
			name: "moved container",
			getter: mockGetter{
				results: map[string]mockGetterResults{
					"1": commonResults,
				},
			},
			resolver: newMockResolver(mockContainer{
				id:          strPtr("1"),
				displayName: strPtr("moved"),
				p:           path.Builder{}.Append("1", "moved"),
				l:           path.Builder{}.Append("1", "moved"),
			}),
			dps: DeltaPaths{
				"1": DeltaPath{
					Delta: "old_delta_url",
					Path:  prevPath(suite.T(), "1", "prev").String(),
				},
			},
			expect: map[string]endState{
				"1": {data.MovedState, false},
			},
		},
		{
			name: "deleted container",
			getter: mockGetter{
				results: map[string]mockGetterResults{},
			},
			resolver: newMockResolver(),
			dps: DeltaPaths{
				"1": DeltaPath{
					Delta: "old_delta_url",
					Path:  prevPath(suite.T(), "1", "deleted").String(),
				},
			},
			expect: map[string]endState{
				"1": {data.DeletedState, false},
			},
		},
		{
			name: "one deleted, one new",
			getter: mockGetter{
				results: map[string]mockGetterResults{
					"2": commonResults,
				},
			},
			resolver: newMockResolver(mockContainer{
				id:          strPtr("2"),
				displayName: strPtr("new"),
				p:           path.Builder{}.Append("2", "new"),
				l:           path.Builder{}.Append("2", "new"),
			}),
			dps: DeltaPaths{
				"1": DeltaPath{
					Delta: "old_delta_url",
					Path:  prevPath(suite.T(), "1", "deleted").String(),
				},
			},
			expect: map[string]endState{
				"1": {data.DeletedState, false},
				"2": {data.NewState, false},
			},
		},
		{
			name: "one deleted, one new, same path",
			getter: mockGetter{
				results: map[string]mockGetterResults{
					"2": commonResults,
				},
			},
			resolver: newMockResolver(mockContainer{
				id:          strPtr("2"),
				displayName: strPtr("same"),
				p:           path.Builder{}.Append("2", "same"),
				l:           path.Builder{}.Append("2", "same"),
			}),
			dps: DeltaPaths{
				"1": DeltaPath{
					Delta: "old_delta_url",
					Path:  prevPath(suite.T(), "1", "same").String(),
				},
			},
			expect: map[string]endState{
				"1": {data.DeletedState, false},
				"2": {data.NewState, false},
			},
		},
		{
			name: "one moved, one new, same path",
			getter: mockGetter{
				results: map[string]mockGetterResults{
					"1": commonResults,
					"2": commonResults,
				},
			},
			resolver: newMockResolver(
				mockContainer{
					id:          strPtr("1"),
					displayName: strPtr("moved"),
					p:           path.Builder{}.Append("1", "moved"),
					l:           path.Builder{}.Append("1", "moved"),
				},
				mockContainer{
					id:          strPtr("2"),
					displayName: strPtr("prev"),
					p:           path.Builder{}.Append("2", "prev"),
					l:           path.Builder{}.Append("2", "prev"),
				},
			),
			dps: DeltaPaths{
				"1": DeltaPath{
					Delta: "old_delta_url",
					Path:  prevPath(suite.T(), "1", "prev").String(),
				},
			},
			expect: map[string]endState{
				"1": {data.MovedState, false},
				"2": {data.NewState, false},
			},
		},
		{
			name: "bad previous path strings",
			getter: mockGetter{
				results: map[string]mockGetterResults{
					"1": commonResults,
				},
			},
			resolver: newMockResolver(mockContainer{
				id:          strPtr("1"),
				displayName: strPtr("not_moved"),
				p:           path.Builder{}.Append("1", "not_moved"),
				l:           path.Builder{}.Append("1", "not_moved"),
			}),
			dps: DeltaPaths{
				"1": DeltaPath{
					Delta: "old_delta_url",
					Path:  "1/fnords/mc/smarfs",
				},
				"2": DeltaPath{
					Delta: "old_delta_url",
					Path:  "2/fnords/mc/smarfs",
				},
			},
			expect: map[string]endState{
				"1": {data.NewState, false},
			},
		},
		{
			name: "delta expiration",
			getter: mockGetter{
				results: map[string]mockGetterResults{
					"1": expiredResults,
				},
			},
			resolver: newMockResolver(mockContainer{
				id:          strPtr("1"),
				displayName: strPtr("same"),
				p:           path.Builder{}.Append("1", "same"),
				l:           path.Builder{}.Append("1", "same"),
			}),
			dps: DeltaPaths{
				"1": DeltaPath{
					Delta: "old_delta_url",
					Path:  prevPath(suite.T(), "1", "same").String(),
				},
			},
			expect: map[string]endState{
				"1": {data.NotMovedState, true},
			},
			skipWhenForcedNoDelta: true, // this is not a valid test for non-delta
		},
		{
			name: "a little bit of everything",
			getter: mockGetter{
				results: map[string]mockGetterResults{
					"1": commonResults,  // new
					"2": commonResults,  // notMoved
					"3": commonResults,  // moved
					"4": expiredResults, // moved
					// "5" gets deleted
				},
			},
			resolver: newMockResolver(
				mockContainer{
					id:          strPtr("1"),
					displayName: strPtr("new"),
					p:           path.Builder{}.Append("1", "new"),
					l:           path.Builder{}.Append("1", "new"),
				},
				mockContainer{
					id:          strPtr("2"),
					displayName: strPtr("not_moved"),
					p:           path.Builder{}.Append("2", "not_moved"),
					l:           path.Builder{}.Append("2", "not_moved"),
				},
				mockContainer{
					id:          strPtr("3"),
					displayName: strPtr("moved"),
					p:           path.Builder{}.Append("3", "moved"),
					l:           path.Builder{}.Append("3", "moved"),
				},
				mockContainer{
					id:          strPtr("4"),
					displayName: strPtr("moved"),
					p:           path.Builder{}.Append("4", "moved"),
					l:           path.Builder{}.Append("4", "moved"),
				},
			),
			dps: DeltaPaths{
				"2": DeltaPath{
					Delta: "old_delta_url",
					Path:  prevPath(suite.T(), "2", "not_moved").String(),
				},
				"3": DeltaPath{
					Delta: "old_delta_url",
					Path:  prevPath(suite.T(), "3", "prev").String(),
				},
				"4": DeltaPath{
					Delta: "old_delta_url",
					Path:  prevPath(suite.T(), "4", "prev").String(),
				},
				"5": DeltaPath{
					Delta: "old_delta_url",
					Path:  prevPath(suite.T(), "5", "deleted").String(),
				},
			},
			expect: map[string]endState{
				"1": {data.NewState, false},
				"2": {data.NotMovedState, false},
				"3": {data.MovedState, false},
				"4": {data.MovedState, true},
				"5": {data.DeletedState, false},
			},
			skipWhenForcedNoDelta: true,
		},
	}
	for _, test := range table {
		for _, deltaBefore := range []bool{true, false} {
			for _, deltaAfter := range []bool{true, false} {
				name := test.name

				if deltaAfter {
					name += "-delta"
				} else {
					if test.skipWhenForcedNoDelta {
						suite.T().Skip("intentionally skipped non-delta case")
					}
					name += "-non-delta"
				}

				suite.Run(name, func() {
					t := suite.T()

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

					ctrlOpts := control.DefaultOptions()
					ctrlOpts.ToggleFeatures.DisableDelta = !deltaAfter

					getter := test.getter
					if !deltaAfter {
						getter.noReturnDelta = false
					}

					mbh := mockBackupHandler{
						mg:       test.getter,
						category: qp.Category,
					}

					dps := test.dps
					if !deltaBefore {
						for k, dp := range dps {
							dp.Delta = ""
							dps[k] = dp
						}
					}

					collections, err := populateCollections(
						ctx,
						qp,
						mbh,
						statusUpdater,
						test.resolver,
						allScope,
						test.dps,
						ctrlOpts,
						fault.New(true))
					assert.NoError(t, err, clues.ToCore(err))

					metadatas := 0
					for _, c := range collections {
						p := c.FullPath()
						if p == nil {
							p = c.PreviousPath()
						}

						require.NotNil(t, p)

						if p.Service() == path.ExchangeMetadataService {
							metadatas++
							continue
						}

						p0 := p.Folders()[0]

						expect, ok := test.expect[p0]
						assert.True(t, ok, "collection is expected in result")

						assert.Equalf(t, expect.state, c.State(), "collection %s state", p0)
						assert.Equalf(t, expect.doNotMerge, c.DoNotMergeItems(), "collection %s DoNotMergeItems", p0)
					}

					assert.Equal(t, 1, metadatas, "metadata collections")
				})
			}
		}
	}
}
