package cucumberexpressions

import (
	"fmt"
	"gopkg.in/yaml.v3"
	"io/ioutil"
	"reflect"
	"regexp"
	"testing"

	"github.com/stretchr/testify/require"
)

type RegularExpressionMatchingExpectation struct {
	Expression   string        `yaml:"expression"`
	Text         string        `yaml:"text"`
	ExpectedArgs []interface{} `yaml:"expected_args"`
}

func TestRegularExpression(t *testing.T) {
	directory := "../testdata/regular-expression/matching/"
	files, err := ioutil.ReadDir(directory)
	require.NoError(t, err)

	for _, file := range files {
		contents, err := ioutil.ReadFile(directory + file.Name())
		require.NoError(t, err)
		t.Run(fmt.Sprintf("%s", file.Name()), func(t *testing.T) {
			var expectation RegularExpressionMatchingExpectation
			err = yaml.Unmarshal(contents, &expectation)
			require.NoError(t, err)

			parameterTypeRegistry := NewParameterTypeRegistry()
			expr := regexp.MustCompile(expectation.Expression)
			expression := NewRegularExpression(expr, parameterTypeRegistry)
			args, err := expression.Match(expectation.Text)
			require.NoError(t, err)
			values := make([]interface{}, len(args))
			for i, arg := range args {
				values[i] = arg.GetValue()
			}

			require.Equal(t, expectation.ExpectedArgs, values)
		})
	}

	t.Run("documents match arguments", func(t *testing.T) {
		parameterTypeRegistry := NewParameterTypeRegistry()

		expr := regexp.MustCompile(`I have (\d+) cukes? in my (\w+) now`)
		expression := NewRegularExpression(expr, parameterTypeRegistry)
		args, err := expression.Match("I have 7 cukes in my belly now")
		require.NoError(t, err)
		require.Equal(t, args[0].GetValue(), 7)
		require.Equal(t, args[1].GetValue(), "belly")
	})

	t.Run("does no transform by default", func(t *testing.T) {
		require.Equal(t, Match(t, `(\d\d)`, "22")[0], "22")
	})

	t.Run("uses type hint for transform when available", func(t *testing.T) {
		require.Equal(t, Match(t, `(\d\d)`, "22", reflect.TypeOf(int(0)))[0], 22)
	})

	t.Run("uses type hint for anonymous parameter type", func(t *testing.T) {
		require.Equal(t, Match(t, `(.*)`, "22", reflect.TypeOf(int(0)))[0], 22)
	})

	t.Run("does no transform when no type hint is available for anonymous parameter type", func(t *testing.T) {
		require.Equal(t, Match(t, `(.*)`, "22")[0], "22")
	})

	t.Run("ignores type hint when parameter type has strong type hint", func(t *testing.T) {
		parameterTypeRegistry := NewParameterTypeRegistry()
		parameterType2, err := NewParameterType(
			"type2",
			[]*regexp.Regexp{regexp.MustCompile("one|two|three")},
			"type2",
			func(args ...*string) interface{} {
				return 42
			},
			false,
			false,
			true,
		)
		require.NoError(t, err)
		err = parameterTypeRegistry.DefineParameterType(parameterType2)
		require.NoError(t, err)
		expr := regexp.MustCompile(`(one|two|three)`)
		expression := NewRegularExpression(expr, parameterTypeRegistry)
		args, err := expression.Match("one", reflect.TypeOf(""))
		require.NoError(t, err)
		require.Equal(t, args[0].GetValue(), 42)
	})

	t.Run("follows type hint when parameter type has does not have strong type hint", func(t *testing.T) {
		parameterTypeRegistry := NewParameterTypeRegistry()
		parameterType2, err := NewParameterType(
			"type2",
			[]*regexp.Regexp{regexp.MustCompile("one|two|three")},
			"type2",
			func(args ...*string) interface{} {
				return 42
			},
			true,
			false,
			false,
		)
		require.NoError(t, err)
		err = parameterTypeRegistry.DefineParameterType(parameterType2)
		require.NoError(t, err)
		expr := regexp.MustCompile(`(one|two|three)`)
		expression := NewRegularExpression(expr, parameterTypeRegistry)
		args, err := expression.Match("one", reflect.TypeOf(""))
		require.NoError(t, err)
		require.Equal(t, args[0].GetValue(), "one")
	})

	t.Run("transforms negative int", func(t *testing.T) {
		require.Equal(t, Match(t, `(-?\d+)`, "-22")[0], -22)
	})

	t.Run("transforms positive int", func(t *testing.T) {
		require.Equal(t, Match(t, `(-?\d+)`, "22")[0], 22)
	})

	t.Run("transforms positive int with hint", func(t *testing.T) {
		require.Equal(t, Match(t, `(-?\d+)`, "22", reflect.TypeOf(int(0)))[0], 22)
	})

	t.Run("transforms positive int with conflicting hint", func(t *testing.T) {
		require.Equal(t, Match(t, `(-?\d+)`, "22", reflect.TypeOf(""))[0], "22")
	})

	t.Run("returns nil when there is no match", func(t *testing.T) {
		require.Nil(t, Match(t, "hello", "world"))
	})

	t.Run("matches nested capture group without match", func(t *testing.T) {
		require.Nil(t, Match(t, `^a user( named "([^"]*)")?$`, "a user")[0])
	})

	t.Run("matches nested capture group with match", func(t *testing.T) {
		require.Equal(t, Match(t, `^a user( named "([^"]*)")?$`, "a user named \"Charlie\"")[0], "Charlie")
	})

	t.Run("matches empty string", func(t *testing.T) {
		require.Equal(t, Match(t, `^The value equals "([^"]*)"$`, "The value equals \"\"")[0], "")
	})

	t.Run("matches capture group nested in optional one", func(t *testing.T) {
		regexp := `^a (pre-commercial transaction |pre buyer fee model )?purchase(?: for \$(\d+))?$`
		require.Equal(t, Match(t, regexp, "a purchase"), []interface{}{nil, nil})
		require.Equal(t, Match(t, regexp, "a purchase for $33"), []interface{}{nil, 33})
		require.Equal(t, Match(t, regexp, "a pre buyer fee model purchase"), []interface{}{"pre buyer fee model ", nil})
	})

	t.Run("ignores non capturing groups", func(t *testing.T) {
		require.Equal(
			t,
			Match(
				t,
				`(\S+) ?(can|cannot)? (?:delete|cancel) the (\d+)(?:st|nd|rd|th) (attachment|slide) ?(?:upload)?`,
				"I can cancel the 1st slide upload",
			),
			[]interface{}{"I", "can", 1, "slide"},
		)
	})

	t.Run("works with escaped parenthesis", func(t *testing.T) {
		require.Equal(
			t,
			Match(t, `Across the line\(s\)`, "Across the line(s)"),
			[]interface{}{},
		)
	})

	t.Run("exposes regexp and source", func(t *testing.T) {
		parameterTypeRegistry := NewParameterTypeRegistry()
		expr := regexp.MustCompile(`I have (\d+) cukes? in my (\w+) now`)
		expression := NewRegularExpression(expr, parameterTypeRegistry)
		require.Equal(t, expression.Regexp(), expr)
		require.Equal(t, expression.Source(), expr.String())
	})

}

func Match(t *testing.T, expr, text string, typeHints ...reflect.Type) []interface{} {
	parameterTypeRegistry := NewParameterTypeRegistry()
	expression := NewRegularExpression(regexp.MustCompile(expr), parameterTypeRegistry)
	args, err := expression.Match(text, typeHints...)
	require.NoError(t, err)
	if args == nil {
		return nil
	}
	result := make([]interface{}, len(args))
	for i, arg := range args {
		result[i] = arg.GetValue()
	}
	return result
}
