//go:build example
// +build example

package example

import (
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"strings"
	"testing"

	"github.com/spf13/pflag"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

type makeTest struct {
	M Make `json:"make"`
}

func TestMake(t *testing.T) {
	assert.Equal(t, "Ford", MakeFord.String())
	assert.Equal(t, "Make(99)", Make(99).String())
	ford := MakeFord
	assert.Implements(t, (*flag.Value)(nil), &ford)
	assert.Implements(t, (*flag.Getter)(nil), &ford)
	assert.Implements(t, (*pflag.Value)(nil), &ford)

	names := MakeNames()
	assert.Len(t, names, 11)
}

var makeTests = []struct {
	name          string
	input         string
	output        *makeTest
	errorExpected bool
	err           error
	caseChanged   bool
}{
	{
		name:          "toyota",
		input:         `{"make":"Toyota"}`,
		output:        &makeTest{M: MakeToyota},
		errorExpected: false,
		err:           nil,
	},
	{
		name:          "chevy",
		input:         `{"make":"Chevy"}`,
		output:        &makeTest{M: MakeChevy},
		errorExpected: false,
		err:           nil,
	},
	{
		name:          "ford",
		input:         `{"make":"Ford"}`,
		output:        &makeTest{M: MakeFord},
		errorExpected: false,
		err:           nil,
	},
	{
		name:          "tesla",
		input:         `{"make":"Tesla"}`,
		output:        &makeTest{M: MakeTesla},
		errorExpected: false,
		err:           nil,
	},
	{
		name:          "hyundai",
		input:         `{"make":"Hyundai"}`,
		output:        &makeTest{M: MakeHyundai},
		errorExpected: false,
		err:           nil,
	},
	{
		name:          "nissan",
		input:         `{"make":"Nissan"}`,
		output:        &makeTest{M: MakeNissan},
		errorExpected: false,
		err:           nil,
	},
	{
		name:          "jaguar",
		input:         `{"make":"Jaguar"}`,
		output:        &makeTest{M: MakeJaguar},
		errorExpected: false,
		err:           nil,
	},
	{
		name:          "audi",
		input:         `{"make":"Audi"}`,
		output:        &makeTest{M: MakeAudi},
		errorExpected: false,
		err:           nil,
	},
	{
		name:          "AUDI",
		input:         `{"make":"AUDI"}`,
		output:        &makeTest{M: MakeAudi},
		errorExpected: false,
		err:           nil,
		caseChanged:   true,
	},
	{
		name:          "bmw",
		input:         `{"make":"BMW"}`,
		output:        &makeTest{M: MakeBMW},
		errorExpected: false,
		err:           nil,
	},
	{
		name:          "mercedes",
		input:         `{"make":"Mercedes-Benz"}`,
		output:        &makeTest{M: MakeMercedesBenz},
		errorExpected: false,
		err:           nil,
	},
	{
		name:          "volkswagon",
		input:         `{"make":"Volkswagon"}`,
		output:        &makeTest{M: MakeVolkswagon},
		errorExpected: false,
		err:           nil,
	},
	{
		name:          "porsche",
		input:         `{"make":"Porsche"}`,
		output:        &makeTest{M: MakeVolkswagon},
		errorExpected: true,
		err:           errors.New("Porsche is not a valid Make, try [Toyota, Chevy, Ford, Tesla, Hyundai, Nissan, Jaguar, Audi, BMW, Mercedes-Benz, Volkswagon]"),
	},
}

func TestMakeUnmarshal(t *testing.T) {
	for _, test := range makeTests {
		t.Run(test.name, func(tt *testing.T) {
			x := &makeTest{}
			err := json.Unmarshal([]byte(test.input), x)
			if !test.errorExpected {
				require.NoError(tt, err, "failed unmarshalling the json.")
				assert.Equal(tt, test.output.M, x.M)

				// Values won't be exactly the same, so we just validate that it was unmarshalled correctly.
				if !test.caseChanged {
					// Marshal back
					raw, err := json.Marshal(test.output)
					require.NoError(tt, err, "failed marshalling back to json")
					require.JSONEq(tt, test.input, string(raw), "json didn't match")
				}
			} else {
				require.Error(tt, err)
				assert.EqualError(tt, err, test.err.Error())
			}
		})
	}
}

func TestFlagInterface(t *testing.T) {
	for _, test := range makeTests {
		t.Run(test.name, func(tt *testing.T) {
			if test.output != nil {
				var tmp Make
				m := &tmp
				require.Equal(tt, m.Type(), "Make")
				require.Equal(tt, Make(0), m.Get(), "Unset value should be default")
				require.NoError(tt, m.Set(test.output.M.String()), "failed setting flag value")
			}
		})
	}
}

func TestNoZeroValues(t *testing.T) {
	t.Run("base", func(tt *testing.T) {
		assert.Equal(tt, 20, int(NoZerosStart))
		assert.Equal(tt, 21, int(NoZerosMiddle))
		assert.Equal(tt, 22, int(NoZerosEnd))
		assert.Equal(tt, 23, int(NoZerosPs))
		assert.Equal(tt, 24, int(NoZerosPps))
		assert.Equal(tt, 25, int(NoZerosPpps))
		assert.Equal(tt, "ppps", NoZerosPpps.String())
		assert.Equal(tt, "NoZeros(4)", NoZeros(4).String())

		assert.Len(tt, NoZerosNames(), 6)

		_, err := ParseNoZeros("pppps")
		assert.EqualError(tt, err, "pppps is not a valid NoZeros, try [start, middle, end, ps, pps, ppps]")

		tmp, _ := ParseNoZeros("ppps")
		assert.Equal(tt, NoZerosPpps, tmp)

		tmp, _ = ParseNoZeros("PppS")
		assert.Equal(tt, NoZerosPpps, tmp)

		val := map[string]*NoZeros{}

		err = json.Unmarshal([]byte(`{"nz":"pppps"}`), &val)
		assert.EqualError(tt, err, "pppps is not a valid NoZeros, try [start, middle, end, ps, pps, ppps]")
	})

	for _, name := range NoZerosNames() {
		t.Run(name, func(tt *testing.T) {
			val := map[string]*NoZeros{}

			rawJSON := fmt.Sprintf(`{"val":"%s"}`, name)

			require.NoError(tt, json.Unmarshal([]byte(rawJSON), &val), "Failed unmarshalling no zero")
			marshalled, err := json.Marshal(val)
			require.NoError(tt, err, "failed marshalling back to json")
			require.JSONEq(tt, rawJSON, string(marshalled), "marshalled json did not match")

			// Flag
			var tmp NoZeros
			nz := &tmp
			require.Equal(tt, nz.Type(), "NoZeros")
			require.Equal(tt, NoZeros(0), nz.Get(), "Unset value should be default")
			require.NoError(tt, nz.Set(name), "failed setting flag value")
		})
	}
}

func BenchmarkMakeParse(b *testing.B) {
	knownItems := map[string]struct {
		input  string
		output Make
		err    error
	}{
		"cased lookup": {
			input:  MakeAudi.String(),
			output: MakeAudi,
		},
		"lowercase lookup": {
			input:  strings.ToLower(MakeVolkswagon.String()),
			output: MakeVolkswagon,
		},
		"hyphenated upper": {
			input:  strings.ToUpper(MakeMercedesBenz.String()),
			output: MakeMercedesBenz,
		},
		// Leave this in to add an int as string parsing option in future.
		// "numeric": {
		// 	input:  "2",
		// 	output: MakeChevy,
		// },
		// "last numeric": {
		// 	input:  "20",
		// 	output: MakeVolkswagon,
		// },
		"failure": {
			input:  "xyz",
			output: MakeToyota,
			err:    errors.New("xyz is not a valid Make, try [Toyota, Chevy, Ford, Tesla, Hyundai, Nissan, Jaguar, Audi, BMW, Mercedes-Benz, Volkswagon]"),
		},
	}

	for name, tc := range knownItems {
		b.Run(name, func(b *testing.B) {
			b.ReportAllocs()

			b.ResetTimer()
			for i := 0; i < b.N; i++ {
				out, err := ParseMake(tc.input)
				assert.Equal(b, tc.output, out)
				if tc.err != nil {
					assert.Error(b, err)
				} else {
					assert.NoError(b, err)
				}
			}
		})
	}
}
