// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package fuzz

import (
	"math"
	"strconv"
	"testing"
	"unicode"
)

func TestUnmarshalMarshal(t *testing.T) {
	var tests = []struct {
		desc   string
		in     string
		reject bool
		want   string // if different from in
	}{
		{
			desc:   "missing version",
			in:     "int(1234)",
			reject: true,
		},
		{
			desc: "malformed string",
			in: `go test fuzz v1
string("a"bcad")`,
			reject: true,
		},
		{
			desc: "empty value",
			in: `go test fuzz v1
int()`,
			reject: true,
		},
		{
			desc: "negative uint",
			in: `go test fuzz v1
uint(-32)`,
			reject: true,
		},
		{
			desc: "int8 too large",
			in: `go test fuzz v1
int8(1234456)`,
			reject: true,
		},
		{
			desc: "multiplication in int value",
			in: `go test fuzz v1
int(20*5)`,
			reject: true,
		},
		{
			desc: "double negation",
			in: `go test fuzz v1
int(--5)`,
			reject: true,
		},
		{
			desc: "malformed bool",
			in: `go test fuzz v1
bool(0)`,
			reject: true,
		},
		{
			desc: "malformed byte",
			in: `go test fuzz v1
byte('aa)`,
			reject: true,
		},
		{
			desc: "byte out of range",
			in: `go test fuzz v1
byte('☃')`,
			reject: true,
		},
		{
			desc: "extra newline",
			in: `go test fuzz v1
string("has extra newline")
`,
			want: `go test fuzz v1
string("has extra newline")`,
		},
		{
			desc: "trailing spaces",
			in: `go test fuzz v1
string("extra")
[]byte("spacing")  
    `,
			want: `go test fuzz v1
string("extra")
[]byte("spacing")`,
		},
		{
			desc: "float types",
			in: `go test fuzz v1
float64(0)
float32(0)`,
		},
		{
			desc: "various types",
			in: `go test fuzz v1
int(-23)
int8(-2)
int64(2342425)
uint(1)
uint16(234)
uint32(352342)
uint64(123)
rune('œ')
byte('K')
byte('ÿ')
[]byte("hello¿")
[]byte("a")
bool(true)
string("hello\\xbd\\xb2=\\xbc ⌘")
float64(-12.5)
float32(2.5)`,
		},
		{
			desc: "float edge cases",
			// The two IEEE 754 bit patterns used for the math.Float{64,32}frombits
			// encodings are non-math.NAN quiet-NaN values. Since they are not equal
			// to math.NaN(), they should be re-encoded to their bit patterns. They
			// are, respectively:
			//   * math.Float64bits(math.NaN())+1
			//   * math.Float32bits(float32(math.NaN()))+1
			in: `go test fuzz v1
float32(-0)
float64(-0)
float32(+Inf)
float32(-Inf)
float32(NaN)
float64(+Inf)
float64(-Inf)
float64(NaN)
math.Float64frombits(0x7ff8000000000002)
math.Float32frombits(0x7fc00001)`,
		},
		{
			desc: "int variations",
			// Although we arbitrarily choose default integer bases (0 or 16), we may
			// want to change those arbitrary choices in the future and should not
			// break the parser. Verify that integers in the opposite bases still
			// parse correctly.
			in: `go test fuzz v1
int(0x0)
int32(0x41)
int64(0xfffffffff)
uint32(0xcafef00d)
uint64(0xffffffffffffffff)
uint8(0b0000000)
byte(0x0)
byte('\000')
byte('\u0000')
byte('\'')
math.Float64frombits(9221120237041090562)
math.Float32frombits(2143289345)`,
			want: `go test fuzz v1
int(0)
rune('A')
int64(68719476735)
uint32(3405705229)
uint64(18446744073709551615)
byte('\x00')
byte('\x00')
byte('\x00')
byte('\x00')
byte('\'')
math.Float64frombits(0x7ff8000000000002)
math.Float32frombits(0x7fc00001)`,
		},
		{
			desc: "rune validation",
			in: `go test fuzz v1
rune(0)
rune(0x41)
rune(-1)
rune(0xfffd)
rune(0xd800)
rune(0x10ffff)
rune(0x110000)
`,
			want: `go test fuzz v1
rune('\x00')
rune('A')
int32(-1)
rune('�')
int32(55296)
rune('\U0010ffff')
int32(1114112)`,
		},
		{
			desc: "int overflow",
			in: `go test fuzz v1
int(0x7fffffffffffffff)
uint(0xffffffffffffffff)`,
			want: func() string {
				switch strconv.IntSize {
				case 32:
					return `go test fuzz v1
int(-1)
uint(4294967295)`
				case 64:
					return `go test fuzz v1
int(9223372036854775807)
uint(18446744073709551615)`
				default:
					panic("unreachable")
				}
			}(),
		},
		{
			desc: "windows new line",
			in:   "go test fuzz v1\r\nint(0)\r\n",
			want: "go test fuzz v1\nint(0)",
		},
	}
	for _, test := range tests {
		t.Run(test.desc, func(t *testing.T) {
			vals, err := unmarshalCorpusFile([]byte(test.in))
			if test.reject {
				if err == nil {
					t.Fatalf("unmarshal unexpected success")
				}
				return
			}
			if err != nil {
				t.Fatalf("unmarshal unexpected error: %v", err)
			}
			newB := marshalCorpusFile(vals...)
			if newB[len(newB)-1] != '\n' {
				t.Error("didn't write final newline to corpus file")
			}

			want := test.want
			if want == "" {
				want = test.in
			}
			want += "\n"
			got := string(newB)
			if got != want {
				t.Errorf("unexpected marshaled value\ngot:\n%s\nwant:\n%s", got, want)
			}
		})
	}
}

// BenchmarkMarshalCorpusFile measures the time it takes to serialize byte
// slices of various sizes to a corpus file. The slice contains a repeating
// sequence of bytes 0-255 to mix escaped and non-escaped characters.
func BenchmarkMarshalCorpusFile(b *testing.B) {
	buf := make([]byte, 1024*1024)
	for i := 0; i < len(buf); i++ {
		buf[i] = byte(i)
	}

	for sz := 1; sz <= len(buf); sz <<= 1 {
		b.Run(strconv.Itoa(sz), func(b *testing.B) {
			for i := 0; i < b.N; i++ {
				b.SetBytes(int64(sz))
				marshalCorpusFile(buf[:sz])
			}
		})
	}
}

// BenchmarkUnmarshalCorpusFile measures the time it takes to deserialize
// files encoding byte slices of various sizes. The slice contains a repeating
// sequence of bytes 0-255 to mix escaped and non-escaped characters.
func BenchmarkUnmarshalCorpusFile(b *testing.B) {
	buf := make([]byte, 1024*1024)
	for i := 0; i < len(buf); i++ {
		buf[i] = byte(i)
	}

	for sz := 1; sz <= len(buf); sz <<= 1 {
		data := marshalCorpusFile(buf[:sz])
		b.Run(strconv.Itoa(sz), func(b *testing.B) {
			for i := 0; i < b.N; i++ {
				b.SetBytes(int64(sz))
				unmarshalCorpusFile(data)
			}
		})
	}
}

func TestByteRoundTrip(t *testing.T) {
	for x := 0; x < 256; x++ {
		b1 := byte(x)
		buf := marshalCorpusFile(b1)
		vs, err := unmarshalCorpusFile(buf)
		if err != nil {
			t.Fatal(err)
		}
		b2 := vs[0].(byte)
		if b2 != b1 {
			t.Fatalf("unmarshaled %v, want %v:\n%s", b2, b1, buf)
		}
	}
}

func TestInt8RoundTrip(t *testing.T) {
	for x := -128; x < 128; x++ {
		i1 := int8(x)
		buf := marshalCorpusFile(i1)
		vs, err := unmarshalCorpusFile(buf)
		if err != nil {
			t.Fatal(err)
		}
		i2 := vs[0].(int8)
		if i2 != i1 {
			t.Fatalf("unmarshaled %v, want %v:\n%s", i2, i1, buf)
		}
	}
}

func FuzzFloat64RoundTrip(f *testing.F) {
	f.Add(math.Float64bits(0))
	f.Add(math.Float64bits(math.Copysign(0, -1)))
	f.Add(math.Float64bits(math.MaxFloat64))
	f.Add(math.Float64bits(math.SmallestNonzeroFloat64))
	f.Add(math.Float64bits(math.NaN()))
	f.Add(uint64(0x7FF0000000000001)) // signaling NaN
	f.Add(math.Float64bits(math.Inf(1)))
	f.Add(math.Float64bits(math.Inf(-1)))

	f.Fuzz(func(t *testing.T, u1 uint64) {
		x1 := math.Float64frombits(u1)

		b := marshalCorpusFile(x1)
		t.Logf("marshaled math.Float64frombits(0x%x):\n%s", u1, b)

		xs, err := unmarshalCorpusFile(b)
		if err != nil {
			t.Fatal(err)
		}
		if len(xs) != 1 {
			t.Fatalf("unmarshaled %d values", len(xs))
		}
		x2 := xs[0].(float64)
		u2 := math.Float64bits(x2)
		if u2 != u1 {
			t.Errorf("unmarshaled %v (bits 0x%x)", x2, u2)
		}
	})
}

func FuzzRuneRoundTrip(f *testing.F) {
	f.Add(rune(-1))
	f.Add(rune(0xd800))
	f.Add(rune(0xdfff))
	f.Add(rune(unicode.ReplacementChar))
	f.Add(rune(unicode.MaxASCII))
	f.Add(rune(unicode.MaxLatin1))
	f.Add(rune(unicode.MaxRune))
	f.Add(rune(unicode.MaxRune + 1))
	f.Add(rune(-0x80000000))
	f.Add(rune(0x7fffffff))

	f.Fuzz(func(t *testing.T, r1 rune) {
		b := marshalCorpusFile(r1)
		t.Logf("marshaled rune(0x%x):\n%s", r1, b)

		rs, err := unmarshalCorpusFile(b)
		if err != nil {
			t.Fatal(err)
		}
		if len(rs) != 1 {
			t.Fatalf("unmarshaled %d values", len(rs))
		}
		r2 := rs[0].(rune)
		if r2 != r1 {
			t.Errorf("unmarshaled rune(0x%x)", r2)
		}
	})
}

func FuzzStringRoundTrip(f *testing.F) {
	f.Add("")
	f.Add("\x00")
	f.Add(string([]rune{unicode.ReplacementChar}))

	f.Fuzz(func(t *testing.T, s1 string) {
		b := marshalCorpusFile(s1)
		t.Logf("marshaled %q:\n%s", s1, b)

		rs, err := unmarshalCorpusFile(b)
		if err != nil {
			t.Fatal(err)
		}
		if len(rs) != 1 {
			t.Fatalf("unmarshaled %d values", len(rs))
		}
		s2 := rs[0].(string)
		if s2 != s1 {
			t.Errorf("unmarshaled %q", s2)
		}
	})
}
