// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

package prefs

import (
	"bytes"
	"encoding/json"
	"errors"
	"net/netip"
	"reflect"
	"testing"

	jsonv2 "github.com/go-json-experiment/json"
	"github.com/go-json-experiment/json/jsontext"
	"github.com/google/go-cmp/cmp"
	"tailscale.com/types/views"
)

//go:generate go run tailscale.com/cmd/viewer --tags=test --type=TestPrefs,TestBundle,TestValueStruct,TestGenericStruct,TestPrefsGroup

type TestPrefs struct {
	Int32Item   Item[int32]  `json:",omitzero"`
	UInt64Item  Item[uint64] `json:",omitzero"`
	StringItem1 Item[string] `json:",omitzero"`
	StringItem2 Item[string] `json:",omitzero"`
	BoolItem1   Item[bool]   `json:",omitzero"`
	BoolItem2   Item[bool]   `json:",omitzero"`
	StringSlice List[string] `json:",omitzero"`
	IntSlice    List[int]    `json:",omitzero"`

	AddrItem Item[netip.Addr] `json:",omitzero"`

	StringStringMap Map[string, string]  `json:",omitzero"`
	IntStringMap    Map[int, string]     `json:",omitzero"`
	AddrIntMap      Map[netip.Addr, int] `json:",omitzero"`

	// Bundles are complex preferences that usually consist of
	// multiple parameters that must be configured atomically.
	Bundle1 Item[*TestBundle]             `json:",omitzero"`
	Bundle2 Item[*TestBundle]             `json:",omitzero"`
	Generic Item[*TestGenericStruct[int]] `json:",omitzero"`

	BundleList StructList[*TestBundle] `json:",omitzero"`

	StringBundleMap StructMap[string, *TestBundle]     `json:",omitzero"`
	IntBundleMap    StructMap[int, *TestBundle]        `json:",omitzero"`
	AddrBundleMap   StructMap[netip.Addr, *TestBundle] `json:",omitzero"`

	// Group is a nested struct that contains one or more preferences.
	// Each preference in a group can be configured individually.
	// Preference groups should be included directly rather than by pointers.
	Group TestPrefsGroup `json:",omitzero"`
}

// MarshalJSONV2 implements [jsonv2.MarshalerV2].
func (p TestPrefs) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
	// The testPrefs type shadows the TestPrefs's method set,
	// causing jsonv2 to use the default marshaler and avoiding
	// infinite recursion.
	type testPrefs TestPrefs
	return jsonv2.MarshalEncode(out, (*testPrefs)(&p), opts)
}

// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
func (p *TestPrefs) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
	// The testPrefs type shadows the TestPrefs's method set,
	// causing jsonv2 to use the default unmarshaler and avoiding
	// infinite recursion.
	type testPrefs TestPrefs
	return jsonv2.UnmarshalDecode(in, (*testPrefs)(p), opts)
}

// MarshalJSON implements [json.Marshaler].
func (p TestPrefs) MarshalJSON() ([]byte, error) {
	return jsonv2.Marshal(p) // uses MarshalJSONV2
}

// UnmarshalJSON implements [json.Unmarshaler].
func (p *TestPrefs) UnmarshalJSON(b []byte) error {
	return jsonv2.Unmarshal(b, p) // uses UnmarshalJSONV2
}

// TestBundle is an example structure type that,
// despite containing multiple values, represents
// a single configurable preference item.
type TestBundle struct {
	Name   string           `json:",omitzero"`
	Nested *TestValueStruct `json:",omitzero"`
}

func (b *TestBundle) Equal(b2 *TestBundle) bool {
	if b == b2 {
		return true
	}
	if b == nil || b2 == nil {
		return false
	}
	return b.Name == b2.Name && b.Nested.Equal(b2.Nested)
}

// TestPrefsGroup contains logically grouped preference items.
// Each preference item in a group can be configured individually.
type TestPrefsGroup struct {
	FloatItem Item[float64] `json:",omitzero"`

	TestStringItem Item[TestStringType] `json:",omitzero"`
}

type TestValueStruct struct {
	Value int
}

func (s *TestValueStruct) Equal(s2 *TestValueStruct) bool {
	if s == s2 {
		return true
	}
	if s == nil || s2 == nil {
		return false
	}
	return *s == *s2
}

type TestGenericStruct[T ImmutableType] struct {
	Value T
}

func (s *TestGenericStruct[T]) Equal(s2 *TestGenericStruct[T]) bool {
	if s == s2 {
		return true
	}
	if s == nil || s2 == nil {
		return false
	}
	return *s == *s2
}

type TestStringType string

func TestMarshalUnmarshal(t *testing.T) {
	tests := []struct {
		name   string
		prefs  *TestPrefs
		indent bool
		want   string
	}{
		{
			name:  "string",
			prefs: &TestPrefs{StringItem1: ItemOf("Value1")},
			want:  `{"StringItem1": {"Value": "Value1"}}`,
		},
		{
			name:  "empty-string",
			prefs: &TestPrefs{StringItem1: ItemOf("")},
			want:  `{"StringItem1": {"Value": ""}}`,
		},
		{
			name:  "managed-string",
			prefs: &TestPrefs{StringItem1: ItemOf("Value1", Managed)},
			want:  `{"StringItem1": {"Value": "Value1", "Managed": true}}`,
		},
		{
			name:  "readonly-item",
			prefs: &TestPrefs{StringItem1: ItemWithOpts[string](ReadOnly)},
			want:  `{"StringItem1": {"ReadOnly": true}}`,
		},
		{
			name:  "readonly-item-with-value",
			prefs: &TestPrefs{StringItem1: ItemOf("RO", ReadOnly)},
			want:  `{"StringItem1": {"Value": "RO", "ReadOnly": true}}`,
		},
		{
			name:  "int32",
			prefs: &TestPrefs{Int32Item: ItemOf[int32](101)},
			want:  `{"Int32Item": {"Value": 101}}`,
		},
		{
			name:  "uint64",
			prefs: &TestPrefs{UInt64Item: ItemOf[uint64](42)},
			want:  `{"UInt64Item": {"Value": 42}}`,
		},
		{
			name:  "bool-true",
			prefs: &TestPrefs{BoolItem1: ItemOf(true)},
			want:  `{"BoolItem1": {"Value": true}}`,
		},
		{
			name:  "bool-false",
			prefs: &TestPrefs{BoolItem1: ItemOf(false)},
			want:  `{"BoolItem1": {"Value": false}}`,
		},
		{
			name:  "empty-slice",
			prefs: &TestPrefs{StringSlice: ListOf([]string{})},
			want:  `{"StringSlice": {"Value": []}}`,
		},
		{
			name:  "string-slice",
			prefs: &TestPrefs{StringSlice: ListOf([]string{"1", "2", "3"})},
			want:  `{"StringSlice": {"Value": ["1", "2", "3"]}}`,
		},
		{
			name:  "int-slice",
			prefs: &TestPrefs{IntSlice: ListOf([]int{4, 8, 15, 16, 23})},
			want:  `{"IntSlice": {"Value": [4, 8, 15, 16, 23]}}`,
		},
		{
			name:  "managed-int-slice",
			prefs: &TestPrefs{IntSlice: ListOf([]int{4, 8, 15, 16, 23}, Managed)},
			want:  `{"IntSlice": {"Value": [4, 8, 15, 16, 23], "Managed": true}}`,
		},
		{
			name:  "netip-addr",
			prefs: &TestPrefs{AddrItem: ItemOf(netip.MustParseAddr("127.0.0.1"))},
			want:  `{"AddrItem": {"Value": "127.0.0.1"}}`,
		},
		{
			name:  "string-string-map",
			prefs: &TestPrefs{StringStringMap: MapOf(map[string]string{"K1": "V1"})},
			want:  `{"StringStringMap": {"Value": {"K1": "V1"}}}`,
		},
		{
			name:  "int-string-map",
			prefs: &TestPrefs{IntStringMap: MapOf(map[int]string{42: "V1"})},
			want:  `{"IntStringMap": {"Value": {"42": "V1"}}}`,
		},
		{
			name:  "addr-int-map",
			prefs: &TestPrefs{AddrIntMap: MapOf(map[netip.Addr]int{netip.MustParseAddr("127.0.0.1"): 42})},
			want:  `{"AddrIntMap": {"Value": {"127.0.0.1": 42}}}`,
		},
		{
			name:  "bundle-list",
			prefs: &TestPrefs{BundleList: StructListOf([]*TestBundle{{Name: "Bundle1"}, {Name: "Bundle2"}})},
			want:  `{"BundleList": {"Value": [{"Name": "Bundle1"},{"Name": "Bundle2"}]}}`,
		},
		{
			name: "string-bundle-map",
			prefs: &TestPrefs{StringBundleMap: StructMapOf(map[string]*TestBundle{
				"K1": {Name: "Bundle1"},
				"K2": {Name: "Bundle2"},
			})},
			want: `{"StringBundleMap": {"Value": {"K1": {"Name": "Bundle1"}, "K2": {"Name": "Bundle2"}}}}`,
		},
		{
			name:  "int-bundle-map",
			prefs: &TestPrefs{IntBundleMap: StructMapOf(map[int]*TestBundle{42: {Name: "Bundle1"}})},
			want:  `{"IntBundleMap": {"Value": {"42": {"Name": "Bundle1"}}}}`,
		},
		{
			name:  "addr-bundle-map",
			prefs: &TestPrefs{AddrBundleMap: StructMapOf(map[netip.Addr]*TestBundle{netip.MustParseAddr("127.0.0.1"): {Name: "Bundle1"}})},
			want:  `{"AddrBundleMap": {"Value": {"127.0.0.1": {"Name": "Bundle1"}}}}`,
		},
		{
			name:  "bundle",
			prefs: &TestPrefs{Bundle1: ItemOf(&TestBundle{Name: "Bundle1"})},
			want:  `{"Bundle1": {"Value": {"Name": "Bundle1"}}}`,
		},
		{
			name:  "managed-bundle",
			prefs: &TestPrefs{Bundle2: ItemOf(&TestBundle{Name: "Bundle2", Nested: &TestValueStruct{Value: 17}}, Managed)},
			want:  `{"Bundle2": {"Value": {"Name": "Bundle2", "Nested": {"Value": 17}}, "Managed": true}}`,
		},
		{
			name:  "subgroup",
			prefs: &TestPrefs{Group: TestPrefsGroup{FloatItem: ItemOf(1.618), TestStringItem: ItemOf(TestStringType("Value"))}},
			want:  `{"Group": {"FloatItem": {"Value": 1.618}, "TestStringItem": {"Value": "Value"}}}`,
		},
		{
			name: "various",
			prefs: &TestPrefs{
				Int32Item:       ItemOf[int32](101),
				UInt64Item:      ItemOf[uint64](42),
				StringItem1:     ItemOf("Value1"),
				StringItem2:     ItemWithOpts[string](ReadOnly),
				BoolItem1:       ItemOf(true),
				BoolItem2:       ItemOf(false, Managed),
				StringSlice:     ListOf([]string{"1", "2", "3"}),
				IntSlice:        ListOf([]int{4, 8, 15, 16, 23}, Managed),
				AddrItem:        ItemOf(netip.MustParseAddr("127.0.0.1")),
				StringStringMap: MapOf(map[string]string{"K1": "V1"}),
				IntStringMap:    MapOf(map[int]string{42: "V1"}),
				AddrIntMap:      MapOf(map[netip.Addr]int{netip.MustParseAddr("127.0.0.1"): 42}),
				BundleList:      StructListOf([]*TestBundle{{Name: "Bundle1"}}),
				StringBundleMap: StructMapOf(map[string]*TestBundle{"K1": {Name: "Bundle1"}}),
				IntBundleMap:    StructMapOf(map[int]*TestBundle{42: {Name: "Bundle1"}}),
				AddrBundleMap:   StructMapOf(map[netip.Addr]*TestBundle{netip.MustParseAddr("127.0.0.1"): {Name: "Bundle1"}}),
				Bundle1:         ItemOf(&TestBundle{Name: "Bundle1"}),
				Bundle2:         ItemOf(&TestBundle{Name: "Bundle2", Nested: &TestValueStruct{Value: 17}}, Managed),
				Group: TestPrefsGroup{
					FloatItem:      ItemOf(1.618),
					TestStringItem: ItemOf(TestStringType("Value")),
				},
			},
			want: `{
				"Int32Item":       {"Value": 101},
				"UInt64Item":      {"Value": 42},
				"StringItem1":     {"Value": "Value1"},
				"StringItem2":     {"ReadOnly": true},
				"BoolItem1":       {"Value": true},
				"BoolItem2":       {"Value": false, "Managed": true},
				"StringSlice":     {"Value": ["1", "2", "3"]},
				"IntSlice":        {"Value": [4, 8, 15, 16, 23], "Managed": true},
				"AddrItem":        {"Value": "127.0.0.1"},
				"StringStringMap": {"Value": {"K1": "V1"}},
				"IntStringMap":    {"Value": {"42": "V1"}},
				"AddrIntMap":      {"Value": {"127.0.0.1": 42}},
				"BundleList":      {"Value": [{"Name": "Bundle1"}]},
				"StringBundleMap": {"Value": {"K1": {"Name": "Bundle1"}}},
				"IntBundleMap":    {"Value": {"42": {"Name": "Bundle1"}}},
				"AddrBundleMap":   {"Value": {"127.0.0.1": {"Name": "Bundle1"}}},
				"Bundle1":         {"Value": {"Name": "Bundle1"}},
				"Bundle2":         {"Value": {"Name": "Bundle2", "Nested": {"Value": 17}}, "Managed": true},
				"Group":           {
				                       "FloatItem":      {"Value": 1.618},
									   "TestStringItem": {"Value": "Value"}
								   }
			}`,
		},
	}

	arshalers := []struct {
		name      string
		marshal   func(in any) (out []byte, err error)
		unmarshal func(in []byte, out any) (err error)
	}{
		{
			name:      "json",
			marshal:   json.Marshal,
			unmarshal: json.Unmarshal,
		},
		{
			name:      "jsonv2",
			marshal:   func(in any) (out []byte, err error) { return jsonv2.Marshal(in) },
			unmarshal: func(in []byte, out any) (err error) { return jsonv2.Unmarshal(in, out) },
		},
	}

	for _, a := range arshalers {
		t.Run(a.name, func(t *testing.T) {
			for _, tt := range tests {
				t.Run(tt.name, func(t *testing.T) {
					t.Run("marshal-directly", func(t *testing.T) {
						gotJSON, err := a.marshal(tt.prefs)
						if err != nil {
							t.Fatalf("marshalling failed: %v", err)
						}

						checkJSON(t, gotJSON, jsontext.Value(tt.want))

						var gotPrefs TestPrefs
						if err = a.unmarshal(gotJSON, &gotPrefs); err != nil {
							t.Fatalf("unmarshalling failed: %v", err)
						}

						if diff := cmp.Diff(tt.prefs, &gotPrefs); diff != "" {
							t.Errorf("mismatch (-want +got):\n%s", diff)
						}
					})

					t.Run("marshal-via-view", func(t *testing.T) {
						gotJSON, err := a.marshal(tt.prefs.View())
						if err != nil {
							t.Fatalf("marshalling failed: %v", err)
						}

						checkJSON(t, gotJSON, jsontext.Value(tt.want))

						var gotPrefs TestPrefsView
						if err = a.unmarshal(gotJSON, &gotPrefs); err != nil {
							t.Fatalf("unmarshalling failed: %v", err)
						}

						if diff := cmp.Diff(tt.prefs, gotPrefs.AsStruct()); diff != "" {
							t.Errorf("mismatch (-want +got):\n%s", diff)
						}
					})
				})
			}
		})
	}
}

func TestPreferenceStates(t *testing.T) {
	const (
		zeroValue = 0
		defValue  = 5
		userValue = 42
		mdmValue  = 1001
	)
	i := ItemWithOpts[int]()
	checkIsSet(t, &i, false)
	checkIsManaged(t, &i, false)
	checkIsReadOnly(t, &i, false)
	checkValueOk(t, &i, zeroValue, false)

	i.SetDefaultValue(defValue)
	checkValue(t, &i, defValue)
	checkValueOk(t, &i, defValue, false)

	checkSetValue(t, &i, userValue)
	checkValue(t, &i, userValue)
	checkValueOk(t, &i, userValue, true)

	i2 := ItemOf(userValue)
	checkIsSet(t, &i2, true)
	checkValue(t, &i2, userValue)
	checkValueOk(t, &i2, userValue, true)
	checkEqual(t, i2, i, true)

	i2.SetManagedValue(mdmValue)
	// Setting a managed value should set the value, mark the preference
	// as managed and read-only, and prevent it from being modified with SetValue.
	checkIsSet(t, &i2, true)
	checkIsManaged(t, &i2, true)
	checkIsReadOnly(t, &i2, true)
	checkValue(t, &i2, mdmValue)
	checkValueOk(t, &i2, mdmValue, true)
	checkCanNotSetValue(t, &i2, userValue, ErrManaged)
	checkValue(t, &i2, mdmValue) // the value must not be changed
	checkCanNotClearValue(t, &i2, ErrManaged)

	i2.ClearManaged()
	// Clearing the managed flag should change the IsManaged and IsReadOnly flags...
	checkIsManaged(t, &i2, false)
	checkIsReadOnly(t, &i2, false)
	// ...but not the value.
	checkValue(t, &i2, mdmValue)

	// We should be able to change the value after clearing the managed flag.
	checkSetValue(t, &i2, userValue)
	checkIsSet(t, &i2, true)
	checkValue(t, &i2, userValue)
	checkValueOk(t, &i2, userValue, true)
	checkEqual(t, i2, i, true)

	i2.SetReadOnly(true)
	checkIsReadOnly(t, &i2, true)
	checkIsManaged(t, &i2, false)
	checkCanNotSetValue(t, &i2, userValue, ErrReadOnly)
	checkCanNotClearValue(t, &i2, ErrReadOnly)

	i2.SetReadOnly(false)
	i2.SetDefaultValue(defValue)
	checkClearValue(t, &i2)
	checkIsSet(t, &i2, false)
	checkValue(t, &i2, defValue)
	checkValueOk(t, &i2, defValue, false)
}

func TestItemView(t *testing.T) {
	i := ItemOf(&TestBundle{Name: "B1"})

	iv := ItemViewOf(&i)
	checkIsSet(t, iv, true)
	checkIsManaged(t, iv, false)
	checkIsReadOnly(t, iv, false)
	checkValue(t, iv, TestBundleView{i.Value()})
	checkValueOk(t, iv, TestBundleView{i.Value()}, true)

	i2 := *iv.AsStruct()
	checkEqual(t, i, i2, true)
	i2.SetValue(&TestBundle{Name: "B2"})

	iv2 := ItemViewOf(&i2)
	checkEqual(t, iv, iv2, false)
}

func TestListView(t *testing.T) {
	l := ListOf([]int{4, 8, 15, 16, 23, 42}, ReadOnly)

	lv := l.View()
	checkIsSet(t, lv, true)
	checkIsManaged(t, lv, false)
	checkIsReadOnly(t, lv, true)
	checkValue(t, lv, views.SliceOf(l.Value()))
	checkValueOk(t, lv, views.SliceOf(l.Value()), true)

	l2 := *lv.AsStruct()
	checkEqual(t, l, l2, true)
}

func TestStructListView(t *testing.T) {
	l := StructListOf([]*TestBundle{{Name: "E1"}, {Name: "E2"}}, ReadOnly)

	lv := StructListViewOf(&l)
	checkIsSet(t, lv, true)
	checkIsManaged(t, lv, false)
	checkIsReadOnly(t, lv, true)
	checkValue(t, lv, views.SliceOfViews(l.Value()))
	checkValueOk(t, lv, views.SliceOfViews(l.Value()), true)

	l2 := *lv.AsStruct()
	checkEqual(t, l, l2, true)
}

func TestStructMapView(t *testing.T) {
	m := StructMapOf(map[string]*TestBundle{
		"K1": {Name: "E1"},
		"K2": {Name: "E2"},
	}, ReadOnly)

	mv := StructMapViewOf(&m)
	checkIsSet(t, mv, true)
	checkIsManaged(t, mv, false)
	checkIsReadOnly(t, mv, true)
	checkValue(t, *mv.AsStruct(), m.Value())
	checkValueOk(t, *mv.AsStruct(), m.Value(), true)

	m2 := *mv.AsStruct()
	checkEqual(t, m, m2, true)
}

// check that the preference types implement the test [pref] interface.
var (
	_ pref[int]                    = (*Item[int])(nil)
	_ pref[*TestBundle]            = (*Item[*TestBundle])(nil)
	_ pref[[]int]                  = (*List[int])(nil)
	_ pref[[]*TestBundle]          = (*StructList[*TestBundle])(nil)
	_ pref[map[string]*TestBundle] = (*StructMap[string, *TestBundle])(nil)
)

// pref is an interface used by [checkSetValue], [checkClearValue], and similar test
// functions that mutate preferences. It is implemented by all preference types, such
// as [Item], [List], [StructList], and [StructMap], and provides both read and write
// access to the preference's value and state.
type pref[T any] interface {
	prefView[T]
	SetValue(v T) error
	ClearValue() error
	SetDefaultValue(v T)
	SetManagedValue(v T)
	ClearManaged()
	SetReadOnly(readonly bool)
}

// check that the preference view types implement the test [prefView] interface.
var (
	_ prefView[int]                                              = (*Item[int])(nil)
	_ prefView[TestBundleView]                                   = (*ItemView[*TestBundle, TestBundleView])(nil)
	_ prefView[views.Slice[int]]                                 = (*ListView[int])(nil)
	_ prefView[views.SliceView[*TestBundle, TestBundleView]]     = (*StructListView[*TestBundle, TestBundleView])(nil)
	_ prefView[views.MapFn[string, *TestBundle, TestBundleView]] = (*StructMapView[string, *TestBundle, TestBundleView])(nil)
)

// prefView is an interface used by [checkIsSet], [checkIsManaged], and similar non-mutating
// test functions. It is implemented by all preference types, such as [Item], [List], [StructList],
// and [StructMap], as well as their corresponding views, such as [ItemView], [ListView], [StructListView],
// and [StructMapView], and provides read-only access to the preference's value and state.
type prefView[T any] interface {
	IsSet() bool
	Value() T
	ValueOk() (T, bool)
	DefaultValue() T
	IsManaged() bool
	IsReadOnly() bool
}

func checkIsSet[T any](tb testing.TB, p prefView[T], wantSet bool) {
	tb.Helper()
	if gotSet := p.IsSet(); gotSet != wantSet {
		tb.Errorf("IsSet: got %v; want %v", gotSet, wantSet)
	}
}

func checkIsManaged[T any](tb testing.TB, p prefView[T], wantManaged bool) {
	tb.Helper()
	if gotManaged := p.IsManaged(); gotManaged != wantManaged {
		tb.Errorf("IsManaged: got %v; want %v", gotManaged, wantManaged)
	}
}

func checkIsReadOnly[T any](tb testing.TB, p prefView[T], wantReadOnly bool) {
	tb.Helper()
	if gotReadOnly := p.IsReadOnly(); gotReadOnly != wantReadOnly {
		tb.Errorf("IsReadOnly: got %v; want %v", gotReadOnly, wantReadOnly)
	}
}

func checkValue[T any](tb testing.TB, p prefView[T], wantValue T) {
	tb.Helper()
	if gotValue := p.Value(); !testComparerFor[T]()(gotValue, wantValue) {
		tb.Errorf("Value: got %v; want %v", gotValue, wantValue)
	}
}

func checkValueOk[T any](tb testing.TB, p prefView[T], wantValue T, wantOk bool) {
	tb.Helper()
	gotValue, gotOk := p.ValueOk()

	if gotOk != wantOk || !testComparerFor[T]()(gotValue, wantValue) {
		tb.Errorf("ValueOk: got (%v, %v); want (%v, %v)", gotValue, gotOk, wantValue, wantOk)
	}
}

func checkEqual[T equatable[T]](tb testing.TB, a, b T, wantEqual bool) {
	tb.Helper()
	if gotEqual := a.Equal(b); gotEqual != wantEqual {
		tb.Errorf("Equal: got %v; want %v", gotEqual, wantEqual)
	}
}

func checkSetValue[T any](tb testing.TB, p pref[T], v T) {
	tb.Helper()
	if err := p.SetValue(v); err != nil {
		tb.Fatalf("SetValue: gotErr %v, wantErr: nil", err)
	}
}

func checkCanNotSetValue[T any](tb testing.TB, p pref[T], v T, wantErr error) {
	tb.Helper()
	if err := p.SetValue(v); err == nil || !errors.Is(err, wantErr) {
		tb.Fatalf("SetValue: gotErr %v, wantErr: %v", err, wantErr)
	}
}

func checkClearValue[T any](tb testing.TB, p pref[T]) {
	tb.Helper()
	if err := p.ClearValue(); err != nil {
		tb.Fatalf("ClearValue: gotErr %v, wantErr: nil", err)
	}
}

func checkCanNotClearValue[T any](tb testing.TB, p pref[T], wantErr error) {
	tb.Helper()
	err := p.ClearValue()
	if err == nil || !errors.Is(err, wantErr) {
		tb.Fatalf("ClearValue: gotErr %v, wantErr: %v", err, wantErr)
	}
}

// testComparerFor is like [comparerFor], but uses [reflect.DeepEqual]
// unless T is [equatable].
func testComparerFor[T any]() func(a, b T) bool {
	return func(a, b T) bool {
		switch a := any(a).(type) {
		case equatable[T]:
			return a.Equal(b)
		default:
			return reflect.DeepEqual(a, b)
		}
	}
}

func checkJSON(tb testing.TB, got, want jsontext.Value) {
	tb.Helper()
	got = got.Clone()
	want = want.Clone()
	// Compare canonical forms.
	if err := got.Canonicalize(); err != nil {
		tb.Error(err)
	}
	if err := want.Canonicalize(); err != nil {
		tb.Error(err)
	}
	if bytes.Equal(got, want) {
		return
	}

	gotMap := make(map[string]any)
	if err := jsonv2.Unmarshal(got, &gotMap); err != nil {
		tb.Fatal(err)
	}
	wantMap := make(map[string]any)
	if err := jsonv2.Unmarshal(want, &wantMap); err != nil {
		tb.Fatal(err)
	}
	tb.Errorf("mismatch (-want +got):\n%s", cmp.Diff(wantMap, gotMap))
}
