package commontest

import (
	"fmt"
	"strings"
)

// This file contains the logic for generating all the valid JSON permutations and error conditions
// that will be tested by ReaderTestSuite and WriterTestSuite.

type testFactory struct {
	valueTestFactory     ValueTestFactory
	readErrorTestFactory ReadErrorTestFactory
	encodingBehavior     encodingBehavior
}

// This struct associates a test Action with some JSON data.
type testDef struct {
	// A descriptive name, used in test output.
	name string

	// For readers, encoding is the JSON input data; for writers, the expected JSON output. It is
	// defined as a list of substrings, with the expectation that there may be any amount of
	// whitespace between each substring.
	encoding []string

	// The test action for either reading or writing this piece of JSON.
	action Action
}

type testDefs []testDef

func (td testDef) then(next testDef) testDef {
	return testDef{
		name:     td.name + ", " + next.name,
		encoding: append(td.encoding, next.encoding...),
		action: func(ctx TestContext) error {
			if err := td.action(ctx); err != nil {
				return err
			}
			return next.action(ctx)
		},
	}
}

func (tds testDefs) then(next testDef) testDefs {
	ret := make(testDefs, 0, len(tds))
	for _, td := range tds {
		ret = append(ret, td.then(next))
	}
	return ret
}

func (f testFactory) MakeAllValueTests() testDefs {
	ret := testDefs{}
	eofTest := testDef{name: "EOF", action: f.valueTestFactory.EOF()}
	ret = append(ret, f.makeScalarValueTests(true).then(eofTest)...)
	ret = append(ret, f.makeArrayTests().then(eofTest)...)
	ret = append(ret, f.makeObjectTests().then(eofTest)...)
	return ret
}

func maybeVariants(vs []ValueVariant) []ValueVariant {
	if len(vs) == 0 {
		return []ValueVariant{""}
	}
	return vs
}

func (f testFactory) MakeAllReadErrorTests() testDefs {
	ret := testDefs{}
	addErrors := func(tds testDefs) {
		for _, td := range tds {
			for i, enc := range td.encoding {
				if enc == "" { // this means we want to force an unexpected EOF
					td.encoding = td.encoding[0:i]
					break
				}
			}
			ret = append(ret, td)
		}
	}
	addErrors(f.makeScalarValueReadErrorTests())
	return ret
}

func (f testFactory) makeScalarValueTests(allPermutations bool) testDefs {
	ret := testDefs{}
	values := f.makeScalarValues(allPermutations)
	for _, tv := range values {
		variants := maybeVariants(f.valueTestFactory.Variants(tv.value))
		for _, variant := range variants {
			name := tv.name
			if variant != "" {
				name = string(variant) + " " + name
			}
			td := testDef{
				name:     name,
				encoding: []string{tv.encoding},
				action:   f.valueTestFactory.Value(tv.value, variant),
			}
			ret = append(ret, td)
		}
	}
	return ret
}

func (f testFactory) makeScalarValueReadErrorTests() testDefs {
	ret := testDefs{}
	values := f.makeScalarValues(false)
	oneVariant := []ValueVariant{""}
	for _, testValue := range values {
		tv := testValue
		variants := f.valueTestFactory.Variants(tv.value)
		if variants == nil {
			variants = oneVariant
		}
		for _, variant := range variants {
			v := variant
			name := tv.name
			if v != "" {
				name = string(v) + " " + name
			}
			testAction := f.valueTestFactory.Value(tv.value, variant)

			// error: want a value, got a value of some other type
			if v != UntypedVariant && v != SkipValueVariant {
				for _, wrongValue := range f.makeScalarValues(false) {
					wv := wrongValue
					if wv.value.Kind == tv.value.Kind {
						continue
					}
					ret = append(ret, testDef{
						name:     fmt.Sprintf("%s (but got %s)", name, wv.name),
						encoding: []string{wv.encoding},
						action: func(c TestContext) error {
							return f.readErrorTestFactory.ExpectWrongTypeError(testAction(c), tv.value.Kind, v, wv.value.Kind)
						},
					})
				}
				if tv.value.Kind != NullValue {
					ret = append(ret, testDef{
						name:     fmt.Sprintf("%s (but got array)", name),
						encoding: []string{"[]"},
						action: func(c TestContext) error {
							return f.readErrorTestFactory.ExpectWrongTypeError(testAction(c), tv.value.Kind, v, ArrayValue)
						},
					})
					ret = append(ret, testDef{
						name:     fmt.Sprintf("%s (but got object)", name),
						encoding: []string{"{}"},
						action: func(c TestContext) error {
							return f.readErrorTestFactory.ExpectWrongTypeError(testAction(c), tv.value.Kind, v, ObjectValue)
						},
					})
				}
			}

			// error: want a value, got some invalid JSON
			for _, badThing := range []struct {
				name     string
				encoding string
			}{
				{"invalid identifier", "bad"},
				{"unknown delimiter", "+"},
				{"unexpected end array", "]"},
				{"unexpected object", "}"},
			} {
				ret = append(ret, testDef{
					name:     fmt.Sprintf("%s (but got %s)", name, badThing.name),
					encoding: []string{badThing.encoding},
					action: func(c TestContext) error {
						return f.readErrorTestFactory.ExpectSyntaxError(testAction(c))
					},
				})
			}
			ret = append(ret, testDef{
				name:     fmt.Sprintf("%s (but got unexpected EOF)", name),
				encoding: []string{""},
				action: func(c TestContext) error {
					return f.readErrorTestFactory.ExpectEOFError(testAction(c))
				},
			})
		}
	}
	return ret
}

func (f testFactory) makeScalarValues(allPermutations bool) []testValue {
	var values []testValue
	values = append(values, testValue{
		name:     "null",
		encoding: "null",
		value:    AnyValue{Kind: NullValue},
	})
	values = append(values, makeBoolTestValues()...)
	values = append(values, makeNumberTestValues(f.encodingBehavior)...)
	values = append(values, makeStringTestValues(f.encodingBehavior, allPermutations)...)
	return values
}

func (f testFactory) makeArrayTests() testDefs {
	ret := testDefs{}
	for elementCount := 0; elementCount <= 2; elementCount++ {
		for _, contents := range f.makeValueListsOfLength(elementCount) {
			var names []string
			var encoding = []string{"["}
			var actions []Action
			for i, td := range contents {
				names = append(names, td.name)
				if i > 0 {
					encoding = append(encoding, ",")
				}
				encoding = append(encoding, td.encoding...)
				actions = append(actions, td.action)
			}
			encoding = append(encoding, "]")
			value := AnyValue{Kind: ArrayValue, Array: actions}
			for _, variant := range maybeVariants(f.valueTestFactory.Variants(value)) {
				arrayTest := testDef{
					name:     "array(" + strings.Join(names, ", ") + ")",
					encoding: encoding,
					action:   f.valueTestFactory.Value(value, variant),
				}
				ret = append(ret, arrayTest)
			}
		}
	}
	return ret
}

func (f testFactory) makeObjectTests() testDefs {
	ret := testDefs{}
	for propertyCount := 0; propertyCount <= 2; propertyCount++ {
		for _, contents := range f.makeValueListsOfLength(propertyCount) {
			var names []string
			var encoding = []string{"{"}
			var propActions []PropertyAction
			for i, td := range contents {
				propName := fmt.Sprintf("prop%d", i)
				names = append(names, fmt.Sprintf("%s: %s", propName, td.name))
				if i > 0 {
					encoding = append(encoding, ",")
				}
				encoding = append(encoding, fmt.Sprintf(`"%s"`, propName))
				encoding = append(encoding, ":")
				encoding = append(encoding, td.encoding...)
				propActions = append(propActions, PropertyAction{Name: propName, Action: td.action})
			}
			encoding = append(encoding, "}")
			value := AnyValue{Kind: ObjectValue, Object: propActions}
			for _, variant := range maybeVariants(f.valueTestFactory.Variants(value)) {
				objectTest := testDef{
					name:     "object(" + strings.Join(names, ", ") + ")",
					encoding: encoding,
					action:   f.valueTestFactory.Value(value, variant),
				}
				ret = append(ret, objectTest)
			}
		}
	}
	return ret
}

func (f testFactory) makeValueListsOfLength(count int) []testDefs {
	if count == 0 {
		return []testDefs{testDefs{}}
	}
	previousLists := f.makeValueListsOfLength(count - 1)
	ret := []testDefs{}
	for _, previous := range previousLists {
		for _, elementTest := range f.makeScalarValueTests(false) {
			ret = append(ret, append(previous, elementTest))
		}
	}
	return ret
}
