package parser

import (
	"fmt"
	"reflect"
	"strings"
	"testing"

	"github.com/kataras/iris/v12/macro/interpreter/ast"
)

type simpleParamType string

func (pt simpleParamType) Indent() string { return string(pt) }

type masterParamType simpleParamType

func (pt masterParamType) Indent() string { return string(pt) }
func (pt masterParamType) Master() bool   { return true }

type wildcardParamType string

func (pt wildcardParamType) Indent() string { return string(pt) }
func (pt wildcardParamType) Trailing() bool { return true }

type aliasedParamType []string

func (pt aliasedParamType) Indent() string { return string(pt[0]) }
func (pt aliasedParamType) Alias() string  { return pt[1] }

var (
	paramTypeString       = masterParamType("string")
	paramTypeNumber       = aliasedParamType{"number", "int"}
	paramTypeInt64        = aliasedParamType{"int64", "long"}
	paramTypeUint8        = simpleParamType("uint8")
	paramTypeUint64       = simpleParamType("uint64")
	paramTypeBool         = aliasedParamType{"bool", "boolean"}
	paramTypeAlphabetical = simpleParamType("alphabetical")
	paramTypeFile         = simpleParamType("file")
	paramTypePath         = wildcardParamType("path")
)

var testParamTypes = []ast.ParamType{
	paramTypeString,
	paramTypeNumber, paramTypeInt64, paramTypeUint8, paramTypeUint64,
	paramTypeBool,
	paramTypeAlphabetical, paramTypeFile, paramTypePath,
}

func TestParseParamError(t *testing.T) {
	// fail
	illegalChar := '$'

	input := "{id" + string(illegalChar) + "int range(1,5) else 404}"
	p := NewParamParser(input)

	_, err := p.Parse(testParamTypes)

	if err == nil {
		t.Fatalf("expecting not empty error on input '%s'", input)
	}

	illIdx := strings.IndexRune(input, illegalChar)
	expectedErr := fmt.Sprintf("[%d:%d] illegal token: %s", illIdx, illIdx, "$")
	if got := err.Error(); got != expectedErr {
		t.Fatalf("expecting error to be '%s' but got: %s", expectedErr, got)
	}
	//

	// success
	input2 := "{id:uint64 range(1,5) else 404}"
	p.Reset(input2)
	_, err = p.Parse(testParamTypes)

	if err != nil {
		t.Fatalf("expecting empty error on input '%s', but got: %s", input2, err.Error())
	}
	//
}

// mustLookupParamType same as `ast.LookupParamType` but it panics if "indent" does not match with a valid Param Type.
func mustLookupParamType(indent string) ast.ParamType {
	pt, found := ast.LookupParamType(indent, testParamTypes...)
	if !found {
		panic("param type '" + indent + "' is not part of the provided param types")
	}

	return pt
}

func TestParseParam(t *testing.T) {
	tests := []struct {
		valid             bool
		expectedStatement ast.ParamStatement
	}{
		{
			true,
			ast.ParamStatement{
				Src:  "{id:int min(1) max(5) else 404}",
				Name: "id",
				Type: mustLookupParamType("number"),
				Funcs: []ast.ParamFunc{
					{
						Name: "min",
						Args: []string{"1"},
					},
					{
						Name: "max",
						Args: []string{"5"},
					},
				},
				ErrorCode: 404,
			},
		}, // 0

		{
			true,
			ast.ParamStatement{
				// test alias of int.
				Src:  "{id:number range(1,5)}",
				Name: "id",
				Type: mustLookupParamType("number"),
				Funcs: []ast.ParamFunc{
					{
						Name: "range",
						Args: []string{"1", "5"},
					},
				},
				ErrorCode: 404,
			},
		}, // 1
		{
			true,
			ast.ParamStatement{
				Src:  "{file:path contains(.)}",
				Name: "file",
				Type: mustLookupParamType("path"),
				Funcs: []ast.ParamFunc{
					{
						Name: "contains",
						Args: []string{"."},
					},
				},
				ErrorCode: 404,
			},
		}, // 2
		{
			true,
			ast.ParamStatement{
				Src:       "{username:alphabetical}",
				Name:      "username",
				Type:      mustLookupParamType("alphabetical"),
				ErrorCode: 404,
			},
		}, // 3
		{
			true,
			ast.ParamStatement{
				Src:       "{myparam}",
				Name:      "myparam",
				Type:      mustLookupParamType("string"),
				ErrorCode: 404,
			},
		}, // 4
		{
			false,
			ast.ParamStatement{
				Src:       "{myparam_:thisianunexpected}",
				Name:      "myparam_",
				Type:      nil,
				ErrorCode: 404,
			},
		}, // 5
		{
			true,
			ast.ParamStatement{
				Src:       "{myparam2}",
				Name:      "myparam2", // we now allow integers to the parameter names.
				Type:      ast.GetMasterParamType(testParamTypes...),
				ErrorCode: 404,
			},
		}, // 6
		{
			true,
			ast.ParamStatement{
				Src:  "{id:int even()}", // test param funcs without any arguments (LPAREN peek for RPAREN)
				Name: "id",
				Type: mustLookupParamType("number"),
				Funcs: []ast.ParamFunc{
					{
						Name: "even",
					},
				},
				ErrorCode: 404,
			},
		}, // 7
		{
			true,
			ast.ParamStatement{
				Src:       "{id:int64 else 404}",
				Name:      "id",
				Type:      mustLookupParamType("int64"),
				ErrorCode: 404,
			},
		}, // 8
		{
			true,
			ast.ParamStatement{
				Src:       "{id:long else 404}", // backwards-compatible test.
				Name:      "id",
				Type:      mustLookupParamType("int64"),
				ErrorCode: 404,
			},
		}, // 9
		{
			true,
			ast.ParamStatement{
				Src:       "{id:long else 404}",
				Name:      "id",
				Type:      mustLookupParamType("int64"), // backwards-compatible test of LookupParamType.
				ErrorCode: 404,
			},
		}, // 10
		{
			true,
			ast.ParamStatement{
				Src:       "{has:bool else 404}",
				Name:      "has",
				Type:      mustLookupParamType("bool"),
				ErrorCode: 404,
			},
		}, // 11
		{
			true,
			ast.ParamStatement{
				Src:       "{has:boolean else 404}", // backwards-compatible test.
				Name:      "has",
				Type:      mustLookupParamType("bool"),
				ErrorCode: 404,
			},
		}, // 12

	}

	p := new(ParamParser)
	for i, tt := range tests {
		p.Reset(tt.expectedStatement.Src)
		resultStmt, err := p.Parse(testParamTypes)

		if tt.valid && err != nil {
			t.Fatalf("tests[%d] - error %s", i, err.Error())
		} else if !tt.valid && err == nil {
			t.Fatalf("tests[%d] - expected to be a failure", i)
		}

		if resultStmt != nil { // is valid here
			if !reflect.DeepEqual(tt.expectedStatement, *resultStmt) {
				t.Fatalf("tests[%d] - wrong statement, expected and result differs. Details:\n%#v\n%#v", i, tt.expectedStatement, *resultStmt)
			}
		}

	}
}

func TestParse(t *testing.T) {
	tests := []struct {
		path               string
		valid              bool
		expectedStatements []ast.ParamStatement
	}{
		{
			"/api/users/{id:int min(1) max(5) else 404}", true,
			[]ast.ParamStatement{
				{
					Src:  "{id:int min(1) max(5) else 404}",
					Name: "id",
					Type: paramTypeNumber,
					Funcs: []ast.ParamFunc{
						{
							Name: "min",
							Args: []string{"1"},
						},
						{
							Name: "max",
							Args: []string{"5"},
						},
					},
					ErrorCode: 404,
				},
			},
		}, // 0
		{
			"/admin/{id:uint64 range(1,5)}", true,
			[]ast.ParamStatement{
				{
					Src:  "{id:uint64 range(1,5)}",
					Name: "id",
					Type: paramTypeUint64,
					Funcs: []ast.ParamFunc{
						{
							Name: "range",
							Args: []string{"1", "5"},
						},
					},
					ErrorCode: 404,
				},
			},
		}, // 1
		{
			"/files/{file:path contains(.)}", true,
			[]ast.ParamStatement{
				{
					Src:  "{file:path contains(.)}",
					Name: "file",
					Type: paramTypePath,
					Funcs: []ast.ParamFunc{
						{
							Name: "contains",
							Args: []string{"."},
						},
					},
					ErrorCode: 404,
				},
			},
		}, // 2
		{
			"/profile/{username:alphabetical}", true,
			[]ast.ParamStatement{
				{
					Src:       "{username:alphabetical}",
					Name:      "username",
					Type:      paramTypeAlphabetical,
					ErrorCode: 404,
				},
			},
		}, // 3
		{
			"/something/here/{myparam}", true,
			[]ast.ParamStatement{
				{
					Src:       "{myparam}",
					Name:      "myparam",
					Type:      paramTypeString,
					ErrorCode: 404,
				},
			},
		}, // 4
		{
			"/unexpected/{myparam_:thisianunexpected}", false,
			[]ast.ParamStatement{
				{
					Src:       "{myparam_:thisianunexpected}",
					Name:      "myparam_",
					Type:      nil,
					ErrorCode: 404,
				},
			},
		}, // 5
		{
			"/p2/{myparam2}", true,
			[]ast.ParamStatement{
				{
					Src:       "{myparam2}",
					Name:      "myparam2", // we now allow integers to the parameter names.
					Type:      paramTypeString,
					ErrorCode: 404,
				},
			},
		}, // 6
		{
			"/assets/{file:path}/invalid", false, // path should be in the end segment
			[]ast.ParamStatement{
				{
					Src:       "{file:path}",
					Name:      "file",
					Type:      paramTypePath,
					ErrorCode: 404,
				},
			},
		}, // 7
	}
	for i, tt := range tests {
		statements, err := Parse(tt.path, testParamTypes)

		if tt.valid && err != nil {
			t.Fatalf("tests[%d] - error %s", i, err.Error())
		} else if !tt.valid && err == nil {
			t.Fatalf("tests[%d] - expected to be a failure", i)
		}
		for j := range statements {
			for l := range tt.expectedStatements {
				if !reflect.DeepEqual(tt.expectedStatements[l], *statements[j]) {
					t.Fatalf("tests[%d] - wrong statements, expected and result differs. Details:\n%#v\n%#v", i, tt.expectedStatements[l], *statements[j])
				}
			}
		}

	}
}
