package guid

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"testing"
)

func Test_DefaultIsVersion4(t *testing.T) {
	subject := NewGUID()
	if ver := subject.Version(); ver != 4 {
		t.Logf("Default GUID should be produced using algorithm: version 4. Actual: version %d\n%s", ver, subject.String())
		t.Fail()
	}
}

func Test_NewGUIDs_NotEmpty(t *testing.T) {
	for strat := range knownStrategies {
		t.Run(string(strat), func(subT *testing.T) {
			subject, err := NewGUIDs(strat)
			if err != nil {
				subT.Error(err)
			}
			if subject == Empty() {
				subT.Logf("unexpected empty encountered")
				subT.Fail()
			}
		})
	}
}

func Test_NewGUIDs_Unsupported(t *testing.T) {
	fauxStrategy := CreationStrategy("invalidStrategy")
	subject, err := NewGUIDs(fauxStrategy)
	if subject != Empty() {
		t.Fail()
	}
	if err.Error() != "Unsupported CreationStrategy" {
		t.Fail()
	}
}

func Test_Format_Empty(t *testing.T) {
	subject := Empty()
	testCases := []struct {
		shortFormat Format
		expected    string
	}{
		{FormatP, "(00000000-0000-0000-0000-000000000000)"},
		{FormatX, "{0x00000000,0x0000,0x0000,{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}}"},
		{FormatN, "00000000000000000000000000000000"},
		{FormatD, "00000000-0000-0000-0000-000000000000"},
		{FormatB, "{00000000-0000-0000-0000-000000000000}"},
	}

	for _, scenario := range testCases {
		t.Run("", func(subT *testing.T) {
			result := subject.Stringf(scenario.shortFormat)
			if result != scenario.expected {
				subT.Logf("\nwant:\t%s\ngot: \t%s", scenario.expected, result)
				subT.Fail()
			}
		})
	}
}

func Test_Parse_Roundtrip(t *testing.T) {
	subject := NewGUID()

	for format := range knownFormats {
		t.Run(string(format), func(subT *testing.T) {
			serialized := subject.Stringf(format)

			parsed, parseErr := Parse(serialized)
			if nil != parseErr {
				subT.Error(parseErr)
			}

			if parsed != subject {
				subT.Logf("\nwant:\t%s\ngot: \t%s", subject.String(), parsed.String())
				subT.Fail()
			}
		})
	}
}

func Test_Parse_Failures(t *testing.T) {
	testCases := []string{
		"",
		"abc",
		"00000000-0000-0000-0000-000000", // Missing digits
	}

	for _, tc := range testCases {
		t.Run("", func(t *testing.T) {
			result, err := Parse(tc)
			if expected := fmt.Sprintf(`"%s" is not in a recognized format`, tc); nil == err || expected != err.Error() {
				t.Logf("\nwant:\t%s\ngot: \t%v", expected, err)
				t.Fail()
			}

			if result != Empty() {
				t.Logf("\nwant:\t%s\ngot: \t%s", Empty().String(), result.String())
				t.Fail()
			}
		})
	}
}

func Test_version4_ReservedBits(t *testing.T) {
	for i := 0; i < 500; i++ {
		result, _ := version4()
		if result.clockSeqHighAndReserved&0xc0 != 0x80 {
			t.Fail()
		}
	}
}

func Test_version4_NoOctetisReliablyZero(t *testing.T) {
	results := make(map[string]uint)

	const iterations uint = 500
	const suspicionThreshold uint = iterations / 10

	results["time_low"] = 0
	results["time_mid"] = 0
	results["time_hi_and_version"] = 0
	results["clock_seq_hi_and_reserved"] = 0
	results["clock_seq_low"] = 0
	results["node"] = 0

	for i := uint(0); i < iterations; i++ {
		current, _ := version4()
		if 0 == current.timeLow {
			results["time_low"]++
		}
		if 0 == current.timeMid {
			results["time_mid"]++
		}
		if 0 == current.timeHighAndVersion {
			results["time_hi_and_version"]++
		}
		if 0 == current.clockSeqHighAndReserved {
			results["clock_seq_hi_and_reserved"]++
		}
		if 0 == current.clockSeqLow {
			results["clock_seq_low"]++
		}
	}

	anySuspicious := false
	for key, val := range results {
		if val > suspicionThreshold {
			anySuspicious = true
			t.Logf("%s reported value 0 enough times (%d of %d) to be suspicious.", key, val, iterations)
		}
	}
	if anySuspicious {
		t.Fail()
	}
}

func Test_SubsequentCallsDiffer(t *testing.T) {
	for strat := range knownStrategies {
		t.Run(string(strat), func(subT *testing.T) {
			seen := make(map[GUID]struct{})
			for i := 0; i < 500; i++ {
				result, err := NewGUIDs(strat)
				if err != nil {
					subT.Error(err)
				}
				if _, present := seen[result]; present == true {
					subT.Logf("The value %s was generated multiple times.", result.String())
					subT.Fail()
				}
				seen[result] = struct{}{}
			}
		})
	}

}

func Test_JSONRoundTrip(t *testing.T) {
	testCases := []GUID{
		Empty(),
		NewGUID(),
	}

	for _, tc := range testCases {
		t.Run("", func(t *testing.T) {
			marshaled, err := json.Marshal(tc)
			if err != nil {
				t.Error(err)
			}

			var unmarshaled GUID
			err = json.Unmarshal(marshaled, &unmarshaled)
			if err != nil {
				t.Error(err)
			}

			if tc != unmarshaled {
				t.Logf("\ngot: \t%s\nwant:\t%s", unmarshaled.String(), tc.String())
				t.Fail()
			}
		})
	}
}

func TestGUID_UnmarshalJSON_Failure(t *testing.T) {
	testCases := []string{
		``,
		`"`,
		`a`,
	}

	for _, tc := range testCases {
		t.Run("", func(t *testing.T) {
			var unmarshaled GUID
			err := json.Unmarshal([]byte(tc), &unmarshaled)
			if err == nil {
				t.Logf("\ngot: \t%v\nwant:\t%v", err, nil)
				t.Fail()
			}
			if unmarshaled != Empty() {
				t.Logf("\ngot: \t%s\nwant:\t%v", unmarshaled.String(), Empty().String())
				t.Fail()
			}
		})
	}
}

func Test_getMACAddress(t *testing.T) {
	subject, err := getMACAddress()
	t.Logf("MAC returned: %02x:%02x:%02x:%02x:%02x:%02x", subject[0], subject[1], subject[2], subject[3], subject[4], subject[5])

	if nil != err {
		t.Error(err)
	}

	nonZeroSeen := false
	for _, octet := range subject {
		if 0 != octet {
			nonZeroSeen = true
			break
		}
	}
	if !nonZeroSeen {
		t.Fail()
	}
}

func Test_setVersion_bounds(t *testing.T) {
	testCases := []uint16{0, 6}
	for _, tc := range testCases {
		t.Run(fmt.Sprint(tc), func(t *testing.T) {
			var fodder GUID
			err := fodder.setVersion(tc)
			if nil == err {
				t.Log("error expected but unfound when version set to 0")
				t.Fail()
			}
		})
	}
}

func Benchmark_NewGUIDs(b *testing.B) {
	for strat := range knownStrategies {
		b.Run(string(strat), func(b *testing.B) {
			for i := 0; i < b.N; i++ {
				NewGUIDs(strat)
			}
		})
	}
}

func Benchmark_String(b *testing.B) {
	rand, _ := NewGUIDs(CreationStrategyVersion4)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		fmt.Fprint(ioutil.Discard, rand.String()) // This slows down the call, but lets `go vet` pass.
	}
}

func Benchmark_Stringf(b *testing.B) {
	rand := NewGUID()
	b.ResetTimer()

	for format := range knownFormats {
		b.Run(string(format), func(b *testing.B) {
			for i := 0; i < b.N; i++ {
				rand.Stringf(format)
			}
		})
	}
}

func Benchmark_Parse(b *testing.B) {
	rand := NewGUID()
	b.ResetTimer()

	for format := range knownFormats {
		printed := rand.Stringf(format)
		b.Run(string(format), func(b *testing.B) {
			for i := 0; i < b.N; i++ {
				Parse(printed)
			}
		})
	}
}

func ExampleGUID_Stringf() {
	fmt.Printf(Empty().Stringf(FormatB))
	// Output: {00000000-0000-0000-0000-000000000000}
}

func ExampleGUID_String() {
	fmt.Printf(Empty().String())
	// Output: 00000000-0000-0000-0000-000000000000
}

func ExampleEmpty() {
	var example GUID
	if example == Empty() {
		fmt.Print("Example is Empty")
	} else {
		fmt.Print("Example is not Empty")
	}
	// Output: Example is Empty
}