package bundle

import (
	"bytes"
	"cmp"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"reflect"
	"slices"
	"strings"
	"testing"

	"github.com/open-policy-agent/opa/internal/file/archive"
	"github.com/open-policy-agent/opa/internal/storage/mock"

	"github.com/open-policy-agent/opa/v1/ast"
	"github.com/open-policy-agent/opa/v1/logging"
	"github.com/open-policy-agent/opa/v1/metrics"
	"github.com/open-policy-agent/opa/v1/util"

	"github.com/open-policy-agent/opa/v1/storage"
	"github.com/open-policy-agent/opa/v1/storage/disk"
	"github.com/open-policy-agent/opa/v1/storage/inmem"
	inmemtst "github.com/open-policy-agent/opa/v1/storage/inmem/test"
)

func TestManifestStoreLifecycleSingleBundle(t *testing.T) {
	store := inmemtst.New()
	tb := Manifest{Revision: "abc123", Roots: &[]string{"/a/b", "/a/c"}}

	verifyWriteManifests(t, store, map[string]Manifest{"test_bundle": tb}) // write one
	verifyReadBundleNames(t, store, nil, "test_bundle")                    // read one
	verifyDeleteManifest(t, store, "test_bundle")                          // delete it
	verifyReadBundleNames(t, store, nil)                                   // ensure it was removed
}

func TestManifestStoreLifecycleMultiBundle(t *testing.T) {
	store := inmemtst.New()
	bundles := map[string]Manifest{
		"bundle1": {
			Revision: "abc123",
			Roots:    &[]string{"/a/b", "/a/c"},
		},
		"bundle2": {
			Revision: "def123",
			Roots:    &[]string{"/x/y", "/z"},
		},
	}

	verifyWriteManifests(t, store, bundles)                    // write multiple
	verifyReadBundleNames(t, store, nil, "bundle1", "bundle2") // read them
	verifyDeleteManifest(t, store, "bundle1")                  // delete one
	verifyReadBundleNames(t, store, nil, "bundle2")            // ensure it was removed
	verifyDeleteManifest(t, store, "bundle2")                  // delete the last one
	verifyReadBundleNames(t, store, nil)                       // ensure it was removed
}

func TestLegacyManifestStoreLifecycle(t *testing.T) {
	store := inmemtst.New()
	tb := Manifest{Revision: "abc123", Roots: &[]string{"/a/b", "/a/c"}}

	// write a "legacy" manifest
	if err := storage.Txn(t.Context(), store, storage.WriteParams, func(txn storage.Transaction) error {
		return LegacyWriteManifestToStore(t.Context(), store, txn, tb)
	}); err != nil {
		t.Fatalf("Failed to write manifest to store: %s", err)
	}

	// make sure it can be retrieved
	verifyReadLegacyRevision(t, store, tb.Revision)

	// delete it
	if err := storage.Txn(t.Context(), store, storage.WriteParams, func(txn storage.Transaction) error {
		return LegacyEraseManifestFromStore(t.Context(), store, txn)
	}); err != nil {
		t.Fatalf("Failed to erase manifest from store: %s", err)
	}

	verifyReadLegacyRevision(t, store, "")
}

func TestMixedManifestStoreLifecycle(t *testing.T) {
	store := inmemtst.New()
	bundles := map[string]Manifest{
		"bundle1": {
			Revision: "abc123",
			Roots:    &[]string{"/a/b", "/a/c"},
		},
		"bundle2": {
			Revision: "def123",
			Roots:    &[]string{"/x/y", "/z"},
		},
	}

	// Write the legacy one first
	if err := storage.Txn(t.Context(), store, storage.WriteParams, func(txn storage.Transaction) error {
		return LegacyWriteManifestToStore(t.Context(), store, txn, bundles["bundle1"])
	}); err != nil {
		t.Fatalf("Failed to write manifest to store: %s", err)
	}

	verifyReadBundleNames(t, store, nil)

	// Write both new ones
	verifyWriteManifests(t, store, bundles)
	verifyReadBundleNames(t, store, nil, "bundle1", "bundle2")

	// Ensure the original legacy one is still there
	verifyReadLegacyRevision(t, store, bundles["bundle1"].Revision)
}

func TestBundleLazyModeNoPolicyOrData(t *testing.T) {
	mockStore := mock.New()
	bundles := map[string]*Bundle{"bundle1": {
		Manifest: Manifest{
			Roots:    &[]string{"a"},
			Revision: "foo",
		},
		Etag:            "foo",
		lazyLoadingMode: true,
	}}

	mustActivate(t, mockStore, &ActivateOpts{Bundles: bundles})

	// Ensure the bundle was activated
	verifyReadBundleNames(t, mockStore, nil, util.Keys(bundles)...)
	verifyResultRead(t, mockStore, `{
		"system": {
			"bundles": {
				"bundle1": {
					"manifest": {
						"revision": "foo",
						"roots": ["a"]
					},
					"etag": "foo"
				}
			}
		}
	}`)
}

func TestBundleLifecycle_ModuleRegoVersions(t *testing.T) {
	type (
		files        [][2]string
		bundles      map[string]files
		deactivation struct {
			bundles map[string]struct{}
			expData string
		}
		activation struct {
			bundles            bundles
			lazy               bool
			readWithBundleName bool
			expData            string
		}
	)

	tests := []struct {
		note               string
		updates            []any
		runtimeRegoVersion ast.RegoVersion
	}{
		// single v0 bundle
		{
			note: "v0 bundle, lazy, read with bundle name",
			updates: []any{
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"], "rego_version": 0}`},
							{"a/policy.rego", `package a
								p[42] { true }`},
						},
					},
					lazy:               true,
					readWithBundleName: true,
					// Lazy mode, bundle reader decides if module name should be prefixed with bundle name; reader initialized with bundle name, so prefix is expected.
					expData: `{
									"system":{
										"bundles":{"bundle1":{"etag":"bar","manifest":{"rego_version":0,"revision":"","roots":["a"]}}},
										"modules":{"bundle1/a/policy.rego":{"rego_version":0}}
									}
								}`,
				},
				deactivation{
					bundles: map[string]struct{}{"bundle1": {}},
					expData: `{"system":{"bundles":{},"modules":{}}}`,
				},
			},
		},
		{
			note: "v0 bundle, not lazy, read with bundle name",
			updates: []any{
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"], "rego_version": 0}`},
							{"a/policy.rego", `package a
								p[42] { true }`},
						},
					},
					readWithBundleName: true,
					// Not lazy mode, bundle store decides that module name should be prefixed with bundle name.
					expData: `{
						"system":{
							"bundles":{"bundle1":{"etag":"bar","manifest":{"rego_version":0,"revision":"","roots":["a"]}}},
							"modules":{"bundle1/a/policy.rego":{"rego_version":0}}
						}
					}`,
				},
				deactivation{
					bundles: map[string]struct{}{"bundle1": {}},
					expData: `{"system":{"bundles":{},"modules":{}}}`,
				},
			},
		},
		{
			note: "v0 bundle, lazy, read with NO bundle name",
			updates: []any{
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"], "rego_version": 0}`},
							{"a/policy.rego", `package a
								p[42] { true }`},
						},
					},
					lazy: true,
					// Lazy mode, bundle reader decides if module name should be prefixed with bundle name; reader not initialized with bundle name, so prefix not expected.
					expData: `{
									"system":{
										"bundles":{"bundle1":{"etag":"bar","manifest":{"rego_version":0,"revision":"","roots":["a"]}}},
										"modules":{"a/policy.rego":{"rego_version":0}}
									}
								}`,
				},
				deactivation{
					bundles: map[string]struct{}{"bundle1": {}},
					expData: `{"system":{"bundles":{},"modules":{}}}`,
				},
			},
		},
		{
			note: "v0 bundle, not lazy, read with NO bundle name",
			updates: []any{
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"], "rego_version": 0}`},
							{"a/policy.rego", `package a
								p[42] { true }`},
						},
					},
					// Not lazy mode, bundle store decides that module name should be prefixed with bundle name.
					expData: `{
								"system":{
									"bundles":{"bundle1":{"etag":"bar","manifest":{"rego_version":0,"revision":"","roots":["a"]}}},
									"modules":{"bundle1/a/policy.rego":{"rego_version":0}}
								}
							}`,
				},
				deactivation{
					bundles: map[string]struct{}{"bundle1": {}},
					expData: `{"system":{"bundles":{},"modules":{}}}`,
				},
			},
		},

		{
			note:               "v0 bundle, not lazy, --v0-compatible",
			runtimeRegoVersion: ast.RegoV0,
			updates: []any{
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"], "rego_version": 0}`},
							{"a/policy.rego", `package a
								p[42] { true }`},
						},
					},
					readWithBundleName: true,
					// Lazy mode, bundle reader decides if module name should be prefixed with bundle name; reader initialized with bundle name, so prefix is expected.
					expData: `{
									"system":{
										"bundles":{"bundle1":{"etag":"bar","manifest":{"rego_version":0,"revision":"","roots":["a"]}}}
									}
								}`,
				},
				deactivation{
					bundles: map[string]struct{}{"bundle1": {}},
					expData: `{"system":{"bundles":{}}}`,
				},
			},
		},
		{
			note:               "v0 bundle, lazy, read with bundle name, --v0-compatible",
			runtimeRegoVersion: ast.RegoV0,
			updates: []any{
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"], "rego_version": 0}`},
							{"a/policy.rego", `package a
								p[42] { true }`},
						},
					},
					lazy:               true,
					readWithBundleName: true,
					// Lazy mode, bundle reader decides if module name should be prefixed with bundle name; reader initialized with bundle name, so prefix is expected.
					expData: `{
									"system":{
										"bundles":{"bundle1":{"etag":"bar","manifest":{"rego_version":0,"revision":"","roots":["a"]}}}
									}
								}`,
				},
				deactivation{
					bundles: map[string]struct{}{"bundle1": {}},
					expData: `{"system":{"bundles":{}}}`,
				},
			},
		},
		{
			note:               "v0 bundle, lazy, read with NO bundle name, --v0-compatible",
			runtimeRegoVersion: ast.RegoV0,
			updates: []any{
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"], "rego_version": 0}`},
							{"a/policy.rego", `package a
								p[42] { true }`},
						},
					},
					lazy: true,
					// Lazy mode, bundle reader decides if module name should be prefixed with bundle name; reader initialized with bundle name, so prefix is expected.
					expData: `{
									"system":{
										"bundles":{"bundle1":{"etag":"bar","manifest":{"rego_version":0,"revision":"","roots":["a"]}}}
									}
								}`,
				},
				deactivation{
					bundles: map[string]struct{}{"bundle1": {}},
					expData: `{"system":{"bundles":{}}}`,
				},
			},
		},

		// single v1 bundle
		{
			note: "v1 bundle, lazy, read with bundle name",
			updates: []any{
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"], "rego_version": 1}`},
							{"a/policy.rego", `package a
								p contains 42 if { true }`},
						},
					},
					lazy:               true,
					readWithBundleName: true,
					// Lazy mode, bundle reader decides if module name should be prefixed with bundle name; reader initialized with bundle name, so prefix is expected.
					expData: `{
									"system":{
										"bundles":{"bundle1":{"etag":"bar","manifest":{"rego_version":1,"revision":"","roots":["a"]}}}
									}
								}`,
				},
				deactivation{
					bundles: map[string]struct{}{"bundle1": {}},
					expData: `{"system":{"bundles":{}}}`,
				},
			},
		},
		{
			note: "v1 bundle, not lazy, read with bundle name",
			updates: []any{
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"], "rego_version": 1}`},
							{"a/policy.rego", `package a
								p contains 42 if { true }`},
						},
					},
					readWithBundleName: true,
					// Not lazy mode, bundle store decides that module name should be prefixed with bundle name.
					expData: `{
									"system":{
										"bundles":{"bundle1":{"etag":"bar","manifest":{"rego_version":1,"revision":"","roots":["a"]}}}
									}
								}`,
				},
				deactivation{
					bundles: map[string]struct{}{"bundle1": {}},
					expData: `{"system":{"bundles":{}}}`,
				},
			},
		},
		{
			note: "v1 bundle, lazy, read with NO bundle name",
			updates: []any{
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"], "rego_version": 1}`},
							{"a/policy.rego", `package a
								p contains 42 if { true }`},
						},
					},
					lazy: true,
					// Lazy mode, bundle reader decides if module name should be prefixed with bundle name; reader not initialized with bundle name, so prefix not expected.
					expData: `{
									"system":{
										"bundles":{"bundle1":{"etag":"bar","manifest":{"rego_version":1,"revision":"","roots":["a"]}}}
									}
								}`,
				},
				deactivation{
					bundles: map[string]struct{}{"bundle1": {}},
					expData: `{"system":{"bundles":{}}}`,
				},
			},
		},
		{
			note: "v1 bundle, not lazy, read with NO bundle name",
			updates: []any{
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"], "rego_version": 1}`},
							{"a/policy.rego", `package a
								p contains 42 if { true }`},
						},
					},
					// Not lazy mode, bundle store decides that module name should be prefixed with bundle name.
					expData: `{
								"system":{
									"bundles":{"bundle1":{"etag":"bar","manifest":{"rego_version":1,"revision":"","roots":["a"]}}}
								}
							}`,
				},
				deactivation{
					bundles: map[string]struct{}{"bundle1": {}},
					expData: `{"system":{"bundles":{}}}`,
				},
			},
		},

		{
			note:               "v1 bundle, not lazy, --v0-compatible",
			runtimeRegoVersion: ast.RegoV0,
			updates: []any{
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"], "rego_version": 1}`},
							{"a/policy.rego", `package a
								p contains 42 if { true }`},
						},
					},
					readWithBundleName: true,
					// Lazy mode, bundle reader decides if module name should be prefixed with bundle name; reader initialized with bundle name, so prefix is expected.
					expData: `{
									"system":{
										"bundles":{"bundle1":{"etag":"bar","manifest":{"rego_version":1,"revision":"","roots":["a"]}}},
										"modules":{"bundle1/a/policy.rego":{"rego_version":1}}
									}
								}`,
				},
				deactivation{
					bundles: map[string]struct{}{"bundle1": {}},
					expData: `{"system":{"bundles":{}, "modules":{}}}`,
				},
			},
		},
		{
			note:               "v1 bundle, lazy, read with bundle name, --v0-compatible",
			runtimeRegoVersion: ast.RegoV0,
			updates: []any{
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"], "rego_version": 1}`},
							{"a/policy.rego", `package a
								p contains 42 if { true }`},
						},
					},
					lazy:               true,
					readWithBundleName: true,
					// Lazy mode, bundle reader decides if module name should be prefixed with bundle name; reader initialized with bundle name, so prefix is expected.
					expData: `{
									"system":{
										"bundles":{"bundle1":{"etag":"bar","manifest":{"rego_version":1,"revision":"","roots":["a"]}}},
										"modules":{"bundle1/a/policy.rego":{"rego_version":1}}
									}
								}`,
				},
				deactivation{
					bundles: map[string]struct{}{"bundle1": {}},
					expData: `{"system":{"bundles":{}, "modules":{}}}`,
				},
			},
		},
		{
			note:               "v1 bundle, lazy, read with NO bundle name, --v0-compatible",
			runtimeRegoVersion: ast.RegoV0,
			updates: []any{
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"], "rego_version": 1}`},
							{"a/policy.rego", `package a
								p contains 42 if { true }`},
						},
					},
					lazy: true,
					// Lazy mode, bundle reader decides if module name should be prefixed with bundle name; reader initialized with bundle name, so prefix is expected.
					expData: `{
									"system":{
										"bundles":{"bundle1":{"etag":"bar","manifest":{"rego_version":1,"revision":"","roots":["a"]}}},
										"modules":{"a/policy.rego":{"rego_version":1}}
									}
								}`,
				},
				deactivation{
					bundles: map[string]struct{}{"bundle1": {}},
					expData: `{"system":{"bundles":{}, "modules":{}}}`,
				},
			},
		},

		{
			note: "custom bundle without rego-version, lazy",
			updates: []any{
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"]}`},
							{"a/policy.rego", `package a
								p contains 42 if { true }`},
						},
					},
					lazy:               true,
					readWithBundleName: true,
					expData: `{
								"system":{
									"bundles":{"bundle1":{"etag":"bar","manifest":{"revision":"","roots":["a"]}}}
								}
							}`,
				},
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"]}`},
							{"a/policy.rego", `package a
								p contains 1337 if { true }`},
						},
					},
					lazy:               true,
					readWithBundleName: true,
					expData: `{
								"system":{
									"bundles":{"bundle1":{"etag":"bar","manifest":{"revision":"","roots":["a"]}}}
								}
							}`,
				},
				deactivation{
					bundles: map[string]struct{}{"bundle1": {}},
					expData: `{"system":{"bundles":{}}}`,
				},
			},
		},
		{
			note:               "custom bundle without rego-version, lazy, v1 runtime (explicit)",
			runtimeRegoVersion: ast.RegoV1,
			updates: []any{
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"]}`},
							{"a/policy.rego", `package a
								p contains 42 if { true }`},
						},
					},
					lazy:               true,
					readWithBundleName: true,
					expData: `{
								"system":{
									"bundles":{"bundle1":{"etag":"bar","manifest":{"revision":"","roots":["a"]}}}
								}
							}`,
				},
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"]}`},
							{"a/policy.rego", `package a
								p contains 1337 if { true }`},
						},
					},
					lazy:               true,
					readWithBundleName: true,
					expData: `{
								"system":{
									"bundles":{"bundle1":{"etag":"bar","manifest":{"revision":"","roots":["a"]}}}
								}
							}`,
				},
				deactivation{
					bundles: map[string]struct{}{"bundle1": {}},
					expData: `{"system":{"bundles":{}}}`,
				},
			},
		},
		{
			note:               "custom bundle without rego-version, lazy, --v0-compatible",
			runtimeRegoVersion: ast.RegoV0,
			updates: []any{
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"]}`},
							{"a/policy.rego", `package a
								p[42] { true }`},
						},
					},
					lazy:               true,
					readWithBundleName: true,
					expData: `{
								"system":{
									"bundles":{"bundle1":{"etag":"bar","manifest":{"revision":"","roots":["a"]}}}
								}
							}`,
				},
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"]}`},
							{"a/policy.rego", `package a
								p[1337] { true }`},
						},
					},
					lazy:               true,
					readWithBundleName: true,
					expData: `{
								"system":{
									"bundles":{"bundle1":{"etag":"bar","manifest":{"revision":"","roots":["a"]}}}
								}
							}`,
				},
				deactivation{
					bundles: map[string]struct{}{"bundle1": {}},
					expData: `{"system":{"bundles":{}}}`,
				},
			},
		},

		{
			note: "custom bundle without rego-version, not lazy",
			updates: []any{
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"]}`},
							{"a/policy.rego", `package a
								p contains 42 if { true }`},
						},
					},
					readWithBundleName: true,
					expData: `{
								"system":{
									"bundles":{"bundle1":{"etag":"bar","manifest":{"revision":"","roots":["a"]}}}
								}
							}`,
				},
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"]}`},
							{"a/policy.rego", `package a
								p contains 1337 if { true }`},
						},
					},
					readWithBundleName: true,
					expData: `{
								"system":{
									"bundles":{"bundle1":{"etag":"bar","manifest":{"revision":"","roots":["a"]}}}
								}
							}`,
				},
				deactivation{
					bundles: map[string]struct{}{"bundle1": {}},
					expData: `{"system":{"bundles":{}}}`,
				},
			},
		},
		{
			note:               "custom bundle without rego-version, not lazy, v1 runtime (explicit)",
			runtimeRegoVersion: ast.RegoV1,
			updates: []any{
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"]}`},
							{"a/policy.rego", `package a
								p contains 42 if { true }`},
						},
					},
					readWithBundleName: true,
					expData: `{
								"system":{
									"bundles":{"bundle1":{"etag":"bar","manifest":{"revision":"","roots":["a"]}}}
								}
							}`,
				},
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"]}`},
							{"a/policy.rego", `package a
								p contains 1337 if { true }`},
						},
					},
					readWithBundleName: true,
					expData: `{
								"system":{
									"bundles":{"bundle1":{"etag":"bar","manifest":{"revision":"","roots":["a"]}}}
								}
							}`,
				},
				deactivation{
					bundles: map[string]struct{}{"bundle1": {}},
					expData: `{"system":{"bundles":{}}}`,
				},
			},
		},
		{
			note:               "custom bundle without rego-version, not lazy, --v0-compatible",
			runtimeRegoVersion: ast.RegoV0,
			updates: []any{
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"]}`},
							{"a/policy.rego", `package a
								p[42] { true }`},
						},
					},
					readWithBundleName: true,
					expData: `{
								"system":{
									"bundles":{"bundle1":{"etag":"bar","manifest":{"revision":"","roots":["a"]}}}
								}
							}`,
				},
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"]}`},
							{"a/policy.rego", `package a
								p[1337] { true }`},
						},
					},
					readWithBundleName: true,
					expData: `{
								"system":{
									"bundles":{"bundle1":{"etag":"bar","manifest":{"revision":"","roots":["a"]}}}
								}
							}`,
				},
				deactivation{
					bundles: map[string]struct{}{"bundle1": {}},
					expData: `{"system":{"bundles":{}}}`,
				},
			},
		},

		{
			note: "v0, lazy replaced by non-lazy",
			updates: []any{
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"], "rego_version": 0}`},
							{"a/policy.rego", `package a
								p[42] { true }`},
						},
					},
					lazy:               true,
					readWithBundleName: true,
					expData: `{
								"system":{
									"bundles":{"bundle1":{"etag":"bar","manifest":{"rego_version":0,"revision":"","roots":["a"]}}},
									"modules":{"bundle1/a/policy.rego":{"rego_version":0}}
								}
							}`,
				},
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"], "rego_version": 1}`},
							{"a/policy.rego", `package a
								p contains 42 if { true }`},
						},
					},
					readWithBundleName: true,
					expData: `{
								"system":{
									"bundles":{"bundle1":{"etag":"bar","manifest":{"rego_version":1,"revision":"","roots":["a"]}}},
									"modules":{}
								}
							}`,
				},
				deactivation{
					bundles: map[string]struct{}{"bundle1": {}},
					expData: `{"system":{"bundles":{},"modules":{}}}`,
				},
			},
		},

		{
			note: "v0 bundle replaced by v1 bundle, lazy",
			updates: []any{
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"], "rego_version": 0}`},
							{"a/policy.rego", `package a
								p[42] { true }`},
						},
					},
					lazy:               true,
					readWithBundleName: true,
					expData: `{
								"system":{
									"bundles":{"bundle1":{"etag":"bar","manifest":{"rego_version":0,"revision":"","roots":["a"]}}},
									"modules":{"bundle1/a/policy.rego":{"rego_version":0}}
								}
							}`,
				},
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"], "rego_version": 1}`},
							{"a/policy.rego", `package a
								p contains 42 if { true }`},
						},
					},
					lazy:               true,
					readWithBundleName: true,
					expData: `{
								"system":{
									"bundles":{"bundle1":{"etag":"bar","manifest":{"rego_version":1,"revision":"","roots":["a"]}}},
									"modules":{}
								}
							}`,
				},
				deactivation{
					bundles: map[string]struct{}{"bundle1": {}},
					expData: `{"system":{"bundles":{},"modules":{}}}`,
				},
			},
		},
		{
			note: "v0 bundle replaced by v1 bundle, not lazy",
			updates: []any{
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"], "rego_version": 0}`},
							{"a/policy.rego", `package a
								p[42] { true }`},
						},
					},
					readWithBundleName: true,
					expData: `{
								"system":{
									"bundles":{"bundle1":{"etag":"bar","manifest":{"rego_version":0,"revision":"","roots":["a"]}}},
									"modules":{"bundle1/a/policy.rego":{"rego_version":0}}
								}
							}`,
				},
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"], "rego_version": 1}`},
							{"a/policy.rego", `package a
								p contains 42 if { true }`},
						},
					},
					readWithBundleName: true,
					expData: `{
								"system":{
									"bundles":{"bundle1":{"etag":"bar","manifest":{"rego_version":1,"revision":"","roots":["a"]}}},
									"modules":{}
								}
							}`,
				},
				deactivation{
					bundles: map[string]struct{}{"bundle1": {}},
					expData: `{"system":{"bundles":{},"modules":{}}}`,
				},
			},
		},
		{
			note: "v0 bundle replaced by custom bundle, not lazy",
			updates: []any{
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"], "rego_version": 0}`},
							{"a/policy.rego", `package a
								p[42] { true }`},
						},
					},
					readWithBundleName: true,
					expData: `{
								"system":{
									"bundles":{"bundle1":{"etag":"bar","manifest":{"rego_version":0,"revision":"","roots":["a"]}}},
									"modules":{"bundle1/a/policy.rego":{"rego_version":0}}
								}
							}`,
				},
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"]}`}, // no rego-version
							{"a/policy.rego", `package a
								p contains 42 if { true }`},
						},
					},
					readWithBundleName: true,
					expData: `{
								"system":{
									"bundles":{"bundle1":{"etag":"bar","manifest":{"revision":"","roots":["a"]}}},
									"modules":{}
								}
							}`,
				},
				deactivation{
					bundles: map[string]struct{}{"bundle1": {}},
					expData: `{"system":{"bundles":{},"modules":{}}}`,
				},
			},
		},

		{
			note: "v1 bundle replaced by v0 bundle, lazy",
			updates: []any{
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"], "rego_version": 1}`},
							{"a/policy.rego", `package a
								p contains 42 if { true }`},
						},
					},
					lazy:               true,
					readWithBundleName: true,
					expData: `{
								"system":{
									"bundles":{"bundle1":{"etag":"bar","manifest":{"rego_version":1,"revision":"","roots":["a"]}}}
								}
							}`,
				},
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"], "rego_version": 0}`},
							{"a/policy.rego", `package a
								p[42] { true }`},
						},
					},
					lazy:               true,
					readWithBundleName: true,
					expData: `{
								"system":{
									"bundles":{"bundle1":{"etag":"bar","manifest":{"rego_version":0,"revision":"","roots":["a"]}}},
									"modules":{"bundle1/a/policy.rego":{"rego_version":0}}
								}
							}`,
				},
				deactivation{
					bundles: map[string]struct{}{"bundle1": {}},
					expData: `{"system":{"bundles":{},"modules":{}}}`,
				},
			},
		},
		{
			note: "v1 bundle replaced by v0 bundle, not lazy",
			updates: []any{
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"], "rego_version": 1}`},
							{"a/policy.rego", `package a
								p contains 42 if { true }`},
						},
					},
					readWithBundleName: true,
					expData: `{
								"system":{
									"bundles":{"bundle1":{"etag":"bar","manifest":{"rego_version":1,"revision":"","roots":["a"]}}}
								}
							}`,
				},
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"], "rego_version": 0}`},
							{"a/policy.rego", `package a
								p[42] { true }`},
						},
					},
					readWithBundleName: true,
					expData: `{
								"system":{
									"bundles":{"bundle1":{"etag":"bar","manifest":{"rego_version":0,"revision":"","roots":["a"]}}},
									"modules":{"bundle1/a/policy.rego":{"rego_version":0}}
								}
							}`,
				},
				deactivation{
					bundles: map[string]struct{}{"bundle1": {}},
					expData: `{"system":{"bundles":{},"modules":{}}}`,
				},
			},
		},
		{
			note: "custom bundle replaced by v0 bundle, lazy",
			updates: []any{
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"]}`}, // no rego-version
							{"a/policy.rego", `package a
								p contains 42 if { true }`},
						},
					},
					lazy:               true,
					readWithBundleName: true,
					expData: `{
								"system":{
									"bundles":{"bundle1":{"etag":"bar","manifest":{"revision":"","roots":["a"]}}}
								}
							}`,
				},
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"], "rego_version": 0}`},
							{"a/policy.rego", `package a
								p[42] { true }`},
						},
					},
					lazy:               true,
					readWithBundleName: true,
					expData: `{
								"system":{
									"bundles":{"bundle1":{"etag":"bar","manifest":{"rego_version":0,"revision":"","roots":["a"]}}},
									"modules":{"bundle1/a/policy.rego":{"rego_version":0}}
								}
							}`,
				},
				deactivation{
					bundles: map[string]struct{}{"bundle1": {}},
					expData: `{"system":{"bundles":{},"modules":{}}}`,
				},
			},
		},

		{
			note: "multiple v0 bundles, all dropped",
			updates: []any{
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"], "rego_version": 0}`},
							{"a/policy.rego", `package a
								p[42] { true }`},
						},
						"bundle2": {
							{"/.manifest", `{"roots": ["b"], "rego_version": 0}`},
							{"b/policy.rego", `package b
								p[42] { true }`},
						},
					},
					lazy:               true,
					readWithBundleName: true,
					expData: `{
								"system":{
									"bundles":{
										"bundle1":{"etag":"bar","manifest":{"rego_version":0,"revision":"","roots":["a"]}},
										"bundle2":{"etag":"bar","manifest":{"rego_version":0,"revision":"","roots":["b"]}}
									},
									"modules":{"bundle1/a/policy.rego":{"rego_version":0},"bundle2/b/policy.rego":{"rego_version":0}}
								}
							}`,
				},
				deactivation{
					bundles: map[string]struct{}{"bundle1": {}, "bundle2": {}},
					expData: `{"system":{"bundles":{},"modules":{}}}`,
				},
			},
		},

		{
			note: "multiple v0 bundles, one dropped",
			updates: []any{
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"], "rego_version": 0}`},
							{"a/policy.rego", `package a
								p[42] { true }`},
						},
						"bundle2": {
							{"/.manifest", `{"roots": ["b"], "rego_version": 0}`},
							{"b/policy.rego", `package b
								p[42] { true }`},
						},
					},
					lazy:               true,
					readWithBundleName: true,
					expData: `{
								"system":{
									"bundles":{
										"bundle1":{"etag":"bar","manifest":{"rego_version":0,"revision":"","roots":["a"]}},
										"bundle2":{"etag":"bar","manifest":{"rego_version":0,"revision":"","roots":["b"]}}
									},
									"modules":{"bundle1/a/policy.rego":{"rego_version":0},"bundle2/b/policy.rego":{"rego_version":0}}
								}
							}`,
				},
				deactivation{
					bundles: map[string]struct{}{"bundle1": {}},
					expData: `{
								"system":{
									"bundles":{
										"bundle2":{"etag":"bar","manifest":{"rego_version":0,"revision":"","roots":["b"]}}
									},
									"modules":{"bundle2/b/policy.rego":{"rego_version":0}}
								}
							}`,
				},
			},
		},

		{
			note: "v0 bundle with v1 bundle added",
			updates: []any{
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a"], "rego_version": 0}`},
							{"a/policy.rego", `package a
								p[42] { true }`},
						},
					},
					lazy:               true,
					readWithBundleName: true,
					expData: `{
								"system":{
									"bundles":{"bundle1":{"etag":"bar","manifest":{"rego_version":0,"revision":"","roots":["a"]}}},
									"modules":{"bundle1/a/policy.rego":{"rego_version":0}}
								}
							}`,
				},
				activation{
					bundles: bundles{
						"bundle2": {
							{"/.manifest", `{"roots": ["b"], "rego_version": 1}`},
							{"b/policy.rego", `package b
								p contains 42 if { true }`},
						},
					},
					lazy:               true,
					readWithBundleName: true,
					expData: `{
								"system":{
									"bundles":{
										"bundle1":{"etag":"bar","manifest":{"rego_version":0,"revision":"","roots":["a"]}},
										"bundle2":{"etag":"bar","manifest":{"rego_version":1,"revision":"","roots":["b"]}}
									},
									"modules":{"bundle1/a/policy.rego":{"rego_version":0}}
								}
							}`,
				},
				deactivation{
					bundles: map[string]struct{}{"bundle1": {}, "bundle2": {}},
					expData: `{"system":{"bundles":{},"modules":{}}}`,
				},
			},
		},

		{
			note: "mixed-version bundles, lazy",
			updates: []any{
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a", "b"], "rego_version": 0, "file_rego_versions": {"/b/policy.rego": 1}}`},
							{"a/policy.rego", `package a
								p[42] { true }`},
							{"b/policy.rego", `package b
								p contains 42 if { true }`},
						},
						"bundle2": {
							{"/.manifest", `{"roots": ["c", "d"], "rego_version": 1, "file_rego_versions": {"/d/policy.rego": 0}}`},
							{"c/policy.rego", `package c
								p contains 42 if { true }`},
							{"d/policy.rego", `package d
								p[42] { true }`},
						},
					},
					lazy:               true,
					readWithBundleName: true,
					expData: `{
								"system":{
									"bundles":{
										"bundle1":{"etag":"bar","manifest":{"file_rego_versions":{"/b/policy.rego":1},"rego_version":0,"revision":"","roots":["a","b"]}},
										"bundle2":{"etag":"bar","manifest":{"file_rego_versions":{"/d/policy.rego":0},"rego_version":1,"revision":"","roots":["c","d"]}}
									},
									"modules":{
										"bundle1/a/policy.rego":{"rego_version":0},
										"bundle2/d/policy.rego":{"rego_version":0}
									}
								}
							}`,
				},
				// replacing bundles
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a", "b"], "rego_version": 0, "file_rego_versions": {"/b/policy2.rego": 1}}`},
							{"a/policy2.rego", `package a
								q[42] { true }`},
							{"b/policy2.rego", `package b
								q contains 42 if { true }`},
						},
						"bundle2": {
							{"/.manifest", `{"roots": ["c", "d"], "rego_version": 1, "file_rego_versions": {"/d/policy2.rego": 0}}`},
							{"c/policy2.rego", `package c
								q contains 42 if { true }`},
							{"d/policy2.rego", `package d
								q[42] { true }`},
						},
					},
					lazy:               true,
					readWithBundleName: true,
					expData: `{
								"system":{
									"bundles":{
										"bundle1":{"etag":"bar","manifest":{"file_rego_versions":{"/b/policy2.rego":1},"rego_version":0,"revision":"","roots":["a","b"]}},
										"bundle2":{"etag":"bar","manifest":{"file_rego_versions":{"/d/policy2.rego":0},"rego_version":1,"revision":"","roots":["c","d"]}}
									},
									"modules":{
										"bundle1/a/policy2.rego":{"rego_version":0},
										"bundle2/d/policy2.rego":{"rego_version":0}
									}
								}
							}`,
				},
				deactivation{
					bundles: map[string]struct{}{"bundle1": {}, "bundle2": {}},
					expData: `{"system":{"bundles":{},"modules":{}}}`,
				},
			},
		},
		{
			note: "mixed-version bundles, lazy, read with NO bundle name",
			updates: []any{
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a", "b"], "rego_version": 0, "file_rego_versions": {"/b/policy.rego": 1}}`},
							{"a/policy.rego", `package a
								p[42] { true }`},
							{"b/policy.rego", `package b
								p contains 42 if { true }`},
						},
						"bundle2": {
							{"/.manifest", `{"roots": ["c", "d"], "rego_version": 1, "file_rego_versions": {"/d/policy.rego": 0}}`},
							{"c/policy.rego", `package c
								p contains 42 if { true }`},
							{"d/policy.rego", `package d
								p[42] { true }`},
						},
					},
					lazy: true,
					expData: `{
								"system":{
									"bundles":{
										"bundle1":{"etag":"bar","manifest":{"file_rego_versions":{"/b/policy.rego":1},"rego_version":0,"revision":"","roots":["a","b"]}},
										"bundle2":{"etag":"bar","manifest":{"file_rego_versions":{"/d/policy.rego":0},"rego_version":1,"revision":"","roots":["c","d"]}}
									},
									"modules":{
										"a/policy.rego":{"rego_version":0},
										"d/policy.rego":{"rego_version":0}
									}
								}
							}`,
				},
				// replacing bundles
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a", "b"], "rego_version": 0, "file_rego_versions": {"/b/policy2.rego": 1}}`},
							{"a/policy2.rego", `package a
								q[42] { true }`},
							{"b/policy2.rego", `package b
								q contains 42 if { true }`},
						},
						"bundle2": {
							{"/.manifest", `{"roots": ["c", "d"], "rego_version": 1, "file_rego_versions": {"/d/policy2.rego": 0}}`},
							{"c/policy2.rego", `package c
								q contains 42 if { true }`},
							{"d/policy2.rego", `package d
								q[42] { true }`},
						},
					},
					lazy: true,
					expData: `{
								"system":{
									"bundles":{
										"bundle1":{"etag":"bar","manifest":{"file_rego_versions":{"/b/policy2.rego":1},"rego_version":0,"revision":"","roots":["a","b"]}},
										"bundle2":{"etag":"bar","manifest":{"file_rego_versions":{"/d/policy2.rego":0},"rego_version":1,"revision":"","roots":["c","d"]}}
									},
									"modules":{
										"a/policy2.rego":{"rego_version":0},
										"d/policy2.rego":{"rego_version":0}
									}
								}
							}`,
				},
				deactivation{
					bundles: map[string]struct{}{"bundle1": {}, "bundle2": {}},
					expData: `{"system":{"bundles":{},"modules":{}}}`,
				},
			},
		},
		{
			note: "mixed-version bundles, not lazy",
			updates: []any{
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a", "b"], "rego_version": 0, "file_rego_versions": {"/b/policy.rego": 1}}`},
							{"a/policy.rego", `package a
								p[42] { true }`},
							{"b/policy.rego", `package b
								p contains 42 if { true }`},
						},
						"bundle2": {
							{"/.manifest", `{"roots": ["c", "d"], "rego_version": 1, "file_rego_versions": {"/d/policy.rego": 0}}`},
							{"c/policy.rego", `package c
								p contains 42 if { true }`},
							{"d/policy.rego", `package d
								p[42] { true }`},
						},
					},
					readWithBundleName: true,
					expData: `{
								"system":{
									"bundles":{
										"bundle1":{"etag":"bar","manifest":{"file_rego_versions":{"/b/policy.rego":1},"rego_version":0,"revision":"","roots":["a","b"]}},
										"bundle2":{"etag":"bar","manifest":{"file_rego_versions":{"/d/policy.rego":0},"rego_version":1,"revision":"","roots":["c","d"]}}
									},
									"modules":{
										"bundle1/a/policy.rego":{"rego_version":0},
										"bundle2/d/policy.rego":{"rego_version":0}
									}
								}
							}`,
				},
				// replacing bundles
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a", "b"], "rego_version": 0, "file_rego_versions": {"/b/policy2.rego": 1}}`},
							{"a/policy2.rego", `package a
								q[42] { true }`},
							{"b/policy2.rego", `package b
								q contains 42 if { true }`},
						},
						"bundle2": {
							{"/.manifest", `{"roots": ["c", "d"], "rego_version": 1, "file_rego_versions": {"/d/policy2.rego": 0}}`},
							{"c/policy2.rego", `package c
								q contains 42 if { true }`},
							{"d/policy2.rego", `package d
								q[42] { true }`},
						},
					},
					readWithBundleName: true,
					expData: `{
								"system":{
									"bundles":{
										"bundle1":{"etag":"bar","manifest":{"file_rego_versions":{"/b/policy2.rego":1},"rego_version":0,"revision":"","roots":["a","b"]}},
										"bundle2":{"etag":"bar","manifest":{"file_rego_versions":{"/d/policy2.rego":0},"rego_version":1,"revision":"","roots":["c","d"]}}
									},
									"modules":{
										"bundle1/a/policy2.rego":{"rego_version":0},
										"bundle2/d/policy2.rego":{"rego_version":0}
									}
								}
							}`,
				},
				deactivation{
					bundles: map[string]struct{}{"bundle1": {}, "bundle2": {}},
					expData: `{"system":{"bundles":{},"modules":{}}}`,
				},
			},
		},

		{
			note:               "mixed-version bundles, lazy, --v0-compatible",
			runtimeRegoVersion: ast.RegoV0,
			updates: []any{
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a", "b"], "rego_version": 0, "file_rego_versions": {"/b/policy.rego": 1}}`},
							{"a/policy.rego", `package a
								p[42] { true }`},
							{"b/policy.rego", `package b
								p contains 42 if { true }`},
						},
						"bundle2": {
							{"/.manifest", `{"roots": ["c", "d"], "rego_version": 1, "file_rego_versions": {"/d/policy.rego": 0}}`},
							{"c/policy.rego", `package c
								p contains 42 if { true }`},
							{"d/policy.rego", `package d
								p[42] { true }`},
						},
					},
					lazy:               true,
					readWithBundleName: true,
					expData: `{
								"system":{
									"bundles":{
										"bundle1":{"etag":"bar","manifest":{"file_rego_versions":{"/b/policy.rego":1},"rego_version":0,"revision":"","roots":["a","b"]}},
										"bundle2":{"etag":"bar","manifest":{"file_rego_versions":{"/d/policy.rego":0},"rego_version":1,"revision":"","roots":["c","d"]}}
									},
									"modules":{
										"bundle1/b/policy.rego":{"rego_version":1},
										"bundle2/c/policy.rego":{"rego_version":1}
									}
								}
							}`,
				},
				// replacing bundles
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a", "b"], "rego_version": 0, "file_rego_versions": {"/b/policy2.rego": 1}}`},
							{"a/policy2.rego", `package a
								q[42] { true }`},
							{"b/policy2.rego", `package b
								q contains 42 if { true }`},
						},
						"bundle2": {
							{"/.manifest", `{"roots": ["c", "d"], "rego_version": 1, "file_rego_versions": {"/d/policy2.rego": 0}}`},
							{"c/policy2.rego", `package c
								q contains 42 if { true }`},
							{"d/policy2.rego", `package d
								q[42] { true }`},
						},
					},
					lazy:               true,
					readWithBundleName: true,
					expData: `{
								"system":{
									"bundles":{
										"bundle1":{"etag":"bar","manifest":{"file_rego_versions":{"/b/policy2.rego":1},"rego_version":0,"revision":"","roots":["a","b"]}},
										"bundle2":{"etag":"bar","manifest":{"file_rego_versions":{"/d/policy2.rego":0},"rego_version":1,"revision":"","roots":["c","d"]}}
									},
									"modules":{
										"bundle1/b/policy2.rego":{"rego_version":1},
										"bundle2/c/policy2.rego":{"rego_version":1}
									}
								}
							}`,
				},
				deactivation{
					bundles: map[string]struct{}{"bundle1": {}, "bundle2": {}},
					expData: `{"system":{"bundles":{},"modules":{}}}`,
				},
			},
		},
		{
			note:               "mixed-version bundles, lazy, read with NO bundle name, --v0-compatible",
			runtimeRegoVersion: ast.RegoV0,
			updates: []any{
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a", "b"], "rego_version": 0, "file_rego_versions": {"/b/policy.rego": 1}}`},
							{"a/policy.rego", `package a
								p[42] { true }`},
							{"b/policy.rego", `package b
								p contains 42 if { true }`},
						},
						"bundle2": {
							{"/.manifest", `{"roots": ["c", "d"], "rego_version": 1, "file_rego_versions": {"/d/policy.rego": 0}}`},
							{"c/policy.rego", `package c
								p contains 42 if { true }`},
							{"d/policy.rego", `package d
								p[42] { true }`},
						},
					},
					lazy: true,
					expData: `{
								"system":{
									"bundles":{
										"bundle1":{"etag":"bar","manifest":{"file_rego_versions":{"/b/policy.rego":1},"rego_version":0,"revision":"","roots":["a","b"]}},
										"bundle2":{"etag":"bar","manifest":{"file_rego_versions":{"/d/policy.rego":0},"rego_version":1,"revision":"","roots":["c","d"]}}
									},
									"modules":{
										"b/policy.rego":{"rego_version":1},
										"c/policy.rego":{"rego_version":1}
									}
								}
							}`,
				},
				// replacing bundles
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a", "b"], "rego_version": 0, "file_rego_versions": {"/b/policy2.rego": 1}}`},
							{"a/policy2.rego", `package a
								q[42] { true }`},
							{"b/policy2.rego", `package b
								q contains 42 if { true }`},
						},
						"bundle2": {
							{"/.manifest", `{"roots": ["c", "d"], "rego_version": 1, "file_rego_versions": {"/d/policy2.rego": 0}}`},
							{"c/policy2.rego", `package c
								q contains 42 if { true }`},
							{"d/policy2.rego", `package d
								q[42] { true }`},
						},
					},
					lazy: true,
					expData: `{
								"system":{
									"bundles":{
										"bundle1":{"etag":"bar","manifest":{"file_rego_versions":{"/b/policy2.rego":1},"rego_version":0,"revision":"","roots":["a","b"]}},
										"bundle2":{"etag":"bar","manifest":{"file_rego_versions":{"/d/policy2.rego":0},"rego_version":1,"revision":"","roots":["c","d"]}}
									},
									"modules":{
										"b/policy2.rego":{"rego_version":1},
										"c/policy2.rego":{"rego_version":1}
									}
								}
							}`,
				},
				deactivation{
					bundles: map[string]struct{}{"bundle1": {}, "bundle2": {}},
					expData: `{"system":{"bundles":{},"modules":{}}}`,
				},
			},
		},
		{
			note:               "mixed-version bundles, not lazy, --v0-compatible",
			runtimeRegoVersion: ast.RegoV0,
			updates: []any{
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a", "b"], "rego_version": 0, "file_rego_versions": {"/b/policy.rego": 1}}`},
							{"a/policy.rego", `package a
								p[42] { true }`},
							{"b/policy.rego", `package b
								p contains 42 if { true }`},
						},
						"bundle2": {
							{"/.manifest", `{"roots": ["c", "d"], "rego_version": 1, "file_rego_versions": {"/d/policy.rego": 0}}`},
							{"c/policy.rego", `package c
								p contains 42 if { true }`},
							{"d/policy.rego", `package d
								p[42] { true }`},
						},
					},
					readWithBundleName: true,
					expData: `{
								"system":{
									"bundles":{
										"bundle1":{"etag":"bar","manifest":{"file_rego_versions":{"/b/policy.rego":1},"rego_version":0,"revision":"","roots":["a","b"]}},
										"bundle2":{"etag":"bar","manifest":{"file_rego_versions":{"/d/policy.rego":0},"rego_version":1,"revision":"","roots":["c","d"]}}
									},
									"modules":{
										"bundle1/b/policy.rego":{"rego_version":1},
										"bundle2/c/policy.rego":{"rego_version":1}
									}
								}
							}`,
				},
				// replacing bundles
				activation{
					bundles: bundles{
						"bundle1": {
							{"/.manifest", `{"roots": ["a", "b"], "rego_version": 0, "file_rego_versions": {"/b/policy2.rego": 1}}`},
							{"a/policy2.rego", `package a
								q[42] { true }`},
							{"b/policy2.rego", `package b
								q contains 42 if { true }`},
						},
						"bundle2": {
							{"/.manifest", `{"roots": ["c", "d"], "rego_version": 1, "file_rego_versions": {"/d/policy2.rego": 0}}`},
							{"c/policy2.rego", `package c
								q contains 42 if { true }`},
							{"d/policy2.rego", `package d
								q[42] { true }`},
						},
					},
					readWithBundleName: true,
					expData: `{
								"system":{
									"bundles":{
										"bundle1":{"etag":"bar","manifest":{"file_rego_versions":{"/b/policy2.rego":1},"rego_version":0,"revision":"","roots":["a","b"]}},
										"bundle2":{"etag":"bar","manifest":{"file_rego_versions":{"/d/policy2.rego":0},"rego_version":1,"revision":"","roots":["c","d"]}}
									},
									"modules":{
										"bundle1/b/policy2.rego":{"rego_version":1},
										"bundle2/c/policy2.rego":{"rego_version":1}
									}
								}
							}`,
				},
				deactivation{
					bundles: map[string]struct{}{"bundle1": {}, "bundle2": {}},
					expData: `{"system":{"bundles":{},"modules":{}}}`,
				},
			},
		},
	}

	for _, tc := range tests {
		t.Run(tc.note, func(t *testing.T) {
			mockStore := mock.New()
			runtimeRegoVersion := cmp.Or(tc.runtimeRegoVersion, ast.DefaultRegoVersion)

			for _, update := range tc.updates {
				if act, ok := update.(activation); ok {
					bundles := map[string]*Bundle{}
					for bundleName, files := range act.bundles {
						br := NewCustomReader(NewTarballLoaderWithBaseURL(archive.MustWriteTarGz(files), "")).
							WithBundleEtag("bar").
							WithLazyLoadingMode(act.lazy).
							WithRegoVersion(runtimeRegoVersion)

						if act.readWithBundleName {
							br = br.WithBundleName(bundleName)
						}

						bundle := must(br.Read())(t)
						bundles[bundleName] = &bundle
					}

					mustActivate(t, mockStore, &ActivateOpts{
						Bundles:       bundles,
						ParserOptions: ast.ParserOptions{RegoVersion: runtimeRegoVersion},
					})
					verifyResultRead(t, mockStore, act.expData)
				} else if deact, ok := update.(deactivation); ok {
					mustDeactivate(t, mockStore, &DeactivateOpts{
						BundleNames:   deact.bundles,
						ParserOptions: ast.ParserOptions{RegoVersion: runtimeRegoVersion},
					})
					verifyResultRead(t, mockStore, deact.expData)
				}
			}
		})
	}
}

func TestBundleLifecycleRootWithSpace(t *testing.T) {
	// A v0 module, so that a rego-version is recorded under /system/modules.
	files := [][2]string{
		{"/.manifest", `{"revision": "rev-1", "roots": ["a/b/foo bar"], "rego_version": 0}`},
		{"/a/b/foo bar/data.json", `{"x": 1}`},
		{"/a/b/foo bar/policy.rego", `package a.b["foo bar"]

			p[42] { true }
		`},
	}

	// The replacement bundle drops the policy, which must therefore be erased.
	files2 := [][2]string{
		{"/.manifest", `{"revision": "rev-2", "roots": ["a/b/foo bar"], "rego_version": 1}`},
		{"/a/b/foo bar/data.json", `{"y": 2}`},
	}

	newStores := map[string]func(testing.TB) storage.Store{
		"inmem": func(testing.TB) storage.Store { return mock.New() },
		"disk": func(tb testing.TB) storage.Store {
			return must(disk.New(tb.Context(), logging.NewNoOpLogger(), nil, disk.Options{Dir: tb.TempDir()}))(tb)
		},
	}

	for _, lazy := range []bool{false, true} {
		for _, name := range util.Sorted(util.Keys(newStores)) {
			t.Run(fmt.Sprintf("lazy=%v/%s", lazy, name), func(t *testing.T) {
				store := newStores[name](t)
				compiler := ast.NewCompiler()

				read := func(files [][2]string) map[string]*Bundle {
					b := must(NewCustomReader(NewTarballLoaderWithBaseURL(archive.MustWriteTarGz(files), "")).
						WithLazyLoadingMode(lazy).
						WithBundleName("bundle1").
						Read())(t)
					return map[string]*Bundle{"bundle1": &b}
				}

				bundles := read(files)
				mustActivate(t, store, &ActivateOpts{Compiler: compiler, Bundles: bundles})

				verifyReadBundleNames(t, store, nil, util.Keys(bundles)...)
				verifyBundleModulesCompiled(t, compiler, bundles)

				// The policy ID has a bundle prefix in non-lazy mode.
				// When round tripped via storage.Policy, IDs must remain
				// unescaped to preserve paths with spaces.
				id := getStoredPolicyID(t, store)

				verifyResultRead(t, store, fmt.Sprintf(`{
					"a": {"b": {"foo bar": {"x": 1}}},
					"system": {
						"bundles": {
							"bundle1": {
								"manifest": {
									"revision": "rev-1",
									"roots": ["a/b/foo bar"],
									"rego_version": 0
								},
								"etag": ""
							}
						},
						"modules": {%q: {"rego_version": 0}}
					}
				}`, id))

				// Activating a bundle that no longer contains the policy must
				// erase both the policy and its rego-version bookkeeping. The
				// disk store drops the emptied /system/modules object entirely,
				// the in-memory store leaves it behind empty.
				mustActivate(t, store, &ActivateOpts{Compiler: ast.NewCompiler(), Bundles: read(files2)})

				modules := `, "modules": {}`
				if name == "disk" {
					modules = ""
				}

				verifyResultRead(t, store, fmt.Sprintf(`{
					"a": {"b": {"foo bar": {"y": 2}}},
					"system": {
						"bundles": {
							"bundle1": {
								"manifest": {
									"revision": "rev-2",
									"roots": ["a/b/foo bar"],
									"rego_version": 1
								},
								"etag": ""
							}
						}%s
					}
				}`, modules))

				txn := storage.NewTransactionOrDie(t.Context(), store)
				defer store.Abort(t.Context(), txn)

				if ids := must(store.ListPolicies(t.Context(), txn))(t); len(ids) != 0 {
					t.Errorf("expected policy %q to have been erased, store still has %v", id, ids)
				}
			})
		}
	}
}

// getStoredPolicyID returns the ID of the single policy in the store, checking
// that it can be read back under that ID.
func getStoredPolicyID(tb testing.TB, store storage.Store) string {
	tb.Helper()

	txn := storage.NewTransactionOrDie(tb.Context(), store)
	defer store.Abort(tb.Context(), txn)

	ids := must(store.ListPolicies(tb.Context(), txn))(tb)
	if len(ids) != 1 {
		tb.Fatalf("expected exactly one policy in store, got %v", ids)
	}

	if _, err := store.GetPolicy(tb.Context(), txn, ids[0]); err != nil {
		tb.Errorf("unexpected error reading policy %q: %s", ids[0], err)
	}

	return ids[0]
}

func TestBundleLazyModeLifecycleRaw(t *testing.T) {
	files := [][2]string{
		{"/a/b/c/data.json", "[1,2,3]"},
		{"/a/b/d/data.json", "true"},
		{"/a/b/y/data.yaml", `foo: 1`},
		{"/example/example.rego", `package example
			p contains 42 if { true }
		`},
		{"/example/example_v0.rego", `package example
			q[42] { true }
		`},
		{"/authz/allow/policy.wasm", `wasm-module`},
		{"/data.json", `{"x": {"y": true}, "a": {"b": {"z": true}}}`},
		{"/.manifest", `{
			"revision": "foo", 
			"roots": ["a", "example", "x", "authz"],
			"wasm":[{"entrypoint": "authz/allow", "module": "/authz/allow/policy.wasm"}],
			"rego_version": 1,
			"file_rego_versions": {"/example/example_v0.rego": 0}
		}`},
	}

	bundle := must(NewCustomReader(NewTarballLoaderWithBaseURL(archive.MustWriteTarGz(files), "")).
		WithBundleEtag("bar").
		WithLazyLoadingMode(true).
		Read())(t)

	mockStore := mock.New()
	compiler := ast.NewCompiler()
	bundles := map[string]*Bundle{"bundle1": &bundle}

	mustActivate(t, mockStore, &ActivateOpts{
		Compiler:     compiler,
		Bundles:      bundles,
		ExtraModules: map[string]*ast.Module{"mod1": ast.MustParseModule("package x\np = true")},
	})

	// Ensure the bundle was activated
	verifyReadBundleNames(t, mockStore, nil, util.Keys(bundles)...)
	verifyBundleModulesCompiled(t, compiler, bundles)
	verifyResultRead(t, mockStore, `{
		"a": {
			"b": {
				"c": [1,2,3],
				"d": true,
				"y": {
					"foo": 1
				},
				"z": true
			}
		},
		"x": {
			"y": true
		},
		"system": {
			"bundles": {
				"bundle1": {
					"manifest": {
						"revision": "foo",
						"roots": ["a", "example", "x", "authz"],
						"wasm": [
							{
								"entrypoint": "authz/allow",
								"module": "/authz/allow/policy.wasm"
							}
						],
						"rego_version": 1,
						"file_rego_versions": {
							"/example/example_v0.rego": 0
						}
					},
					"etag": "bar",
					"wasm": {
						"/authz/allow/policy.wasm": "d2FzbS1tb2R1bGU="
					}
				}
			},
			"modules":{
				"example/example.rego":{
					"rego_version":1
				},
				"example/example_v0.rego":{
					"rego_version":0
				}
			}
		}
	}`)

	// Ensure that the extra module was included
	if _, ok := compiler.Modules["mod1"]; !ok {
		t.Fatal("expected extra module to be compiled")
	}

	mustDeactivate(t, mockStore, &DeactivateOpts{BundleNames: map[string]struct{}{"bundle1": {}}})

	// Expect the store to have been cleared out after deactivating the bundle
	verifyReadBundleNames(t, mockStore, nil)
	verifyResultRead(t, mockStore, `{"system": {"bundles": {}, "modules": {}}}`)

	mockStore.AssertValid(t)
}

func TestBundleLazyModeLifecycleRawInvalidData(t *testing.T) {
	tests := map[string]struct {
		files [][2]string
		err   error
	}{
		"non-object root": {[][2]string{{"/data.json", `[1,2,3]`}}, errors.New("root value must be object")},
		"invalid yaml":    {[][2]string{{"/a/b/data.yaml", `"foo`}}, errors.New("yaml: found unexpected end of stream")},
	}

	for name, tc := range tests {
		t.Run(name, func(t *testing.T) {
			bundle := must(NewCustomReader(NewTarballLoaderWithBaseURL(archive.MustWriteTarGz(tc.files), "")).
				WithBundleEtag("bar").
				WithLazyLoadingMode(true).
				Read())(t)

			mockStore := mock.New()
			txn := storage.NewTransactionOrDie(t.Context(), mockStore, storage.WriteParams)

			err := Activate(&ActivateOpts{
				Ctx:      t.Context(),
				Store:    mockStore,
				Txn:      txn,
				Compiler: ast.NewCompiler(),
				Metrics:  metrics.NoOp(),
				Bundles:  map[string]*Bundle{"bundle1": &bundle},
			})

			if err == nil {
				t.Fatal("Expected error but got none")
			}

			if tc.err != nil && tc.err.Error() != err.Error() {
				t.Fatalf("Expected error message %v but got %v", tc.err.Error(), err.Error())
			}
		})
	}
}

func TestBundleLazyModeLifecycle(t *testing.T) {
	mockStore := mock.New()
	compiler := ast.NewCompiler()
	extraMods := map[string]*ast.Module{"mod1": ast.MustParseModule("package x\np = true")}

	// v1 bundle
	b1Files := [][2]string{
		{"/.manifest", `{"roots": ["a"], "rego_version": 1}`},
		{"a/policy.rego", "package a\np contains 42 if { true }"},
		{"/data.json", `{"a": {"b": "foo"}}`},
	}

	bundle1 := must(NewCustomReader(NewTarballLoaderWithBaseURL(archive.MustWriteTarGz(b1Files), "")).
		WithBundleEtag("foo").
		WithLazyLoadingMode(true).
		WithBundleName("bundle1").
		Read())(t)

	// v0 bundle
	bundle2 := bundleFromFiles(t, "bundle2", [][2]string{
		{"/.manifest", `{"roots": ["b", "c"], "rego_version": 0}`},
		{"b/policy.rego", `package b
			p[42] { true }
		`},
		{"/data.json", `{}`},
	})

	bundles := map[string]*Bundle{"bundle1": &bundle1, "bundle2": &bundle2}

	mustActivate(t, mockStore, &ActivateOpts{Compiler: compiler, Bundles: bundles, ExtraModules: extraMods})

	// Ensure the bundle was activated
	verifyReadBundleNames(t, mockStore, nil, util.Keys(bundles)...)
	verifyBundleModulesCompiled(t, compiler, bundles)
	verifyResultRead(t, mockStore, `{
		"a": {
			"b": "foo"
		},
		"system": {
			"bundles": {
				"bundle1": {
					"manifest": {
						"revision": "",
						"roots": ["a"],
						"rego_version": 1
					},
					"etag": "foo"
				},
				"bundle2": {
					"manifest": {
						"revision": "",
						"roots": ["b", "c"],
						"rego_version": 0
					},
					"etag": ""
				}
			},
			"modules":{
				"bundle1/a/policy.rego":{
					"rego_version":1
				},
				"bundle2/b/policy.rego":{
					"rego_version":0
				}
			}
		}
	}`)

	// Ensure that the extra module was included
	if _, ok := compiler.Modules["mod1"]; !ok {
		t.Fatal("expected extra module to be compiled")
	}

	mustDeactivate(t, mockStore, &DeactivateOpts{BundleNames: map[string]struct{}{"bundle1": {}, "bundle2": {}}})

	// Expect the store to have been cleared out after deactivating the bundles
	verifyReadBundleNames(t, mockStore, nil)
	verifyResultRead(t, mockStore, `{"system": {"bundles": {}, "modules": {}}}`)

	mockStore.AssertValid(t)
}

func TestBundleLazyModeLifecycleRawNoBundleRoots(t *testing.T) {
	files := [][2]string{
		{"/a/b/c/data.json", "[1,2,3]"},
		{"/a/b/d/data.json", "true"},
		{"/a/b/y/data.yaml", `foo: 1`},
		{"/example/example.rego", `package example`},
		{"/data.json", `{"x": {"y": true}, "a": {"b": {"z": true}}}`},
		{"/.manifest", `{"revision": "rev-1"}`},
	}

	bundle := must(NewCustomReader(NewTarballLoaderWithBaseURL(archive.MustWriteTarGz(files), "")).
		WithBundleEtag("foo").
		WithLazyLoadingMode(true).
		Read())(t)

	compiler := ast.NewCompiler()
	mockStore := mock.New()
	bundles := map[string]*Bundle{"bundle1": &bundle}

	mustActivate(t, mockStore, &ActivateOpts{Bundles: bundles, Compiler: compiler})

	// Ensure the bundle was activated
	verifyReadBundleNames(t, mockStore, nil, util.Keys(bundles)...)
	verifyBundleModulesCompiled(t, compiler, bundles)
	verifyResultRead(t, mockStore, `{
		"a": {
			"b": {
				"c": [1,2,3],
				"d": true,
				"y": {
					"foo": 1
				},
				"z": true
			}
		},
		"x": {
			"y": true
		},
		"system": {
			"bundles": {
				"bundle1": {
					"manifest": {
						"revision": "rev-1",
						"roots": [""]
					},
					"etag": "foo"
				}
			}
		}
	}`)

	files = [][2]string{
		{"/c/data.json", `{"hello": "world"}`},
		{"/.manifest", `{"revision": "rev-2"}`},
	}

	bundle = must(NewCustomReader(NewTarballLoaderWithBaseURL(archive.MustWriteTarGz(files), "")).
		WithBundleEtag("bar").
		WithLazyLoadingMode(true).
		Read())(t)

	mustActivate(t, mockStore, &ActivateOpts{Compiler: compiler, Bundles: map[string]*Bundle{"bundle1": &bundle}})
	verifyResultRead(t, mockStore, `{
		"c": {
			"hello": "world"
		},
		"system": {
		"bundles": {
			"bundle1": {
				"manifest": {
					"revision": "rev-2",
					"roots": [""]
				},
				"etag": "bar"
			}
		}
		}
	}`)
}

func TestBundleLazyModeLifecycleRawNoBundleRootsDiskStorage(t *testing.T) {
	dir := t.TempDir()
	t.Log(dir)
	store := must(disk.New(t.Context(), logging.NewNoOpLogger(), nil, disk.Options{Dir: dir}))(t)
	compiler := ast.NewCompiler()

	files := [][2]string{
		{"/a/b/c/data.json", "[1,2,3]"},
		{"/a/b/d/data.json", "true"},
		{"/a/b/y/data.yaml", `foo: 1`},
		{"/example/example.rego", `package example`},
		{"/data.json", `{"x": {"y": true}, "a": {"b": {"z": true}}}`},
		{"/.manifest", `{"revision": "rev-1"}`},
	}

	bundle := must(NewCustomReader(NewTarballLoaderWithBaseURL(archive.MustWriteTarGz(files), "")).
		WithBundleEtag("foo").
		WithLazyLoadingMode(true).
		Read())(t)

	bundles := map[string]*Bundle{"bundle1": &bundle}

	mustActivate(t, store, &ActivateOpts{Compiler: compiler, Bundles: bundles})

	// Ensure the bundle was activated
	verifyReadBundleNames(t, store, nil, util.Keys(bundles)...)
	verifyBundleModulesCompiled(t, compiler, bundles)
	verifyResultRead(t, store, `{
			"a": {
				"b": {
					"c": [1,2,3],
					"d": true,
					"y": {
						"foo": 1
					},
					"z": true
				}
			},
			"x": {
				"y": true
			},
			"system": {
				"bundles": {
					"bundle1": {
						"manifest": {
							"revision": "rev-1",
							"roots": [""]
						},
						"etag": "foo"
					}
				}
			}
		}`)

	files = [][2]string{
		{"/c/data.json", `{"hello": "world"}`},
		{"/.manifest", `{"revision": "rev-2"}`},
	}

	bundle = must(NewCustomReader(NewTarballLoaderWithBaseURL(archive.MustWriteTarGz(files), "")).
		WithBundleEtag("bar").
		WithLazyLoadingMode(true).
		Read())(t)

	bundles = map[string]*Bundle{"bundle1": &bundle}

	mustActivate(t, store, &ActivateOpts{Compiler: compiler, Bundles: bundles})
	verifyResultRead(t, store, `{
			"c": {
				"hello": "world"
			},
			"system": {
				"bundles": {
				"bundle1": {
					"manifest": {
						"revision": "rev-2",
						"roots": [""]
					},
					"etag": "bar"
				}
				}
			}
		}`)
}

func TestBundleLazyModeLifecycleNoBundleRoots(t *testing.T) {
	mockStore := mock.New()
	compiler := ast.NewCompiler()

	mod1 := "package a\np = true"

	b := Bundle{
		Manifest: Manifest{Revision: "rev-1"},
		Data: unpack(map[string]any{
			"a.b":   "foo",
			"a.e.f": "bar",
			"a.x":   []map[string]string{{"name": "john"}, {"name": "jane"}},
		}),
		Modules: []ModuleFile{moduleFile("a/policy.rego", mod1)},
		Etag:    "foo",
	}

	bundle1 := bundleFromRoundtrip(t, "bundle1", b)
	bundles := map[string]*Bundle{"bundle1": &bundle1}

	mustActivate(t, mockStore, &ActivateOpts{Compiler: compiler, Bundles: bundles})
	verifyResultRead(t, mockStore, `{
         "a": {
            "b": "foo",
            "e": {
               "f": "bar"
            },
            "x": [{"name": "john"}, {"name": "jane"}]
         },
         "system": {
            "bundles": {
               "bundle1": {
                  "manifest": {
                     "revision": "rev-1",
                     "roots": [""]
                  },
                  "etag": ""
               }
            }
         }
    }`)

	// add a new bundle with no roots. this means all the data from the currently activated should be removed
	bundle2 := bundleFromRoundtrip(t, "bundle1", Bundle{
		Manifest: Manifest{Revision: "rev-2"},
		Data:     unpack(map[string]any{"c.hello": "world"}),
		Etag:     "bar",
	})

	mustActivate(t, mockStore, &ActivateOpts{Compiler: compiler, Bundles: map[string]*Bundle{"bundle1": &bundle2}})
	verifyResultRead(t, mockStore, `{
		"c": {
			"hello": "world"
		},
		"system": {
			"bundles": {
			"bundle1": {
				"manifest": {
					"revision": "rev-2",
					"roots": [""]
				},
				"etag": ""
			}
			}
		}
	}`)
}

func TestBundleLazyModeLifecycleNoBundleRootsDiskStorage(t *testing.T) {
	store := must(disk.New(t.Context(), logging.NewNoOpLogger(), nil, disk.Options{Dir: t.TempDir()}))(t)
	compiler := ast.NewCompiler()
	mod1 := "package a\np = true"

	b := Bundle{
		Manifest: Manifest{Revision: "rev-1"},
		Data: unpack(map[string]any{
			"a.b":   "foo",
			"a.e.f": "bar",
			"a.x":   []map[string]string{{"name": "john"}, {"name": "jane"}},
		}),
		Modules: []ModuleFile{moduleFile("a/policy.rego", mod1)},
		Etag:    "foo",
	}

	bundle1 := bundleFromRoundtrip(t, "bundle1", b)
	bundles := map[string]*Bundle{"bundle1": &bundle1}

	mustActivate(t, store, &ActivateOpts{Compiler: compiler, Bundles: bundles})

	// Ensure the snapshot bundle was activated
	verifyReadBundleNames(t, store, nil, util.Keys(bundles)...)
	verifyBundleModulesCompiled(t, compiler, bundles)
	verifyResultRead(t, store, `{
			"a": {
				"b": "foo",
				"e": {
				"f": "bar"
				},
				"x": [{"name": "john"}, {"name": "jane"}]
			},
			"system": {
				"bundles": {
				"bundle1": {
					"manifest": {
						"revision": "rev-1",
						"roots": [""]
					},
					"etag": ""
				}
				}
			}
		}`)

	// add a new bundle with no roots. this means all the data from the currently activated should be removed
	bundle2 := bundleFromRoundtrip(t, "bundle1", Bundle{
		Manifest: Manifest{Revision: "rev-2"},
		Data:     unpack(map[string]any{"c.hello": "world"}),
		Etag:     "bar",
	})

	mustActivate(t, store, &ActivateOpts{Compiler: compiler, Bundles: map[string]*Bundle{"bundle1": &bundle2}})
	verifyResultRead(t, store, `{
			"c": {
				"hello": "world"
			},
			"system": {
				"bundles": {
				"bundle1": {
					"manifest": {
						"revision": "rev-2",
						"roots": [""]
					},
					"etag": ""
				}
				}
			}
		}`)
}

func TestBundleLazyModeLifecycleMixBundleTypeActivationDiskStorage(t *testing.T) {
	store := must(disk.New(t.Context(), logging.NewNoOpLogger(), nil, disk.Options{Dir: t.TempDir()}))(t)
	compiler := ast.NewCompiler()
	mod1 := "package a\np = true"

	bundle1 := bundleFromRoundtrip(t, "bundle1", Bundle{
		Manifest: Manifest{Revision: "snap-1", Roots: &[]string{"a"}},
		Data: unpack(map[string]any{
			"a.b":   "foo",
			"a.e.f": "bar",
			"a.x":   []map[string]string{{"name": "john"}, {"name": "jane"}},
		}),
		Modules: []ModuleFile{moduleFile("a/policy.rego", mod1)},
		Etag:    "foo",
	})

	// create a delta bundle and activate it

	// add a new object member
	bundle2 := bundleFromRoundtrip(t, "bundle2", Bundle{
		Manifest: Manifest{Revision: "delta-1", Roots: &[]string{"x"}},
		Patch:    Patch{Data: []PatchOperation{{Op: "upsert", Path: "/x/y", Value: []string{"foo", "bar"}}}},
		Etag:     "bar",
	})
	bundles := map[string]*Bundle{"bundle1": &bundle1, "bundle2": &bundle2}

	mustActivate(t, store, &ActivateOpts{Compiler: compiler, Bundles: bundles})

	// Ensure the patches were applied
	verifyResultRead(t, store, `{
			"a": {
				"b": "foo",
				"e": {
				"f": "bar"
				},
				"x": [{"name": "john"}, {"name": "jane"}]
			},
			"x": {
				"y": ["foo","bar"]
			},
			"system": {
				"bundles": {
				"bundle1": {
					"manifest": {
						"revision": "snap-1",
						"roots": ["a"]
					},
					"etag": ""
				},
				"bundle2": {
					"manifest": {
						"revision": "delta-1",
						"roots": ["x"]
					},
					"etag": ""
				}
				}
			}
		}`)
}

func TestBundleLazyModeLifecycleOldBundleEraseDiskStorage(t *testing.T) {
	store := must(disk.New(t.Context(), logging.NewNoOpLogger(), nil, disk.Options{Dir: t.TempDir()}))(t)
	compiler := ast.NewCompiler()
	mod1 := "package a\np = true"

	b := Bundle{
		Manifest: Manifest{Revision: "rev-1", Roots: &[]string{"a"}},
		Data: unpack(map[string]any{
			"a.b":   "foo",
			"a.e.f": "bar",
			"a.x":   []map[string]string{{"name": "john"}, {"name": "jane"}},
		}),
		Modules: []ModuleFile{moduleFile("a/policy.rego", mod1)},
		Etag:    "foo",
	}

	bundle1 := bundleFromRoundtrip(t, "bundle1", b)
	bundles := map[string]*Bundle{"bundle1": &bundle1}

	mustActivate(t, store, &ActivateOpts{Compiler: compiler, Bundles: bundles})

	// Ensure the snapshot bundle was activated
	verifyReadBundleNames(t, store, nil, util.Keys(bundles)...)
	verifyBundleModulesCompiled(t, compiler, bundles)
	verifyResultRead(t, store, `{
			"a": {
				"b": "foo",
				"e": {
					"f": "bar"
				},
				"x": [{"name": "john"}, {"name": "jane"}]
			},
			"system": {
				"bundles": {
					"bundle1": {
						"manifest": {
							"revision": "rev-1",
							"roots": ["a"]
						},
						"etag": ""
					}
				}
			}
		}`)

	// add a new bundle and verify data from the currently activated is removed
	bundle2 := bundleFromRoundtrip(t, "bundle1", Bundle{
		Manifest: Manifest{Revision: "rev-2", Roots: &[]string{"c"}},
		Data:     unpack(map[string]any{"c.hello": "world"}),
		Etag:     "bar",
	})
	bundles = map[string]*Bundle{"bundle1": &bundle2}

	mustActivate(t, store, &ActivateOpts{Compiler: compiler, Bundles: bundles})

	// Ensure the snapshot bundle was activated
	verifyResultRead(t, store, `{
			"c": {
				"hello": "world"
			},
			"system": {
				"bundles": {
				"bundle1": {
					"manifest": {
						"revision": "rev-2",
						"roots": ["c"]
					},
					"etag": ""
				}
				}
			}
		}`)
}

func TestBundleLazyModeLifecycleRestoreBackupDB(t *testing.T) {
	dir := t.TempDir()
	store := must(disk.New(t.Context(), logging.NewNoOpLogger(), nil, disk.Options{Dir: dir}))(t)
	compiler := ast.NewCompiler()

	b := Bundle{
		Manifest: Manifest{Revision: "rev-1", Roots: &[]string{"a"}},
		Data: unpack(map[string]any{
			"a.b":   "foo",
			"a.e.f": "bar",
			"a.x":   []map[string]string{{"name": "john"}, {"name": "jane"}},
		}),
		Modules: []ModuleFile{moduleFile("a/policy.rego", "package a\np = true")},
		Etag:    "foo",
	}

	bundle1 := bundleFromRoundtrip(t, "bundle1", b)
	bundles := map[string]*Bundle{"bundle1": &bundle1}

	mustActivate(t, store, &ActivateOpts{Compiler: compiler, Bundles: bundles})

	// Ensure the snapshot bundle was activated
	verifyReadBundleNames(t, store, nil, util.Keys(bundles)...)
	verifyBundleModulesCompiled(t, compiler, bundles)
	verifyResultRead(t, store, `{
			"a": {
				"b": "foo",
				"e": {
				"f": "bar"
				},
				"x": [{"name": "john"}, {"name": "jane"}]
			},
			"system": {
				"bundles": {
					"bundle1": {
						"manifest": {
							"revision": "rev-1",
							"roots": ["a"]
						},
						"etag": ""
					}
				}
			}
		}`)

	// add a new bundle but abort the transaction and verify only old the bundle data is kept in store
	bundle2 := bundleFromRoundtrip(t, "bundle1", Bundle{
		Manifest: Manifest{Revision: "rev-2", Roots: &[]string{"c"}},
		Data:     unpack(map[string]any{"c.hello": "world"}),
		Etag:     "bar",
	})

	// can't use mustActivate here because we want to abort the txn!
	txn := storage.NewTransactionOrDie(t.Context(), store, storage.WriteParams)
	err := Activate(&ActivateOpts{
		Ctx:      t.Context(),
		Store:    store,
		Txn:      txn,
		Metrics:  metrics.NoOp(),
		Compiler: compiler,
		Bundles:  map[string]*Bundle{"bundle1": &bundle2},
	})
	if err != nil {
		t.Fatalf("unexpected error: %s", err)
	}
	store.Abort(t.Context(), txn)

	verifyResultRead(t, store, `{
			"a": {
				"b": "foo",
				"e": {
					"f": "bar"
				},
				"x": [{"name": "john"}, {"name": "jane"}]
			},
			"system": {
				"bundles": {
					"bundle1": {
						"manifest": {
							"revision": "rev-1",
							"roots": ["a"]
						},
						"etag": ""
					}
				}
			}
		}`)

	// check symlink is created and symlink target
	symlink := filepath.Join(dir, "active")
	must(os.Lstat(symlink))(t)
	must(filepath.EvalSymlinks(symlink))(t)
}

func TestDeltaBundleLazyModeLifecycleDiskStorage(t *testing.T) {
	store := must(disk.New(t.Context(), logging.NewNoOpLogger(), nil, disk.Options{Dir: t.TempDir()}))(t)
	compiler := ast.NewCompiler()

	mod1 := "package a\np = true"
	mod2 := "package b\np = true"
	bundle1 := bundleFromRoundtrip(t, "bundle1", Bundle{
		Manifest: Manifest{Roots: &[]string{"a"}},
		Data: unpack(map[string]any{
			"a.b":   "foo",
			"a.e.f": "bar",
			"a.x":   []map[string]string{{"name": "john"}, {"name": "jane"}},
		}),
		Modules: []ModuleFile{moduleFile("a/policy.rego", mod1)},
		Etag:    "foo",
	})

	bundle2 := bundleFromRoundtrip(t, "bundle2", Bundle{
		Manifest: Manifest{Roots: &[]string{"b", "c"}},
		Modules:  []ModuleFile{moduleFile("b/policy.rego", mod2)},
		Etag:     "foo",
	})

	bundles := map[string]*Bundle{"bundle1": &bundle1, "bundle2": &bundle2}

	mustActivate(t, store, &ActivateOpts{Compiler: compiler, Bundles: bundles})

	// Ensure the snapshot bundles were activated
	verifyReadBundleNames(t, store, nil, util.Keys(bundles)...)
	verifyBundleModulesCompiled(t, compiler, bundles)

	// create a delta bundle and activate it
	deltaBundles := map[string]*Bundle{
		"bundle1": {
			Manifest: Manifest{Revision: "delta-1", Roots: &[]string{"a"}},
			Patch: Patch{Data: []PatchOperation{
				{Op: "upsert", Path: "/a/c/d", Value: []string{"foo", "bar"}},
				{Op: "upsert", Path: "/a/c/d/-", Value: "baz"}, // append value to array
				{Op: "replace", Path: "a/b", Value: "bar"},     // replace a value
			}},
			Etag: "bar",
		},
		"bundle2": {
			Manifest: Manifest{Revision: "delta-2", Roots: &[]string{"b", "c"}},
			Patch: Patch{Data: []PatchOperation{
				{Op: "upsert", Path: "/c/d", Value: []string{"foo", "bar"}},
			}},
			Etag: "baz",
		},
		"bundle3": {
			Manifest: Manifest{Roots: &[]string{"d"}},
			Data:     unpack(map[string]any{"d.e": "foo"}),
		},
	}

	mustActivate(t, store, &ActivateOpts{Compiler: compiler, Bundles: deltaBundles})

	// check the modules from the snapshot bundles are on the compiler
	verifyBundleModulesCompiled(t, compiler, bundles)
	verifyResultRead(t, store, `{
			"a": {
		     	"b": "bar",
		     	"c": {
					"d": ["foo", "bar", "baz"]
		     	},
				"e": {
					"f": "bar"
				},
			   "x": [{"name": "john"}, {"name": "jane"}]
			},
			"c": {"d": ["foo", "bar"]},
			"d": {"e": "foo"},
			"system": {
				"bundles": {
					"bundle1": {
						"manifest": {
							"revision": "delta-1",
							"roots": ["a"]
						},
						"etag": "bar"
					},
					"bundle2": {
						"manifest": {
							"revision": "delta-2",
							"roots": ["b", "c"]
						},
						"etag": "baz"
					},
					"bundle3": {
						"manifest": {
							"revision": "",
							"roots": ["d"]
						},
						"etag": ""
					}
				}
			}
		}`)
}

func TestBundleLazyModeLifecycleOverlappingBundleRoots(t *testing.T) {
	mockStore := mock.New()

	bundle1 := bundleFromRoundtrip(t, "bundle1", Bundle{
		Manifest: Manifest{Revision: "foo", Roots: &[]string{"a/b", "a/c", "a/d"}},
		Data: unpack(map[string]any{
			"a.b":   "foo",
			"a.c.d": "bar",
			"a.d":   []map[string]string{{"name": "john"}, {"name": "jane"}},
		}),
	})

	bundle2 := bundleFromRoundtrip(t, "bundle2", Bundle{
		Manifest: Manifest{Revision: "bar", Roots: &[]string{"a/e"}},
		Data:     unpack(map[string]any{"a.e.f": "bar"}),
	})

	bundles := map[string]*Bundle{"bundle1": &bundle1, "bundle2": &bundle2}

	mustActivate(t, mockStore, &ActivateOpts{Bundles: bundles})

	// Ensure the snapshot bundles were activated
	verifyReadBundleNames(t, mockStore, nil, util.Keys(bundles)...)
	verifyResultRead(t, mockStore, `{
		"a": {
			"b": "foo",
			"c": {
				"d": "bar"
			},
			"e": {
				"f": "bar"
			},
			"d": [{"name": "john"}, {"name": "jane"}]
		},
		"system": {
			"bundles": {
				"bundle1": {
					"manifest": {
						"revision": "foo",
						"roots": ["a/b", "a/c", "a/d"]
					},
					"etag": ""
				},
				"bundle2": {
					"manifest": {
						"revision": "bar",
						"roots": ["a/e"]
					},
					"etag": ""
				}
			}
		}
	}`)
}

func TestBundleLazyModeLifecycleOverlappingBundleRootsDiskStorage(t *testing.T) {
	store := must(disk.New(t.Context(), logging.NewNoOpLogger(), nil, disk.Options{Dir: t.TempDir()}))(t)
	compiler := ast.NewCompiler()

	bundle1 := bundleFromRoundtrip(t, "bundle1", Bundle{
		Manifest: Manifest{Revision: "foo", Roots: &[]string{"a/b/c", "a/b/d", "a/b/e"}},
		Data: unpack(map[string]any{
			"a.b.c": "bar",
			"a.b.d": []map[string]string{{"name": "john"}, {"name": "jane"}},
			"a.b.e": []string{"foo", "bar"},
		}),
	})

	bundle2 := bundleFromRoundtrip(t, "bundle2", Bundle{
		Manifest: Manifest{Revision: "bar", Roots: &[]string{"a/b/f"}},
		Data:     unpack(map[string]any{"a.b.f.hello": "world"}),
	})

	bundles := map[string]*Bundle{"bundle1": &bundle1, "bundle2": &bundle2}

	mustActivate(t, store, &ActivateOpts{Compiler: compiler, Bundles: bundles})

	// Ensure the snapshot bundles were activated
	verifyReadBundleNames(t, store, nil, util.Keys(bundles)...)

	// Ensure the patches were applied
	verifyResultRead(t, store, `{
			"a": {
				"b": {
					"c": "bar",
					"d": [{"name": "john"}, {"name": "jane"}],
					"e": ["foo", "bar"],
					"f": {"hello": "world"}
				}
			},
			"system": {
				"bundles": {
					"bundle1": {
						"manifest": {
							"revision": "foo",
							"roots": ["a/b/c", "a/b/d", "a/b/e"]
						},
						"etag": ""
					},
					"bundle2": {
						"manifest": {
							"revision": "bar",
							"roots": ["a/b/f"]
						},
						"etag": ""
					}
				}
			}
		}`)
}

func TestBundleLazyModeLifecycleRawOverlappingBundleRoots(t *testing.T) {
	bundle1 := bundleFromFiles(t, "bundle1", [][2]string{
		{"/a/b/x/data.json", "[1,2,3]"},
		{"/a/c/y/data.json", "true"},
		{"/a/d/z/data.yaml", `foo: 1`},
		{"/data.json", `{"a": {"b": {"z": true}}}`},
		{"/.manifest", `{"revision": "foo", "roots": ["a/b", "a/c", "a/d"]}`},
	})

	bundle2 := bundleFromFiles(t, "bundle2", [][2]string{
		{"/a/e/x/data.json", "[4,5,6]"},
		{"/data.json", `{"a": {"e": {"f": true}}}`},
		{"/.manifest", `{"revision": "bar", "roots": ["a/e"]}`},
	})

	bundles := map[string]*Bundle{"bundle1": &bundle1, "bundle2": &bundle2}
	mockStore := mock.New()

	mustActivate(t, mockStore, &ActivateOpts{Bundles: bundles})

	// Ensure the snapshot bundles were activated
	verifyReadBundleNames(t, mockStore, nil, util.Keys(bundles)...)

	// Ensure the patches were applied
	verifyResultRead(t, mockStore, `{
		"a": {
			"b": {
				"x": [1,2,3],
				"z": true
			},
			"c": {
				"y": true
			},
			"d": {
				"z": {"foo": 1}
			},
			"e": {
				"x": [4,5,6],
				"f": true
			}
		},
		"system": {
			"bundles": {
				"bundle1": {
					"manifest": {
						"revision": "foo",
						"roots": ["a/b", "a/c", "a/d"]
					},
					"etag": ""
				},
				"bundle2": {
					"manifest": {
						"revision": "bar",
						"roots": ["a/e"]
					},
					"etag": ""
				}
			}
		}
	}`)
}

func TestBundleLazyModeLifecycleRawOverlappingBundleRootsDiskStorage(t *testing.T) {
	store := must(disk.New(t.Context(), logging.NewNoOpLogger(), nil, disk.Options{Dir: t.TempDir()}))(t)
	compiler := ast.NewCompiler()

	bundle1 := bundleFromFiles(t, "bundle1", [][2]string{
		{"/a/b/u/data.json", "[1,2,3]"},
		{"/a/b/v/data.json", "true"},
		{"/a/b/w/data.yaml", `foo: 1`},
		{"/data.json", `{"a": {"b": {"x": true}}}`},
		{"/.manifest", `{"revision": "foo", "roots": ["a/b"]}`},
	})

	bundle2 := bundleFromFiles(t, "bundle2", [][2]string{
		{"/a/c/x/data.json", "[4,5,6]"},
		{"/data.json", `{"a": {"c": {"y": true}}}`},
		{"/.manifest", `{"revision": "bar", "roots": ["a/c"]}`},
	})

	bundles := map[string]*Bundle{"bundle1": &bundle1, "bundle2": &bundle2}

	mustActivate(t, store, &ActivateOpts{Compiler: compiler, Bundles: bundles})

	// Ensure the snapshot bundles were activated
	verifyReadBundleNames(t, store, nil, util.Keys(bundles)...)

	// Ensure the patches were applied
	verifyResultRead(t, store, `{
			"a": {
				"b": {
					"u": [1,2,3],
					"v": true,
					"w": {"foo": 1},
					"x": true
				},
				"c": {
					"x": [4,5,6],
					"y": true
				}
			},
			"system": {
				"bundles": {
					"bundle1": {
						"manifest": {
							"revision": "foo",
							"roots": ["a/b"]
						},
						"etag": ""
					},
					"bundle2": {
						"manifest": {
							"revision": "bar",
							"roots": ["a/c"]
						},
						"etag": ""
					}
				}
			}
		}`)
}

func TestDeltaBundleLazyModeLifecycle(t *testing.T) {
	mockStore := mock.New()
	compiler := ast.NewCompiler()

	mod1 := "package a\np = true"
	mod2 := "package b\np = true"

	bundle1 := bundleFromRoundtrip(t, "bundle1", Bundle{
		Manifest: Manifest{Roots: &[]string{"a"}},
		Data: unpack(map[string]any{
			"a.b":   "foo",
			"a.e.f": "bar",
			"a.x":   []map[string]string{{"name": "john"}, {"name": "jane"}},
		}),
		Modules: []ModuleFile{moduleFile("policy.rego", mod1)},
		Etag:    "foo",
	})

	bundle2 := bundleFromRoundtrip(t, "bundle2", Bundle{
		Manifest:        Manifest{Roots: &[]string{"b", "c"}},
		Modules:         []ModuleFile{moduleFile("policy.rego", mod2)},
		Etag:            "foo",
		lazyLoadingMode: true,
		sizeLimitBytes:  DefaultSizeLimitBytes + 1,
	})

	bundles := map[string]*Bundle{"bundle1": &bundle1, "bundle2": &bundle2}

	mustActivate(t, mockStore, &ActivateOpts{Compiler: compiler, Bundles: bundles})

	// Ensure the snapshot bundles were activated
	verifyReadBundleNames(t, mockStore, nil, util.Keys(bundles)...)
	verifyBundleModulesCompiled(t, compiler, bundles)

	// create a delta bundle and activate it
	deltaBundles := map[string]*Bundle{
		"bundle1": {
			Manifest: Manifest{Revision: "delta-1", Roots: &[]string{"a"}},
			Patch: Patch{Data: []PatchOperation{
				{Op: "upsert", Path: "/a/c/d", Value: []string{"foo", "bar"}},
				{Op: "upsert", Path: "/a/c/d/-", Value: "baz"},
				{Op: "upsert", Path: "/a/x/1", Value: map[string]string{"name": "alice"}},
				{Op: "replace", Path: "a/b", Value: "bar"},
				{Op: "remove", Path: "a/e"},
				{Op: "upsert", Path: "a/y/~0z", Value: []int{1, 2, 3}},
			}},
			Etag: "bar",
		},
		"bundle2": {
			Manifest: Manifest{Revision: "delta-2", Roots: &[]string{"b", "c"}},
			Patch:    Patch{Data: []PatchOperation{{Op: "upsert", Path: "/c/d", Value: []string{"foo", "bar"}}}},
			Etag:     "baz",
		},
		"bundle3": {
			Manifest: Manifest{Roots: &[]string{"d"}},
			Data:     unpack(map[string]any{"d.e": "foo"}),
		},
	}

	mustActivate(t, mockStore, &ActivateOpts{Compiler: compiler, Bundles: deltaBundles})

	// check the modules from the snapshot bundles are on the compiler
	verifyBundleModulesCompiled(t, compiler, bundles)

	// Ensure the patches were applied
	verifyResultRead(t, mockStore, `{
		"a": {
          "b": "bar",
	       "c": {
				"d": ["foo", "bar", "baz"]
          },
		   "x": [{"name": "john"}, {"name": "alice"}, {"name": "jane"}],
		   "y": {"~z": [1, 2, 3]}
		},
		"c": {"d": ["foo", "bar"]},
		"d": {"e": "foo"},
		"system": {
			"bundles": {
				"bundle1": {
					"manifest": {
						"revision": "delta-1",
						"roots": ["a"]
					},
					"etag": "bar"
				},
				"bundle2": {
					"manifest": {
						"revision": "delta-2",
						"roots": ["b", "c"]
					},
					"etag": "baz"
				},
				"bundle3": {
					"manifest": {
						"revision": "",
						"roots": ["d"]
					},
					"etag": ""
				}
			}
		}
	}`)

	mockStore.AssertValid(t)
}

func TestDeltaBundleLazyModeWithDefaultRules(t *testing.T) {
	mockStore := mock.New()
	compiler := ast.NewCompiler()

	bundle1 := bundleFromRoundtrip(t, "bundle1", Bundle{
		Manifest: Manifest{Roots: &[]string{"a"}},
		Data: unpack(map[string]any{
			"a.b":   "foo",
			"a.e.f": "bar",
			"a.x":   []map[string]string{{"name": "john"}, {"name": "jane"}},
		}),
		Modules: []ModuleFile{moduleFile("policy.rego", "package a\ndefault p = true")},
		Etag:    "foo",
	})

	bundle2 := bundleFromRoundtrip(t, "bundle2", Bundle{
		Manifest:        Manifest{Roots: &[]string{"b", "c"}},
		Modules:         []ModuleFile{moduleFile("policy.rego", "package b\ndefault p = true")},
		Etag:            "foo",
		lazyLoadingMode: true,
		sizeLimitBytes:  DefaultSizeLimitBytes + 1,
	})

	bundles := map[string]*Bundle{"bundle1": &bundle1, "bundle2": &bundle2}

	mustActivate(t, mockStore, &ActivateOpts{Compiler: compiler, Bundles: bundles})

	// Ensure the snapshot bundles were activated
	verifyReadBundleNames(t, mockStore, nil, util.Keys(bundles)...)
	verifyBundleModulesCompiled(t, compiler, bundles)

	// create a delta bundle and activate it

	// add a new object member
	p1 := PatchOperation{Op: "upsert", Path: "/a/c/d", Value: []string{"foo", "bar"}}

	// append value to array
	p2 := PatchOperation{Op: "upsert", Path: "/a/c/d/-", Value: "baz"}

	// insert value in array
	p3 := PatchOperation{Op: "upsert", Path: "/a/x/1", Value: map[string]string{"name": "alice"}}

	// replace a value
	p4 := PatchOperation{Op: "replace", Path: "a/b", Value: "bar"}

	// remove a value
	p5 := PatchOperation{Op: "remove", Path: "a/e"}

	// add a new object with an escaped character in the path
	p6 := PatchOperation{Op: "upsert", Path: "a/y/~0z", Value: []int{1, 2, 3}}

	// add a new object root
	p7 := PatchOperation{Op: "upsert", Path: "/c/d", Value: []string{"foo", "bar"}}

	deltaBundles := map[string]*Bundle{
		"bundle1": {
			Manifest: Manifest{Revision: "delta-1", Roots: &[]string{"a"}},
			Patch:    Patch{Data: []PatchOperation{p1, p2, p3, p4, p5, p6}},
			Etag:     "bar",
		},
		"bundle2": {
			Manifest: Manifest{Revision: "delta-2", Roots: &[]string{"b", "c"}},
			Patch:    Patch{Data: []PatchOperation{p7}},
			Etag:     "baz",
		},
		"bundle3": {
			Manifest: Manifest{Roots: &[]string{"d"}},
			Data:     unpack(map[string]any{"d.e": "foo"}),
		},
	}

	expectedModuleCount := len(compiler.Modules)
	mustActivate(t, mockStore, &ActivateOpts{Compiler: compiler, Bundles: deltaBundles})

	if expectedModuleCount != len(compiler.Modules) {
		t.Fatalf("Expected %d modules, got %d", expectedModuleCount, len(compiler.Modules))
	}

	// check the modules from the snapshot bundles are on the compiler
	verifyBundleModulesCompiled(t, compiler, bundles)

	// Ensure the patches were applied
	verifyResultRead(t, mockStore, `{
		"a": {
          "b": "bar",
	       "c": {
				"d": ["foo", "bar", "baz"]
          },
		   "x": [{"name": "john"}, {"name": "alice"}, {"name": "jane"}],
		   "y": {"~z": [1, 2, 3]}
		},
		"c": {"d": ["foo", "bar"]},
		"d": {"e": "foo"},
		"system": {
			"bundles": {
				"bundle1": {
					"manifest": {
						"revision": "delta-1",
						"roots": ["a"]
					},
					"etag": "bar"
				},
				"bundle2": {
					"manifest": {
						"revision": "delta-2",
						"roots": ["b", "c"]
					},
					"etag": "baz"
				},
				"bundle3": {
					"manifest": {
						"revision": "",
						"roots": ["d"]
					},
					"etag": ""
				}
			}
		}
	}`)
	mockStore.AssertValid(t)
}

func TestBundleLifecycle(t *testing.T) {
	tests := []struct {
		note    string
		readAst bool
	}{
		{note: "read raw", readAst: false},
		{note: "read ast", readAst: true},
	}

	for _, tc := range tests {
		t.Run(tc.note, func(t *testing.T) {
			mockStore := mock.New(inmem.OptReturnASTValuesOnRead(tc.readAst))
			compiler := ast.NewCompiler()
			extraMods := map[string]*ast.Module{
				"mod1": ast.MustParseModule("package x\np = true"),
			}

			bundles := map[string]*Bundle{
				"bundle1": {
					Manifest: Manifest{Roots: &[]string{"a"}},
					Data:     unpack(map[string]any{"a.b": "foo"}),
					Modules:  []ModuleFile{moduleFile("a/policy.rego", "package a\np = true")},
					Etag:     "foo",
				},
				"bundle2": {
					Manifest: Manifest{Roots: &[]string{"b", "c"}},
					Modules:  []ModuleFile{moduleFile("b/policy.rego", "package b\np = true")},
				},
			}

			mustActivate(t, mockStore, &ActivateOpts{Compiler: compiler, Bundles: bundles, ExtraModules: extraMods})

			// Ensure the bundle was activated
			verifyReadBundleNames(t, mockStore, nil, util.Keys(bundles)...)
			verifyBundleModulesCompiled(t, compiler, bundles)

			actual := mustRead(t, mockStore, nil, storage.RootPath)
			expectedRaw := `{
				"a": {
					"b": "foo"
				},
				"system": {
					"bundles": {
						"bundle1": {
							"manifest": {
								"revision": "",
								"roots": ["a"]
							},
							"etag": "foo"
						},
						"bundle2": {
							"manifest": {
								"revision": "",
								"roots": ["b", "c"]
							},
							"etag": ""
						}
					},
					"modules": {
						"bundle1/a/policy.rego": {
							"rego_version": 1
						},
						"bundle2/b/policy.rego": {
							"rego_version": 1
						}
					}
				}
			}`
			assertEqual(t, tc.readAst, expectedRaw, actual)

			// Ensure that the extra module was included
			if _, ok := compiler.Modules["mod1"]; !ok {
				t.Fatal("expected extra module to be compiled")
			}

			mustDeactivate(t, mockStore, &DeactivateOpts{
				BundleNames: map[string]struct{}{"bundle1": {}, "bundle2": {}},
			})

			// Expect the store to have been cleared out after deactivating the bundles
			txn := storage.NewTransactionOrDie(t.Context(), mockStore)
			verifyReadBundleNames(t, mockStore, txn)

			expectedRaw = `{"system": {"bundles": {}, "modules": {}}}`
			assertEqual(t, tc.readAst, expectedRaw, mustRead(t, mockStore, txn, storage.RootPath))

			mockStore.Abort(t.Context(), txn)
			mockStore.AssertValid(t)
		})
	}
}

func TestDeltaBundleLifecycle(t *testing.T) {
	tests := []struct {
		note    string
		readAst bool
	}{
		{note: "read raw", readAst: false},
		{note: "read ast", readAst: true},
	}

	for _, tc := range tests {
		t.Run(tc.note, func(t *testing.T) {
			mockStore := mock.New(inmem.OptReturnASTValuesOnRead(tc.readAst))
			compiler := ast.NewCompiler()

			bundles := map[string]*Bundle{
				"bundle1": {
					Manifest: Manifest{Roots: &[]string{"a"}},
					Data: unpack(map[string]any{
						"a.b":   "foo",
						"a.e.f": "bar",
						"a.x":   []map[string]string{{"name": "john"}, {"name": "jane"}},
					}),
					Modules: []ModuleFile{moduleFile("a/policy.rego", "package a\ndefault p = true")},
					Etag:    "foo",
				},
				"bundle2": {
					Manifest: Manifest{Roots: &[]string{"b", "c"}},
					Modules:  []ModuleFile{moduleFile("b/policy.rego", "package b\ndefault p = true")},
				},
			}

			mustActivate(t, mockStore, &ActivateOpts{Compiler: compiler, Bundles: bundles})

			// Ensure the snapshot bundles were activated
			verifyReadBundleNames(t, mockStore, nil, util.Keys(bundles)...)
			verifyBundleModulesCompiled(t, compiler, bundles)

			// create a delta bundle and activate it

			// add a new object member
			p1 := PatchOperation{Op: "upsert", Path: "/a/c/d", Value: []string{"foo", "bar"}}

			// append value to array
			p2 := PatchOperation{Op: "upsert", Path: "/a/c/d/-", Value: "baz"}

			// insert value in array
			p3 := PatchOperation{Op: "upsert", Path: "/a/x/1", Value: map[string]string{"name": "alice"}}

			// replace a value
			p4 := PatchOperation{Op: "replace", Path: "a/b", Value: "bar"}

			// remove a value
			p5 := PatchOperation{Op: "remove", Path: "a/e"}

			// add a new object with an escaped character in the path
			p6 := PatchOperation{Op: "upsert", Path: "a/y/~0z", Value: []int{1, 2, 3}}

			// add a new object root
			p7 := PatchOperation{Op: "upsert", Path: "/c/d", Value: []string{"foo", "bar"}}

			deltaBundles := map[string]*Bundle{
				"bundle1": {
					Manifest: Manifest{Revision: "delta-1", Roots: &[]string{"a"}},
					Patch:    Patch{Data: []PatchOperation{p1, p2, p3, p4, p5, p6}},
					Etag:     "bar",
				},
				"bundle2": {
					Manifest: Manifest{Revision: "delta-2", Roots: &[]string{"b", "c"}},
					Patch:    Patch{Data: []PatchOperation{p7}},
					Etag:     "baz",
				},
				"bundle3": {
					Manifest: Manifest{Roots: &[]string{"d"}},
					Data:     unpack(map[string]any{"d.e": "foo"}),
				},
			}

			mustActivate(t, mockStore, &ActivateOpts{Compiler: compiler, Bundles: deltaBundles})

			// check the modules from the snapshot bundles are on the compiler
			verifyBundleModulesCompiled(t, compiler, bundles)

			// Ensure the patches were applied
			actual := mustRead(t, mockStore, nil, storage.RootPath)
			expectedRaw := `{
				"a": {
					"b": "bar",
					"c": {
						"d": ["foo", "bar", "baz"]
					},
					"x": [{"name": "john"}, {"name": "alice"}, {"name": "jane"}],
					"y": {"~z": [1, 2, 3]}
				},
				"c": {"d": ["foo", "bar"]},
				"d": {"e": "foo"},
				"system": {
					"bundles": {
						"bundle1": {
							"manifest": {
								"revision": "delta-1",
								"roots": ["a"]
							},
							"etag": "bar"
						},
						"bundle2": {
							"manifest": {
								"revision": "delta-2",
								"roots": ["b", "c"]
							},
							"etag": "baz"
						},
						"bundle3": {
							"manifest": {
								"revision": "",
								"roots": ["d"]
							},
							"etag": ""
						}
					},
					"modules":{
						"bundle1/a/policy.rego":{
							"rego_version":1
						},
						"bundle2/b/policy.rego":{
							"rego_version":1
						}
					}
				}
			}`
			assertEqual(t, tc.readAst, expectedRaw, actual)
			mockStore.AssertValid(t)
		})
	}
}

func TestDeltaBundleActivate(t *testing.T) {
	tests := []struct {
		note    string
		readAst bool
	}{
		{note: "read raw", readAst: false},
		{note: "read ast", readAst: true},
	}

	for _, tc := range tests {
		t.Run(tc.note, func(t *testing.T) {
			mockStore := mock.New(inmem.OptReturnASTValuesOnRead(tc.readAst))

			// create a delta bundle
			p1 := PatchOperation{Op: "upsert", Path: "/a/c/d", Value: []string{"foo", "bar"}}

			deltaBundles := map[string]*Bundle{"bundle1": {
				Manifest: Manifest{Revision: "delta", Roots: &[]string{"a"}},
				Patch:    Patch{Data: []PatchOperation{p1}},
				Etag:     "foo",
			}}

			mustActivate(t, mockStore, &ActivateOpts{Bundles: deltaBundles})

			txn := storage.NewTransactionOrDie(t.Context(), mockStore)

			// Ensure the delta bundle was activated
			verifyReadBundleNames(t, mockStore, txn, util.Keys(deltaBundles)...)

			// Ensure the patches were applied
			actual := mustRead(t, mockStore, txn, storage.RootPath)
			expectedRaw := `{
				"a": {
					"c": {
						"d": ["foo", "bar"]
					}
				},
				"system": {
					"bundles": {
						"bundle1": {
							"manifest": {
								"revision": "delta",
								"roots": ["a"]
							},
							"etag": "foo"
						}
					}
				}
			}`
			assertEqual(t, tc.readAst, expectedRaw, actual)

			// Stop the "read" transaction
			mockStore.Abort(t.Context(), txn)
			mockStore.AssertValid(t)
		})
	}
}

func TestDeltaBundleBadManifest(t *testing.T) {
	mockStore := mock.New()

	bundles := map[string]*Bundle{"bundle1": {
		Manifest: Manifest{Roots: &[]string{"a"}},
		Modules:  []ModuleFile{moduleFile("a/policy.rego", "package a\np = true")},
	}}

	mustActivate(t, mockStore, &ActivateOpts{Bundles: bundles})

	// Ensure the snapshot bundle was activated
	verifyReadBundleNames(t, mockStore, nil, util.Keys(bundles)...)

	// create a delta bundle with a different manifest from the snapshot bundle
	deltaBundles := map[string]*Bundle{"bundle1": {
		Manifest: Manifest{Roots: &[]string{"b"}},
		Patch:    Patch{Data: []PatchOperation{{Op: "upsert", Path: "/a/c/d", Value: []string{"foo", "bar"}}}},
	}}

	txn := storage.NewTransactionOrDie(t.Context(), mockStore, storage.WriteParams)

	err := Activate(&ActivateOpts{
		Ctx:      t.Context(),
		Store:    mockStore,
		Txn:      txn,
		Compiler: ast.NewCompiler(),
		Metrics:  metrics.NoOp(),
		Bundles:  deltaBundles,
	})
	if err == nil {
		t.Fatal("expected error but got nil")
	}

	expected := "delta bundle 'bundle1' has wasm resolvers or manifest roots that are different from those in the store"
	if err.Error() != expected {
		t.Fatalf("Expected error %v but got %v", expected, err.Error())
	}

	mockStore.AssertValid(t)
}

func TestEraseData(t *testing.T) {
	storeReadModes := []struct {
		note    string
		readAst bool
	}{
		{note: "read raw", readAst: false},
		{note: "read ast", readAst: true},
	}

	cases := []struct {
		note        string
		initialData map[string]any
		roots       []string
		expectErr   bool
		expected    string
	}{
		{
			note: "erase all",
			initialData: map[string]any{
				"a.b": "foo",
				"b":   "bar",
			},
			roots:    []string{"a", "b"},
			expected: `{}`,
		},
		{
			note: "erase none",
			initialData: map[string]any{
				"a.b": "foo",
				"b":   "bar",
			},
			roots:    []string{},
			expected: `{"a": {"b": "foo"}, "b": "bar"}`,
		},
		{
			note: "erase partial",
			initialData: map[string]any{
				"a.b": "foo",
				"b":   "bar",
			},
			roots:    []string{"a"},
			expected: `{"b": "bar"}`,
		},
		{
			note: "erase partial path",
			initialData: map[string]any{
				"a.b":   "foo",
				"a.c.d": 123,
			},
			roots:    []string{"a/c/d"},
			expected: `{"a": {"b": "foo", "c":{}}}`,
		},
	}

	for _, rm := range storeReadModes {
		t.Run(rm.note, func(t *testing.T) {
			for _, tc := range cases {
				t.Run(tc.note, func(t *testing.T) {
					mockStore := mock.NewWithData(unpack(tc.initialData), inmem.OptReturnASTValuesOnRead(rm.readAst))
					txn := storage.NewTransactionOrDie(t.Context(), mockStore, storage.WriteParams)

					roots := map[string]struct{}{}
					for _, root := range tc.roots {
						roots[root] = struct{}{}
					}

					if err := eraseData(t.Context(), mockStore, txn, roots); !tc.expectErr && err != nil {
						t.Fatalf("unepected error: %s", err)
					} else if tc.expectErr && err == nil {
						t.Fatalf("expected error, got: %s", err)
					}

					mustCommit(t, mockStore, txn)
					mockStore.AssertValid(t)

					txn = storage.NewTransactionOrDie(t.Context(), mockStore)
					assertEqual(t, rm.readAst, tc.expected, mustRead(t, mockStore, txn, storage.RootPath))
				})
			}
		})
	}
}

func TestErasePolicies(t *testing.T) {
	cases := []struct {
		note              string
		initialPolicies   map[string][]byte
		roots             []string
		expectErr         bool
		expectedRemaining []string
	}{
		{
			note: "erase all",
			initialPolicies: map[string][]byte{
				"mod1": []byte("package a\np = true"),
			},
			roots:             []string{""},
			expectedRemaining: []string{},
		},
		{
			note: "erase none",
			initialPolicies: map[string][]byte{
				"mod1": []byte("package a\np = true"),
				"mod2": []byte("package b\np = true"),
			},
			roots:             []string{"c"},
			expectedRemaining: []string{"mod1", "mod2"},
		},
		{
			note: "erase correct paths",
			initialPolicies: map[string][]byte{
				"mod1": []byte("package a.test\np = true"),
				"mod2": []byte("package a.test_v2\np = true"),
			},
			roots:             []string{"a/test"},
			expectedRemaining: []string{"mod2"},
		},
		{
			note: "erase some",
			initialPolicies: map[string][]byte{
				"mod1": []byte("package a\np = true"),
				"mod2": []byte("package b\np = true"),
			},
			roots:             []string{"b"},
			expectedRemaining: []string{"mod1"},
		},
		{
			note: "error: parsing module",
			initialPolicies: map[string][]byte{
				"mod1": []byte("package a\np = true"),
				"mod2": []byte("bad-policy-syntax"),
			},
			roots:             []string{"b"},
			expectErr:         true,
			expectedRemaining: []string{"mod1"},
		},
	}

	for _, tc := range cases {
		t.Run(tc.note, func(t *testing.T) {
			mockStore := mock.New()
			txn := storage.NewTransactionOrDie(t.Context(), mockStore, storage.WriteParams)

			for name, mod := range tc.initialPolicies {
				if err := mockStore.UpsertPolicy(t.Context(), txn, name, mod); err != nil {
					t.Fatalf("unexpected error: %s", err)
				}
			}

			roots := map[string]struct{}{}
			for _, root := range tc.roots {
				roots[root] = struct{}{}
			}
			remaining, _, err := erasePolicies(t.Context(), mockStore, txn, ast.ParserOptions{}, roots)
			if !tc.expectErr && err != nil {
				t.Fatalf("unepected error: %s", err)
			} else if tc.expectErr && err == nil {
				t.Fatalf("expected error, got: %s", err)
			}

			if !tc.expectErr {
				if len(remaining) != len(tc.expectedRemaining) {
					t.Fatalf("expected %d modules remaining, got %d", len(tc.expectedRemaining), len(remaining))
				}
				for _, name := range tc.expectedRemaining {
					if _, ok := remaining[name]; !ok {
						t.Fatalf("expected remaining module %s not found", name)
					}
				}

				mustCommit(t, mockStore, txn)
				mockStore.AssertValid(t)

				txn = storage.NewTransactionOrDie(t.Context(), mockStore)
				actualRemaining := must(mockStore.ListPolicies(t.Context(), txn))(t)

				if len(actualRemaining) != len(tc.expectedRemaining) {
					t.Fatalf("expected %d modules remaining in the store, got %d", len(tc.expectedRemaining), len(actualRemaining))
				}
				for _, expectedName := range tc.expectedRemaining {
					found := slices.Contains(actualRemaining, expectedName)
					if !found {
						t.Fatalf("expected remaining module %s not found", expectedName)
					}
				}
			}
		})
	}
}

func TestWriteData(t *testing.T) {
	storeReadModes := []struct {
		note    string
		readAst bool
	}{
		{note: "read raw", readAst: false},
		{note: "read ast", readAst: true},
	}

	cases := []struct {
		note         string
		existingData map[string]any
		roots        []string
		data         map[string]any
		expected     string
		expectErr    bool
	}{
		{
			note:     "single root",
			roots:    []string{"a"},
			data:     map[string]any{"a.b.c": 123},
			expected: `{"a": {"b": {"c": 123}}}`,
		},
		{
			note:  "multiple roots",
			roots: []string{"a", "b/c/d"},
			data: map[string]any{
				"a":     "foo",
				"b.c.d": "bar",
			},
			expected: `{"a": "foo","b": {"c": {"d": "bar"}}}`,
		},
		{
			note:  "data not in roots",
			roots: []string{"a"},
			data: map[string]any{
				"a":     "foo",
				"b.c.d": "bar",
			},
			expected: `{"a": "foo"}`,
		},
		{
			note:         "no data",
			roots:        []string{"a"},
			existingData: map[string]any{},
			data:         map[string]any{},
			expected:     `{}`,
		},
		{
			note:         "no new data",
			roots:        []string{"a"},
			existingData: map[string]any{"a": "foo"},
			data:         map[string]any{},
			expected:     `{"a": "foo"}`,
		},
		{
			note:         "overwrite data",
			roots:        []string{"a"},
			existingData: map[string]any{"a.b": "foo"},
			data:         map[string]any{"a": "bar"},
			expected:     `{"a": "bar"}`,
		},
	}

	for _, rm := range storeReadModes {
		t.Run(rm.note, func(t *testing.T) {
			for _, tc := range cases {
				t.Run(tc.note, func(t *testing.T) {
					mockStore := mock.NewWithData(unpack(tc.existingData), inmem.OptReturnASTValuesOnRead(rm.readAst))
					txn := storage.NewTransactionOrDie(t.Context(), mockStore, storage.WriteParams)

					err := writeData(t.Context(), mockStore, txn, tc.roots, unpack(tc.data))
					if !tc.expectErr && err != nil {
						t.Fatalf("unepected error: %s", err)
					} else if tc.expectErr && err == nil {
						t.Fatalf("expected error, got: %s", err)
					}

					mustCommit(t, mockStore, txn)
					mockStore.AssertValid(t)

					txn = storage.NewTransactionOrDie(t.Context(), mockStore)
					assertEqual(t, rm.readAst, tc.expected, mustRead(t, mockStore, txn, storage.RootPath))
				})
			}
		})
	}
}

func TestDoDFS(t *testing.T) {
	cases := []struct {
		note    string
		input   map[string]json.RawMessage
		path    string
		roots   []string
		wantErr bool
		err     error
	}{
		{
			note:  "bundle owns all",
			path:  "/",
			roots: []string{""},
		},
		{
			note:  "data within roots root case",
			input: map[string]json.RawMessage{"a": json.RawMessage(`true`)},
			path:  "",
			roots: []string{"a"},
		},
		{
			note:  "data within roots nested 1",
			input: map[string]json.RawMessage{"d": json.RawMessage(`true`)},
			path:  filepath.Dir("a/b/c/data.json"),
			roots: []string{"a/b/c"},
		},
		{
			note:  "data within roots nested 2",
			input: map[string]json.RawMessage{"d": json.RawMessage(`{"hello": "world"}`)},
			path:  filepath.Dir("a/b/c/data.json"),
			roots: []string{"a/b/c"},
		},
		{
			note:  "data within roots nested 3",
			input: map[string]json.RawMessage{"d": json.RawMessage(`{"hello": "world"}`)},
			path:  filepath.Dir("a/data.json"),
			roots: []string{"a/d"},
		},
		{
			note:  "data within multiple roots 1",
			input: map[string]json.RawMessage{"a": json.RawMessage(`{"b": "c"}`), "c": json.RawMessage(`true`)},
			path:  ".",
			roots: []string{"a/b", "c"},
		},
		{
			note:  "data within multiple roots 2",
			input: map[string]json.RawMessage{"a": json.RawMessage(`{"b": "c"}`), "c": []byte(`{"d": {"e": {"f": true}}}`)},
			path:  ".",
			roots: []string{"a/b", "c/d/e"},
		},
		{
			note:    "data outside roots 1",
			input:   map[string]json.RawMessage{"d": json.RawMessage(`{"hello": "world"}`)},
			path:    ".",
			roots:   []string{"a/d"},
			wantErr: true,
			err:     errors.New("manifest roots [a/d] do not permit data at path '/d' (hint: check bundle directory structure)"),
		},
		{
			note:    "data outside roots 2",
			input:   map[string]json.RawMessage{"a": []byte(`{"b": {"c": {"e": true}}}`)},
			path:    filepath.Dir("x/data.json"),
			roots:   []string{"x/a/b/c/d"},
			wantErr: true,
			err:     errors.New("manifest roots [x/a/b/c/d] do not permit data at path '/x/a/b/c/e' (hint: check bundle directory structure)"),
		},
		{
			note:    "data outside roots 3",
			input:   map[string]json.RawMessage{"a": []byte(`{"b": {"c": true}}`)},
			path:    ".",
			roots:   []string{"a/b/c/d"},
			wantErr: true,
			err:     errors.New("manifest roots [a/b/c/d] do not permit data at path '/a/b/c' (hint: check bundle directory structure)"),
		},
		{
			note:    "data outside multiple roots",
			input:   map[string]json.RawMessage{"a": json.RawMessage(`{"b": "c"}`), "e": []byte(`{"b": {"c": true}}`)},
			path:    ".",
			roots:   []string{"a/b", "c"},
			wantErr: true,
			err:     errors.New("manifest roots [a/b c] do not permit data at path '/e' (hint: check bundle directory structure)"),
		},
		{
			note:    "data outside multiple roots 2",
			input:   map[string]json.RawMessage{"a": json.RawMessage(`{"b": "c"}`), "c": []byte(`{"d": true}`)},
			path:    ".",
			roots:   []string{"a/b", "c/d/e"},
			wantErr: true,
			err:     errors.New("manifest roots [a/b c/d/e] do not permit data at path '/c/d' (hint: check bundle directory structure)"),
		},
	}

	for _, tc := range cases {
		t.Run(tc.note, func(t *testing.T) {
			if err := doDFS(tc.input, tc.path, tc.roots); tc.wantErr {
				if err == nil {
					t.Fatal("Expected error but got nil")
				}

				if tc.err != nil && tc.err.Error() != err.Error() {
					t.Fatalf("Expected error message %v but got %v", tc.err.Error(), err.Error())
				}
			} else if err != nil {
				t.Fatalf("Unexpected error %v", err)
			}
		})
	}
}

func TestHasRootsOverlap(t *testing.T) {
	cases := []struct {
		note           string
		storeRoots     map[string]*[]string
		newBundleRoots map[string]*[]string
		expectedError  string
	}{
		{
			note:           "no overlap between store and new bundles",
			storeRoots:     map[string]*[]string{"bundle1": {"a", "b"}},
			newBundleRoots: map[string]*[]string{"bundle2": {"c"}},
		},
		{
			note:           "no overlap between store and multiple new bundles",
			storeRoots:     map[string]*[]string{"bundle1": {"a", "b"}},
			newBundleRoots: map[string]*[]string{"bundle2": {"c"}, "bundle3": {"d"}},
		},
		{
			note:           "no overlap with empty store",
			storeRoots:     map[string]*[]string{},
			newBundleRoots: map[string]*[]string{"bundle1": {"a", "b"}},
		},
		{
			note:           "no overlap between multiple new bundles with empty store",
			storeRoots:     map[string]*[]string{},
			newBundleRoots: map[string]*[]string{"bundle1": {"a", "b"}, "bundle2": {"c"}},
		},
		{
			note:           "overlap between multiple new bundles with empty store",
			storeRoots:     map[string]*[]string{},
			newBundleRoots: map[string]*[]string{"bundle1": {"a", "b"}, "bundle2": {"a", "c"}},
			expectedError:  "detected overlapping roots in manifests for these bundles: [bundle1, bundle2] (root a is in multiple bundles)",
		},
		{
			note:           "overlap between store and new bundle",
			storeRoots:     map[string]*[]string{"bundle1": {"a", "b"}},
			newBundleRoots: map[string]*[]string{"bundle2": {"c", "a"}},
			expectedError:  "detected overlapping roots in manifests for these bundles: [bundle1, bundle2] (root a is in multiple bundles)",
		},
		{
			note:           "overlap between store and multiple new bundles",
			storeRoots:     map[string]*[]string{"bundle1": {"a", "b"}},
			newBundleRoots: map[string]*[]string{"bundle2": {"c", "a"}, "bundle3": {"a"}},
			expectedError:  "detected overlapping roots in manifests for these bundles: [bundle1, bundle2, bundle3] (root a is in multiple bundles)",
		},
		{
			note:           "overlap between store bundle and new empty root bundle",
			storeRoots:     map[string]*[]string{"bundle1": {"a", "b"}},
			newBundleRoots: map[string]*[]string{"bundle2": {""}},
			expectedError:  "bundles [bundle1, bundle2] have overlapping roots and cannot be activated simultaneously because bundle(s) [bundle2] specify empty root paths ('') which overlap with any other bundle root",
		},
		{
			note:           "overlap between multiple new empty root bundles",
			storeRoots:     map[string]*[]string{},
			newBundleRoots: map[string]*[]string{"bundle1": {""}, "bundle2": {""}},
			expectedError:  "bundles [bundle1, bundle2] have overlapping roots and cannot be activated simultaneously because bundle(s) [bundle1, bundle2] specify empty root paths ('') which overlap with any other bundle root",
		},
		{
			note:           "overlap between new empty root and new regular root bundles",
			storeRoots:     map[string]*[]string{},
			newBundleRoots: map[string]*[]string{"bundle1": {"a"}, "bundle2": {""}},
			expectedError:  "bundles [bundle1, bundle2] have overlapping roots and cannot be activated simultaneously because bundle(s) [bundle2] specify empty root paths ('') which overlap with any other bundle root",
		},
		{
			note:           "overlap between nested paths",
			storeRoots:     map[string]*[]string{},
			newBundleRoots: map[string]*[]string{"bundle1": {"a"}, "bundle2": {"a/b"}},
			expectedError:  "detected overlapping roots in manifests for these bundles: [bundle1, bundle2] (a overlaps a/b)",
		},
		{
			note:           "overlap between store nested path and new bundle path",
			storeRoots:     map[string]*[]string{"bundle1": {"a/b"}},
			newBundleRoots: map[string]*[]string{"bundle2": {"a"}},
			expectedError:  "detected overlapping roots in manifests for these bundles: [bundle1, bundle2] (a overlaps a/b)",
		},
	}

	for _, tc := range cases {
		t.Run(tc.note, func(t *testing.T) {
			mockStore := mock.New()
			txn := storage.NewTransactionOrDie(t.Context(), mockStore, storage.WriteParams)

			for name, roots := range tc.storeRoots {
				if err := WriteManifestToStore(t.Context(), mockStore, txn, name, Manifest{Roots: roots}); err != nil {
					t.Fatalf("unexpected error: %s", err)
				}
			}

			bundles := map[string]*Bundle{}
			for name, roots := range tc.newBundleRoots {
				bundles[name] = &Bundle{Manifest: Manifest{Roots: roots}}
			}

			if err := hasRootsOverlap(t.Context(), mockStore, txn, bundles); tc.expectedError != "" {
				if err == nil {
					t.Fatalf("expected error %q, got nil", tc.expectedError)
				}
				if err.Error() != tc.expectedError {
					t.Fatalf("expected error message %q, got %q", tc.expectedError, err.Error())
				}
			} else if err != nil {
				t.Fatalf("unexpected error: %s", err)
			}

			mustCommit(t, mockStore, txn)
			mockStore.AssertValid(t)
		})
	}
}

func TestBundleStoreHelpers(t *testing.T) {
	storeReadModes := []struct {
		note    string
		readAst bool
	}{
		{note: "read raw", readAst: false},
		{note: "read ast", readAst: true},
	}

	bundles := map[string]*Bundle{
		"bundle1": {
			Manifest: Manifest{Roots: &[]string{}},
		},
		"bundle2": {
			Manifest: Manifest{
				Roots:    &[]string{"a"},
				Revision: "foo",
				Metadata: map[string]any{"a": "b"},
				WasmResolvers: []WasmResolver{{
					Entrypoint: "foo/bar",
					Module:     "m.wasm",
				}},
			},
			Etag: "bar",
			WasmModules: []WasmModuleFile{{
				Path: "/m.wasm",
				Raw:  []byte("d2FzbS1tb2R1bGU="),
			}},
		},
	}

	for _, srm := range storeReadModes {
		t.Run(srm.note, func(t *testing.T) {
			mockStore := mock.NewWithData(nil, inmem.OptReturnASTValuesOnRead(srm.readAst))

			mustActivate(t, mockStore, &ActivateOpts{Bundles: bundles})

			txn := storage.NewTransactionOrDie(t.Context(), mockStore)

			verifyReadBundleNames(t, mockStore, txn, util.Keys(bundles)...)

			// Etag

			if etag, err := ReadBundleEtagFromStore(t.Context(), mockStore, txn, "bundle1"); err != nil {
				t.Fatalf("unexpected error: %s", err)
			} else if etag != "" {
				t.Errorf("expected empty etag but got %s", etag)
			}

			if etag, err := ReadBundleEtagFromStore(t.Context(), mockStore, txn, "bundle2"); err != nil {
				t.Fatalf("unexpected error: %s", err)
			} else if exp := "bar"; etag != exp {
				t.Errorf("expected etag %s but got %s", exp, etag)
			}

			// Revision

			if rev, err := ReadBundleRevisionFromStore(t.Context(), mockStore, txn, "bundle1"); err != nil {
				t.Fatalf("unexpected error: %s", err)
			} else if rev != "" {
				t.Errorf("expected empty revision but got %s", rev)
			}

			if rev, err := ReadBundleRevisionFromStore(t.Context(), mockStore, txn, "bundle2"); err != nil {
				t.Fatalf("unexpected error: %s", err)
			} else if exp := "foo"; rev != exp {
				t.Errorf("expected revision %s but got %s", exp, rev)
			}

			// Roots

			if roots, err := ReadBundleRootsFromStore(t.Context(), mockStore, txn, "bundle1"); err != nil {
				t.Fatalf("unexpected error: %s", err)
			} else if len(roots) != 0 {
				t.Errorf("expected empty roots but got %v", roots)
			}

			if roots, err := ReadBundleRootsFromStore(t.Context(), mockStore, txn, "bundle2"); err != nil {
				t.Fatalf("unexpected error: %s", err)
			} else if exp := *bundles["bundle2"].Manifest.Roots; !reflect.DeepEqual(exp, roots) {
				t.Errorf("expected roots %v but got %v", exp, roots)
			}

			// Bundle metadata

			if meta, err := ReadBundleMetadataFromStore(t.Context(), mockStore, txn, "bundle1"); err != nil {
				t.Fatalf("unexpected error: %s", err)
			} else if len(meta) != 0 {
				t.Errorf("expected empty metadata but got %v", meta)
			}

			if meta, err := ReadBundleMetadataFromStore(t.Context(), mockStore, txn, "bundle2"); err != nil {
				t.Fatalf("unexpected error: %s", err)
			} else if exp := bundles["bundle2"].Manifest.Metadata; !reflect.DeepEqual(exp, meta) {
				t.Errorf("expected metadata %v but got %v", exp, meta)
			}

			// Wasm metadata

			if _, err := ReadWasmMetadataFromStore(t.Context(), mockStore, txn, "bundle1"); err == nil {
				t.Fatal("expected error but got nil")
			} else if exp, act := "storage_not_found_error: /bundles/bundle1/manifest/wasm: document does not exist", err.Error(); !strings.Contains(act, exp) {
				t.Fatalf("expected error:\n\n%s\n\nbut got:\n\n%v", exp, act)
			}

			if resolvers, err := ReadWasmMetadataFromStore(t.Context(), mockStore, txn, "bundle2"); err != nil {
				t.Fatalf("unexpected error: %s", err)
			} else if exp := bundles["bundle2"].Manifest.WasmResolvers; !reflect.DeepEqual(exp, resolvers) {
				t.Errorf("expected wasm metadata:\n\n%v\n\nbut got:\n\n%v", exp, resolvers)
			}

			// Wasm modules

			if _, err := ReadWasmModulesFromStore(t.Context(), mockStore, txn, "bundle1"); err == nil {
				t.Fatal("expected error but got nil")
			} else if exp, act := "storage_not_found_error: /bundles/bundle1/wasm: document does not exist", err.Error(); !strings.Contains(act, exp) {
				t.Fatalf("expected error:\n\n%s\n\nbut got:\n\n%v", exp, act)
			}

			if modules, err := ReadWasmModulesFromStore(t.Context(), mockStore, txn, "bundle2"); err != nil {
				t.Fatalf("unexpected error: %s", err)
			} else if exp := bundles["bundle2"].WasmModules; len(exp) != len(modules) {
				t.Errorf("expected wasm modules:\n\n%v\n\nbut got:\n\n%v", exp, modules)
			} else {
				for _, exp := range bundles["bundle2"].WasmModules {
					act := modules[exp.Path]
					if act == nil {
						t.Errorf("expected wasm module %s but got nil", exp.Path)
					}
					if !bytes.Equal(exp.Raw, act) {
						t.Errorf("expected wasm module %s to have raw data:\n\n%v\n\nbut got:\n\n%v", exp.Path, exp.Raw, act)
					}
				}
			}

		})
	}
}

func TestActivate_DefaultRegoVersion(t *testing.T) {
	tests := []struct {
		note              string
		module            string
		customRegoVersion ast.RegoVersion
		expErrs           []string
	}{
		// NOT default rego-version
		{
			note: "v0 module",
			module: `package test
					p[x] { 
						x = "a" 
					}`,
			expErrs: []string{
				"rego_parse_error: `if` keyword is required before rule body",
				"rego_parse_error: `contains` keyword is required for partial set rules",
			},
		},

		// cross-rego-version
		{
			note: "rego.v1 import, no v1 parse-time violations",
			module: `package test
					import rego.v1

					p contains x if { 
						x = "a" 
					}`,
		},
		{
			note: "rego.v1 import, v1 parse-time violations",
			module: `package test
					import rego.v1

					p contains x { 
						x = "a" 
					}`,
			expErrs: []string{
				"rego_parse_error: `if` keyword is required before rule body",
			},
		},

		// default rego-version
		{
			note: "v1 module, no v1 parse-time violations",
			module: `package test

					p contains x if { 
						x = "a" 
					}`,
		},
		{
			note: "v1 module, v1 parse-time violations",
			module: `package test

					p contains x { 
						x = "a" 
					}`,
			expErrs: []string{
				"rego_parse_error: `if` keyword is required before rule body",
			},
		},

		// custom rego-version
		{
			note: "v0 module, v0 custom rego-version",
			module: `package test
					p[x] { 
						x = "a" 
					}`,
			customRegoVersion: ast.RegoV0,
		},
	}

	for _, tc := range tests {
		t.Run(tc.note, func(t *testing.T) {
			store := mock.New()
			txn := storage.NewTransactionOrDie(t.Context(), store, storage.WriteParams)

			modulePath := "test/policy.rego"

			// We want to make assert that the default rego-version is used, which it is when a module is erased from storage and we don't know what version it has.
			// Therefore, we add a module to the store, which is the replaced by the Activate() call, causing an erase.
			err := store.UpsertPolicy(t.Context(), txn, modulePathWithPrefix("bundle1", modulePath), []byte(tc.module))
			if err != nil {
				t.Fatalf("unexpected error: %s", err)
			}

			bundles := map[string]*Bundle{"bundle1": {
				Manifest: Manifest{Roots: &[]string{"test"}},
				Modules:  []ModuleFile{moduleFile(modulePath, "package test")},
			}}

			opts := ActivateOpts{
				Ctx:      t.Context(),
				Txn:      txn,
				Store:    store,
				Compiler: ast.NewCompiler().WithDefaultRegoVersion(ast.RegoV0CompatV1),
				Metrics:  metrics.NoOp(),
				Bundles:  bundles,
			}

			if tc.customRegoVersion != ast.RegoUndefined {
				opts.ParserOptions.RegoVersion = tc.customRegoVersion
			}

			if err = Activate(&opts); len(tc.expErrs) > 0 {
				if err == nil {
					t.Fatalf("Expected error but got nil for test: %s", tc.note)
				}
				for _, expErr := range tc.expErrs {
					if err := err.Error(); !strings.Contains(err, expErr) {
						t.Fatalf("Expected error to contain:\n\n%s\n\nbut got:\n\n%s", expErr, err)
					}
				}
			} else if err != nil {
				t.Fatalf("Unexpected error: %v", err)
			}
		})
	}
}

// TestActivate_LogicalKeywords asserts that modules using the `and`/`or`
// keywords survive activation, including the re-parse of a module already in
// the store that activation erases.
func TestActivate_LogicalKeywords(t *testing.T) {
	v1Module := `package test
		import future.keywords.and
		import future.keywords.or

		allow if {
			input.user == "alice" or (input.role == "admin" and input.verified)
		}`

	v0Module := `package test
		import future.keywords.and
		import future.keywords.or

		allow {
			input.user == "alice" or (input.role == "admin" and input.verified)
		}`

	tests := []struct {
		note              string
		storedModule      string
		customRegoVersion ast.RegoVersion
		capabilities      *ast.Capabilities
		expErrs           []string
	}{
		{
			note:         "per-keyword imports",
			storedModule: v1Module,
		},
		{
			note: "wildcard future.keywords import",
			storedModule: `package test
				import future.keywords

				allow if {
					input.user == "alice" or (input.role == "admin" and input.verified)
				}`,
		},
		{
			note: "no import",
			storedModule: `package test
				allow if {
					input.user == "alice" or input.role == "admin"
				}`,
			expErrs: []string{"rego_parse_error: unexpected identifier token"},
		},
		{
			note:              "v0 module, v0 custom rego-version",
			storedModule:      v0Module,
			customRegoVersion: ast.RegoV0,
		},
		{
			note:         "capabilities without and/or",
			storedModule: v1Module,
			capabilities: capabilitiesWithoutFutureKeywords(t, "and", "or"),
			expErrs:      []string{"rego_parse_error: unexpected keyword, must be one of [contains every if in not]"},
		},
	}

	for _, tc := range tests {
		t.Run(tc.note, func(t *testing.T) {
			store := mock.New()
			txn := storage.NewTransactionOrDie(t.Context(), store, storage.WriteParams)

			modulePath := "test/policy.rego"

			// Activation erases and re-parses the module already in the store.
			err := store.UpsertPolicy(t.Context(), txn, modulePathWithPrefix("bundle1", modulePath), []byte(tc.storedModule))
			if err != nil {
				t.Fatalf("unexpected error: %s", err)
			}

			bundles := map[string]*Bundle{"bundle1": {
				Manifest: Manifest{Roots: &[]string{"test"}},
				Modules:  []ModuleFile{moduleFile(modulePath, "package test")},
			}}

			compiler := ast.NewCompiler()
			opts := ActivateOpts{
				Ctx:      t.Context(),
				Txn:      txn,
				Store:    store,
				Compiler: compiler,
				Metrics:  metrics.NoOp(),
				Bundles:  bundles,
			}

			if tc.customRegoVersion != ast.RegoUndefined {
				opts.ParserOptions.RegoVersion = tc.customRegoVersion
			}
			if tc.capabilities != nil {
				opts.ParserOptions.Capabilities = tc.capabilities
			}

			err = Activate(&opts)

			if len(tc.expErrs) > 0 {
				if err == nil {
					t.Fatalf("Expected error(s):\n\n%v\n\nbut got nil", tc.expErrs)
				}
				for _, expErr := range tc.expErrs {
					if !strings.Contains(err.Error(), expErr) {
						t.Fatalf("Expected error:\n\n%s\n\nbut got:\n\n%s", expErr, err)
					}
				}
				return
			}

			if err != nil {
				t.Fatalf("Unexpected error: %v", err)
			}
			verifyBundleModulesCompiled(t, compiler, bundles)
		})
	}
}

// TestDeltaBundleActivate_LogicalKeywords asserts that activating a delta
// bundle leaves the `and`/`or` modules of the snapshot bundle it patches
// intact.
func TestDeltaBundleActivate_LogicalKeywords(t *testing.T) {
	logicalModule := `package a
		import future.keywords.and
		import future.keywords.or
		
		allow if {
			input.user == "alice" or (input.role == "admin" and input.verified)
		}`

	store := mock.New()
	compiler := ast.NewCompiler()

	bundles := map[string]*Bundle{"bundle1": {
		Manifest: Manifest{Roots: &[]string{"a"}},
		Data:     unpack(map[string]any{"a.b": "foo"}),
		Modules:  []ModuleFile{moduleFile("a/policy.rego", logicalModule)},
		Etag:     "foo",
	}}

	mustActivate(t, store, &ActivateOpts{Compiler: compiler, Bundles: bundles})
	verifyBundleModulesCompiled(t, compiler, bundles)

	deltaCompiler := ast.NewCompiler()
	deltaBundles := map[string]*Bundle{"bundle1": {
		Manifest: Manifest{Revision: "delta", Roots: &[]string{"a"}},
		Patch:    Patch{Data: []PatchOperation{{Op: "upsert", Path: "/a/c", Value: "bar"}}},
		Etag:     "bar",
	}}

	mustActivate(t, store, &ActivateOpts{Compiler: deltaCompiler, Bundles: deltaBundles})

	// The delta bundle carries no modules, so the snapshot bundle's module has
	// to be re-read from the store and re-parsed.
	verifyBundleModulesCompiled(t, deltaCompiler, bundles)

	var ands, ors int
	ast.WalkExprs(deltaCompiler.Modules["bundle1/a/policy.rego"], func(e *ast.Expr) bool {
		switch {
		case e.IsAnd():
			ands++
		case e.IsOr():
			ors++
		}
		return false
	})
	if ands != 1 || ors != 1 {
		t.Fatalf("expected 1 and-expression and 1 or-expression after delta activation, got %d and %d", ands, ors)
	}

	store.AssertValid(t)
}

// Regression test for https://github.com/open-policy-agent/opa/issues/8797.
func TestActivate_PreservesPerModuleRegoVersionWithLeadingSlashID(t *testing.T) {
	ctx := t.Context()
	store := mock.New()

	v1Module := `package test

p contains 42`

	policyID := "/test/policy.rego"
	txn := storage.NewTransactionOrDie(ctx, store, storage.WriteParams)
	if err := store.UpsertPolicy(ctx, txn, policyID, []byte(v1Module)); err != nil {
		t.Fatalf("UpsertPolicy failed: %v", err)
	}
	if err := write(ctx, store, txn, moduleRegoVersionPath(policyID), int64(ast.RegoV1.Int())); err != nil {
		t.Fatalf("writing rego_version failed: %v", err)
	}
	if err := store.Commit(ctx, txn); err != nil {
		t.Fatalf("commit failed: %v", err)
	}

	mustActivate(t, store, &ActivateOpts{
		Compiler:      ast.NewCompiler().WithDefaultRegoVersion(ast.RegoV0CompatV1),
		ParserOptions: ast.ParserOptions{RegoVersion: ast.RegoV0},
		Bundles: map[string]*Bundle{
			"other": {
				Manifest: Manifest{Roots: &[]string{"other"}},
			},
		},
	})
}

func TestDeactivate_DefaultRegoVersion(t *testing.T) {
	tests := []struct {
		note              string
		module            string
		customRegoVersion ast.RegoVersion
		expErrs           []string
	}{
		// NOT default rego-version
		{
			note: "v0 module",
			module: `package test
					p[x] { 
						x = "a" 
					}`,
			expErrs: []string{
				"rego_parse_error: `if` keyword is required before rule body",
				"rego_parse_error: `contains` keyword is required for partial set rules",
			},
		},

		// cross-rego-version
		{
			note: "rego.v1 import, no v1 parse-time violations",
			module: `package test
					import rego.v1

					p contains x if { 
						x = "a" 
					}`,
		},
		{
			note: "rego.v1 import, v1 parse-time violations",
			module: `package test
					import rego.v1

					p contains x { 
						x = "a" 
					}`,
			expErrs: []string{
				"rego_parse_error: `if` keyword is required before rule body",
			},
		},

		// default rego-version
		{
			note: "v1 module, no v1 parse-time violations",
			module: `package test

					p contains x if { 
						x = "a" 
					}`,
		},
		{
			note: "v1 module, v1 parse-time violations",
			module: `package test

					p contains x { 
						x = "a" 
					}`,
			expErrs: []string{
				"rego_parse_error: `if` keyword is required before rule body",
			},
		},

		// custom rego-version
		{
			note: "v0 module, v0 custom rego-version",
			module: `package test
					p[x] { 
						x = "a" 
					}`,
			customRegoVersion: ast.RegoV0,
		},
	}

	for _, tc := range tests {
		t.Run(tc.note, func(t *testing.T) {
			store := mock.New()
			txn := storage.NewTransactionOrDie(t.Context(), store, storage.WriteParams)
			modulePath := "test/policy.rego"

			// We want to make assert that the default rego-version is used, which it is when a module is erased from storage and we don't know what version it has.
			// Therefore, we add a module to the store, which is the replaced by the Activate() call, causing an erase.
			err := store.UpsertPolicy(t.Context(), txn, modulePathWithPrefix("bundle1", modulePath), []byte(tc.module))
			if err != nil {
				t.Fatalf("unexpected error: %s", err)
			}

			opts := DeactivateOpts{
				Ctx:         t.Context(),
				Txn:         txn,
				Store:       store,
				BundleNames: map[string]struct{}{modulePathWithPrefix("bundle1", modulePath): {}},
			}

			if tc.customRegoVersion != ast.RegoUndefined {
				opts.ParserOptions.RegoVersion = tc.customRegoVersion
			}

			if err := Deactivate(&opts); len(tc.expErrs) > 0 {
				if err == nil {
					t.Fatalf("Expected error but got nil for test: %s", tc.note)
				}
				for _, expErr := range tc.expErrs {
					if err := err.Error(); !strings.Contains(err, expErr) {
						t.Fatalf("Expected error to contain:\n\n%s\n\nbut got:\n\n%s", expErr, err)
					}
				}
			} else if err != nil {
				t.Fatalf("Unexpected error: %v", err)
			}
		})
	}
}

func mustActivate(tb testing.TB, store storage.Store, opts *ActivateOpts) {
	tb.Helper()

	txn := storage.NewTransactionOrDie(tb.Context(), store, storage.WriteParams)
	base := &ActivateOpts{
		Ctx:           tb.Context(),
		Store:         store,
		Txn:           txn,
		Metrics:       metrics.NoOp(),
		Bundles:       opts.Bundles,
		Compiler:      opts.Compiler,
		ParserOptions: opts.ParserOptions,
		ExtraModules:  opts.ExtraModules,
	}

	if opts.Compiler == nil {
		base.Compiler = ast.NewCompiler()
	}

	if err := Activate(base); err != nil {
		tb.Fatalf("unexpected error activating bundles: %s", err)
	}

	if err := store.Commit(tb.Context(), txn); err != nil {
		tb.Fatalf("unexpected error committing transaction: %s", err)
	}
}

func mustDeactivate(tb testing.TB, store storage.Store, opts *DeactivateOpts) {
	tb.Helper()

	txn := storage.NewTransactionOrDie(tb.Context(), store, storage.WriteParams)
	base := &DeactivateOpts{
		Ctx:           tb.Context(),
		Store:         store,
		Txn:           txn,
		BundleNames:   opts.BundleNames,
		ParserOptions: opts.ParserOptions,
	}

	if err := Deactivate(base); err != nil {
		tb.Fatalf("unexpected error deactivating bundles: %s", err)
	}

	if err := store.Commit(tb.Context(), txn); err != nil {
		tb.Fatalf("unexpected error committing transaction: %s", err)
	}
}

func verifyDeleteManifest(tb testing.TB, store storage.Store, name string) {
	tb.Helper()

	if err := storage.Txn(tb.Context(), store, storage.WriteParams, func(txn storage.Transaction) error {
		return EraseManifestFromStore(tb.Context(), store, txn, name)
	}); err != nil {
		tb.Fatalf("Unexpected error deleting manifest: %s", err)
	}
}

func verifyWriteManifests(tb testing.TB, store storage.Store, bundles map[string]Manifest) {
	tb.Helper()

	for name, manifest := range bundles {
		err := storage.Txn(tb.Context(), store, storage.WriteParams, func(txn storage.Transaction) error {
			err := WriteManifestToStore(tb.Context(), store, txn, name, manifest)
			if err != nil {
				tb.Fatalf("Failed to write manifest to store: %s", err)
			}
			return err
		})
		if err != nil {
			tb.Fatalf("Unexpected error finishing transaction: %s", err)
		}
	}
}

func verifyReadBundleNames(tb testing.TB, store storage.Store, txn storage.Transaction, expected ...string) {
	tb.Helper()

	if txn == nil {
		txn = storage.NewTransactionOrDie(tb.Context(), store)
		defer store.Abort(tb.Context(), txn)
	}

	names, err := ReadBundleNamesFromStore(tb.Context(), store, txn)
	if err != nil {
		if storage.IsNotFound(err) && len(expected) == 0 {
			return
		}
		tb.Fatalf("unexpected error: %s", err)
	}

	if len(names) != len(expected) {
		tb.Fatalf("expected %d bundles in store, found %d", len(expected), len(names))
	}

	expMap := map[string]struct{}{}
	for _, name := range expected {
		expMap[name] = struct{}{}
	}

	for _, name := range names {
		if _, ok := expMap[name]; !ok {
			tb.Fatalf("unexpected bundle %s to be found in store, found %v", name, names)
		}
	}
}

func verifyReadLegacyRevision(tb testing.TB, store storage.Store, expected string) {
	tb.Helper()

	if err := storage.Txn(tb.Context(), store, storage.WriteParams, func(txn storage.Transaction) error {
		actual, err := LegacyReadRevisionFromStore(tb.Context(), store, txn)
		if err != nil && !storage.IsNotFound(err) {
			tb.Fatalf("Failed to read manifest revision from store: %s", err)
		}

		if actual != expected {
			tb.Fatalf("Expected revision %s, got %s", expected, actual)
		}

		return nil
	}); err != nil {
		tb.Fatalf("Unexpected error finishing transaction: %s", err)
	}
}

func verifyBundleModulesCompiled(tb testing.TB, compiler *ast.Compiler, bundles map[string]*Bundle) {
	tb.Helper()

	for bundleName, bundle := range bundles {
		for modName := range bundle.ParsedModules(bundleName) {
			if _, ok := compiler.Modules[modName]; !ok {
				tb.Fatalf("expected module %s from bundle %s to have been compiled", modName, bundleName)
			}
		}
	}
}

func verifyResultRead(tb testing.TB, store storage.Store, expected string) {
	tb.Helper()

	txn := storage.NewTransactionOrDie(tb.Context(), store)
	defer store.Abort(tb.Context(), txn)

	act, err := store.Read(tb.Context(), txn, storage.RootPath)
	if err != nil {
		tb.Fatalf("unexpected error: %s", err)
	}

	if exp := loadExpectedResult(tb, expected); !reflect.DeepEqual(exp, act) {
		tb.Errorf("expected %v, got %v", exp, act)
	}
}

func assertEqual(tb testing.TB, expectAst bool, expected string, actual any) {
	tb.Helper()

	if expectAst {
		if exp := ast.MustParseTerm(expected); ast.Compare(exp, actual) != 0 {
			tb.Errorf("expected:\n\n%v\n\ngot:\n\n%v", expected, actual)
		}
	} else if exp := loadExpectedResult(tb, expected); !reflect.DeepEqual(exp, actual) {
		tb.Errorf("expected:\n\n%v\n\ngot:\n\n%v", expected, actual)
	}
}

func loadExpectedResult(tb testing.TB, input string) (data any) {
	if len(input) > 0 {
		if err := util.UnmarshalJSON(util.StringToByteSlice(input), &data); err != nil {
			tb.Fatalf("failed to unmarshal expected result: %s", err)
		}
	}
	return data
}

func bundleFromFiles(tb testing.TB, name string, files [][2]string) Bundle {
	tb.Helper()

	return bundleFromBuffer(tb, name, archive.MustWriteTarGz(files))
}

func bundleFromRoundtrip(tb testing.TB, name string, b Bundle) Bundle {
	tb.Helper()

	var buf bytes.Buffer
	if err := NewWriter(&buf).UseModulePath(true).Write(b); err != nil {
		tb.Fatal("Unexpected error:", err)
	}

	return bundleFromBuffer(tb, name, &buf)
}

func bundleFromBuffer(tb testing.TB, name string, buf *bytes.Buffer) Bundle {
	tb.Helper()

	return must(NewCustomReader(NewTarballLoaderWithBaseURL(buf, "")).
		WithLazyLoadingMode(true).
		WithBundleName(name).
		Read())(tb)
}

func must[T any](val T, err error) func(tb testing.TB) T {
	return func(tb testing.TB) T {
		tb.Helper()

		if err != nil {
			tb.Fatalf("unexpected error: %s", err)
		}
		return val
	}
}

func mustRead(tb testing.TB, s storage.Store, txn storage.Transaction, path storage.Path) any {
	tb.Helper()

	if txn == nil {
		txn = storage.NewTransactionOrDie(tb.Context(), s)
		defer s.Abort(tb.Context(), txn)
	}

	return must(s.Read(tb.Context(), txn, path))(tb)
}

func mustCommit(tb testing.TB, s storage.Store, txn storage.Transaction) {
	tb.Helper()

	if err := s.Commit(tb.Context(), txn); err != nil {
		tb.Fatalf("unexpected error trying to commit to store: %s", err)
	}
}

func moduleFile(path, raw string) ModuleFile {
	return ModuleFile{
		Path:   path,
		Raw:    util.StringToByteSlice(raw),
		Parsed: ast.MustParseModule(raw),
	}
}

// unpack takes flat map[string]any where nested keys are dot-separated,
// and returns a "unpacked" map[string]any with proper nesting.
func unpack(m map[string]any) map[string]any {
	result := make(map[string]any, len(m))

	for k, v := range m {
		var ok bool
		currMap := result
		curr, rest, _ := strings.Cut(k, ".")

		for rest != "" {
			if _, ok = currMap[curr]; !ok {
				currMap[curr] = make(map[string]any)
			}
			if currMap, ok = currMap[curr].(map[string]any); !ok {
				panic("key conflict")
			}
			curr, rest, _ = strings.Cut(rest, ".")
		}

		currMap[curr] = v
	}

	return result
}
