package selectors

import (
	"fmt"
	"io"
	"strings"
	"testing"

	"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"
)

// ---------------------------------------------------------------------------
// tests
// ---------------------------------------------------------------------------

type SelectorScopesSuite struct {
	tester.Suite
}

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

func (suite *SelectorScopesSuite) TestContains() {
	table := []struct {
		name   string
		scope  func() mockScope
		check  string
		expect assert.BoolAssertionFunc
	}{
		{
			name: "any",
			scope: func() mockScope {
				stub := stubScope("")
				return stub
			},
			check:  rootCatStub.String(),
			expect: assert.True,
		},
		{
			name: "none",
			scope: func() mockScope {
				stub := stubScope("")
				stub[rootCatStub.String()] = failAny
				return stub
			},
			check:  rootCatStub.String(),
			expect: assert.False,
		},
		{
			name: "blank value",
			scope: func() mockScope {
				stub := stubScope("")
				stub[rootCatStub.String()] = filters.Equal([]string{""})
				return stub
			},
			check:  rootCatStub.String(),
			expect: assert.False,
		},
		{
			name: "blank target",
			scope: func() mockScope {
				stub := stubScope("")
				stub[rootCatStub.String()] = filterFor(scopeConfig{}, "fnords")
				return stub
			},
			check:  "",
			expect: assert.False,
		},
		{
			name: "matching target",
			scope: func() mockScope {
				stub := stubScope("")
				stub[rootCatStub.String()] = filterFor(scopeConfig{}, rootCatStub.String())
				return stub
			},
			check:  rootCatStub.String(),
			expect: assert.True,
		},
		{
			name: "non-matching target",
			scope: func() mockScope {
				stub := stubScope("")
				stub[rootCatStub.String()] = filterFor(scopeConfig{}, rootCatStub.String())
				return stub
			},
			check:  "smarf",
			expect: assert.False,
		},
	}
	for _, test := range table {
		suite.Run(test.name, func() {
			t := suite.T()

			test.expect(
				t,
				matches(test.scope(), rootCatStub, test.check))
		})
	}
}

func (suite *SelectorScopesSuite) TestGetCatValue() {
	t := suite.T()

	stub := stubScope("")
	stub[rootCatStub.String()] = filterFor(scopeConfig{}, rootCatStub.String())

	assert.Equal(t,
		[]string{rootCatStub.String()},
		getCatValue(stub, rootCatStub))
	assert.Equal(t,
		None(),
		getCatValue(stub, mockCategorizer("foo")))
}

func (suite *SelectorScopesSuite) TestIsAnyTarget() {
	t := suite.T()
	stub := stubScope("")
	assert.True(t, IsAnyTarget(stub, rootCatStub))
	assert.True(t, IsAnyTarget(stub, leafCatStub))
	assert.False(t, IsAnyTarget(stub, mockCategorizer("smarf")))

	stub = stubScope("none")
	assert.False(t, IsAnyTarget(stub, rootCatStub))
	assert.False(t, IsAnyTarget(stub, leafCatStub))
	assert.False(t, IsAnyTarget(stub, mockCategorizer("smarf")))
}

var reduceTestTable = []struct {
	name               string
	sel                func() mockSel
	expectLen          int
	expectPassesReduce assert.BoolAssertionFunc
	expectPasses       assert.BoolAssertionFunc
}{
	{
		name: "include all resource owners",
		sel: func() mockSel {
			sel := stubSelector(Any())
			sel.Filters = nil
			sel.Excludes = nil
			return sel
		},
		expectLen:    1,
		expectPasses: assert.True,
	},
	{
		name: "include all scopes",
		sel: func() mockSel {
			sel := stubSelector(Any())
			sel.Filters = nil
			sel.Excludes = nil
			return sel
		},
		expectLen:    1,
		expectPasses: assert.True,
	},
	{
		name: "include none resource owners",
		sel: func() mockSel {
			sel := stubSelector(None())
			sel.Includes[0] = scope(stubScope(AnyTgt))
			sel.Filters = nil
			sel.Excludes = nil
			return sel
		},
		expectLen:    0,
		expectPasses: assert.True, // passes() does not check owners
	},
	{
		name: "include none scopes",
		sel: func() mockSel {
			sel := stubSelector(Any())
			sel.Includes[0] = scope(stubScope("none"))
			sel.Filters = nil
			sel.Excludes = nil
			return sel
		},
		expectLen:    0,
		expectPasses: assert.False,
	},
	{
		name: "filter and include all",
		sel: func() mockSel {
			sel := stubSelector(Any())
			sel.Excludes = nil
			return sel
		},
		expectLen:    1,
		expectPasses: assert.True,
	},
	{
		name: "include all filter none",
		sel: func() mockSel {
			sel := stubSelector(Any())
			sel.Filters[0] = scope(stubInfoScope("none"))
			sel.Excludes = nil
			return sel
		},
		expectLen:    0,
		expectPasses: assert.False,
	},
	{
		name: "include all exclude all",
		sel: func() mockSel {
			sel := stubSelector(Any())
			sel.Filters = nil
			return sel
		},
		expectLen:    0,
		expectPasses: assert.False,
	},
	{
		name: "include all exclude none",
		sel: func() mockSel {
			sel := stubSelector(Any())
			sel.Filters = nil
			sel.Excludes[0] = scope(stubScope("none"))
			return sel
		},
		expectLen:    1,
		expectPasses: assert.True,
	},
	{
		name: "filter all exclude all",
		sel: func() mockSel {
			sel := stubSelector(Any())
			sel.Includes = nil
			return sel
		},
		expectLen:    0,
		expectPasses: assert.False,
	},
	{
		name: "filter all exclude none",
		sel: func() mockSel {
			sel := stubSelector(Any())
			sel.Includes = nil
			sel.Excludes[0] = scope(stubScope("none"))
			return sel
		},
		expectLen:    1,
		expectPasses: assert.True,
	},
}

func (suite *SelectorScopesSuite) TestReduce() {
	deets := func() details.Details {
		return details.Details{
			DetailsModel: details.DetailsModel{
				Entries: []details.Entry{
					{
						RepoRef: stubRepoRef(
							pathServiceStub,
							pathCatStub,
							rootCatStub.String(),
							"stub",
							leafCatStub.String()),
					},
				},
			},
		}
	}
	dataCats := map[path.CategoryType]mockCategorizer{
		pathCatStub: rootCatStub,
	}

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

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

			errs := fault.New(true)

			ds := deets()
			result := reduce[mockScope](
				ctx,
				&ds,
				test.sel().Selector,
				dataCats,
				errs)
			require.NotNil(t, result)
			require.NoError(t, errs.Failure(), "no recoverable errors", clues.ToCore(errs.Failure()))
			assert.Len(t, result.Entries, test.expectLen)
		})
	}
}

func (suite *SelectorScopesSuite) TestReduce_locationRef() {
	deets := func() details.Details {
		return details.Details{
			DetailsModel: details.DetailsModel{
				Entries: []details.Entry{
					{
						RepoRef: stubRepoRef(
							pathServiceStub,
							pathCatStub,
							rootCatStub.String(),
							"stub",
							leafCatStub.String()),
						LocationRef: "a/b/c//defg",
					},
				},
			},
		}
	}
	dataCats := map[path.CategoryType]mockCategorizer{
		pathCatStub: rootCatStub,
	}

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

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

			ds := deets()
			result := reduce[mockScope](
				ctx,
				&ds,
				test.sel().Selector,
				dataCats,
				fault.New(true))
			require.NotNil(t, result)
			assert.Len(t, result.Entries, test.expectLen)
		})
	}
}

func (suite *SelectorScopesSuite) TestScopesByCategory() {
	t := suite.T()
	s1 := stubScope("")
	s2 := stubScope("")
	s2[scopeKeyCategory] = filterFor(scopeConfig{}, unknownCatStub.String())
	result := scopesByCategory[mockScope](
		[]scope{scope(s1), scope(s2)},
		map[path.CategoryType]mockCategorizer{
			path.UnknownCategory: rootCatStub,
		},
		false)
	assert.Len(t, result, 1)
	assert.Len(t, result[rootCatStub], 2)
	assert.Empty(t, result[leafCatStub])
}

func (suite *SelectorScopesSuite) TestPasses() {
	var (
		cat   = rootCatStub
		pth   = stubPath(suite.T(), "uid", []string{"fld"}, path.EventsCategory)
		entry = details.Entry{
			RepoRef: pth.String(),
		}
	)

	pvs, err := cat.pathValues(pth, entry, Config{})
	require.NoError(suite.T(), err)

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

			sel := test.sel()
			excl := toMockScope(sel.Excludes)
			filt := toMockScope(sel.Filters)
			incl := toMockScope(sel.Includes)
			result := passes(
				cat,
				pvs,
				entry,
				excl, filt, incl)
			test.expectPasses(t, result)
		})
	}
}

func toMockScope(sc []scope) []mockScope {
	if len(sc) == 0 {
		return nil
	}

	ms := []mockScope{}

	for _, s := range sc {
		ms = append(ms, mockScope(s))
	}

	return ms
}

func (suite *SelectorScopesSuite) TestMatchesPathValues() {
	cat := rootCatStub
	short := "brunheelda"

	table := []struct {
		name     string
		cat      mockCategorizer
		rootVal  string
		leafVal  string
		shortRef string
		expect   assert.BoolAssertionFunc
	}{
		{
			name:    "matching values",
			rootVal: rootCatStub.String(),
			leafVal: leafCatStub.String(),
			expect:  assert.True,
		},
		{
			name:    "any",
			rootVal: AnyTgt,
			leafVal: AnyTgt,
			expect:  assert.True,
		},
		{
			name:    "none",
			rootVal: NoneTgt,
			leafVal: NoneTgt,
			expect:  assert.False,
		},
		{
			name:    "mismatched values",
			rootVal: "fnords",
			leafVal: "smarf",
			expect:  assert.False,
		},
		{
			name:     "leaf matches shortRef",
			rootVal:  rootCatStub.String(),
			leafVal:  short,
			shortRef: short,
			expect:   assert.True,
		},
	}
	for _, test := range table {
		suite.Run(test.name, func() {
			t := suite.T()
			pvs := stubPathValues()
			pvs[leafCatStub] = append(pvs[leafCatStub], test.shortRef)

			sc := stubScope("")
			sc[rootCatStub.String()] = filterFor(scopeConfig{}, test.rootVal)
			sc[leafCatStub.String()] = filterFor(scopeConfig{}, test.leafVal)

			test.expect(t, matchesPathValues(sc, cat, pvs))
		})
	}
}

func (suite *SelectorScopesSuite) TestDefaultItemOptions() {
	table := []struct {
		name   string
		cfg    Config
		match  []string
		target string
		expect assert.BoolAssertionFunc
	}{
		{
			name:   "no config, matches same value",
			cfg:    Config{},
			match:  []string{"foo"},
			target: "foo",
			expect: assert.True,
		},
		{
			name:   "no config, does not match different case",
			cfg:    Config{},
			match:  []string{"bar"},
			target: "BAR",
			expect: assert.False,
		},
		{
			name:   "no config, does not match substring",
			cfg:    Config{},
			match:  []string{"bar"},
			target: "ba",
			expect: assert.False,
		},
		{
			name:   "only names, matches same same value",
			cfg:    Config{OnlyMatchItemNames: true},
			match:  []string{"fnords"},
			target: "fnords",
			expect: assert.True,
		},
		{
			name:   "only names, matches different case",
			cfg:    Config{OnlyMatchItemNames: true},
			match:  []string{"smarf"},
			target: "SMARF",
			expect: assert.True,
		},
		{
			name:   "only names, does not match substring",
			cfg:    Config{OnlyMatchItemNames: true},
			match:  []string{"brunhilda"},
			target: "unhild",
			expect: assert.False,
		},
	}
	for _, test := range table {
		suite.Run(test.name, func() {
			t := suite.T()
			sc := makeStubScope(test.cfg, test.match)

			test.expect(t, sc.Matches(leafCatStub, test.target))
		})
	}
}

func (suite *SelectorScopesSuite) TestClean() {
	table := []struct {
		name   string
		input  []string
		expect []string
	}{
		{
			name:   "nil",
			input:  nil,
			expect: None(),
		},
		{
			name:   "has anyTgt",
			input:  []string{"a", AnyTgt},
			expect: Any(),
		},
		{
			name:   "has noneTgt",
			input:  []string{"a", NoneTgt},
			expect: None(),
		},
		{
			name:   "has anyTgt and noneTgt, any first",
			input:  []string{"a", AnyTgt, NoneTgt},
			expect: Any(),
		},
		{
			name:   "has noneTgt and anyTgt, none first",
			input:  []string{"a", NoneTgt, AnyTgt},
			expect: None(),
		},
		{
			name:   "already clean",
			input:  []string{"a", "b"},
			expect: []string{"a", "b"},
		},
	}
	for _, test := range table {
		suite.Run(test.name, func() {
			t := suite.T()

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

func (suite *SelectorScopesSuite) TestScopeConfig() {
	input := "input"

	table := []struct {
		name   string
		config scopeConfig
		expect string
	}{
		{
			name:   "no configs set",
			config: scopeConfig{},
			expect: filters.EqualTo,
		},
		{
			name:   "force prefix",
			config: scopeConfig{usePrefixFilter: true},
			expect: filters.TargetPrefixes,
		},
	}
	for _, test := range table {
		suite.Run(test.name, func() {
			t := suite.T()

			result := filterFor(test.config, input)
			assert.Equal(t, test.expect, string(result.Comparator))
		})
	}
}

var _ fmt.State = &mockFMTState{}

type mockFMTState struct {
	w io.Writer
}

func (ms mockFMTState) Write(bs []byte) (int, error) { return ms.w.Write(bs) }
func (ms mockFMTState) Width() (int, bool)           { return 0, false }
func (ms mockFMTState) Precision() (int, bool)       { return 0, false }
func (ms mockFMTState) Flag(int) bool                { return false }

func (suite *SelectorScopesSuite) TestScopesPII() {
	table := []struct {
		name          string
		s             mockScope
		contains      []string
		containsPlain []string
	}{
		{
			name:          "empty",
			s:             mockScope{},
			contains:      []string{`{}`},
			containsPlain: []string{`{}`},
		},
		{
			name: "multiple filters",
			s: mockScope{
				"pass": filterFor(scopeConfig{}, "*"),
				"fail": filterFor(scopeConfig{}, ""),
				"foo":  filterFor(scopeConfig{}, "bar"),
				"qux":  filterFor(scopeConfig{}, "fnords", "smarf"),
			},
			contains: []string{
				`"pass":"Pass"`,
				`"fail":"Fail"`,
				`"foo":"EQ:bar"`,
				`"qux":"EQ:fnords,smarf"`,
			},
			containsPlain: []string{
				`"pass":"Pass"`,
				`"fail":"Fail"`,
				`"foo":"EQ:bar"`,
				`"qux":"EQ:fnords,smarf"`,
			},
		},
	}
	for _, test := range table {
		suite.Run(test.name, func() {
			t := suite.T()

			result := conceal(test.s)
			for _, c := range test.contains {
				assert.Contains(t, result, c, "conceal")
			}

			result = plainString(test.s)
			for _, c := range test.containsPlain {
				assert.Contains(t, result, c, "plainString")
			}

			sb := &strings.Builder{}
			fs := mockFMTState{sb}

			format(test.s, &fs, 0)
			for _, c := range test.contains {
				assert.Contains(t, sb.String(), c, "conceal")
			}
		})
	}
}
