// Copyright 2017 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 strings_test

import (
	"bytes"
	. "strings"
	"testing"
	"unicode/utf8"
)

func check(t *testing.T, b *Builder, want string) {
	t.Helper()
	got := b.String()
	if got != want {
		t.Errorf("String: got %#q; want %#q", got, want)
		return
	}
	if n := b.Len(); n != len(got) {
		t.Errorf("Len: got %d; but len(String()) is %d", n, len(got))
	}
	if n := b.Cap(); n < len(got) {
		t.Errorf("Cap: got %d; but len(String()) is %d", n, len(got))
	}
}

func TestBuilder(t *testing.T) {
	var b Builder
	check(t, &b, "")
	n, err := b.WriteString("hello")
	if err != nil || n != 5 {
		t.Errorf("WriteString: got %d,%s; want 5,nil", n, err)
	}
	check(t, &b, "hello")
	if err = b.WriteByte(' '); err != nil {
		t.Errorf("WriteByte: %s", err)
	}
	check(t, &b, "hello ")
	n, err = b.WriteString("world")
	if err != nil || n != 5 {
		t.Errorf("WriteString: got %d,%s; want 5,nil", n, err)
	}
	check(t, &b, "hello world")
}

func TestBuilderString(t *testing.T) {
	var b Builder
	b.WriteString("alpha")
	check(t, &b, "alpha")
	s1 := b.String()
	b.WriteString("beta")
	check(t, &b, "alphabeta")
	s2 := b.String()
	b.WriteString("gamma")
	check(t, &b, "alphabetagamma")
	s3 := b.String()

	// Check that subsequent operations didn't change the returned strings.
	if want := "alpha"; s1 != want {
		t.Errorf("first String result is now %q; want %q", s1, want)
	}
	if want := "alphabeta"; s2 != want {
		t.Errorf("second String result is now %q; want %q", s2, want)
	}
	if want := "alphabetagamma"; s3 != want {
		t.Errorf("third String result is now %q; want %q", s3, want)
	}
}

func TestBuilderReset(t *testing.T) {
	var b Builder
	check(t, &b, "")
	b.WriteString("aaa")
	s := b.String()
	check(t, &b, "aaa")
	b.Reset()
	check(t, &b, "")

	// Ensure that writing after Reset doesn't alter
	// previously returned strings.
	b.WriteString("bbb")
	check(t, &b, "bbb")
	if want := "aaa"; s != want {
		t.Errorf("previous String result changed after Reset: got %q; want %q", s, want)
	}
}

func TestBuilderGrow(t *testing.T) {
	for _, growLen := range []int{0, 100, 1000, 10000, 100000} {
		p := bytes.Repeat([]byte{'a'}, growLen)
		allocs := testing.AllocsPerRun(100, func() {
			var b Builder
			b.Grow(growLen) // should be only alloc, when growLen > 0
			if b.Cap() < growLen {
				t.Fatalf("growLen=%d: Cap() is lower than growLen", growLen)
			}
			b.Write(p)
			if b.String() != string(p) {
				t.Fatalf("growLen=%d: bad data written after Grow", growLen)
			}
		})
		wantAllocs := 1
		if growLen == 0 {
			wantAllocs = 0
		}
		if g, w := int(allocs), wantAllocs; g != w {
			t.Errorf("growLen=%d: got %d allocs during Write; want %v", growLen, g, w)
		}
	}
	// when growLen < 0, should panic
	var a Builder
	n := -1
	defer func() {
		if r := recover(); r == nil {
			t.Errorf("a.Grow(%d) should panic()", n)
		}
	}()
	a.Grow(n)
}

func TestBuilderWrite2(t *testing.T) {
	const s0 = "hello 世界"
	for _, tt := range []struct {
		name string
		fn   func(b *Builder) (int, error)
		n    int
		want string
	}{
		{
			"Write",
			func(b *Builder) (int, error) { return b.Write([]byte(s0)) },
			len(s0),
			s0,
		},
		{
			"WriteRune",
			func(b *Builder) (int, error) { return b.WriteRune('a') },
			1,
			"a",
		},
		{
			"WriteRuneWide",
			func(b *Builder) (int, error) { return b.WriteRune('世') },
			3,
			"世",
		},
		{
			"WriteString",
			func(b *Builder) (int, error) { return b.WriteString(s0) },
			len(s0),
			s0,
		},
	} {
		t.Run(tt.name, func(t *testing.T) {
			var b Builder
			n, err := tt.fn(&b)
			if err != nil {
				t.Fatalf("first call: got %s", err)
			}
			if n != tt.n {
				t.Errorf("first call: got n=%d; want %d", n, tt.n)
			}
			check(t, &b, tt.want)

			n, err = tt.fn(&b)
			if err != nil {
				t.Fatalf("second call: got %s", err)
			}
			if n != tt.n {
				t.Errorf("second call: got n=%d; want %d", n, tt.n)
			}
			check(t, &b, tt.want+tt.want)
		})
	}
}

func TestBuilderWriteByte(t *testing.T) {
	var b Builder
	if err := b.WriteByte('a'); err != nil {
		t.Error(err)
	}
	if err := b.WriteByte(0); err != nil {
		t.Error(err)
	}
	check(t, &b, "a\x00")
}

func TestBuilderAllocs(t *testing.T) {
	// Issue 23382; verify that copyCheck doesn't force the
	// Builder to escape and be heap allocated.
	n := testing.AllocsPerRun(10000, func() {
		var b Builder
		b.Grow(5)
		b.WriteString("abcde")
		_ = b.String()
	})
	if n != 1 {
		t.Errorf("Builder allocs = %v; want 1", n)
	}
}

func TestBuilderCopyPanic(t *testing.T) {
	tests := []struct {
		name      string
		fn        func()
		wantPanic bool
	}{
		{
			name:      "String",
			wantPanic: false,
			fn: func() {
				var a Builder
				a.WriteByte('x')
				b := a
				_ = b.String() // appease vet
			},
		},
		{
			name:      "Len",
			wantPanic: false,
			fn: func() {
				var a Builder
				a.WriteByte('x')
				b := a
				b.Len()
			},
		},
		{
			name:      "Cap",
			wantPanic: false,
			fn: func() {
				var a Builder
				a.WriteByte('x')
				b := a
				b.Cap()
			},
		},
		{
			name:      "Reset",
			wantPanic: false,
			fn: func() {
				var a Builder
				a.WriteByte('x')
				b := a
				b.Reset()
				b.WriteByte('y')
			},
		},
		{
			name:      "Write",
			wantPanic: true,
			fn: func() {
				var a Builder
				a.Write([]byte("x"))
				b := a
				b.Write([]byte("y"))
			},
		},
		{
			name:      "WriteByte",
			wantPanic: true,
			fn: func() {
				var a Builder
				a.WriteByte('x')
				b := a
				b.WriteByte('y')
			},
		},
		{
			name:      "WriteString",
			wantPanic: true,
			fn: func() {
				var a Builder
				a.WriteString("x")
				b := a
				b.WriteString("y")
			},
		},
		{
			name:      "WriteRune",
			wantPanic: true,
			fn: func() {
				var a Builder
				a.WriteRune('x')
				b := a
				b.WriteRune('y')
			},
		},
		{
			name:      "Grow",
			wantPanic: true,
			fn: func() {
				var a Builder
				a.Grow(1)
				b := a
				b.Grow(2)
			},
		},
	}
	for _, tt := range tests {
		didPanic := make(chan bool)
		go func() {
			defer func() { didPanic <- recover() != nil }()
			tt.fn()
		}()
		if got := <-didPanic; got != tt.wantPanic {
			t.Errorf("%s: panicked = %v; want %v", tt.name, got, tt.wantPanic)
		}
	}
}

func TestBuilderWriteInvalidRune(t *testing.T) {
	// Invalid runes, including negative ones, should be written as
	// utf8.RuneError.
	for _, r := range []rune{-1, utf8.MaxRune + 1} {
		var b Builder
		b.WriteRune(r)
		check(t, &b, "\uFFFD")
	}
}

var someBytes = []byte("some bytes sdljlk jsklj3lkjlk djlkjw")

var sinkS string

func benchmarkBuilder(b *testing.B, f func(b *testing.B, numWrite int, grow bool)) {
	b.Run("1Write_NoGrow", func(b *testing.B) {
		b.ReportAllocs()
		f(b, 1, false)
	})
	b.Run("3Write_NoGrow", func(b *testing.B) {
		b.ReportAllocs()
		f(b, 3, false)
	})
	b.Run("3Write_Grow", func(b *testing.B) {
		b.ReportAllocs()
		f(b, 3, true)
	})
}

func BenchmarkBuildString_Builder(b *testing.B) {
	benchmarkBuilder(b, func(b *testing.B, numWrite int, grow bool) {
		for i := 0; i < b.N; i++ {
			var buf Builder
			if grow {
				buf.Grow(len(someBytes) * numWrite)
			}
			for i := 0; i < numWrite; i++ {
				buf.Write(someBytes)
			}
			sinkS = buf.String()
		}
	})
}

func BenchmarkBuildString_WriteString(b *testing.B) {
	someString := string(someBytes)
	benchmarkBuilder(b, func(b *testing.B, numWrite int, grow bool) {
		for i := 0; i < b.N; i++ {
			var buf Builder
			if grow {
				buf.Grow(len(someString) * numWrite)
			}
			for i := 0; i < numWrite; i++ {
				buf.WriteString(someString)
			}
			sinkS = buf.String()
		}
	})
}

func BenchmarkBuildString_ByteBuffer(b *testing.B) {
	benchmarkBuilder(b, func(b *testing.B, numWrite int, grow bool) {
		for i := 0; i < b.N; i++ {
			var buf bytes.Buffer
			if grow {
				buf.Grow(len(someBytes) * numWrite)
			}
			for i := 0; i < numWrite; i++ {
				buf.Write(someBytes)
			}
			sinkS = buf.String()
		}
	})
}

func TestBuilderGrowSizeclasses(t *testing.T) {
	s := Repeat("a", 19)
	allocs := testing.AllocsPerRun(100, func() {
		var b Builder
		b.Grow(18)
		b.WriteString(s)
		_ = b.String()
	})
	if allocs > 1 {
		t.Fatalf("unexpected amount of allocations: %v, want: 1", allocs)
	}
}
