package path

import (
	"fmt"
	"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"
)

type testData struct {
	name           string
	input          []string
	expectedString string
}

// Test cases that are the same with and without escaping by the
// system-under-test.
var genericCases = []testData{
	{
		name: "SimplePath",
		input: []string{
			`this`,
			`is`,
			`a`,
			`path`,
		},
		expectedString: "this/is/a/path",
	},
	{
		name: "EmptyElement",
		input: []string{
			`this`,
			`is`,
			``,
			`a`,
			`path`,
		},
		expectedString: `this/is/a/path`,
	},
	{
		name:           "EmptyInput",
		expectedString: "",
	},
}

// Inputs that should be escaped.
var basicUnescapedInputs = []testData{
	{
		name: "EscapeSeparator",
		input: []string{
			`this`,
			`is/a`,
			`path`,
		},
		expectedString: `this/is\/a/path`,
	},
	{
		name: "EscapeEscapeChar",
		input: []string{
			`this`,
			`is\`,
			`a`,
			`path`,
		},
		expectedString: `this/is\\/a/path`,
	},
	{
		name: "EscapeEscapeAndSeparator",
		input: []string{
			`this`,
			`is\/a`,
			`path`,
		},
		expectedString: `this/is\\\/a/path`,
	},
	{
		name: "SeparatorAtEndOfElement",
		input: []string{
			`this`,
			`is/`,
			`a`,
			`path`,
		},
		expectedString: `this/is\//a/path`,
	},
	{
		name: "SeparatorAtEndOfPath",
		input: []string{
			`this`,
			`is`,
			`a`,
			`path/`,
		},
		expectedString: `this/is/a/path\/`,
	},
}

// Inputs that are already escaped.
var basicEscapedInputs = []testData{
	{
		name: "EscapedSeparator",
		input: []string{
			`this`,
			`is\/a`,
			`path`,
		},
		expectedString: `this/is\/a/path`,
	},
	{
		name: "EscapedEscapeChar",
		input: []string{
			`this`,
			`is\\`,
			`a`,
			`path`,
		},
		expectedString: `this/is\\/a/path`,
	},
	{
		name: "EscapedEscapeAndSeparator",
		input: []string{
			`this`,
			`is\\\/a`,
			`path`,
		},
		expectedString: `this/is\\\/a/path`,
	},
	{
		name: "EscapedSeparatorAtEndOfElement",
		input: []string{
			`this`,
			`is\/`,
			`a`,
			`path`,
		},
		expectedString: `this/is\//a/path`,
	},
	{
		name: "EscapedSeparatorAtEndOfPath",
		input: []string{
			`this`,
			`is`,
			`a`,
			`path\/`,
		},
		expectedString: `this/is/a/path\/`,
	},
	{
		name: "ElementOfSeparator",
		input: []string{
			`this`,
			`is`,
			`/`,
			`a`,
			`path`,
		},
		expectedString: `this/is/a/path`,
	},
	{
		name: "TrailingElementSeparator",
		input: []string{
			`this`,
			`is`,
			`a/`,
			`path`,
		},
		expectedString: `this/is/a/path`,
	},
	{
		name: "MultipleTrailingElementSeparator",
		input: []string{
			`this`,
			`is`,
			`a///`,
			`path`,
		},
		expectedString: `this/is/a/path`,
	},
	{
		name: "TrailingSeparatorAtEnd",
		input: []string{
			`this`,
			`is`,
			`a`,
			`path/`,
		},
		expectedString: `this/is/a/path`,
	},
	{
		name: "MultipleTrailingSeparatorAtEnd",
		input: []string{
			`this`,
			`is`,
			`a`,
			`path///`,
		},
		expectedString: `this/is/a/path`,
	},
	{
		name: "TrailingSeparatorWithEmptyElementAtEnd",
		input: []string{
			`this`,
			`is`,
			`a`,
			`path/`,
			``,
		},
		expectedString: `this/is/a/path`,
	},
}

type PathUnitSuite struct {
	tester.Suite
}

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

// set the clues hashing to mask for the span of this suite
func (suite *PathUnitSuite) SetupSuite() {
	clues.SetHasher(clues.HashCfg{HashAlg: clues.Flatmask})
}

// revert clues hashing to plaintext for all other tests
func (suite *PathUnitSuite) TeardownSuite() {
	clues.SetHasher(clues.NoHash())
}

func (suite *PathUnitSuite) TestAppend() {
	table := append(append([]testData{}, genericCases...), basicUnescapedInputs...)
	for _, test := range table {
		suite.Run(test.name, func() {
			t := suite.T()

			p := Builder{}.Append(test.input...)
			assert.Equal(t, test.expectedString, p.String())
		})
	}
}

func (suite *PathUnitSuite) TestAppendItem() {
	t := suite.T()

	p, err := Build("t", "ro", ExchangeService, EmailCategory, false, "foo", "bar")
	require.NoError(t, err, clues.ToCore(err))

	pb := p.ToBuilder()
	assert.Equal(t, pb.String(), p.String())

	pb = pb.Append("qux")

	p, err = p.AppendItem("qux")

	require.NoError(t, err, clues.ToCore(err))
	assert.Equal(t, pb.String(), p.String())

	_, err = p.AppendItem("fnords")
	require.Error(t, err, clues.ToCore(err))
}

func (suite *PathUnitSuite) TestUnescapeAndAppend() {
	table := append(append([]testData{}, genericCases...), basicEscapedInputs...)
	for _, test := range table {
		suite.Run(test.name, func() {
			t := suite.T()

			p, err := Builder{}.UnescapeAndAppend(test.input...)
			require.NoError(t, err, clues.ToCore(err))

			assert.Equal(t, test.expectedString, p.String())
		})
	}
}

func (suite *PathUnitSuite) TestEscapedFailure() {
	target := "i_s"

	for c := range charactersToEscape {
		suite.Run(fmt.Sprintf("Unescaped-%c", c), func() {
			tmp := strings.ReplaceAll(target, "_", string(c))

			_, err := Builder{}.UnescapeAndAppend("this", tmp, "path")
			assert.Errorf(suite.T(), err, "path with unescaped %s did not error", string(c))
		})
	}
}

func (suite *PathUnitSuite) TestBadEscapeSequenceErrors() {
	target := `i\_s/a`
	notEscapes := []rune{'a', 'b', '#', '%'}

	for _, c := range notEscapes {
		suite.Run(fmt.Sprintf("Escaped-%c", c), func() {
			tmp := strings.ReplaceAll(target, "_", string(c))

			_, err := Builder{}.UnescapeAndAppend("this", tmp, "path")
			assert.Errorf(
				suite.T(),
				err,
				"path with bad escape sequence %c%c did not error",
				escapeCharacter,
				c)
		})
	}
}

func (suite *PathUnitSuite) TestTrailingEscapeChar() {
	base := []string{"this", "is", "a", "path"}

	for i := 0; i < len(base); i++ {
		suite.Run(fmt.Sprintf("Element%v", i), func() {
			path := make([]string, len(base))
			copy(path, base)
			path[i] = path[i] + string(escapeCharacter)

			_, err := Builder{}.UnescapeAndAppend(path...)
			assert.Error(
				suite.T(),
				err,
				"path with trailing escape character did not error")
		})
	}
}

func (suite *PathUnitSuite) TestElements() {
	table := []struct {
		name     string
		input    []string
		output   []string
		pathFunc func(elements []string) (*Builder, error)
	}{
		{
			name:   "SimpleEscapedPath",
			input:  []string{"this", "is", "a", "path"},
			output: []string{"this", "is", "a", "path"},
			pathFunc: func(elements []string) (*Builder, error) {
				return Builder{}.UnescapeAndAppend(elements...)
			},
		},
		{
			name:   "SimpleUnescapedPath",
			input:  []string{"this", "is", "a", "path"},
			output: []string{"this", "is", "a", "path"},
			pathFunc: func(elements []string) (*Builder, error) {
				return Builder{}.Append(elements...), nil
			},
		},
		{
			name:   "EscapedPath",
			input:  []string{"this", `is\/`, "a", "path"},
			output: []string{"this", "is/", "a", "path"},
			pathFunc: func(elements []string) (*Builder, error) {
				return Builder{}.UnescapeAndAppend(elements...)
			},
		},
	}
	for _, test := range table {
		suite.Run(test.name, func() {
			t := suite.T()

			p, err := test.pathFunc(test.input)
			require.NoError(t, err, clues.ToCore(err))

			assert.Equal(t, Elements(test.output), p.Elements())
		})
	}
}

func (suite *PathUnitSuite) TestPopFront() {
	table := []struct {
		name           string
		base           *Builder
		expectedString string
	}{
		{
			name:           "Empty",
			base:           &Builder{},
			expectedString: "",
		},
		{
			name:           "OneElement",
			base:           Builder{}.Append("something"),
			expectedString: "",
		},
		{
			name:           "TwoElements",
			base:           Builder{}.Append("something", "else"),
			expectedString: "else",
		},
	}
	for _, test := range table {
		suite.Run(test.name, func() {
			t := suite.T()

			assert.Equal(t, test.expectedString, test.base.PopFront().String())
		})
	}
}

func (suite *PathUnitSuite) TestShortRef() {
	table := []struct {
		name          string
		inputElements []string
		expectedLen   int
	}{
		{
			name:          "PopulatedPath",
			inputElements: []string{"this", "is", "a", "path"},
			expectedLen:   shortRefCharacters,
		},
		{
			name:          "EmptyPath",
			inputElements: nil,
			expectedLen:   0,
		},
	}
	for _, test := range table {
		suite.Run(test.name, func() {
			pb := Builder{}.Append(test.inputElements...)
			ref := pb.ShortRef()
			assert.Len(suite.T(), ref, test.expectedLen)
		})
	}
}

func (suite *PathUnitSuite) TestShortRefIsStable() {
	t := suite.T()
	pb := Builder{}.Append("this", "is", "a", "path")
	prevRef := pb.ShortRef()
	assert.Len(t, prevRef, shortRefCharacters)

	for i := 0; i < 5; i++ {
		ref := pb.ShortRef()
		assert.Len(t, ref, shortRefCharacters)
		assert.Equal(t, prevRef, ref, "ShortRef changed between calls")

		prevRef = ref
	}
}

func (suite *PathUnitSuite) TestShortRefIsUnique() {
	pb1 := Builder{}.Append("this", "is", "a", "path")
	pb2 := pb1.Append("also")

	require.NotEqual(suite.T(), pb1, pb2)
	assert.NotEqual(suite.T(), pb1.ShortRef(), pb2.ShortRef())
}

// TestShortRefUniqueWithEscaping tests that two paths that output the same
// unescaped string but different escaped strings have different shortrefs. This
// situation can occur when one path has embedded path separators while the
// other does not but contains the same characters.
func (suite *PathUnitSuite) TestShortRefUniqueWithEscaping() {
	pb1 := Builder{}.Append(`this`, `is`, `a`, `path`)
	pb2 := Builder{}.Append(`this`, `is/a`, `path`)

	require.NotEqual(suite.T(), pb1, pb2)
	assert.NotEqual(suite.T(), pb1.ShortRef(), pb2.ShortRef())
}

func (suite *PathUnitSuite) TestFromStringErrors() {
	table := []struct {
		name        string
		escapedPath string
	}{
		{
			name:        "TooFewElements",
			escapedPath: `some/short/path`,
		},
		{
			name:        "TooFewElementsEmptyElement",
			escapedPath: `tenant/exchange//email/folder`,
		},
		{
			name:        "BadEscapeSequence",
			escapedPath: `tenant/exchange/user/email/folder\a`,
		},
		{
			name:        "TrailingEscapeCharacter",
			escapedPath: `tenant/exchange/user/email/folder\`,
		},
		{
			name:        "UnknownService",
			escapedPath: `tenant/badService/user/email/folder`,
		},
		{
			name:        "UnknownCategory",
			escapedPath: `tenant/exchange/user/badCategory/folder`,
		},
		{
			name:        "NoFolderOrItem",
			escapedPath: `tenant/exchange/user/email`,
		},
		{
			name:        "EmptyPath",
			escapedPath: ``,
		},
		{
			name:        "JustPathSeparator",
			escapedPath: `/`,
		},
		{
			name:        "JustMultiplePathSeparators",
			escapedPath: `//`,
		},
	}
	for _, test := range table {
		suite.Run(test.name, func() {
			t := suite.T()

			_, err := FromDataLayerPath(test.escapedPath, false)
			assert.Error(t, err)
		})
	}
}

func (suite *PathUnitSuite) TestFolder() {
	table := []struct {
		name         string
		p            func(t *testing.T) Path
		escape       bool
		expectFolder string
		expectSplit  []string
	}{
		{
			name: "clean path",
			p: func(t *testing.T) Path {
				p, err := Builder{}.
					Append("a", "b", "c").
					ToDataLayerExchangePathForCategory("t", "u", EmailCategory, false)
				require.NoError(t, err, clues.ToCore(err))

				return p
			},
			expectFolder: "a/b/c",
			expectSplit:  []string{"a", "b", "c"},
		},
		{
			name: "clean path escaped",
			p: func(t *testing.T) Path {
				p, err := Builder{}.
					Append("a", "b", "c").
					ToDataLayerExchangePathForCategory("t", "u", EmailCategory, false)
				require.NoError(t, err, clues.ToCore(err))

				return p
			},
			escape:       true,
			expectFolder: "a/b/c",
			expectSplit:  []string{"a", "b", "c"},
		},
		{
			name: "escapable path",
			p: func(t *testing.T) Path {
				p, err := Builder{}.
					Append("a/", "b", "c").
					ToDataLayerExchangePathForCategory("t", "u", EmailCategory, false)
				require.NoError(t, err, clues.ToCore(err))

				return p
			},
			expectFolder: "a//b/c",
			expectSplit:  []string{"a", "b", "c"},
		},
		{
			name: "escapable path escaped",
			p: func(t *testing.T) Path {
				p, err := Builder{}.
					Append("a/", "b", "c").
					ToDataLayerExchangePathForCategory("t", "u", EmailCategory, false)
				require.NoError(t, err, clues.ToCore(err))

				return p
			},
			escape:       true,
			expectFolder: "a\\//b/c",
			expectSplit:  []string{"a\\/", "b", "c"},
		},
	}
	for _, test := range table {
		suite.Run(test.name, func() {
			t := suite.T()

			p := test.p(t)
			result := p.Folder(test.escape)
			assert.Equal(t, test.expectFolder, result)
			assert.Equal(t, test.expectSplit, Split(result))
		})
	}
}

func (suite *PathUnitSuite) TestFromString() {
	const (
		testTenant         = "tenant"
		testUser           = "user"
		testElement1       = "folder/"
		testElementTrimmed = "folder"
		testElement2       = "folder2"
		testElement3       = "other"
	)

	isItem := []struct {
		name   string
		isItem bool
	}{
		{
			name:   "Folder",
			isItem: false,
		},
		{
			name:   "Item",
			isItem: true,
		},
	}
	table := []struct {
		name string
		// Should have placeholders of '%s' for service and category.
		unescapedPath string
		// Expected result for Folder() if path is marked as a folder.
		expectedFolder string
		// Expected result for Item() if path is marked as an item.
		// Expected result for Split(Folder()) if path is marked as a folder.
		expectedSplit []string
		expectedItem  string
		// Expected result for Folder() if path is marked as an item.
		expectedItemFolder string
		// Expected result for Split(Folder()) if path is marked as an item.
		expectedItemSplit []string
	}{
		{
			name: "BasicPath",
			unescapedPath: fmt.Sprintf(
				"%s/%%s/%s/%%s/%s/%s/%s",
				testTenant,
				testUser,
				testElement1,
				testElement2,
				testElement3,
			),
			expectedFolder: fmt.Sprintf(
				"%s/%s/%s",
				testElementTrimmed,
				testElement2,
				testElement3,
			),
			expectedSplit: []string{
				testElementTrimmed,
				testElement2,
				testElement3,
			},
			expectedItem: testElement3,
			expectedItemFolder: fmt.Sprintf(
				"%s/%s",
				testElementTrimmed,
				testElement2,
			),
			expectedItemSplit: []string{
				testElementTrimmed,
				testElement2,
			},
		},
		{
			name: "PathWithEmptyElements",
			unescapedPath: fmt.Sprintf(
				"/%s//%%s//%s//%%s//%s///%s//%s//",
				testTenant,
				testUser,
				testElementTrimmed,
				testElement2,
				testElement3,
			),
			expectedFolder: fmt.Sprintf(
				"%s/%s/%s",
				testElementTrimmed,
				testElement2,
				testElement3,
			),
			expectedSplit: []string{
				testElementTrimmed,
				testElement2,
				testElement3,
			},
			expectedItem: testElement3,
			expectedItemFolder: fmt.Sprintf(
				"%s/%s",
				testElementTrimmed,
				testElement2,
			),
			expectedItemSplit: []string{
				testElementTrimmed,
				testElement2,
			},
		},
	}

	for service, cats := range serviceCategories {
		for cat := range cats {
			for _, item := range isItem {
				suite.Run(fmt.Sprintf("%s-%s-%s", service, cat, item.name), func() {
					for _, test := range table {
						suite.Run(test.name, func() {
							t := suite.T()
							testPath := fmt.Sprintf(test.unescapedPath, service, cat)

							p, err := FromDataLayerPath(testPath, item.isItem)
							require.NoError(t, err, clues.ToCore(err))

							assert.Equal(t, service, p.Service(), "service")
							assert.Equal(t, cat, p.Category(), "category")
							assert.Equal(t, testTenant, p.Tenant(), "tenant")
							assert.Equal(t, testUser, p.ResourceOwner(), "resource owner")

							fld := p.Folder(false)
							escfld := p.Folder(true)

							if item.isItem {
								assert.Equal(t, test.expectedItemFolder, fld, "item folder")
								assert.Equal(t, test.expectedItemSplit, Split(fld), "item split")
								assert.Equal(t, test.expectedItemFolder, escfld, "escaped item folder")
								assert.Equal(t, test.expectedItemSplit, Split(escfld), "escaped item split")
								assert.Equal(t, test.expectedItem, p.Item(), "item")
							} else {
								assert.Equal(t, test.expectedFolder, fld, "dir folder")
								assert.Equal(t, test.expectedSplit, Split(fld), "dir split")
								assert.Equal(t, test.expectedFolder, escfld, "escaped dir folder")
								assert.Equal(t, test.expectedSplit, Split(escfld), "escaped dir split")
							}
						})
					}
				})
			}
		}
	}
}

func (suite *PathUnitSuite) TestPath_piiHandling() {
	p, err := Build("t", "ro", ExchangeService, EventsCategory, true, "dir", "item")
	require.NoError(suite.T(), err)

	table := []struct {
		name        string
		p           Path
		expect      string
		expectPlain string
	}{
		{
			name:        "standard path",
			p:           p,
			expect:      "***/exchange/***/events/***/***",
			expectPlain: "t/exchange/ro/events/dir/item",
		},
	}
	for _, test := range table {
		suite.Run(test.name, func() {
			t := suite.T()

			assert.Equal(t, test.expect, test.p.Conceal(), "conceal")
			assert.Equal(t, test.expectPlain, test.p.String(), "string")
			assert.Equal(t, test.expect, fmt.Sprintf("%s", test.p), "fmt %%s")
			assert.Equal(t, test.expect, fmt.Sprintf("%+v", test.p), "fmt %%+v")
			assert.Equal(t, test.expectPlain, test.p.PlainString(), "plain")
		})
	}
}

func (suite *PathUnitSuite) TestToServicePrefix() {
	table := []struct {
		name      string
		service   ServiceType
		category  CategoryType
		tenant    string
		owner     string
		expect    string
		expectErr require.ErrorAssertionFunc
	}{
		{
			name:      "ok",
			service:   ExchangeService,
			category:  ContactsCategory,
			tenant:    "t",
			owner:     "ro",
			expect:    join([]string{"t", ExchangeService.String(), "ro", ContactsCategory.String()}),
			expectErr: require.NoError,
		},
		{
			name:      "bad category",
			service:   ExchangeService,
			category:  FilesCategory,
			tenant:    "t",
			owner:     "ro",
			expectErr: require.Error,
		},
		{
			name:      "bad tenant",
			service:   ExchangeService,
			category:  ContactsCategory,
			tenant:    "",
			owner:     "ro",
			expectErr: require.Error,
		},
		{
			name:      "bad owner",
			service:   ExchangeService,
			category:  ContactsCategory,
			tenant:    "t",
			owner:     "",
			expectErr: require.Error,
		},
	}
	for _, test := range table {
		suite.Run(test.name, func() {
			t := suite.T()

			r, err := ServicePrefix(test.tenant, test.owner, test.service, test.category)
			test.expectErr(t, err, clues.ToCore(err))

			if r == nil {
				return
			}

			assert.Equal(t, test.expect, r.String())
			assert.NotPanics(t, func() {
				r.Folders()
				r.Item()
			}, "runs Folders() and Item()")
		})
	}
}
