package selectors

import (
	"strings"
	"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/tester"
	"github.com/alcionai/corso/src/pkg/backup/details"
	"github.com/alcionai/corso/src/pkg/fault"
	"github.com/alcionai/corso/src/pkg/filters"
	"github.com/alcionai/corso/src/pkg/path"
)

type TeamsChatsSelectorSuite struct {
	tester.Suite
}

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

func (suite *TeamsChatsSelectorSuite) TestNewTeamsChatsBackup() {
	t := suite.T()
	eb := NewTeamsChatsBackup(nil)
	assert.Equal(t, eb.Service, ServiceTeamsChats)
	assert.NotZero(t, eb.Scopes())
}

func (suite *TeamsChatsSelectorSuite) TestToTeamsChatsBackup() {
	t := suite.T()
	eb := NewTeamsChatsBackup(nil)
	s := eb.Selector
	eb, err := s.ToTeamsChatsBackup()
	require.NoError(t, err, clues.ToCore(err))
	assert.Equal(t, eb.Service, ServiceTeamsChats)
	assert.NotZero(t, eb.Scopes())
}

func (suite *TeamsChatsSelectorSuite) TestNewTeamsChatsRestore() {
	t := suite.T()
	er := NewTeamsChatsRestore(nil)
	assert.Equal(t, er.Service, ServiceTeamsChats)
	assert.NotZero(t, er.Scopes())
}

func (suite *TeamsChatsSelectorSuite) TestToTeamsChatsRestore() {
	t := suite.T()
	eb := NewTeamsChatsRestore(nil)
	s := eb.Selector
	eb, err := s.ToTeamsChatsRestore()
	require.NoError(t, err, clues.ToCore(err))
	assert.Equal(t, eb.Service, ServiceTeamsChats)
	assert.NotZero(t, eb.Scopes())
}

func (suite *TeamsChatsSelectorSuite) TestTeamsChatsSelector_Exclude_TeamsChats() {
	t := suite.T()

	const (
		user   = "user"
		folder = AnyTgt
		c1     = "c1"
		c2     = "c2"
	)

	sel := NewTeamsChatsBackup([]string{user})
	sel.Exclude(sel.Chats([]string{c1, c2}))
	scopes := sel.Excludes
	require.Len(t, scopes, 1)

	scopeMustHave(
		t,
		TeamsChatsScope(scopes[0]),
		map[categorizer][]string{
			TeamsChatsChat: {c1, c2},
		})
}

func (suite *TeamsChatsSelectorSuite) TestTeamsChatsSelector_Include_TeamsChats() {
	t := suite.T()

	const (
		user   = "user"
		folder = AnyTgt
		c1     = "c1"
		c2     = "c2"
	)

	sel := NewTeamsChatsBackup([]string{user})
	sel.Include(sel.Chats([]string{c1, c2}))
	scopes := sel.Includes
	require.Len(t, scopes, 1)

	scopeMustHave(
		t,
		TeamsChatsScope(scopes[0]),
		map[categorizer][]string{
			TeamsChatsChat: {c1, c2},
		})

	assert.Equal(t, sel.Scopes()[0].Category(), TeamsChatsChat)
}

func (suite *TeamsChatsSelectorSuite) TestTeamsChatsSelector_Exclude_AllData() {
	t := suite.T()

	const (
		u1 = "u1"
		u2 = "u2"
	)

	sel := NewTeamsChatsBackup([]string{u1, u2})
	sel.Exclude(sel.AllData())
	scopes := sel.Excludes
	require.Len(t, scopes, 1)

	for _, sc := range scopes {
		if sc[scopeKeyCategory].Compare(TeamsChatsChat.String()) {
			scopeMustHave(
				t,
				TeamsChatsScope(sc),
				map[categorizer][]string{
					TeamsChatsChat: Any(),
				})
		}
	}
}

func (suite *TeamsChatsSelectorSuite) TestTeamsChatsSelector_Include_AllData() {
	t := suite.T()

	const (
		u1 = "u1"
		u2 = "u2"
	)

	sel := NewTeamsChatsBackup([]string{u1, u2})
	sel.Include(sel.AllData())
	scopes := sel.Includes
	require.Len(t, scopes, 1)

	for _, sc := range scopes {
		if sc[scopeKeyCategory].Compare(TeamsChatsChat.String()) {
			scopeMustHave(
				t,
				TeamsChatsScope(sc),
				map[categorizer][]string{
					TeamsChatsChat: Any(),
				})
		}
	}
}

func (suite *TeamsChatsSelectorSuite) TestTeamsChatsBackup_Scopes() {
	eb := NewTeamsChatsBackup(Any())
	eb.Include(eb.AllData())

	scopes := eb.Scopes()
	assert.Len(suite.T(), scopes, 1)

	for _, sc := range scopes {
		cat := sc.Category()
		suite.Run(cat.String(), func() {
			t := suite.T()

			switch sc.Category() {
			case TeamsChatsChat:
				assert.True(t, sc.IsAny(TeamsChatsChat))
			}
		})
	}
}

func (suite *TeamsChatsSelectorSuite) TestTeamsChatsScope_Category() {
	table := []struct {
		is     teamsChatsCategory
		expect teamsChatsCategory
		check  assert.ComparisonAssertionFunc
	}{
		{TeamsChatsCategoryUnknown, TeamsChatsCategoryUnknown, assert.Equal},
		{TeamsChatsCategoryUnknown, TeamsChatsUser, assert.NotEqual},
		{TeamsChatsChat, TeamsChatsChat, assert.Equal},
		{TeamsChatsUser, TeamsChatsUser, assert.Equal},
		{TeamsChatsUser, TeamsChatsCategoryUnknown, assert.NotEqual},
	}
	for _, test := range table {
		suite.Run(test.is.String()+test.expect.String(), func() {
			eb := NewTeamsChatsBackup(Any())
			eb.Includes = []scope{
				{scopeKeyCategory: filters.Identity(test.is.String())},
			}
			scope := eb.Scopes()[0]
			test.check(suite.T(), test.expect, scope.Category())
		})
	}
}

func (suite *TeamsChatsSelectorSuite) TestTeamsChatsScope_IncludesCategory() {
	table := []struct {
		is     teamsChatsCategory
		expect teamsChatsCategory
		check  assert.BoolAssertionFunc
	}{
		{TeamsChatsCategoryUnknown, TeamsChatsCategoryUnknown, assert.False},
		{TeamsChatsCategoryUnknown, TeamsChatsUser, assert.True},
		{TeamsChatsUser, TeamsChatsUser, assert.True},
		{TeamsChatsUser, TeamsChatsCategoryUnknown, assert.True},
	}
	for _, test := range table {
		suite.Run(test.is.String()+test.expect.String(), func() {
			eb := NewTeamsChatsBackup(Any())
			eb.Includes = []scope{
				{scopeKeyCategory: filters.Identity(test.is.String())},
			}
			scope := eb.Scopes()[0]
			test.check(suite.T(), scope.IncludesCategory(test.expect))
		})
	}
}

func (suite *TeamsChatsSelectorSuite) TestTeamsChatsScope_Get() {
	eb := NewTeamsChatsBackup(Any())
	eb.Include(eb.AllData())

	scopes := eb.Scopes()

	table := []teamsChatsCategory{
		TeamsChatsChat,
	}
	for _, test := range table {
		suite.Run(test.String(), func() {
			t := suite.T()

			for _, sc := range scopes {
				switch sc.Category() {
				case TeamsChatsChat:
					assert.Equal(t, Any(), sc.Get(TeamsChatsChat))
				}
				assert.Equal(t, None(), sc.Get(TeamsChatsCategoryUnknown))
			}
		})
	}
}

func (suite *TeamsChatsSelectorSuite) TestTeamsChatsScope_MatchesInfo() {
	cs := NewTeamsChatsRestore(Any())

	const (
		name    = "smarf mcfnords"
		member  = "cooks@2many.smarf"
		subject = "I have seen the fnords!"
	)

	var (
		now    = time.Now()
		future = now.Add(1 * time.Minute)
	)

	infoWith := func(itype details.ItemType) details.ItemInfo {
		return details.ItemInfo{
			TeamsChats: &details.TeamsChatsInfo{
				ItemType: itype,
				Chat: details.ChatInfo{
					CreatedAt:          now,
					HasExternalMembers: false,
					LastMessageAt:      future,
					LastMessagePreview: "preview",
					Members:            []string{member},
					MessageCount:       1,
					Name:               name,
				},
			},
		}
	}

	table := []struct {
		name   string
		itype  details.ItemType
		scope  []TeamsChatsScope
		expect assert.BoolAssertionFunc
	}{
		{"chat with a different member", details.TeamsChat, cs.ChatMember("blarps"), assert.False},
		{"chat with the same member", details.TeamsChat, cs.ChatMember(member), assert.True},
		{"chat with a member submatch search", details.TeamsChat, cs.ChatMember(member[2:5]), assert.True},
		{"chat with a different name", details.TeamsChat, cs.ChatName("blarps"), assert.False},
		{"chat with the same name", details.TeamsChat, cs.ChatName(name), assert.True},
		{"chat with a subname search", details.TeamsChat, cs.ChatName(name[2:5]), assert.True},
	}
	for _, test := range table {
		suite.Run(test.name, func() {
			t := suite.T()

			scopes := setScopesToDefault(test.scope)
			for _, scope := range scopes {
				test.expect(t, scope.matchesInfo(infoWith(test.itype)))
			}
		})
	}
}

func (suite *TeamsChatsSelectorSuite) TestTeamsChatsScope_MatchesPath() {
	const (
		user = "userID"
		chat = "chatID"
	)

	repoRef, err := path.Build("tid", user, path.TeamsChatsService, path.ChatsCategory, true, chat)
	require.NoError(suite.T(), err, clues.ToCore(err))

	var (
		loc   = strings.Join([]string{chat}, "/")
		short = "thisisahashofsomekind"
		cs    = NewTeamsChatsRestore(Any())
		ent   = details.Entry{
			RepoRef:     repoRef.String(),
			ShortRef:    short,
			ItemRef:     chat,
			LocationRef: loc,
		}
	)

	table := []struct {
		name     string
		scope    []TeamsChatsScope
		shortRef string
		expect   assert.BoolAssertionFunc
	}{
		{"all items", cs.AllData(), "", assert.True},
		{"all chats", cs.Chats(Any()), "", assert.True},
		{"no chats", cs.Chats(None()), "", assert.False},
		{"matching chats", cs.Chats([]string{chat}), "", assert.True},
		{"non-matching chats", cs.Chats([]string{"smarf"}), "", assert.False},
		{"one of multiple chats", cs.Chats([]string{"smarf", chat}), "", assert.True},
		{"chats short ref", cs.Chats([]string{short}), short, assert.True},
		{"non-leaf short ref", cs.Chats([]string{"foo"}), short, assert.False},
	}
	for _, test := range table {
		suite.Run(test.name, func() {
			t := suite.T()

			scopes := setScopesToDefault(test.scope)
			var aMatch bool
			for _, scope := range scopes {
				pvs, err := TeamsChatsChat.pathValues(repoRef, ent, Config{})
				require.NoError(t, err)

				if matchesPathValues(scope, TeamsChatsChat, pvs) {
					aMatch = true
					break
				}
			}
			test.expect(t, aMatch)
		})
	}
}

func (suite *TeamsChatsSelectorSuite) TestTeamsChatsRestore_Reduce() {
	chat, err := path.Build("tid", "uid", path.TeamsChatsService, path.ChatsCategory, true, "cid")
	require.NoError(suite.T(), err, clues.ToCore(err))

	toRR := func(p path.Path) string {
		newElems := []string{}

		for _, e := range p.Folders() {
			newElems = append(newElems, e+".d")
		}

		joinedFldrs := strings.Join(newElems, "/")

		return stubRepoRef(p.Service(), p.Category(), p.ProtectedResource(), joinedFldrs, p.Item())
	}

	makeDeets := func(refs ...path.Path) *details.Details {
		deets := &details.Details{
			DetailsModel: details.DetailsModel{
				Entries: []details.Entry{},
			},
		}

		for _, r := range refs {
			itype := details.UnknownType

			switch r {
			case chat:
				itype = details.TeamsChat
			}

			deets.Entries = append(deets.Entries, details.Entry{
				RepoRef: toRR(r),
				// Don't escape because we assume nice paths.
				LocationRef: r.Folder(false),
				ItemInfo: details.ItemInfo{
					TeamsChats: &details.TeamsChatsInfo{
						ItemType: itype,
					},
				},
			})
		}

		return deets
	}

	table := []struct {
		name         string
		deets        *details.Details
		makeSelector func() *TeamsChatsRestore
		expect       []string
	}{
		{
			"no refs",
			makeDeets(),
			func() *TeamsChatsRestore {
				er := NewTeamsChatsRestore(Any())
				er.Include(er.AllData())
				return er
			},
			[]string{},
		},
		{
			"chat only",
			makeDeets(chat),
			func() *TeamsChatsRestore {
				er := NewTeamsChatsRestore(Any())
				er.Include(er.AllData())
				return er
			},
			[]string{toRR(chat)},
		},
	}
	for _, test := range table {
		suite.Run(test.name, func() {
			t := suite.T()

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

			sel := test.makeSelector()
			results := sel.Reduce(ctx, test.deets, fault.New(true))
			paths := results.Paths()
			assert.Equal(t, test.expect, paths)
		})
	}
}

func (suite *TeamsChatsSelectorSuite) TestTeamsChatsRestore_Reduce_locationRef() {
	var (
		chat         = stubRepoRef(path.TeamsChatsService, path.ChatsCategory, "uid", "", "cid")
		chatLocation = "chatname"
	)

	makeDeets := func(refs ...string) *details.Details {
		deets := &details.Details{
			DetailsModel: details.DetailsModel{
				Entries: []details.Entry{},
			},
		}

		for _, r := range refs {
			var (
				location string
				itype    = details.UnknownType
			)

			switch r {
			case chat:
				itype = details.TeamsChat
				location = chatLocation
			}

			deets.Entries = append(deets.Entries, details.Entry{
				RepoRef:     r,
				LocationRef: location,
				ItemInfo: details.ItemInfo{
					TeamsChats: &details.TeamsChatsInfo{
						ItemType: itype,
					},
				},
			})
		}

		return deets
	}

	arr := func(s ...string) []string {
		return s
	}

	table := []struct {
		name         string
		deets        *details.Details
		makeSelector func() *TeamsChatsRestore
		expect       []string
	}{
		{
			"no refs",
			makeDeets(),
			func() *TeamsChatsRestore {
				er := NewTeamsChatsRestore(Any())
				er.Include(er.AllData())
				return er
			},
			[]string{},
		},
		{
			"chat only",
			makeDeets(chat),
			func() *TeamsChatsRestore {
				er := NewTeamsChatsRestore(Any())
				er.Include(er.AllData())
				return er
			},
			arr(chat),
		},
	}
	for _, test := range table {
		suite.Run(test.name, func() {
			t := suite.T()

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

			sel := test.makeSelector()
			results := sel.Reduce(ctx, test.deets, fault.New(true))
			paths := results.Paths()
			assert.Equal(t, test.expect, paths)
		})
	}
}

func (suite *TeamsChatsSelectorSuite) TestScopesByCategory() {
	var (
		cs         = NewTeamsChatsRestore(Any())
		teamsChats = cs.Chats(Any())
	)

	type expect struct {
		chat int
	}

	type input []scope

	makeInput := func(cs ...[]TeamsChatsScope) []scope {
		mss := []scope{}

		for _, sl := range cs {
			for _, s := range sl {
				mss = append(mss, scope(s))
			}
		}

		return mss
	}
	cats := map[path.CategoryType]teamsChatsCategory{
		path.ChatsCategory: TeamsChatsChat,
	}

	table := []struct {
		name   string
		scopes input
		expect expect
	}{
		{"teamsChats only", makeInput(teamsChats), expect{1}},
	}
	for _, test := range table {
		suite.Run(test.name, func() {
			t := suite.T()

			result := scopesByCategory[TeamsChatsScope](test.scopes, cats, false)
			assert.Len(t, result[TeamsChatsChat], test.expect.chat)
		})
	}
}

func (suite *TeamsChatsSelectorSuite) TestPasses() {
	const (
		chatID = "chatID"
		cat    = TeamsChatsChat
	)

	short := "thisisahashofsomekind"
	entry := details.Entry{
		ShortRef: short,
		ItemRef:  chatID,
	}

	repoRef, err := path.Build("tid", "user", path.TeamsChatsService, path.ChatsCategory, true, chatID)
	require.NoError(suite.T(), err, clues.ToCore(err))

	var (
		cs        = NewTeamsChatsRestore(Any())
		otherChat = setScopesToDefault(cs.Chats([]string{"smarf"}))
		chat      = setScopesToDefault(cs.Chats([]string{chatID}))
		noChat    = setScopesToDefault(cs.Chats(None()))
		allChats  = setScopesToDefault(cs.Chats(Any()))
		ent       = details.Entry{
			RepoRef: repoRef.String(),
		}
	)

	table := []struct {
		name                        string
		excludes, filters, includes []TeamsChatsScope
		expect                      assert.BoolAssertionFunc
	}{
		{"empty", nil, nil, nil, assert.False},
		{"in Chat", nil, nil, chat, assert.True},
		{"in Other", nil, nil, otherChat, assert.False},
		{"in no Chat", nil, nil, noChat, assert.False},
		{"ex None filter chat", allChats, chat, nil, assert.False},
		{"ex Chat", chat, nil, allChats, assert.False},
		{"ex Other", otherChat, nil, allChats, assert.True},
		{"in and ex Chat", chat, nil, chat, assert.False},
		{"filter Chat", nil, chat, allChats, assert.True},
		{"filter Other", nil, otherChat, allChats, assert.False},
	}
	for _, test := range table {
		suite.Run(test.name, func() {
			t := suite.T()

			pvs, err := cat.pathValues(repoRef, ent, Config{})
			require.NoError(t, err)

			result := passes(
				cat,
				pvs,
				entry,
				test.excludes,
				test.filters,
				test.includes)
			test.expect(t, result)
		})
	}
}

func (suite *TeamsChatsSelectorSuite) TestContains() {
	target := "fnords"

	var (
		cs      = NewTeamsChatsRestore(Any())
		noChat  = setScopesToDefault(cs.Chats(None()))
		does    = setScopesToDefault(cs.Chats([]string{target}))
		doesNot = setScopesToDefault(cs.Chats([]string{"smarf"}))
	)

	table := []struct {
		name   string
		scopes []TeamsChatsScope
		expect assert.BoolAssertionFunc
	}{
		{"no chat", noChat, assert.False},
		{"does contain", does, assert.True},
		{"does not contain", doesNot, assert.False},
	}
	for _, test := range table {
		suite.Run(test.name, func() {
			t := suite.T()

			var result bool
			for _, scope := range test.scopes {
				if scope.Matches(TeamsChatsChat, target) {
					result = true
					break
				}
			}
			test.expect(t, result)
		})
	}
}

func (suite *TeamsChatsSelectorSuite) TestIsAny() {
	var (
		cs           = NewTeamsChatsRestore(Any())
		specificChat = setScopesToDefault(cs.Chats([]string{"chat"}))
		anyChat      = setScopesToDefault(cs.Chats(Any()))
	)

	table := []struct {
		name   string
		scopes []TeamsChatsScope
		cat    teamsChatsCategory
		expect assert.BoolAssertionFunc
	}{
		{"specific chat", specificChat, TeamsChatsChat, assert.False},
		{"any chat", anyChat, TeamsChatsChat, assert.True},
		{"wrong category", anyChat, TeamsChatsUser, assert.False},
	}
	for _, test := range table {
		suite.Run(test.name, func() {
			t := suite.T()

			var result bool
			for _, scope := range test.scopes {
				if scope.IsAny(test.cat) {
					result = true
					break
				}
			}
			test.expect(t, result)
		})
	}
}

func (suite *TeamsChatsSelectorSuite) TestTeamsChatsCategory_leafCat() {
	table := []struct {
		cat    teamsChatsCategory
		expect teamsChatsCategory
	}{
		{teamsChatsCategory("foo"), teamsChatsCategory("foo")},
		{TeamsChatsCategoryUnknown, TeamsChatsCategoryUnknown},
		{TeamsChatsUser, TeamsChatsUser},
		{TeamsChatsChat, TeamsChatsChat},
	}
	for _, test := range table {
		suite.Run(test.cat.String(), func() {
			assert.Equal(suite.T(), test.expect, test.cat.leafCat(), test.cat.String())
		})
	}
}

func (suite *TeamsChatsSelectorSuite) TestTeamsChatsCategory_PathValues() {
	t := suite.T()

	chatPath, err := path.Build("tid", "u", path.TeamsChatsService, path.ChatsCategory, true, "chatitem.d")
	require.NoError(t, err, clues.ToCore(err))

	chatLoc, err := path.Build("tid", "u", path.TeamsChatsService, path.ChatsCategory, true, "chatitem")
	require.NoError(t, err, clues.ToCore(err))

	var (
		chatMap = map[categorizer][]string{
			TeamsChatsChat: {chatPath.Item(), "chat-short"},
		}
		chatOnlyNameMap = map[categorizer][]string{
			TeamsChatsChat: {"chat-short"},
		}
	)

	table := []struct {
		cat            teamsChatsCategory
		path           path.Path
		loc            path.Path
		short          string
		expect         map[categorizer][]string
		expectOnlyName map[categorizer][]string
	}{
		{TeamsChatsChat, chatPath, chatLoc, "chat-short", chatMap, chatOnlyNameMap},
	}
	for _, test := range table {
		suite.Run(string(test.cat), func() {
			t := suite.T()
			ent := details.Entry{
				RepoRef:     test.path.String(),
				ShortRef:    test.short,
				LocationRef: test.loc.Folder(true),
				ItemRef:     test.path.Item(),
			}

			pvs, err := test.cat.pathValues(test.path, ent, Config{})
			require.NoError(t, err)

			for k := range test.expect {
				assert.ElementsMatch(t, test.expect[k], pvs[k])
			}

			pvs, err = test.cat.pathValues(test.path, ent, Config{OnlyMatchItemNames: true})
			require.NoError(t, err)

			for k := range test.expectOnlyName {
				assert.ElementsMatch(t, test.expectOnlyName[k], pvs[k], k)
			}
		})
	}
}

func (suite *TeamsChatsSelectorSuite) TestTeamsChatsCategory_PathKeys() {
	chat := []categorizer{TeamsChatsChat}
	user := []categorizer{TeamsChatsUser}

	var empty []categorizer

	table := []struct {
		cat    teamsChatsCategory
		expect []categorizer
	}{
		{TeamsChatsCategoryUnknown, empty},
		{TeamsChatsChat, chat},
		{TeamsChatsUser, user},
	}
	for _, test := range table {
		suite.Run(string(test.cat), func() {
			assert.Equal(suite.T(), test.cat.pathKeys(), test.expect)
		})
	}
}

func (suite *TeamsChatsSelectorSuite) TestCategoryFromItemType() {
	table := []struct {
		name   string
		input  details.ItemType
		expect teamsChatsCategory
	}{
		{
			name:   "chat",
			input:  details.TeamsChat,
			expect: TeamsChatsChat,
		},
		{
			name:   "unknown",
			input:  details.UnknownType,
			expect: TeamsChatsCategoryUnknown,
		},
	}
	for _, test := range table {
		suite.Run(test.name, func() {
			t := suite.T()

			result := teamsChatsCategoryFromItemType(test.input)
			assert.Equal(t, test.expect, result)
		})
	}
}

func (suite *TeamsChatsSelectorSuite) TestCategory_PathType() {
	table := []struct {
		cat      teamsChatsCategory
		pathType path.CategoryType
	}{
		{TeamsChatsCategoryUnknown, path.UnknownCategory},
		{TeamsChatsChat, path.ChatsCategory},
		{TeamsChatsUser, path.UnknownCategory},
	}
	for _, test := range table {
		suite.Run(test.cat.String(), func() {
			assert.Equal(suite.T(), test.pathType, test.cat.PathType())
		})
	}
}
