// Copyright 2016 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 text

import (
	"bytes"
	"fmt"
	"image"
	"io"
	"math/rand"
	"reflect"
	"sort"
	"strings"
	"testing"
	"unicode/utf8"

	"golang.org/x/image/font"
	"golang.org/x/image/math/fixed"
)

func readAllText(dst []byte, f *Frame) []byte {
	for p := f.FirstParagraph(); p != nil; p = p.Next(f) {
		for l := p.FirstLine(f); l != nil; l = l.Next(f) {
			for b := l.FirstBox(f); b != nil; b = b.Next(f) {
				dst = append(dst, b.Text(f)...)
			}
		}
	}
	return dst
}

func readAllRunes(dst []rune, rr io.RuneReader) ([]rune, error) {
	for {
		r, size, err := rr.ReadRune()
		if err == io.EOF {
			return dst, nil
		}
		if err != nil {
			return nil, err
		}

		// Check that r and size are consistent.
		if wantSize := utf8.RuneLen(r); size == wantSize {
			// OK.
		} else if r == utf8.RuneError && size == 1 {
			// Also OK; one byte of invalid UTF-8 was replaced by '\ufffd'.
		} else {
			return nil, fmt.Errorf("rune %#x: got size %d, want %d", r, size, wantSize)
		}

		dst = append(dst, r)
	}
}

func runesEqual(xs, ys []rune) bool {
	if len(xs) != len(ys) {
		return false
	}
	for i := range xs {
		if xs[i] != ys[i] {
			return false
		}
	}
	return true
}

func allASCII(rs []rune) bool {
	for _, r := range rs {
		if r >= 128 {
			return false
		}
	}
	return true
}

func runesLen(rs []rune) (n int) {
	for _, r := range rs {
		n += utf8.RuneLen(r)
	}
	return n
}

// insert inserts the insert argument into buf at the given offset.
//
// cap(buf) must be >= len(buf) + len(insert).
func insert(buf, insert []byte, offset int) []byte {
	n := len(insert)
	buf = buf[:len(buf)+n]
	copy(buf[offset+n:], buf[offset:])
	copy(buf[offset:offset+n], insert)
	return buf
}

func rngIntPair(rng *rand.Rand, n int) (x, y int) {
	x = rng.Intn(n)
	y = rng.Intn(n)
	if x > y {
		x, y = y, x
	}
	return x, y
}

// toyFace implements the font.Face interface by measuring every rune's width
// as 1 pixel.
type toyFace struct{}

func (toyFace) Close() error {
	return nil
}

func (toyFace) Glyph(dot fixed.Point26_6, r rune) (image.Rectangle, image.Image, image.Point, fixed.Int26_6, bool) {
	panic("unimplemented")
}

func (toyFace) GlyphBounds(r rune) (fixed.Rectangle26_6, fixed.Int26_6, bool) {
	panic("unimplemented")
}

func (toyFace) GlyphAdvance(r rune) (fixed.Int26_6, bool) {
	return fixed.I(1), true
}

func (toyFace) Kern(r0, r1 rune) fixed.Int26_6 {
	return 0
}

func (toyFace) Metrics() font.Metrics {
	return font.Metrics{
		Ascent:  fixed.I(2),
		Descent: fixed.I(1),
	}
}

const toyFaceLineHeight = 3

// iRobot is some text that contains both ASCII and non-ASCII runes.
const iRobot = "\"I, Robot\" in Russian is \"Я, робот\".\nIt's about robots.\n"

var (
	iRobotBytes = []byte(iRobot)
	iRobotRunes = []rune(iRobot)
)

func iRobotFrame(maxWidth int) *Frame {
	f := new(Frame)
	f.SetFace(toyFace{})
	f.SetMaxWidth(fixed.I(maxWidth))
	c := f.NewCaret()
	c.WriteString(iRobot)
	c.Close()
	return f
}

func TestZeroFrame(t *testing.T) {
	f := new(Frame)
	if err := checkInvariants(f); err != nil {
		t.Fatal(err)
	}
}

func TestSeek(t *testing.T) {
	f := iRobotFrame(10)
	c := f.NewCaret()
	defer c.Close()
	rng := rand.New(rand.NewSource(1))
	seen := [1 + len(iRobot)]bool{}
	for i := 0; i < 10*len(iRobot); i++ {
		wantPos := int64(rng.Intn(len(iRobot) + 1))
		gotPos, err := c.Seek(wantPos, SeekSet)
		if err != nil {
			t.Fatalf("i=%d: Seek: %v", i, err)
		}
		if gotPos != wantPos {
			t.Fatalf("i=%d: Seek: got %d, want %d", i, gotPos, wantPos)
		}
		seen[gotPos] = true
		if err := checkInvariants(f); err != nil {
			t.Fatalf("i=%d: %v", i, err)
		}

		gotByte, gotErr := c.ReadByte()
		wantByte, wantErr := byte(0), io.EOF
		if gotPos < int64(len(iRobot)) {
			wantByte, wantErr = iRobot[gotPos], nil
		}
		if gotByte != wantByte || gotErr != wantErr {
			t.Fatalf("i=%d: ReadByte: got %d, %v, want %d, %v", i, gotByte, gotErr, wantByte, wantErr)
		}
	}
	for i, s := range seen {
		if !s {
			t.Errorf("randomly generated positions weren't exhaustive: position %d / %d not seen", i, len(iRobot))
		}
	}
}

func testRead(f *Frame, want string) error {
	c := f.NewCaret()
	defer c.Close()
	asBytes, err := io.ReadAll(c)
	if err != nil {
		return fmt.Errorf("ReadAll: %v", err)
	}
	if got := string(asBytes); got != want {
		return fmt.Errorf("Read\ngot:  %q\nwant: %q", got, want)
	}
	return nil
}

func TestRead(t *testing.T) {
	f := iRobotFrame(10)
	if err := checkInvariants(f); err != nil {
		t.Fatal(err)
	}
	if err := testRead(f, iRobot); err != nil {
		t.Fatal(err)
	}
}

func TestReadByte(t *testing.T) {
	f := iRobotFrame(10)
	c := f.NewCaret()
	defer c.Close()
	got, want := []byte(nil), []byte(iRobot)
	for {
		x, err := c.ReadByte()
		if err == io.EOF {
			break
		}
		if err != nil {
			t.Fatalf("ReadByte: %v", err)
		}
		if err := checkInvariants(f); err != nil {
			t.Fatal(err)
		}
		got = append(got, x)
	}
	if err := checkInvariants(f); err != nil {
		t.Fatal(err)
	}
	if !reflect.DeepEqual(got, want) {
		t.Fatalf("\ngot:  %v\nwant: %v", got, want)
	}
}

func TestReadRune(t *testing.T) {
	f := iRobotFrame(10)
	c := f.NewCaret()
	defer c.Close()
	got, want := []rune(nil), []rune(iRobot)
	for {
		r, _, err := c.ReadRune()
		if err == io.EOF {
			break
		}
		if err != nil {
			t.Fatalf("ReadRune: %v", err)
		}
		if err := checkInvariants(f); err != nil {
			t.Fatal(err)
		}
		got = append(got, r)
	}
	if err := checkInvariants(f); err != nil {
		t.Fatal(err)
	}
	if !reflect.DeepEqual(got, want) {
		t.Fatalf("\ngot:  %v\nwant: %v", got, want)
	}
}

func TestWrite(t *testing.T) {
	f := new(Frame)
	c := f.NewCaret()
	c.Write([]byte{0xff, 0xfe, 0xfd})
	c.WriteByte(0x80)
	c.WriteRune('\U0001f4a9')
	c.WriteString("abc\x00")
	c.Close()
	if err := checkInvariants(f); err != nil {
		t.Fatal(err)
	}
	got := f.text[:f.len]
	want := []byte{0xff, 0xfe, 0xfd, 0x80, 0xf0, 0x9f, 0x92, 0xa9, 0x61, 0x62, 0x63, 0x00}
	if !bytes.Equal(got, want) {
		t.Fatalf("\ngot  % x\nwant % x", got, want)
	}
}

func TestManyWrites(t *testing.T) {
	f := new(Frame)
	f.SetFace(toyFace{})
	f.SetMaxWidth(fixed.I(10))
	c := f.NewCaret()
	defer c.Close()

	const n, abc16 = 100, "abcdefghijkl\n   "
	rng := rand.New(rand.NewSource(1))
	buf := make([]byte, n)
	for i := range buf {
		buf[i] = abc16[rng.Intn(len(abc16))]
	}

	for i := 0; i < 100; i++ {
		x, y := rngIntPair(rng, len(buf)+1)
		c.Write(buf[x:y])
		if err := checkInvariants(f); err != nil {
			t.Fatalf("i=%d: %v", i, err)
		}
	}
}

func TestRandomAccessWrite(t *testing.T) {
	f := new(Frame)
	c := f.NewCaret()
	defer c.Close()

	rng := rand.New(rand.NewSource(1))
	gotBuf := make([]byte, 0, 10000)
	want := make([]byte, 0, 10000)
	buf := make([]byte, 10)
	x := byte(0)
	for i := 0; i < 100; i++ {
		n := rng.Intn(len(buf) + 1)
		for j := range buf[:n] {
			buf[j] = x
			x++
		}
		offset := rng.Intn(len(want) + 1)

		// Insert buf[:n] into the Frame at the given offset.
		c.Seek(int64(offset), SeekSet)
		c.Write(buf[:n])
		if err := checkInvariants(f); err != nil {
			t.Fatalf("i=%d: %v", i, err)
		}

		// Insert buf[:n] into want at the given offset.
		want = insert(want, buf[:n], offset)

		if got := readAllText(gotBuf[:0], f); !bytes.Equal(got, want) {
			t.Errorf("i=%d:\ngot  % x\nwant % x", i, got, want)
		}
	}
}

func TestWriteAtStart(t *testing.T) {
	f := iRobotFrame(10)
	c := f.NewCaret()
	defer c.Close()

	prefix := "The Truth of the Matter\n(insofaras): "
	gotBuf := make([]byte, len(prefix)+len(iRobot))
	want := make([]byte, len(iRobot), len(prefix)+len(iRobot))
	copy(want, iRobot)

	for i := 0; i < len(prefix); i++ {
		x := prefix[len(prefix)-1-i]

		c.Seek(0, SeekSet)
		c.WriteByte(x)
		if err := checkInvariants(f); err != nil {
			t.Fatalf("i=%d: %v", i, err)
		}

		want = want[:len(want)+1]
		copy(want[1:], want)
		want[0] = x

		if got := readAllText(gotBuf[:0], f); !bytes.Equal(got, want) {
			t.Fatalf("i=%d:\ngot  % x\nwant % x", i, got, want)
		}
	}

	if nl, nb := f.nUsedLines(), f.nUsedBoxes(); nl == nb {
		t.Fatalf("before compaction: using %d lines and %d boxes, should be not equal", nl, nb)
	}
	f.compactText()
	if nl, nb := f.nUsedLines(), f.nUsedBoxes(); nl != nb {
		t.Fatalf("after compaction: using %d lines and %d boxes, should be equal", nl, nb)
	}
}

func TestLineCount(t *testing.T) {
	f := iRobotFrame(0)

	testCases := []struct {
		desc           string
		maxWidth       int
		wantLines      []string
		wantLineCounts []int
	}{{
		desc:     "infinite maxWidth",
		maxWidth: 0,
		wantLines: []string{
			`"I, Robot" in Russian is "Я, робот".`,
			`It's about robots.`,
			``,
		},
		wantLineCounts: []int{1, 1, 1},
	}, {
		desc:     "finite maxWidth",
		maxWidth: 10,
		wantLines: []string{
			`"I, Robot"`,
			`in Russian`,
			`is "Я,`,
			`робот".`,
			`It's about`,
			`robots.`,
			``,
		},
		wantLineCounts: []int{4, 2, 1},
	}}
	for _, tc := range testCases {
		f.SetMaxWidth(fixed.I(tc.maxWidth))
		lines, lineCounts := []string{}, []int{}
		for p := f.FirstParagraph(); p != nil; p = p.Next(f) {
			for l := p.FirstLine(f); l != nil; l = l.Next(f) {
				var line []byte
				for b := l.FirstBox(f); b != nil; b = b.Next(f) {
					line = append(line, b.TrimmedText(f)...)
				}
				lines = append(lines, string(line))
			}
			lineCounts = append(lineCounts, p.LineCount(f))
		}

		if got, want := f.LineCount(), len(tc.wantLines); got != want {
			t.Errorf("%s: LineCount: got %d, want %d", tc.desc, got, want)
			continue
		}
		if !reflect.DeepEqual(lines, tc.wantLines) {
			t.Errorf("%s: lines: got %q, want %q", tc.desc, lines, tc.wantLines)
			continue
		}
		if got, want := f.ParagraphCount(), len(tc.wantLineCounts); got != want {
			t.Errorf("%s: ParagraphCount: got %d, want %d", tc.desc, got, want)
			continue
		}
		if !reflect.DeepEqual(lineCounts, tc.wantLineCounts) {
			t.Errorf("%s: lineCounts: got %d, want %d", tc.desc, lineCounts, tc.wantLineCounts)
			continue
		}
		if got, want := f.Height(), toyFaceLineHeight*len(tc.wantLines); got != want {
			t.Errorf("%s: Height: got %d, want %d", tc.desc, got, want)
			continue
		}
	}
}

func TestSetMaxWidth(t *testing.T) {
	f := new(Frame)
	f.SetFace(toyFace{})
	want := ""
	rng := rand.New(rand.NewSource(1))
	for i := 0; i < 100; i++ {
		if i%20 == 5 {
			c := f.NewCaret()
			c.WriteString(iRobot)
			c.Close()
			want += iRobot
		}
		maxWidth := rng.Intn(20)
		f.SetMaxWidth(fixed.I(maxWidth))
		if err := checkInvariants(f); err != nil {
			t.Fatalf("i=%d: %v", i, err)
		}
		if err := testRead(f, want); err != nil {
			t.Fatalf("i=%d: %v", i, err)
		}
		if want == "" {
			continue
		}
		// Check that a zero (which means infinite) maxWidth corresponds to one
		// Line per Paragraph.
		np, nl := f.nUsedParagraphs(), f.nUsedLines()
		if maxWidth == 0 {
			if np != nl {
				t.Fatalf("i=%d: maxWidth=0: got %d paragraphs and %d lines, should be equal", i, np, nl)
			}
		} else {
			if np == nl {
				t.Fatalf("i=%d: maxWidth=%d: got %d paragraphs and %d lines, should be unequal", i, maxWidth, np, nl)
			}
		}
	}
}

func TestReadRuneAcrossBoxes(t *testing.T) {
	rng := rand.New(rand.NewSource(1))
	for i := 0; i < 6; i++ {
		// text is a mixture of valid and invalid UTF-8.
		text := "ab_\u0100\u0101_\ufffd中文_\xff\xffñ\xff_z_\U0001f4a9_\U0001f600" +
			strings.Repeat("\xff", i)

		wantBytes := []byte(text)
		wantRunes := []rune(text)
		gotBytesBuf := make([]byte, 0, len(text))
		gotRunesBuf := make([]rune, 0, len(text))

		for j := 0; j < 100; j++ {
			f := new(Frame)

			{
				c := f.NewCaret()
				c.WriteString(text)
				// Split the sole Line into multiple Boxes at random byte offsets,
				// possibly cutting across multi-byte UTF-8 encoded runes.
				for {
					if rng.Intn(8) == 0 {
						break
					}
					c.Seek(int64(rng.Intn(len(text)+1)), SeekSet)
					c.splitBox(false)
				}
				c.Close()

				if err := checkInvariants(f); err != nil {
					t.Fatalf("i=%d, j=%d: %v", i, j, err)
				}
				if gotBytes := readAllText(gotBytesBuf[:0], f); !bytes.Equal(gotBytes, wantBytes) {
					t.Fatalf("i=%d, j=%d: bytes\ngot  % x\nwant % x", i, j, gotBytes, wantBytes)
				}
			}

			// Test lineReader.ReadRune.
			{
				p := f.firstP
				l := f.paragraphs[p].firstL
				b := f.lines[l].firstB
				lr := f.lineReader(b, f.boxes[b].i)
				gotRunes, err := readAllRunes(gotRunesBuf[:0], lr)
				if err != nil {
					t.Fatalf("i=%d, j=%d: lineReader readAllRunes: %v", i, j, err)
				}
				if !runesEqual(gotRunes, wantRunes) {
					t.Fatalf("i=%d, j=%d: lineReader readAllRunes:\ngot  %#x\nwant %#x",
						i, j, gotRunes, wantRunes)
				}
			}

			// Test Caret.ReadRune.
			{
				c := f.NewCaret()
				gotRunes, err := readAllRunes(gotRunesBuf[:0], c)
				c.Close()
				if err != nil {
					t.Fatalf("i=%d, j=%d: Caret readAllRunes: %v", i, j, err)
				}
				if !runesEqual(gotRunes, wantRunes) {
					t.Fatalf("i=%d, j=%d: Caret readAllRunes:\ngot  %#x\nwant %#x",
						i, j, gotRunes, wantRunes)
				}
			}
		}
	}
}

func TestDelete(t *testing.T) {
	rng := rand.New(rand.NewSource(1))
	gotBytesBuf := make([]byte, 0, len(iRobot))
	wantBytesBuf := make([]byte, 0, len(iRobot))

	for i := 0; i < 100; i++ {
		f := iRobotFrame(rng.Intn(len(iRobot) + 1))
		c := f.NewCaret()
		defer c.Close()
		wantBytes := append(wantBytesBuf[:0], iRobot...)
		for j := 0; j < 8; j++ {
			// Delete the text in the byte range [x, y).
			x, y := rngIntPair(rng, len(wantBytes)+1)
			got, want := 0, y-x
			if rng.Intn(2) == 0 {
				c.Seek(int64(x), SeekSet)
				got = c.Delete(Forwards, want)
			} else {
				c.Seek(int64(y), SeekSet)
				got = c.Delete(Backwards, want)
			}
			if err := checkInvariants(f); err != nil {
				t.Fatalf("i=%d, j=%d, x=%d, y=%d: %v", i, j, x, y, err)
			}
			if got != want {
				t.Fatalf("i=%d, j=%d, x=%d, y=%d: Delete: got %d, want %d", i, j, x, y, got, want)
			}

			gotBytes := readAllText(gotBytesBuf[:0], f)
			wantBytes = append(wantBytes[:x], wantBytes[y:]...)
			if !bytes.Equal(gotBytes, wantBytes) {
				t.Fatalf("i=%d, j=%d, x=%d, y=%d:\ngot  %q\nwant %q", i, j, x, y, gotBytes, wantBytes)
			}
		}
	}
}

func TestDeleteRunes(t *testing.T) {
	rng := rand.New(rand.NewSource(1))
	gotBytesBuf := make([]byte, 0, len(iRobot))
	wantRunesBuf := make([]rune, 0, len(iRobot))
	wantBytesBuf := make([]byte, 0, len(iRobot))

	for i := 0; i < 100; i++ {
		f := iRobotFrame(rng.Intn(len(iRobot) + 1))
		c := f.NewCaret()
		defer c.Close()
		wantRunes := append(wantRunesBuf[:0], iRobotRunes...)
		wantBytes := append(wantBytesBuf[:0], iRobot...)
		for j := 0; j < 8; j++ {
			xR, yR := rngIntPair(rng, len(wantRunes)+1)
			xB := runesLen(wantRunes[:xR])
			yB := runesLen(wantRunes[:yR])

			// Delete the text in the byte range [xB, yB).
			gotR, gotB, wantR, wantB := 0, 0, yR-xR, yB-xB
			if rng.Intn(2) == 0 {
				c.Seek(int64(xB), SeekSet)
				gotR, gotB = c.DeleteRunes(Forwards, wantR)
			} else {
				c.Seek(int64(yB), SeekSet)
				gotR, gotB = c.DeleteRunes(Backwards, wantR)
			}
			if err := checkInvariants(f); err != nil {
				t.Fatalf("i=%d, j=%d, xR=%d, xB=%d, yR=%d, yB=%d: %v", i, j, xR, xB, yR, yB, err)
			}
			if gotR != wantR || gotB != wantB {
				t.Fatalf("i=%d, j=%d, xR=%d, xB=%d, yR=%d, yB=%d: Delete: got %d runes, %d bytes, want %d, %d",
					i, j, xR, xB, yR, yB, gotR, gotB, wantR, wantB)
			}

			gotBytes := readAllText(gotBytesBuf[:0], f)
			wantRunes = append(wantRunes[:xR], wantRunes[yR:]...)
			wantBytes = append(wantBytes[:xB], wantBytes[yB:]...)
			if !bytes.Equal(gotBytes, wantBytes) {
				t.Fatalf("i=%d, j=%d, xR=%d, xB=%d, yR=%d, yB=%d:\ngot  %q\nwant %q",
					i, j, xR, xB, yR, yB, gotBytes, wantBytes)
			}
		}
	}
}

func TestDeleteTooMuch(t *testing.T) {
	if len(iRobot) < 17+15 {
		t.Fatal("iRobot string is too short")
	}

	f := iRobotFrame(10)
	c := f.NewCaret()
	defer c.Close()

	c.Seek(-15, SeekEnd)
	if got, want := c.Delete(Forwards, 16), 15; got != want {
		t.Errorf("Delete Forwards: got %d, want %d", got, want)
	}

	c.Seek(+17, SeekSet)
	if got, want := c.Delete(Backwards, 18), 17; got != want {
		t.Errorf("Delete Backwards: got %d, want %d", got, want)
	}

	got := string(readAllText(nil, f))
	want := iRobot[17 : len(iRobot)-15]
	if got != want {
		t.Errorf("\ngot  %q\nwant %q", got, want)
	}
}

func TestDeleteRunesTooMuch(t *testing.T) {
	if len(iRobotRunes) < 30+24 {
		t.Fatal("iRobotRunes []rune is too short")
	}
	prefix := iRobotRunes[:30]
	suffix := iRobotRunes[len(iRobotRunes)-24:]
	if allASCII(prefix) {
		t.Fatal("prefix contains no non-ASCII runes")
	}
	if allASCII(suffix) {
		t.Fatal("suffix contains no non-ASCII runes")
	}
	prefixB := runesLen(prefix)
	suffixB := runesLen(suffix)

	f := iRobotFrame(10)
	c := f.NewCaret()
	defer c.Close()

	c.Seek(-int64(suffixB), SeekEnd)
	wantR, wantB := len(suffix), suffixB
	if gotR, gotB := c.DeleteRunes(Forwards, 25); gotR != wantR || gotB != wantB {
		t.Errorf("DeleteRunes Forwards: got %d runes, %d bytes, want %d, %d", gotR, gotB, wantR, wantB)
	}

	c.Seek(+int64(prefixB), SeekSet)
	wantR, wantB = len(prefix), prefixB
	if gotR, gotB := c.DeleteRunes(Backwards, 31); gotR != wantR || gotB != wantB {
		t.Errorf("DeleteRunes Backwards: got %d runes, %d bytes, want %d, %d", gotR, gotB, wantR, wantB)
	}

	got := string(readAllText(nil, f))
	want := iRobot[prefixB : len(iRobot)-suffixB]
	if got != want {
		t.Errorf("\ngot  %q\nwant %q", got, want)
	}
}

func TestCompactText(t *testing.T) {
	f := new(Frame)
	f.SetFace(toyFace{})
	f.SetMaxWidth(fixed.I(80))
	c := f.NewCaret()
	defer c.Close()

	const n, abc64 = 1024, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\n           "
	rng := rand.New(rand.NewSource(1))
	buf := make([]byte, n)
	for i := range buf {
		buf[i] = abc64[rng.Intn(len(abc64))]
	}

	written, wantLen := 0, 0
	for i := 0; written < 131072; i++ {
		if rng.Intn(10) != 0 {
			x, y := rngIntPair(rng, len(buf)+1)
			c.Seek(int64(rng.Intn(f.Len()+1)), SeekSet)
			c.Write(buf[x:y])
			written += y - x
			wantLen += y - x
		}

		if rng.Intn(10) != 0 {
			x, y := rngIntPair(rng, wantLen+1)
			c.Seek(int64(x), SeekSet)
			c.Delete(Forwards, y-x)
			wantLen -= y - x
		}

		// Calculate cached counts, some of the time.
		if rng.Intn(3) == 0 {
			f.ParagraphCount()
		}
		if rng.Intn(4) == 0 {
			f.LineCount()
		}
		if rng.Intn(5) == 0 {
			c.Seek(int64(rng.Intn(f.Len()+1)), SeekSet)
			f.paragraphs[c.p].LineCount(f)
		}

		if err := checkInvariants(f); err != nil {
			t.Fatalf("i=%d: %v", i, err)
		}
		if gotLen := f.Len(); gotLen != wantLen {
			t.Fatalf("i=%d: length: got %d, want %d", i, gotLen, wantLen)
		}
	}
	if len(f.text) == written {
		t.Fatal("f.text was not compacted")
	}
}

func TestMergeIntoOneLine(t *testing.T) {
	rng := rand.New(rand.NewSource(1))
	for i := 0; i < 100; i++ {
		f := new(Frame)
		f.SetMaxWidth(fixed.I(100))

		if !f.initialized() {
			f.initialize()
		}
		p := f.firstP
		pp := &f.paragraphs[p]
		l := pp.firstL
		ll := &f.lines[l]
		b := ll.firstB
		bb := &f.boxes[b]

		// Make some Lines and Boxes.
		wantNUsedBoxes := 0
		prevIJ := int32(0)
		emptyRun := true
		for j := 0; ; j++ {
			length := rng.Intn(3)
			bb.i = prevIJ
			prevIJ += int32(length)
			bb.j = prevIJ
			f.len += length
			if length > 0 && emptyRun {
				emptyRun = false
				wantNUsedBoxes++
			}

			if rng.Intn(20) == 0 {
				break
			}

			if rng.Intn(4) == 0 {
				// Make a new Box on a new Line.
				l1, realloc := f.newLine()
				if realloc {
					ll = &f.lines[l]
				}
				ll1 := &f.lines[l1]
				ll.next = l1
				ll1.prev = l
				l, ll = l1, ll1

				ll.firstB, _ = f.newBox()
				b = ll.firstB
				bb = &f.boxes[b]

			} else {
				// Make a new Box on the same Line.
				b1, realloc := f.newBox()
				if realloc {
					bb = &f.boxes[b]
				}
				bb1 := &f.boxes[b1]
				bb.next = b1
				bb1.prev = b
				b, bb = b1, bb1
			}

			if rng.Intn(5) == 0 {
				// Put an i/j gap between this run and the previous one.
				prevIJ += 1 + int32(rng.Intn(5))
				emptyRun = true
			}
		}

		// We normally remove all empty Boxes. However, if there is no text
		// whatsoever, we still need one Box.
		if f.len == 0 {
			if wantNUsedBoxes != 0 {
				t.Fatalf("i=%d: no text: wantNUsedBoxes: got %d, want 0", i, wantNUsedBoxes)
			}
			wantNUsedBoxes = 1
		}

		// Make f.text long enough to hold all of the Boxes' i's and j's.
		f.text = make([]byte, prevIJ)
		for i := range f.text {
			f.text[i] = byte('a' + i%16)
		}

		// Do the merge.
		if err := checkSomeInvariants(f, ignoreInvariantEmptyBoxes); err != nil {
			t.Fatalf("i=%d: before: %v", i, err)
		}
		f.mergeIntoOneLine(p)
		if err := checkInvariants(f); err != nil {
			t.Fatalf("i=%d: after: %v", i, err)
		}

		// Check that there is only one Line in use.
		if n := f.nUsedLines(); n != 1 {
			t.Errorf("i=%d: nUsedLines: got %d, want %d", i, n, 1)
		}

		// Check the number of Boxes in use.
		if n := f.nUsedBoxes(); n != wantNUsedBoxes {
			t.Errorf("i=%d: nUsedBoxes: got %d, want %d", i, n, wantNUsedBoxes)
		}
	}
}

// TestMultipleCarets tests multiple Carets all seeking, reading, writing and
// deleting text.
func TestMultipleCarets(t *testing.T) {
	f := new(Frame)
	f.SetFace(toyFace{})
	f.SetMaxWidth(fixed.I(10))

	rng := rand.New(rand.NewSource(1))
	gotBuf := make([]byte, 0, 10000)
	want := make([]byte, 0, 10000)
	positions := map[*Caret]int{}
	carets := []*Caret{}
	defer func() {
		for _, c := range carets {
			c.Close()
		}
	}()

	for i := 0; i < 1000; i++ {
		if err := checkInvariants(f); err != nil {
			t.Fatalf("i=%d: %v", i, err)
		}
		if len(carets) == 0 || rng.Intn(20) == 0 {
			c := f.NewCaret()
			carets = append(carets, c)
			positions[c] = 0
			continue
		}
		caretsIndex := rng.Intn(len(carets))
		c := carets[caretsIndex]

		if rng.Intn(40) == 0 {
			c.Close()
			carets[caretsIndex] = carets[len(carets)-1]
			carets[len(carets)-1] = nil
			carets = carets[:len(carets)-1]
			delete(positions, c)
			continue
		}

		// Seek, read, delete or write, with a slight bias to writing.
		switch rng.Intn(5) {
		case 0: // Seek.
			p := rng.Intn(len(want) + 1)
			c.Seek(int64(p), SeekSet)
			positions[c] = p

		case 1: // ReadByte.
			gotB, gotErr := c.ReadByte()
			wantB, wantErr := byte(0), io.EOF
			if p := positions[c]; p < len(want) {
				wantB, wantErr = want[p], nil
				positions[c] = p + 1
			}
			if gotB != wantB || gotErr != wantErr {
				t.Fatalf("i=%d: ReadByte: got %d, %v, want %d, %v", i, gotB, gotErr, wantB, wantErr)
			}

		case 2: // Delete.
			dir := Direction(rng.Intn(2) == 0)
			nBytes, dBytes := rng.Intn(len(want)), 0
			pos, x, y := positions[c], 0, 0
			if dir == Forwards {
				dBytes = min(nBytes, len(want)-pos)
				x, y = pos, pos+dBytes
			} else {
				dBytes = min(nBytes, pos)
				x, y = pos-dBytes, pos
			}
			if d := c.Delete(dir, nBytes); d != dBytes {
				t.Fatalf("i=%d: Delete: got %d bytes, want %d", i, d, dBytes)
			}
			want = append(want[:x], want[y:]...)
			for _, cc := range carets {
				if cc == c {
					positions[cc] = x
					continue
				}
				switch p := positions[cc]; {
				case p <= x:
					// No-op.
				case p <= y:
					positions[cc] = x
				default:
					positions[cc] -= dBytes
				}
			}

		default: // Write.
			i, j := rngIntPair(rng, len(iRobotBytes))
			s := iRobotBytes[i:j]
			c.Write(s)
			pos := positions[c]
			want = insert(want, s, pos)
			for _, cc := range carets {
				if cc == c || positions[cc] > pos {
					positions[cc] += j - i
				}
			}
		}

		badPos := false
		for ci, cc := range carets {
			if int(cc.pos) != positions[cc] {
				badPos = true
				t.Errorf("i=%d, ci=%d: position: got %d, want %d", i, ci, cc.pos, positions[cc])
			}
		}
		if badPos {
			t.Fatalf("i=%d: there were inconsistent Caret positions", i)
		}

		if got := readAllText(gotBuf[:0], f); !bytes.Equal(got, want) {
			t.Fatalf("i=%d:\ngot  % x\nwant % x", i, got, want)
		}
	}
	if err := checkInvariants(f); err != nil {
		t.Fatal(err)
	}
}

type ijRange struct {
	i, j int32
}

type byI []ijRange

func (b byI) Len() int           { return len(b) }
func (b byI) Less(x, y int) bool { return b[x].i < b[y].i }
func (b byI) Swap(x, y int)      { b[x], b[y] = b[y], b[x] }

const (
	ignoreInvariantEmptyBoxes = 1 << iota
)

func checkInvariants(f *Frame) error {
	return checkSomeInvariants(f, 0)
}

func checkSomeInvariants(f *Frame, ignoredInvariants uint32) error {
	const infinity = 1e6

	if !f.initialized() {
		if !reflect.DeepEqual(*f, Frame{}) {
			return fmt.Errorf("uninitialized Frame is not zero-valued")
		}
		return nil
	}

	// Iterate through the Paragraphs, Lines and Boxes. Check that every first
	// child has no previous sibling, and no child is the first child of
	// multiple parents. Also check the cached Paragraph and Line counts.
	nUsedParagraphs, nUsedLines, nUsedBoxes := 0, 0, 0
	{
		firstLines := map[int32]bool{}
		firstBoxes := map[int32]bool{}
		p := f.firstP
		if p == 0 {
			return fmt.Errorf("firstP is zero")
		}
		if x := f.paragraphs[p].prev; x != 0 {
			return fmt.Errorf("first Paragraph %d's prev: got %d, want 0", p, x)
		}

		for ; p != 0; p = f.paragraphs[p].next {
			l := f.paragraphs[p].firstL
			if l == 0 {
				return fmt.Errorf("paragraphs[%d].firstL is zero", p)
			}
			if x := f.lines[l].prev; x != 0 {
				return fmt.Errorf("first Line %d's prev: got %d, want 0", l, x)
			}
			if firstLines[l] {
				return fmt.Errorf("duplicate first Line %d", l)
			}
			firstLines[l] = true

			nUsedLinesThisParagraph := 0
			for ; l != 0; l = f.lines[l].next {
				b := f.lines[l].firstB
				if b == 0 {
					return fmt.Errorf("lines[%d].firstB is zero", l)
				}
				if x := f.boxes[b].prev; x != 0 {
					return fmt.Errorf("first Box %d's prev: got %d, want 0", b, x)
				}
				if firstBoxes[b] {
					return fmt.Errorf("duplicate first Box %d", b)
				}
				firstBoxes[b] = true

				for ; b != 0; b = f.boxes[b].next {
					nUsedBoxes++
					if nUsedBoxes >= infinity {
						return fmt.Errorf("too many used Boxes (infinite loop?)")
					}
				}
				nUsedLines++
				nUsedLinesThisParagraph++
			}
			nUsedParagraphs++

			if n := int(f.paragraphs[p].cachedLineCountPlus1 - 1); n >= 0 && n != nUsedLinesThisParagraph {
				return fmt.Errorf("Paragraph cached Line count: got %d, want %d", n, nUsedLinesThisParagraph)
			}
		}
		if n := int(f.cachedLineCountPlus1 - 1); n >= 0 && n != nUsedLines {
			return fmt.Errorf("Frame cached Line count: got %d, want %d", n, nUsedLines)
		}
		if n := int(f.cachedParagraphCountPlus1 - 1); n >= 0 && n != nUsedParagraphs {
			return fmt.Errorf("Frame cached Paragraph count: got %d, want %d", n, nUsedParagraphs)
		}
	}

	// Check that only the last Box can be empty.
	if ignoredInvariants&ignoreInvariantEmptyBoxes == 0 {
		for p := f.firstP; p != 0; {
			nextP := f.paragraphs[p].next
			for l := f.paragraphs[p].firstL; l != 0; {
				nextL := f.lines[l].next
				for b := f.lines[l].firstB; b != 0; {
					nextB := f.boxes[b].next

					emptyBox := f.boxes[b].i == f.boxes[b].j
					lastBox := nextP == 0 && nextL == 0 && nextB == 0
					if emptyBox && !lastBox {
						return fmt.Errorf("boxes[%d] is empty, but isn't the last Box", b)
					}

					b = nextB
				}
				l = nextL
			}
			p = nextP
		}
	}

	// Check the paragraphs.
	for p, pp := range f.paragraphs {
		if p == 0 {
			if pp != (Paragraph{}) {
				return fmt.Errorf("paragraphs[0] is a non-zero Paragraph: %v", pp)
			}
			continue
		}
		if pp.next < 0 || len(f.paragraphs) <= int(pp.next) {
			return fmt.Errorf("invalid paragraphs[%d].next: got %d, want in [0, %d)", p, pp.next, len(f.paragraphs))
		}
		if len(f.paragraphs) <= int(pp.prev) {
			return fmt.Errorf("invalid paragraphs[%d].prev: got %d, want in [0, %d)", p, pp.prev, len(f.paragraphs))
		}
		if pp.prev < 0 {
			// The Paragraph is in the free-list, which is checked separately below.
			continue
		}
		if pp.next != 0 && f.paragraphs[pp.next].prev != int32(p) {
			return fmt.Errorf("invalid links: paragraphs[%d].next=%d, paragraphs[%d].prev=%d",
				p, pp.next, pp.next, f.paragraphs[pp.next].prev)
		}
		if pp.prev != 0 && f.paragraphs[pp.prev].next != int32(p) {
			return fmt.Errorf("invalid links: paragraphs[%d].prev=%d, paragraphs[%d].next=%d",
				p, pp.prev, pp.prev, f.paragraphs[pp.prev].next)
		}
	}

	// Check the paragraphs' free-list.
	nFreeParagraphs := 0
	for p := f.freeP; p != 0; nFreeParagraphs++ {
		if nFreeParagraphs >= infinity {
			return fmt.Errorf("Paragraph free-list is too long (infinite loop?)")
		}
		if p < 0 || len(f.paragraphs) <= int(p) {
			return fmt.Errorf("invalid Paragraph free-list index: got %d, want in [0, %d)", p, len(f.paragraphs))
		}
		pp := &f.paragraphs[p]
		if pp.prev >= 0 {
			return fmt.Errorf("paragraphs[%d] is an invalid free-list element: %#v", p, *pp)
		}
		p = pp.next
	}

	// Check the lines.
	for l, ll := range f.lines {
		if l == 0 {
			if ll != (Line{}) {
				return fmt.Errorf("lines[0] is a non-zero Line: %v", ll)
			}
			continue
		}
		if ll.next < 0 || len(f.lines) <= int(ll.next) {
			return fmt.Errorf("invalid lines[%d].next: got %d, want in [0, %d)", l, ll.next, len(f.lines))
		}
		if len(f.lines) <= int(ll.prev) {
			return fmt.Errorf("invalid lines[%d].prev: got %d, want in [0, %d)", l, ll.prev, len(f.lines))
		}
		if ll.prev < 0 {
			// The Line is in the free-list, which is checked separately below.
			continue
		}
		if ll.next != 0 && f.lines[ll.next].prev != int32(l) {
			return fmt.Errorf("invalid links: lines[%d].next=%d, lines[%d].prev=%d",
				l, ll.next, ll.next, f.lines[ll.next].prev)
		}
		if ll.prev != 0 && f.lines[ll.prev].next != int32(l) {
			return fmt.Errorf("invalid links: lines[%d].prev=%d, lines[%d].next=%d",
				l, ll.prev, ll.prev, f.lines[ll.prev].next)
		}
	}

	// Check the lines' free-list.
	nFreeLines := 0
	for l := f.freeL; l != 0; nFreeLines++ {
		if nFreeLines >= infinity {
			return fmt.Errorf("Line free-list is too long (infinite loop?)")
		}
		if l < 0 || len(f.lines) <= int(l) {
			return fmt.Errorf("invalid Line free-list index: got %d, want in [0, %d)", l, len(f.lines))
		}
		ll := &f.lines[l]
		if ll.prev >= 0 {
			return fmt.Errorf("lines[%d] is an invalid free-list element: %#v", l, *ll)
		}
		l = ll.next
	}

	// Check the boxes.
	for b, bb := range f.boxes {
		if b == 0 {
			if bb != (Box{}) {
				return fmt.Errorf("boxes[0] is a non-zero Box: %v", bb)
			}
			continue
		}
		if bb.next < 0 || len(f.boxes) <= int(bb.next) {
			return fmt.Errorf("invalid boxes[%d].next: got %d, want in [0, %d)", b, bb.next, len(f.boxes))
		}
		if len(f.boxes) <= int(bb.prev) {
			return fmt.Errorf("invalid boxes[%d].prev: got %d, want in [0, %d)", b, bb.prev, len(f.boxes))
		}
		if bb.prev < 0 {
			// The Box is in the free-list, which is checked separately below.
			continue
		}
		if bb.next != 0 && f.boxes[bb.next].prev != int32(b) {
			return fmt.Errorf("invalid links: boxes[%d].next=%d, boxes[%d].prev=%d",
				b, bb.next, bb.next, f.boxes[bb.next].prev)
		}
		if bb.prev != 0 && f.boxes[bb.prev].next != int32(b) {
			return fmt.Errorf("invalid links: boxes[%d].prev=%d, boxes[%d].next=%d",
				b, bb.prev, bb.prev, f.boxes[bb.prev].next)
		}
		if 0 > bb.i || bb.i > bb.j || bb.j > int32(len(f.text)) {
			return fmt.Errorf("invalid boxes[%d] i/j range: i=%d, j=%d, len=%d", b, bb.i, bb.j, len(f.text))
		}
	}

	// Check the boxes' free-list.
	nFreeBoxes := 0
	for b := f.freeB; b != 0; nFreeBoxes++ {
		if nFreeBoxes >= infinity {
			return fmt.Errorf("Box free-list is too long (infinite loop?)")
		}
		if b < 0 || len(f.boxes) <= int(b) {
			return fmt.Errorf("invalid Box free-list index: got %d, want in [0, %d)", b, len(f.boxes))
		}
		bb := &f.boxes[b]
		if bb.i != 0 || bb.j != 0 || bb.prev >= 0 {
			return fmt.Errorf("boxes[%d] is an invalid free-list element: %#v", b, *bb)
		}
		b = bb.next
	}

	// Check that the boxes' i:j ranges do not overlap, and their total length
	// equals f.len.
	nText, ijRanges := 0, []ijRange{}
	for _, bb := range f.boxes {
		if bb.i < bb.j {
			nText += int(bb.j - bb.i)
			ijRanges = append(ijRanges, ijRange{i: bb.i, j: bb.j})
		}
	}
	sort.Sort(byI(ijRanges))
	for x := range ijRanges {
		if x == 0 {
			continue
		}
		if ijRanges[x-1].j > ijRanges[x].i {
			return fmt.Errorf("overlapping Box i:j ranges: %v and %v", ijRanges[x-1], ijRanges[x])
		}
	}
	if nText != f.len {
		return fmt.Errorf("text length: got %d, want %d", nText, f.len)
	}

	// Check that every Paragraph, Line and Box, other than the 0th of each, is
	// either used or free.
	if len(f.paragraphs) != 1+nUsedParagraphs+nFreeParagraphs {
		return fmt.Errorf("#paragraphs (%d) != 1 + #used (%d) + #free (%d)",
			len(f.paragraphs), nUsedParagraphs, nFreeParagraphs)
	}
	if len(f.lines) != 1+nUsedLines+nFreeLines {
		return fmt.Errorf("#lines (%d) != 1 + #used (%d) + #free (%d)", len(f.lines), nUsedLines, nFreeLines)
	}
	if len(f.boxes) != 1+nUsedBoxes+nFreeBoxes {
		return fmt.Errorf("#boxes (%d) != 1 + #used (%d) + #free (%d)", len(f.boxes), nUsedBoxes, nFreeBoxes)
	}

	// Check that each Caret's pos is in the Frame's 0:len range. If the
	// Caret's cached {p,l,b,k} values are valid, also check that the Caret's k
	// is in its Box's i:j range, and its Box b is in its Line l is in its
	// Paragraph p.
	for i, c := range f.carets {
		if c.pos < 0 || f.len < int(c.pos) {
			return fmt.Errorf("caret[%d]: pos %d outside range [0, %d]", i, c.pos, f.len)
		}
		if c.seqNum != f.seqNum {
			continue
		}

		if c.b < 1 || len(f.boxes) < int(c.b) {
			return fmt.Errorf("caret[%d]: c.b %d outside range [1, %d]", i, c.b, len(f.boxes))
		}
		bb := &f.boxes[c.b]
		if c.k < bb.i || bb.j < c.k {
			return fmt.Errorf("caret[%d]: c.k %d outside range [%d, %d]", i, c.k, bb.i, bb.j)
		}

		if c.l < 1 || len(f.lines) < int(c.l) {
			return fmt.Errorf("caret[%d]: c.l %d outside range [1, %d]", i, c.l, len(f.lines))
		}
		if !f.lines[c.l].contains(f, c.b) {
			return fmt.Errorf("caret[%d]: Line %d does not contain Box %d", i, c.l, c.b)
		}

		if c.p < 1 || len(f.paragraphs) < int(c.p) {
			return fmt.Errorf("caret[%d]: c.p %d outside range [1, %d]", i, c.p, len(f.paragraphs))
		}
		if !f.paragraphs[c.p].contains(f, c.l) {
			return fmt.Errorf("caret[%d]: Paragraph %d does not contain Line %d", i, c.p, c.l)
		}
	}

	return nil
}

func (f *Frame) nUsedParagraphs() (n int) {
	for p, pp := range f.paragraphs {
		// The 0th index is a special case. A negative prev means that the
		// Paragraph is in the free-list.
		if p != 0 && pp.prev >= 0 {
			n++
		}
	}
	return n
}

func (f *Frame) nUsedLines() (n int) {
	for l, ll := range f.lines {
		// The 0th index is a special case. A negative prev means that the
		// Line is in the free-list.
		if l != 0 && ll.prev >= 0 {
			n++
		}
	}
	return n
}

func (f *Frame) nUsedBoxes() (n int) {
	for b, bb := range f.boxes {
		// The 0th index is a special case. A negative prev means that the
		// Box is in the free-list.
		if b != 0 && bb.prev >= 0 {
			n++
		}
	}
	return n
}

func (p *Paragraph) contains(f *Frame, l int32) bool {
	for x := p.firstL; x != 0; x = f.lines[x].next {
		if x == l {
			return true
		}
	}
	return false
}

func (l *Line) contains(f *Frame, b int32) bool {
	for x := l.firstB; x != 0; x = f.boxes[x].next {
		if x == b {
			return true
		}
	}
	return false
}

// dump is used for debugging.
func dump(w io.Writer, f *Frame) {
	for p := f.FirstParagraph(); p != nil; p = p.Next(f) {
		for l := p.FirstLine(f); l != nil; l = l.Next(f) {
			for b := l.FirstBox(f); b != nil; b = b.Next(f) {
				fmt.Fprintf(w, "[%s]", b.TrimmedText(f))
			}
			fmt.Fprintln(w)
		}
	}
}
