package main

import (
	"bytes"
	"encoding/binary"
	"hash/crc32"
	"testing"

	"github.com/klauspost/compress/zstd"
)

// ---- wire builders ----------------------------------------------------

func u32s(values ...uint32) []byte {
	out := make([]byte, 4*len(values))
	for i, v := range values {
		binary.LittleEndian.PutUint32(out[i*4:], v)
	}
	return out
}

func rec(opcode byte, parts ...[]byte) []byte {
	size := 0
	for _, p := range parts {
		size += len(p)
	}
	out := make([]byte, recordHeaderSize, recordHeaderSize+size)
	out[0] = opcode
	binary.LittleEndian.PutUint32(out[4:], uint32(size))
	for _, p := range parts {
		out = append(out, p...)
	}
	return out
}

func wireFragments(kind byte, flags uint16, session, seq, frame uint32,
	payload []byte, fragment int) [][]byte {
	count := (len(payload) + fragment - 1) / fragment
	if count == 0 {
		count = 1
	}
	var out [][]byte
	for i := 0; i < count; i++ {
		start := i * fragment
		end := start + fragment
		if end > len(payload) {
			end = len(payload)
		}
		piece := payload[start:end]
		f := flags
		if i+1 == count {
			f |= flagLast
		}
		data := make([]byte, envelopeSize+len(piece))
		binary.LittleEndian.PutUint32(data, envelopeMagic)
		data[4] = envelopeVersion
		data[5] = kind
		binary.LittleEndian.PutUint16(data[6:], f)
		binary.LittleEndian.PutUint32(data[8:], session)
		binary.LittleEndian.PutUint32(data[12:], seq+uint32(i))
		binary.LittleEndian.PutUint32(data[16:], frame)
		binary.LittleEndian.PutUint16(data[20:], uint16(i))
		binary.LittleEndian.PutUint16(data[22:], uint16(count))
		binary.LittleEndian.PutUint32(data[24:], uint32(len(piece)))
		binary.LittleEndian.PutUint32(data[28:], crc32.ChecksumIEEE(piece))
		copy(data[envelopeSize:], piece)
		out = append(out, data)
	}
	return out
}

// resourceMessage builds one plain RESOURCE message as wire fragments.
func resourceMessage(session, seq uint32, records ...[]byte) [][]byte {
	return wireFragments(kindResource, 0, session, seq, 0,
		bytes.Join(records, nil), wireFragment)
}

func compressedResourceMessage(t *testing.T, session, seq uint32,
	records ...[]byte) [][]byte {
	t.Helper()
	plain := bytes.Join(records, nil)
	enc, err := zstd.NewWriter(nil)
	if err != nil {
		t.Fatal(err)
	}
	payload := make([]byte, 8)
	binary.LittleEndian.PutUint32(payload, uint32(len(plain)))
	payload[4] = codecZstd
	payload = enc.EncodeAll(plain, payload)
	return wireFragments(kindResource, flagCompressed, session, seq, 0, payload,
		wireFragment)
}

func keyframeMessage(session, seq, frame uint32, payload []byte) [][]byte {
	return wireFragments(kindFrame, flagKeyframe, session, seq, frame, payload,
		wireFragment)
}

func createTexture(id, gen, w, h, levels, format uint32) []byte {
	return rec(opCreateTexture, u32s(id, gen, w, h, levels, 0, format, 0))
}

func updateTexture(id, gen, level, x, y, w, h, format uint32, pixels []byte) []byte {
	layout, _ := textureLayout(format, w, h)
	return rec(opUpdateTexture, u32s(id, gen, level, x, y, w, h, layout.pitch,
		format, uint32(len(pixels))), pixels)
}

func defineBlob(id uint32, content []byte) []byte {
	header := make([]byte, defineBlobHeader)
	binary.LittleEndian.PutUint32(header, id)
	binary.LittleEndian.PutUint32(header[4:], uint32(len(content)))
	return rec(opDefineBlob, header, content)
}

func updateTextureImageBlob(id, gen, level, w, h, format, blobID uint32,
	quality, method byte) []byte {
	layout, _ := textureLayout(format, w, h)
	payload := append([]byte(nil), u32s(id, gen, level, 0, 0, w, h,
		layout.pitch, format, layout.pitch*layout.rows)...)
	payload = append(payload, u32s(blobID)...)
	payload = append(payload, 1, 1, quality, method)
	return rec(opUpdateTextureImageBlob, payload)
}

func updateTextureImage(id, gen, level, w, h, format uint32,
	encoded []byte) []byte {
	layout, _ := textureLayout(format, w, h)
	payload := append([]byte(nil), u32s(id, gen, level, 0, 0, w, h,
		layout.pitch, format, layout.pitch*layout.rows)...)
	payload = append(payload, 1, 0, 0, 0)
	payload = append(payload, u32s(uint32(len(encoded)))...)
	payload = append(payload, encoded...)
	return rec(opUpdateTextureImage, payload)
}

func messageOpcodes(t *testing.T, records []byte) []byte {
	t.Helper()
	var opcodes []byte
	for offset := 0; offset < len(records); {
		if len(records)-offset < recordHeaderSize {
			t.Fatal("truncated record header")
		}
		opcode := records[offset]
		size := int(binary.LittleEndian.Uint32(records[offset+4:]))
		offset += recordHeaderSize
		if size > len(records)-offset {
			t.Fatal("truncated record payload")
		}
		opcodes = append(opcodes, opcode)
		offset += size
	}
	return opcodes
}

func fill(n int, value byte) []byte {
	out := make([]byte, n)
	for i := range out {
		out[i] = value
	}
	return out
}

func feedAll(m *seatMirror, fragments ...[][]byte) {
	for _, message := range fragments {
		for _, fragment := range message {
			m.feedResource(fragment)
		}
	}
}

func freshMirror() *seatMirror {
	m := newSeatMirror(3, nil)
	m.markEpoch()
	return m
}

// ---- tests ------------------------------------------------------------

func TestMirrorAppliesTextureUpdatesLikeTheBrowser(t *testing.T) {
	m := freshMirror()
	// 8x8 A8R8G8B8, two levels (8x8 and 4x4).
	feedAll(m, resourceMessage(7, 1000,
		createTexture(5, 1, 8, 8, 2, 21),
		updateTexture(5, 2, 0, 0, 0, 8, 8, 21, fill(8*8*4, 0x11)),
		updateTexture(5, 3, 1, 0, 0, 4, 4, 21, fill(4*4*4, 0x22))))
	texture := m.textures[5]
	if texture == nil || len(texture.mips) != 2 {
		t.Fatalf("texture not mirrored: %+v", texture)
	}
	if !bytes.Equal(texture.mips[0].bytes, fill(256, 0x11)) ||
		!bytes.Equal(texture.mips[1].bytes, fill(64, 0x22)) {
		t.Fatal("full level updates not applied")
	}
	// Sub-rect update: a 2x2 patch at (4, 2) on level 0.
	feedAll(m, resourceMessage(7, 1001,
		updateTexture(5, 4, 0, 4, 2, 2, 2, 21, fill(2*2*4, 0x33))))
	level0 := texture.mips[0].bytes
	for y := 0; y < 8; y++ {
		for x := 0; x < 8; x++ {
			want := byte(0x11)
			if y >= 2 && y < 4 && x >= 4 && x < 6 {
				want = 0x33
			}
			if level0[(y*8+x)*4] != want {
				t.Fatalf("pixel (%d,%d) = %#x, want %#x", x, y, level0[(y*8+x)*4], want)
			}
		}
	}
	if texture.generation != 4 {
		t.Fatalf("generation %d, want 4", texture.generation)
	}
	// XOR update on level 1: flip bytes 0 and 5 (words 0 and 1).
	base := append([]byte(nil), texture.mips[1].bytes...)
	result := append([]byte(nil), base...)
	result[0] ^= 0xff
	result[5] ^= 0x0f
	mask := make([]byte, (16+1)/2) // 16 words -> 8 mask bytes
	mask[0] |= 1 << 0              // word 0, byte 0 (low nibble)
	mask[0] |= 1 << (4 + 1)        // word 1, byte 1 (high nibble)
	xor := rec(opUpdateTextureXor, u32s(5, 9, 1, 0, 0, 4, 4, 16, 21, 64),
		u32s(crc32.ChecksumIEEE(base), crc32.ChecksumIEEE(result)), mask,
		[]byte{0xff, 0x0f})
	feedAll(m, resourceMessage(7, 1002, xor))
	if !bytes.Equal(texture.mips[1].bytes, result) {
		t.Fatal("xor update not applied")
	}
	// A stale base CRC is skipped, like the browser skips it.
	feedAll(m, resourceMessage(7, 1003, xor))
	if !bytes.Equal(texture.mips[1].bytes, result) || m.xorMismatches != 1 {
		t.Fatalf("stale xor must be skipped (mismatches=%d)", m.xorMismatches)
	}
	if m.broken != "" {
		t.Fatalf("mirror broke: %s", m.broken)
	}
	// Destroy frees the texture.
	feedAll(m, resourceMessage(7, 1004, rec(opDestroyResource, u32s(5))))
	if m.textures[5] != nil || m.bytes != 0 {
		t.Fatalf("destroy left state behind: bytes=%d", m.bytes)
	}
}

func TestMirrorTextureBlobAliasesAndCopiesOnWrite(t *testing.T) {
	m := freshMirror()
	// DXT1 4x4: one 8-byte block.
	content := fill(8, 0xaa)
	feedAll(m,
		resourceMessage(7, 10, createTexture(6, 1, 4, 4, 1, formatDXT1)),
		resourceMessage(7, 11, defineBlob(77, content)),
		resourceMessage(7, 12, rec(opUpdateTextureBlob,
			u32s(6, 2, 0, 0, 0, 4, 4, 8, formatDXT1, 8, 77))))
	mip := m.textures[6].mips[0]
	if !mip.shared || mip.blobID != 77 || !bytes.Equal(mip.bytes, content) {
		t.Fatalf("full-level blob update must alias the blob: %+v", mip)
	}
	blobBytes := m.blobs[77].payload[defineBlobHeader:]
	// An XOR on the level must not touch the blob's bytes.
	result := append([]byte(nil), content...)
	result[3] ^= 0x01
	mask := make([]byte, 1)
	mask[0] |= 1 << 3
	feedAll(m, resourceMessage(7, 13, rec(opUpdateTextureXor,
		u32s(6, 3, 0, 0, 0, 4, 4, 8, formatDXT1, 8),
		u32s(crc32.ChecksumIEEE(content), crc32.ChecksumIEEE(result)), mask,
		[]byte{0x01})))
	if !bytes.Equal(mip.bytes, result) || mip.shared || mip.blobID != 0 {
		t.Fatalf("xor must unshare the level: %+v", mip)
	}
	if !bytes.Equal(blobBytes, content) {
		t.Fatal("the retained blob was modified through the level")
	}
}

func TestMirrorWebPImageBlobSnapshotRoundTrips(t *testing.T) {
	m := freshMirror()
	encoded := fill(113, 0x7a)
	feedAll(m,
		resourceMessage(7, 1000, createTexture(8, 1, 4, 4, 1, 21)),
		resourceMessage(7, 1001, defineBlob(77, encoded)),
		resourceMessage(7, 1002,
			updateTextureImageBlob(8, 9, 0, 4, 4, 21, 77, 95, 4)))
	mip := m.textures[8].mips[0]
	if m.unknownRecords != 0 || m.skipped != 0 || mip.imagePayload == nil ||
		mip.imageBlobID != 77 || mip.bytes != nil || m.textures[8].generation != 9 {
		t.Fatalf("image bind was not mirrored: mip=%+v unknown=%d skipped=%d",
			mip, m.unknownRecords, m.skipped)
	}
	if !bytes.Equal(mip.imagePayload[updateTextureImageHeader:], encoded) ||
		mip.imageMeta != [4]byte{1, 1, 95, 4} {
		t.Fatal("canonical image state did not preserve WebP bytes and metadata")
	}
	wantBytes := int64(len(m.blobs[77].payload) + len(mip.imagePayload))
	if m.bytes != wantBytes {
		t.Fatalf("mirror bytes=%d, want %d", m.bytes, wantBytes)
	}

	plain := m.collectLocked()
	defineMessage, bindMessage := -1, -1
	for index, message := range plain {
		for _, opcode := range messageOpcodes(t, message) {
			if opcode == opDefineBlob {
				defineMessage = index
			}
			if opcode == opUpdateTextureImageBlob {
				bindMessage = index
			}
		}
	}
	if defineMessage < 0 || bindMessage <= defineMessage {
		t.Fatalf("image definition/bind message order = %d/%d", defineMessage,
			bindMessage)
	}

	for _, fragment := range keyframeMessage(7, 1, 100, fill(40, 0)) {
		m.feedRecovery(fragment)
	}
	if err := m.ensureSnapshot(); err != nil {
		t.Fatal(err)
	}
	bundle, err := m.takeBundle()
	if err != nil {
		t.Fatal(err)
	}
	clone := decodeSnapshot(t, bundle.resource, bundle.s0)
	cloned := clone.textures[8].mips[0]
	if clone.unknownRecords != 0 || clone.skipped != 0 ||
		!bytes.Equal(cloned.imagePayload, mip.imagePayload) ||
		cloned.imageBlobID != 77 || clone.textures[8].generation != 9 {
		t.Fatalf("image snapshot differs: clone=%+v unknown=%d skipped=%d",
			cloned, clone.unknownRecords, clone.skipped)
	}
}

func TestMirrorWebPImageSurvivesBlobGCAndReset(t *testing.T) {
	m := freshMirror()
	encoded := fill(61, 0x55)
	feedAll(m,
		resourceMessage(7, 2000, createTexture(9, 1, 4, 4, 1, 21)),
		resourceMessage(7, 2001, defineBlob(88, encoded)),
		resourceMessage(7, 2002,
			updateTextureImageBlob(9, 3, 0, 4, 4, 21, 88, 80, 0)))
	for generation, frame := range []uint32{100, 200} {
		for _, fragment := range keyframeMessage(7, uint32(generation+1), frame,
			fill(40, 0)) {
			m.feedRecovery(fragment)
		}
	}
	if m.blobs[88] != nil || m.textures[9].mips[0].imagePayload == nil {
		t.Fatal("blob GC must retain the current inline image, not its old blob")
	}
	plain := m.collectLocked()
	foundInline, foundBind := false, false
	for _, message := range plain {
		for _, opcode := range messageOpcodes(t, message) {
			foundInline = foundInline || opcode == opUpdateTextureImage
			foundBind = foundBind || opcode == opUpdateTextureImageBlob
		}
	}
	if !foundInline || foundBind {
		t.Fatalf("post-GC snapshot inline=%t bind=%t", foundInline, foundBind)
	}
	m.dropCache()
	if err := m.ensureSnapshot(); err != nil {
		t.Fatal(err)
	}
	bundle, _ := m.takeBundle()
	clone := decodeSnapshot(t, bundle.resource, bundle.s0)
	if !bytes.Equal(clone.textures[9].mips[0].imagePayload,
		m.textures[9].mips[0].imagePayload) {
		t.Fatal("inline fallback did not survive a synthesized snapshot")
	}

	// RESET_BLOB_CACHE also clears live lookup but must not erase pixels that
	// the browser already decoded into the texture.
	feedAll(m, resourceMessage(7, 2003, rec(opResetBlobCache)))
	if len(m.blobs) != 0 || m.textures[9].mips[0].imagePayload == nil {
		t.Fatal("blob reset did not preserve the decoded image state")
	}
	missing := updateTextureImageBlob(9, 4, 0, 4, 4, 21, 88, 80, 0)
	if err := m.applyRecordInner(opUpdateTextureImageBlob,
		missing[recordHeaderSize:]); err == nil {
		t.Fatal("a post-reset image bind must require a fresh blob definition")
	}
}

func TestMirrorWebPImageNeedsExactRawRebaseBeforeMutation(t *testing.T) {
	setup := func() *seatMirror {
		m := freshMirror()
		feedAll(m,
			resourceMessage(7, 3000, createTexture(10, 1, 4, 4, 1, 21)),
			resourceMessage(7, 3001, defineBlob(99, fill(37, 0x99))),
			resourceMessage(7, 3002,
				updateTextureImageBlob(10, 2, 0, 4, 4, 21, 99, 40, 4)))
		return m
	}

	m := setup()
	exact := fill(4*4*4, 0x22)
	feedAll(m, resourceMessage(7, 3003,
		updateTexture(10, 3, 0, 0, 0, 4, 4, 21, exact)))
	mip := m.textures[10].mips[0]
	if mip.imagePayload != nil || !bytes.Equal(mip.bytes, exact) {
		t.Fatal("full exact update did not replace the image state")
	}
	patch := fill(2*2*4, 0x33)
	feedAll(m, resourceMessage(7, 3004,
		updateTexture(10, 4, 0, 1, 1, 2, 2, 21, patch)))
	if m.broken != "" || mip.bytes[(1*4+1)*4] != 0x33 {
		t.Fatalf("partial update after exact rebase failed: %s", m.broken)
	}

	m = setup()
	partial := updateTexture(10, 3, 0, 1, 1, 2, 2, 21, patch)
	if err := m.applyRecordInner(opUpdateTexture,
		partial[recordHeaderSize:]); err == nil {
		t.Fatal("partial raw update directly on an image base must fail closed")
	}
	m = setup()
	xor := rec(opUpdateTextureXor,
		u32s(10, 3, 0, 0, 0, 4, 4, 16, 21, 64), u32s(0, 0))
	if err := m.applyRecordInner(opUpdateTextureXor,
		xor[recordHeaderSize:]); err == nil {
		t.Fatal("XOR directly on an image base must fail closed")
	}
}

func TestMirrorInlineWebPImageRoundTrips(t *testing.T) {
	m := freshMirror()
	encoded := fill(29, 0x44)
	feedAll(m, resourceMessage(7, 3500,
		createTexture(11, 1, 4, 4, 1, 23),
		updateTextureImage(11, 6, 0, 4, 4, 23, encoded)))
	for _, fragment := range keyframeMessage(7, 1, 100, fill(40, 0)) {
		m.feedRecovery(fragment)
	}
	if err := m.ensureSnapshot(); err != nil {
		t.Fatal(err)
	}
	bundle, _ := m.takeBundle()
	clone := decodeSnapshot(t, bundle.resource, bundle.s0)
	mip := clone.textures[11].mips[0]
	if mip.imageBlobID != 0 ||
		!bytes.Equal(mip.imagePayload[updateTextureImageHeader:], encoded) ||
		clone.unknownRecords != 0 {
		t.Fatalf("inline image snapshot differs: %+v", mip)
	}
}

func TestMirrorWebPSnapshotDeltaResetKeepsDecodedTexture(t *testing.T) {
	m := freshMirror()
	feedAll(m,
		resourceMessage(7, 4000, createTexture(12, 1, 4, 4, 1, 21)),
		resourceMessage(7, 4001, defineBlob(101, fill(43, 0x61))),
		resourceMessage(7, 4002,
			updateTextureImageBlob(12, 2, 0, 4, 4, 21, 101, 95, 4)))
	for _, fragment := range keyframeMessage(7, 1, 100, fill(40, 0)) {
		m.feedRecovery(fragment)
	}
	if err := m.ensureSnapshot(); err != nil {
		t.Fatal(err)
	}
	snapshotFragments := len(m.cache.messages)
	if err := m.applyRecordInner(opResetBlobCache, []byte{1}); err == nil ||
		m.blobs[101] == nil {
		t.Fatal("a malformed reset must fail without clearing the blob cache")
	}
	feedAll(m, resourceMessage(7, 4003, rec(opResetBlobCache)))
	bundle, err := m.takeBundle()
	if err != nil {
		t.Fatal(err)
	}
	if bundle.delta != 1 {
		t.Fatalf("reset delta fragments=%d, want 1", bundle.delta)
	}
	clone := decodeSnapshot(t, bundle.resource[:snapshotFragments], bundle.s0)
	for _, fragment := range bundle.resource[snapshotFragments:] {
		clone.feedResource(fragment)
	}
	if clone.broken != "" || len(clone.blobs) != 0 ||
		clone.textures[12].mips[0].imagePayload == nil {
		t.Fatalf("reset delta lost decoded texture: broken=%q blobs=%d mip=%+v",
			clone.broken, len(clone.blobs), clone.textures[12].mips[0])
	}
}

func TestMirrorWebPSnapshotDeltaAdvancesImage(t *testing.T) {
	m := freshMirror()
	imageA := fill(41, 0x41)
	imageB := fill(53, 0x42)
	feedAll(m,
		resourceMessage(7, 4250, createTexture(14, 1, 4, 4, 1, 21)),
		resourceMessage(7, 4251, defineBlob(111, imageA)),
		resourceMessage(7, 4252,
			updateTextureImageBlob(14, 2, 0, 4, 4, 21, 111, 95, 4)))
	for _, fragment := range keyframeMessage(7, 1, 100, fill(40, 0)) {
		m.feedRecovery(fragment)
	}
	if err := m.ensureSnapshot(); err != nil {
		t.Fatal(err)
	}
	snapshotFragments := len(m.cache.messages)
	feedAll(m,
		resourceMessage(7, 4253, defineBlob(112, imageB)),
		resourceMessage(7, 4254,
			updateTextureImageBlob(14, 3, 0, 4, 4, 21, 112, 80, 4)))
	bundle, err := m.takeBundle()
	if err != nil {
		t.Fatal(err)
	}
	if bundle.delta != 2 {
		t.Fatalf("image delta fragments=%d, want 2", bundle.delta)
	}
	clone := decodeSnapshot(t, bundle.resource[:snapshotFragments], bundle.s0)
	for _, fragment := range bundle.resource[snapshotFragments:] {
		clone.feedResource(fragment)
	}
	mip := clone.textures[14].mips[0]
	if clone.broken != "" || clone.textures[14].generation != 3 ||
		mip.imageBlobID != 112 ||
		!bytes.Equal(mip.imagePayload[updateTextureImageHeader:], imageB) {
		t.Fatalf("image delta did not advance state: broken=%q texture=%+v mip=%+v",
			clone.broken, clone.textures[14], mip)
	}
}

func TestMirrorWebPSnapshotUsesLatestTextureGeneration(t *testing.T) {
	m := freshMirror()
	feedAll(m,
		resourceMessage(7, 4500, createTexture(13, 1, 4, 4, 2, 21)),
		resourceMessage(7, 4501, defineBlob(102, fill(47, 0x62))),
		resourceMessage(7, 4502,
			updateTextureImageBlob(13, 2, 0, 4, 4, 21, 102, 80, 4)),
		resourceMessage(7, 4503,
			updateTexture(13, 7, 1, 0, 0, 2, 2, 21, fill(2*2*4, 0x77))))
	if m.textures[13].generation != 7 {
		t.Fatalf("texture generation=%d, want 7", m.textures[13].generation)
	}
	found := false
	for _, message := range m.collectLocked() {
		for offset := 0; offset < len(message); {
			opcode := message[offset]
			size := int(binary.LittleEndian.Uint32(message[offset+4:]))
			payload := message[offset+recordHeaderSize : offset+recordHeaderSize+size]
			if opcode == opUpdateTextureImageBlob || opcode == opUpdateTextureImage {
				found = true
				if got := binary.LittleEndian.Uint32(payload[4:]); got != 7 {
					t.Fatalf("image generation=%d, want latest 7", got)
				}
			}
			offset += recordHeaderSize + size
		}
	}
	if !found {
		t.Fatal("snapshot omitted the image-backed mip")
	}
}

func TestMirrorKeepsTwoBlobGenerations(t *testing.T) {
	// The browser's rule: a keyframe reclaims the epoch bases defined
	// before the PREVIOUS keyframe, so re-sends that precede a keyframe
	// survive it and retention is bounded at one extra epoch.
	m := freshMirror()
	feedAll(m, resourceMessage(7, 1, defineBlob(1, fill(16, 1))))
	for _, f := range keyframeMessage(7, 1, 100, fill(40, 0)) {
		m.feedRecovery(f)
	}
	if m.blobs[1] == nil {
		t.Fatal("the bootstrap's bases survive the first keyframe")
	}
	feedAll(m, resourceMessage(7, 2, defineBlob(2, fill(16, 2))))
	for _, f := range keyframeMessage(7, 2, 200, fill(40, 0)) {
		m.feedRecovery(f)
	}
	if m.blobs[1] != nil {
		t.Fatal("a base defined before the previous keyframe is reclaimed")
	}
	if m.blobs[2] == nil {
		t.Fatal("a base defined after the previous keyframe survives")
	}
	feedAll(m, resourceMessage(7, 3, defineBlob(3, fill(16, 3))))
	for _, f := range keyframeMessage(7, 3, 300, fill(40, 0)) {
		m.feedRecovery(f)
	}
	if m.blobs[2] != nil || m.blobs[3] == nil {
		t.Fatal("one generation rolls off per keyframe")
	}
	if m.keyframe == nil || m.keyframe.frame != 300 || m.keyframeGen != 3 {
		t.Fatalf("keyframe tracking: %+v gen=%d", m.keyframe, m.keyframeGen)
	}
	dict := m.keyframe.dict[0]
	if binary.LittleEndian.Uint16(dict[6:])&flagDictionaryOnly == 0 {
		t.Fatal("the dictionary copy must carry W3CS_DICTIONARY_ONLY")
	}
	if binary.LittleEndian.Uint16(m.keyframe.fragments[0][6:])&flagDictionaryOnly != 0 {
		t.Fatal("the verbatim keyframe must be untouched")
	}
}

func TestMirrorReadyNeedsAnEpochAndAKeyframe(t *testing.T) {
	m := newSeatMirror(1, nil)
	for _, f := range keyframeMessage(7, 1, 100, fill(40, 0)) {
		m.feedRecovery(f)
	}
	if m.ready() {
		t.Fatal("a keyframe before any epoch marker is not a usable state")
	}
	m.markEpoch()
	if m.ready() || !m.epochOpen() {
		t.Fatal("after the marker the epoch is open but not ready")
	}
	for _, f := range keyframeMessage(7, 2, 101, fill(40, 0)) {
		m.feedRecovery(f)
	}
	if !m.ready() || m.epochOpen() {
		t.Fatal("the first keyframe after the marker makes the mirror ready")
	}
	m.markReset()
	if m.ready() {
		t.Fatal("a reset forgets everything until the next marker")
	}
	if !m.epochOpen() {
		t.Fatal("after a reset the host's own epoch is expected: no join request")
	}
}

func TestMirrorDecodesCompressedBundlesAndReferences(t *testing.T) {
	fetched := map[string][]byte{}
	m := newSeatMirror(2, func(hexDigest string) ([]byte, error) {
		return fetched[hexDigest], nil
	})
	m.markEpoch()
	// Compressed RESOURCE message.
	feedAll(m, compressedResourceMessage(t, 7, 1,
		createTexture(9, 1, 4, 4, 1, 22)))
	if m.textures[9] == nil {
		t.Fatal("compressed message not decoded")
	}
	// A bundle: 32-byte digest + records.
	blobRecord := defineBlob(5, fill(64, 5))
	digest := fill(32, 0xab)
	bundle := wireFragments(kindBundle, 0, 7, 2, 0,
		append(append([]byte(nil), digest...), blobRecord...), wireFragment)
	feedAll(m, bundle)
	if m.blobs[5] == nil {
		t.Fatal("bundle records not applied")
	}
	// A reference: digest + size, resolved through the fetcher.
	refRecord := defineBlob(6, fill(48, 6))
	fetched["abababababababababababababababababababababababababababababababab"] = refRecord
	reference := wireFragments(kindReference, 0, 7, 3, 0,
		append(append([]byte(nil), digest...), u32s(uint32(len(refRecord)))...),
		wireFragment)
	feedAll(m, reference)
	if m.blobs[6] == nil || m.references != 1 {
		t.Fatalf("reference not resolved (refs=%d)", m.references)
	}
	if m.broken != "" {
		t.Fatalf("mirror broke: %s", m.broken)
	}
}

// decodeSnapshot feeds stamped snapshot fragments into a fresh mirror and
// returns it, checking the stamping invariants on the way.
func decodeSnapshot(t *testing.T, fragments [][]byte, s0 uint32) *seatMirror {
	t.Helper()
	clone := freshMirror()
	var previous uint32
	for i, fragment := range fragments {
		e, _, err := parseEnvelope(fragment)
		if err != nil {
			t.Fatalf("fragment %d: %v", i, err)
		}
		if e.kind != kindResource {
			t.Fatalf("fragment %d kind %d", i, e.kind)
		}
		if i > 0 && e.sequence != previous+1 {
			t.Fatalf("fragment %d sequence %d after %d", i, e.sequence, previous)
		}
		previous = e.sequence
		clone.feedResource(fragment)
	}
	if previous != s0 {
		t.Fatalf("last stamped sequence %d, want s0=%d", previous, s0)
	}
	if clone.broken != "" {
		t.Fatalf("snapshot did not apply: %s", clone.broken)
	}
	return clone
}

func TestMirrorSnapshotRoundTrips(t *testing.T) {
	m := freshMirror()
	big := make([]byte, 256*256*4) // 256 KiB A8R8G8B8 level, spans fragments
	for i := range big {
		big[i] = byte(i * 7)
	}
	feedAll(m,
		resourceMessage(7, 5000, rec(opCreateBuffer, append(u32s(1, 1, 4096, 0, 0x142, 0), 1))),
		resourceMessage(7, 5001, createTexture(2, 1, 256, 256, 1, 21)),
		resourceMessage(7, 5002, updateTexture(2, 2, 0, 0, 0, 256, 256, 21, big)),
		resourceMessage(7, 5003, createTexture(3, 1, 4, 4, 2, formatDXT1)),
		resourceMessage(7, 5004, defineBlob(40, fill(8, 0x40))),
		resourceMessage(7, 5005, rec(opUpdateTextureBlob,
			u32s(3, 2, 0, 0, 0, 4, 4, 8, formatDXT1, 8, 40))),
		resourceMessage(7, 5006, defineBlob(41, fill(300, 0x41))),
		resourceMessage(7, 5007, rec(opSetCursor, u32s(1, 1, 2, 2, 8, 21, 32), fill(32, 9)),
			rec(opShowCursor, u32s(1))))
	for _, f := range keyframeMessage(7, 1, 100, fill(40, 0)) {
		m.feedRecovery(f)
	}
	if err := m.ensureSnapshot(); err != nil {
		t.Fatal(err)
	}
	bundle, err := m.takeBundle()
	if err != nil {
		t.Fatal(err)
	}
	if bundle.s0 != 5007 || bundle.keyframe != 100 || bundle.delta != 0 {
		t.Fatalf("bundle %+v", bundle)
	}
	clone := decodeSnapshot(t, bundle.resource, bundle.s0)
	if !bytes.Equal(clone.buffers[1], m.buffers[1]) {
		t.Fatal("buffer create differs")
	}
	for id, texture := range m.textures {
		other := clone.textures[id]
		if other == nil || other.generation != texture.generation ||
			len(other.mips) != len(texture.mips) {
			t.Fatalf("texture %d differs: %+v vs %+v", id, other, texture)
		}
		for level := range texture.mips {
			if !bytes.Equal(other.mips[level].bytes, texture.mips[level].bytes) {
				t.Fatalf("texture %d level %d bytes differ", id, level)
			}
		}
	}
	for id, blob := range m.blobs {
		if other := clone.blobs[id]; other == nil ||
			!bytes.Equal(other.payload, blob.payload) {
			t.Fatalf("blob %d differs", id)
		}
	}
	if !bytes.Equal(clone.cursor, m.cursor) || !bytes.Equal(clone.cursorShow, m.cursorShow) {
		t.Fatal("cursor differs")
	}
	// The aliased DXT1 level travels as a blob reference, the big level raw.
	if !clone.textures[3].mips[0].shared || clone.textures[3].mips[0].blobID != 40 {
		t.Fatal("a blob-backed level must be re-sent as a blob update")
	}
	if len(bundle.recovery) != 1 ||
		binary.LittleEndian.Uint16(bundle.recovery[0][6:])&flagDictionaryOnly == 0 {
		t.Fatal("the bundle carries the dictionary keyframe")
	}
	// A second take reuses the cache; a new reliable message lands in the
	// delta and is part of the next bundle.
	feedAll(m, resourceMessage(7, 5008, defineBlob(42, fill(8, 0x42))))
	again, err := m.takeBundle()
	if err != nil {
		t.Fatal(err)
	}
	if again.delta != 1 || len(again.resource) != len(bundle.resource)+1 {
		t.Fatalf("delta not recorded: %+v", again)
	}
	last := again.resource[len(again.resource)-1]
	if e, _, _ := parseEnvelope(last); e.sequence != 5008 {
		t.Fatalf("delta fragment sequence %d", e.sequence)
	}
}

func TestMirrorSnapshotNeedsRoomBelowS0(t *testing.T) {
	m := freshMirror()
	feedAll(m, resourceMessage(7, 1, createTexture(2, 1, 64, 64, 1, 21),
		updateTexture(2, 2, 0, 0, 0, 64, 64, 21, fill(64*64*4, 1))))
	for _, f := range keyframeMessage(7, 1, 100, fill(40, 0)) {
		m.feedRecovery(f)
	}
	if err := m.ensureSnapshot(); err == nil {
		t.Fatal("a snapshot cannot be stamped below sequence 1")
	}
	if m.cache != nil {
		t.Fatal("a failed build leaves no cache")
	}
}

func TestMirrorDeltaSeedsAMessageStillArriving(t *testing.T) {
	m := freshMirror()
	feedAll(m, resourceMessage(7, 3000, createTexture(2, 1, 4, 4, 1, 21)))
	for _, f := range keyframeMessage(7, 1, 100, fill(40, 0)) {
		m.feedRecovery(f)
	}
	// A two-fragment message: only its first fragment has arrived.
	parts := wireFragments(kindResource, 0, 7, 3001, 0,
		updateTexture(2, 2, 0, 0, 0, 4, 4, 21, fill(64, 3)), 40)
	if len(parts) < 2 {
		t.Fatalf("test needs a multi-fragment message, got %d", len(parts))
	}
	m.feedResource(parts[0])
	if err := m.ensureSnapshot(); err != nil {
		t.Fatal(err)
	}
	bundle, _ := m.takeBundle()
	if bundle.s0 != 3000 || bundle.delta != 1 {
		t.Fatalf("the consistency point is the last complete message: %+v", bundle)
	}
	for _, part := range parts[1:] {
		m.feedResource(part)
	}
	bundle, _ = m.takeBundle()
	if bundle.delta != len(parts) {
		t.Fatalf("delta %d, want the whole message (%d fragments)", bundle.delta, len(parts))
	}
	clone := decodeSnapshot(t, bundle.resource[:len(bundle.resource)-len(parts)], 3000)
	for _, part := range bundle.resource[len(bundle.resource)-len(parts):] {
		clone.feedResource(part)
	}
	if !bytes.Equal(clone.textures[2].mips[0].bytes, fill(64, 3)) {
		t.Fatal("the delta-delivered update must apply on the clone")
	}
}

func TestMirrorOversizeDeclaresItselfUnavailable(t *testing.T) {
	old := mirrorMaxBytes
	mirrorMaxBytes = 1024
	defer func() { mirrorMaxBytes = old }()
	m := freshMirror()
	feedAll(m, resourceMessage(7, 1, createTexture(2, 1, 64, 64, 1, 21)))
	for _, f := range keyframeMessage(7, 1, 100, fill(40, 0)) {
		m.feedRecovery(f)
	}
	if m.ready() || m.broken == "" {
		t.Fatalf("an oversize mirror must not serve joins (broken=%q)", m.broken)
	}
	m.markEpoch()
	if m.broken != "" {
		t.Fatal("the next epoch marker clears the condition")
	}
}
