package registry_test

import (
	"context"
	"crypto/rand"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"math"
	"math/big"
	"os"
	"path/filepath"
	"reflect"
	"strings"
	"testing"

	"github.com/blang/semver/v4"
	"github.com/sirupsen/logrus"
	"github.com/stretchr/testify/require"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	utilerrors "k8s.io/apimachinery/pkg/util/errors"
	"sigs.k8s.io/yaml"

	"github.com/operator-framework/api/pkg/lib/version"
	"github.com/operator-framework/api/pkg/operators/v1alpha1"

	"github.com/operator-framework/operator-registry/pkg/api"
	"github.com/operator-framework/operator-registry/pkg/image"
	"github.com/operator-framework/operator-registry/pkg/lib/bundle"
	"github.com/operator-framework/operator-registry/pkg/registry"
	"github.com/operator-framework/operator-registry/pkg/sqlite"
)

func CreateTestDB(t *testing.T) (*sql.DB, func()) {
	r, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
	require.NoError(t, err)
	dbName := fmt.Sprintf("test-%d.db", r.Int64())

	db, err := sqlite.Open(dbName)
	require.NoError(t, err)

	return db, func() {
		defer func() {
			if err := os.Remove(dbName); err != nil {
				t.Fatal(err)
			}
		}()
		if err := db.Close(); err != nil {
			t.Fatal(err)
		}
	}
}

func createAndPopulateDB(db *sql.DB) (*sqlite.SQLQuerier, error) {
	load, err := sqlite.NewSQLLiteLoader(db)
	if err != nil {
		return nil, err
	}
	err = load.Migrate(context.TODO())
	if err != nil {
		return nil, err
	}
	query := sqlite.NewSQLLiteQuerierFromDb(db)

	graphLoader, err := sqlite.NewSQLGraphLoaderFromDB(db)
	if err != nil {
		return nil, err
	}

	populate := func(names []string) error {
		refMap := make(map[image.Reference]string, 0)
		for _, name := range names {
			refMap[image.SimpleReference("quay.io/test/"+name)] = "../../bundles/" + name
		}
		return registry.NewDirectoryPopulator(
			load,
			graphLoader,
			query,
			refMap,
			nil).Populate(registry.ReplacesMode)
	}
	names := []string{"etcd.0.9.0", "etcd.0.9.2", "prometheus.0.22.2", "prometheus.0.14.0", "prometheus.0.15.0"}
	if err := populate(names); err != nil {
		return nil, err
	}

	return query, nil
}

func TestImageLoader(t *testing.T) {
	logrus.SetLevel(logrus.DebugLevel)
	db, cleanup := CreateTestDB(t)
	defer cleanup()

	_, err := createAndPopulateDB(db)
	require.NoError(t, err)
}

func TestQuerierForImage(t *testing.T) {
	logrus.SetLevel(logrus.DebugLevel)
	db, cleanup := CreateTestDB(t)
	defer cleanup()

	store, err := createAndPopulateDB(db)
	require.NoError(t, err)

	foundPackages, err := store.ListPackages(context.TODO())
	require.NoError(t, err)
	require.ElementsMatch(t, []string{"etcd", "prometheus"}, foundPackages)

	etcdPackage, err := store.GetPackage(context.TODO(), "etcd")
	require.NoError(t, err)
	require.Equal(t, &registry.PackageManifest{
		PackageName:        "etcd",
		DefaultChannelName: "alpha",
		Channels: []registry.PackageChannel{
			{
				Name:           "alpha",
				CurrentCSVName: "etcdoperator.v0.9.2",
			},
			{
				Name:           "beta",
				CurrentCSVName: "etcdoperator.v0.9.0",
			},
			{
				Name:           "stable",
				CurrentCSVName: "etcdoperator.v0.9.2",
			},
		},
	}, etcdPackage)

	etcdBundleByChannel, err := store.GetBundleForChannel(context.TODO(), "etcd", "alpha")
	require.NoError(t, err)
	expectedBundle := &api.Bundle{
		CsvName:     "etcdoperator.v0.9.2",
		PackageName: "etcd",
		ChannelName: "alpha",
		CsvJson:     "{\"apiVersion\":\"operators.coreos.com/v1alpha1\",\"kind\":\"ClusterServiceVersion\",\"metadata\":{\"annotations\":{\"alm-examples\":\"[{\\\"apiVersion\\\":\\\"etcd.database.coreos.com/v1beta2\\\",\\\"kind\\\":\\\"EtcdCluster\\\",\\\"metadata\\\":{\\\"name\\\":\\\"example\\\",\\\"namespace\\\":\\\"default\\\"},\\\"spec\\\":{\\\"size\\\":3,\\\"version\\\":\\\"3.2.13\\\"}},{\\\"apiVersion\\\":\\\"etcd.database.coreos.com/v1beta2\\\",\\\"kind\\\":\\\"EtcdRestore\\\",\\\"metadata\\\":{\\\"name\\\":\\\"example-etcd-cluster\\\"},\\\"spec\\\":{\\\"etcdCluster\\\":{\\\"name\\\":\\\"example-etcd-cluster\\\"},\\\"backupStorageType\\\":\\\"S3\\\",\\\"s3\\\":{\\\"path\\\":\\\"\\u003cfull-s3-path\\u003e\\\",\\\"awsSecret\\\":\\\"\\u003caws-secret\\u003e\\\"}}},{\\\"apiVersion\\\":\\\"etcd.database.coreos.com/v1beta2\\\",\\\"kind\\\":\\\"EtcdBackup\\\",\\\"metadata\\\":{\\\"name\\\":\\\"example-etcd-cluster-backup\\\"},\\\"spec\\\":{\\\"etcdEndpoints\\\":[\\\"\\u003cetcd-cluster-endpoints\\u003e\\\"],\\\"storageType\\\":\\\"S3\\\",\\\"s3\\\":{\\\"path\\\":\\\"\\u003cfull-s3-path\\u003e\\\",\\\"awsSecret\\\":\\\"\\u003caws-secret\\u003e\\\"}}}]\",\"tectonic-visibility\":\"ocs\"},\"name\":\"etcdoperator.v0.9.2\",\"namespace\":\"placeholder\"},\"spec\":{\"customresourcedefinitions\":{\"owned\":[{\"description\":\"Represents a cluster of etcd nodes.\",\"displayName\":\"etcd Cluster\",\"kind\":\"EtcdCluster\",\"name\":\"etcdclusters.etcd.database.coreos.com\",\"resources\":[{\"kind\":\"Service\",\"version\":\"v1\"},{\"kind\":\"Pod\",\"version\":\"v1\"}],\"specDescriptors\":[{\"description\":\"The desired number of member Pods for the etcd cluster.\",\"displayName\":\"Size\",\"path\":\"size\",\"x-descriptors\":[\"urn:alm:descriptor:com.tectonic.ui:podCount\"]},{\"description\":\"Limits describes the minimum/maximum amount of compute resources required/allowed\",\"displayName\":\"Resource Requirements\",\"path\":\"pod.resources\",\"x-descriptors\":[\"urn:alm:descriptor:com.tectonic.ui:resourceRequirements\"]}],\"statusDescriptors\":[{\"description\":\"The status of each of the member Pods for the etcd cluster.\",\"displayName\":\"Member Status\",\"path\":\"members\",\"x-descriptors\":[\"urn:alm:descriptor:com.tectonic.ui:podStatuses\"]},{\"description\":\"The service at which the running etcd cluster can be accessed.\",\"displayName\":\"Service\",\"path\":\"serviceName\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes:Service\"]},{\"description\":\"The current size of the etcd cluster.\",\"displayName\":\"Cluster Size\",\"path\":\"size\"},{\"description\":\"The current version of the etcd cluster.\",\"displayName\":\"Current Version\",\"path\":\"currentVersion\"},{\"description\":\"The target version of the etcd cluster, after upgrading.\",\"displayName\":\"Target Version\",\"path\":\"targetVersion\"},{\"description\":\"The current status of the etcd cluster.\",\"displayName\":\"Status\",\"path\":\"phase\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes.phase\"]},{\"description\":\"Explanation for the current status of the cluster.\",\"displayName\":\"Status Details\",\"path\":\"reason\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes.phase:reason\"]}],\"version\":\"v1beta2\"},{\"description\":\"Represents the intent to backup an etcd cluster.\",\"displayName\":\"etcd Backup\",\"kind\":\"EtcdBackup\",\"name\":\"etcdbackups.etcd.database.coreos.com\",\"specDescriptors\":[{\"description\":\"Specifies the endpoints of an etcd cluster.\",\"displayName\":\"etcd Endpoint(s)\",\"path\":\"etcdEndpoints\",\"x-descriptors\":[\"urn:alm:descriptor:etcd:endpoint\"]},{\"description\":\"The full AWS S3 path where the backup is saved.\",\"displayName\":\"S3 Path\",\"path\":\"s3.path\",\"x-descriptors\":[\"urn:alm:descriptor:aws:s3:path\"]},{\"description\":\"The name of the secret object that stores the AWS credential and config files.\",\"displayName\":\"AWS Secret\",\"path\":\"s3.awsSecret\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes:Secret\"]}],\"statusDescriptors\":[{\"description\":\"Indicates if the backup was successful.\",\"displayName\":\"Succeeded\",\"path\":\"succeeded\",\"x-descriptors\":[\"urn:alm:descriptor:text\"]},{\"description\":\"Indicates the reason for any backup related failures.\",\"displayName\":\"Reason\",\"path\":\"reason\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes.phase:reason\"]}],\"version\":\"v1beta2\"},{\"description\":\"Represents the intent to restore an etcd cluster from a backup.\",\"displayName\":\"etcd Restore\",\"kind\":\"EtcdRestore\",\"name\":\"etcdrestores.etcd.database.coreos.com\",\"specDescriptors\":[{\"description\":\"References the EtcdCluster which should be restored,\",\"displayName\":\"etcd Cluster\",\"path\":\"etcdCluster.name\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes:EtcdCluster\",\"urn:alm:descriptor:text\"]},{\"description\":\"The full AWS S3 path where the backup is saved.\",\"displayName\":\"S3 Path\",\"path\":\"s3.path\",\"x-descriptors\":[\"urn:alm:descriptor:aws:s3:path\"]},{\"description\":\"The name of the secret object that stores the AWS credential and config files.\",\"displayName\":\"AWS Secret\",\"path\":\"s3.awsSecret\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes:Secret\"]}],\"statusDescriptors\":[{\"description\":\"Indicates if the restore was successful.\",\"displayName\":\"Succeeded\",\"path\":\"succeeded\",\"x-descriptors\":[\"urn:alm:descriptor:text\"]},{\"description\":\"Indicates the reason for any restore related failures.\",\"displayName\":\"Reason\",\"path\":\"reason\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes.phase:reason\"]}],\"version\":\"v1beta2\"}],\"required\":[{\"description\":\"Represents a cluster of etcd nodes.\",\"displayName\":\"etcd Cluster\",\"kind\":\"EtcdCluster\",\"name\":\"etcdclusters.etcd.database.coreos.com\",\"resources\":[{\"kind\":\"Service\",\"version\":\"v1\"},{\"kind\":\"Pod\",\"version\":\"v1\"}],\"specDescriptors\":[{\"description\":\"The desired number of member Pods for the etcd cluster.\",\"displayName\":\"Size\",\"path\":\"size\",\"x-descriptors\":[\"urn:alm:descriptor:com.tectonic.ui:podCount\"]}],\"version\":\"v1beta2\"}]},\"description\":\"etcd is a distributed key value store that provides a reliable way to store data across a cluster of machines. It’s open-source and available on GitHub. etcd gracefully handles leader elections during network partitions and will tolerate machine failure, including the leader. Your applications can read and write data into etcd.\\nA simple use-case is to store database connection details or feature flags within etcd as key value pairs. These values can be watched, allowing your app to reconfigure itself when they change. Advanced uses take advantage of the consistency guarantees to implement database leader elections or do distributed locking across a cluster of workers.\\n\\n_The etcd Open Cloud Service is Public Alpha. The goal before Beta is to fully implement backup features._\\n\\n### Reading and writing to etcd\\n\\nCommunicate with etcd though its command line utility `etcdctl` or with the API using the Kubernetes Service.\\n\\n[Read the complete guide to using the etcd Open Cloud Service](https://coreos.com/tectonic/docs/latest/alm/etcd-ocs.html)\\n\\n### Supported Features\\n\\n\\n**High availability**\\n\\n\\nMultiple instances of etcd are networked together and secured. Individual failures or networking issues are transparently handled to keep your cluster up and running.\\n\\n\\n**Automated updates**\\n\\n\\nRolling out a new etcd version works like all Kubernetes rolling updates. Simply declare the desired version, and the etcd service starts a safe rolling update to the new version automatically.\\n\\n\\n**Backups included**\\n\\n\\nComing soon, the ability to schedule backups to happen on or off cluster.\\n\",\"displayName\":\"etcd\",\"icon\":[{\"base64data\":\" \",\"mediatype\":\"image/png\"}],\"install\":{\"spec\":{\"deployments\":[{\"name\":\"etcd-operator\",\"spec\":{\"replicas\":1,\"selector\":{\"matchLabels\":{\"name\":\"etcd-operator-alm-owned\"}},\"template\":{\"metadata\":{\"labels\":{\"name\":\"etcd-operator-alm-owned\"},\"name\":\"etcd-operator-alm-owned\"},\"spec\":{\"containers\":[{\"command\":[\"etcd-operator\",\"--create-crd=false\"],\"env\":[{\"name\":\"MY_POD_NAMESPACE\",\"valueFrom\":{\"fieldRef\":{\"fieldPath\":\"metadata.namespace\"}}},{\"name\":\"MY_POD_NAME\",\"valueFrom\":{\"fieldRef\":{\"fieldPath\":\"metadata.name\"}}}],\"image\":\"quay.io/coreos/etcd-operator@sha256:c0301e4686c3ed4206e370b42de5a3bd2229b9fb4906cf85f3f30650424abec2\",\"name\":\"etcd-operator\"},{\"command\":[\"etcd-backup-operator\",\"--create-crd=false\"],\"env\":[{\"name\":\"MY_POD_NAMESPACE\",\"valueFrom\":{\"fieldRef\":{\"fieldPath\":\"metadata.namespace\"}}},{\"name\":\"MY_POD_NAME\",\"valueFrom\":{\"fieldRef\":{\"fieldPath\":\"metadata.name\"}}}],\"image\":\"quay.io/coreos/etcd-operator@sha256:c0301e4686c3ed4206e370b42de5a3bd2229b9fb4906cf85f3f30650424abec2\",\"name\":\"etcd-backup-operator\"},{\"command\":[\"etcd-restore-operator\",\"--create-crd=false\"],\"env\":[{\"name\":\"MY_POD_NAMESPACE\",\"valueFrom\":{\"fieldRef\":{\"fieldPath\":\"metadata.namespace\"}}},{\"name\":\"MY_POD_NAME\",\"valueFrom\":{\"fieldRef\":{\"fieldPath\":\"metadata.name\"}}}],\"image\":\"quay.io/coreos/etcd-operator@sha256:c0301e4686c3ed4206e370b42de5a3bd2229b9fb4906cf85f3f30650424abec2\",\"name\":\"etcd-restore-operator\"}],\"serviceAccountName\":\"etcd-operator\"}}}}],\"permissions\":[{\"rules\":[{\"apiGroups\":[\"etcd.database.coreos.com\"],\"resources\":[\"etcdclusters\",\"etcdbackups\",\"etcdrestores\"],\"verbs\":[\"*\"]},{\"apiGroups\":[\"\"],\"resources\":[\"pods\",\"services\",\"endpoints\",\"persistentvolumeclaims\",\"events\"],\"verbs\":[\"*\"]},{\"apiGroups\":[\"apps\"],\"resources\":[\"deployments\"],\"verbs\":[\"*\"]},{\"apiGroups\":[\"\"],\"resources\":[\"secrets\"],\"verbs\":[\"get\"]}],\"serviceAccountName\":\"etcd-operator\"}]},\"strategy\":\"deployment\"},\"keywords\":[\"etcd\",\"key value\",\"database\",\"coreos\",\"open source\"],\"labels\":{\"alm-owner-etcd\":\"etcdoperator\",\"operated-by\":\"etcdoperator\"},\"links\":[{\"name\":\"Blog\",\"url\":\"https://coreos.com/etcd\"},{\"name\":\"Documentation\",\"url\":\"https://coreos.com/operators/etcd/docs/latest/\"},{\"name\":\"etcd Operator Source Code\",\"url\":\"https://github.com/coreos/etcd-operator\"}],\"maintainers\":[{\"email\":\"support@coreos.com\",\"name\":\"CoreOS, Inc\"}],\"maturity\":\"alpha\",\"provider\":{\"name\":\"CoreOS, Inc\"},\"replaces\":\"etcdoperator.v0.9.0\",\"selector\":{\"matchLabels\":{\"alm-owner-etcd\":\"etcdoperator\",\"operated-by\":\"etcdoperator\"}},\"skips\":[\"etcdoperator.v0.9.1\"],\"version\":\"0.9.2\"}}",
		Object:      []string{"{\"apiVersion\":\"apiextensions.k8s.io/v1beta1\",\"kind\":\"CustomResourceDefinition\",\"metadata\":{\"name\":\"etcdbackups.etcd.database.coreos.com\"},\"spec\":{\"group\":\"etcd.database.coreos.com\",\"names\":{\"kind\":\"EtcdBackup\",\"listKind\":\"EtcdBackupList\",\"plural\":\"etcdbackups\",\"singular\":\"etcdbackup\"},\"scope\":\"Namespaced\",\"version\":\"v1beta2\"}}", "{\"apiVersion\":\"apiextensions.k8s.io/v1beta1\",\"kind\":\"CustomResourceDefinition\",\"metadata\":{\"name\":\"etcdclusters.etcd.database.coreos.com\"},\"spec\":{\"group\":\"etcd.database.coreos.com\",\"names\":{\"kind\":\"EtcdCluster\",\"listKind\":\"EtcdClusterList\",\"plural\":\"etcdclusters\",\"shortNames\":[\"etcdclus\",\"etcd\"],\"singular\":\"etcdcluster\"},\"scope\":\"Namespaced\",\"version\":\"v1beta2\"}}", "{\"apiVersion\":\"operators.coreos.com/v1alpha1\",\"kind\":\"ClusterServiceVersion\",\"metadata\":{\"annotations\":{\"alm-examples\":\"[{\\\"apiVersion\\\":\\\"etcd.database.coreos.com/v1beta2\\\",\\\"kind\\\":\\\"EtcdCluster\\\",\\\"metadata\\\":{\\\"name\\\":\\\"example\\\",\\\"namespace\\\":\\\"default\\\"},\\\"spec\\\":{\\\"size\\\":3,\\\"version\\\":\\\"3.2.13\\\"}},{\\\"apiVersion\\\":\\\"etcd.database.coreos.com/v1beta2\\\",\\\"kind\\\":\\\"EtcdRestore\\\",\\\"metadata\\\":{\\\"name\\\":\\\"example-etcd-cluster\\\"},\\\"spec\\\":{\\\"etcdCluster\\\":{\\\"name\\\":\\\"example-etcd-cluster\\\"},\\\"backupStorageType\\\":\\\"S3\\\",\\\"s3\\\":{\\\"path\\\":\\\"\\u003cfull-s3-path\\u003e\\\",\\\"awsSecret\\\":\\\"\\u003caws-secret\\u003e\\\"}}},{\\\"apiVersion\\\":\\\"etcd.database.coreos.com/v1beta2\\\",\\\"kind\\\":\\\"EtcdBackup\\\",\\\"metadata\\\":{\\\"name\\\":\\\"example-etcd-cluster-backup\\\"},\\\"spec\\\":{\\\"etcdEndpoints\\\":[\\\"\\u003cetcd-cluster-endpoints\\u003e\\\"],\\\"storageType\\\":\\\"S3\\\",\\\"s3\\\":{\\\"path\\\":\\\"\\u003cfull-s3-path\\u003e\\\",\\\"awsSecret\\\":\\\"\\u003caws-secret\\u003e\\\"}}}]\",\"tectonic-visibility\":\"ocs\"},\"name\":\"etcdoperator.v0.9.2\",\"namespace\":\"placeholder\"},\"spec\":{\"customresourcedefinitions\":{\"owned\":[{\"description\":\"Represents a cluster of etcd nodes.\",\"displayName\":\"etcd Cluster\",\"kind\":\"EtcdCluster\",\"name\":\"etcdclusters.etcd.database.coreos.com\",\"resources\":[{\"kind\":\"Service\",\"version\":\"v1\"},{\"kind\":\"Pod\",\"version\":\"v1\"}],\"specDescriptors\":[{\"description\":\"The desired number of member Pods for the etcd cluster.\",\"displayName\":\"Size\",\"path\":\"size\",\"x-descriptors\":[\"urn:alm:descriptor:com.tectonic.ui:podCount\"]},{\"description\":\"Limits describes the minimum/maximum amount of compute resources required/allowed\",\"displayName\":\"Resource Requirements\",\"path\":\"pod.resources\",\"x-descriptors\":[\"urn:alm:descriptor:com.tectonic.ui:resourceRequirements\"]}],\"statusDescriptors\":[{\"description\":\"The status of each of the member Pods for the etcd cluster.\",\"displayName\":\"Member Status\",\"path\":\"members\",\"x-descriptors\":[\"urn:alm:descriptor:com.tectonic.ui:podStatuses\"]},{\"description\":\"The service at which the running etcd cluster can be accessed.\",\"displayName\":\"Service\",\"path\":\"serviceName\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes:Service\"]},{\"description\":\"The current size of the etcd cluster.\",\"displayName\":\"Cluster Size\",\"path\":\"size\"},{\"description\":\"The current version of the etcd cluster.\",\"displayName\":\"Current Version\",\"path\":\"currentVersion\"},{\"description\":\"The target version of the etcd cluster, after upgrading.\",\"displayName\":\"Target Version\",\"path\":\"targetVersion\"},{\"description\":\"The current status of the etcd cluster.\",\"displayName\":\"Status\",\"path\":\"phase\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes.phase\"]},{\"description\":\"Explanation for the current status of the cluster.\",\"displayName\":\"Status Details\",\"path\":\"reason\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes.phase:reason\"]}],\"version\":\"v1beta2\"},{\"description\":\"Represents the intent to backup an etcd cluster.\",\"displayName\":\"etcd Backup\",\"kind\":\"EtcdBackup\",\"name\":\"etcdbackups.etcd.database.coreos.com\",\"specDescriptors\":[{\"description\":\"Specifies the endpoints of an etcd cluster.\",\"displayName\":\"etcd Endpoint(s)\",\"path\":\"etcdEndpoints\",\"x-descriptors\":[\"urn:alm:descriptor:etcd:endpoint\"]},{\"description\":\"The full AWS S3 path where the backup is saved.\",\"displayName\":\"S3 Path\",\"path\":\"s3.path\",\"x-descriptors\":[\"urn:alm:descriptor:aws:s3:path\"]},{\"description\":\"The name of the secret object that stores the AWS credential and config files.\",\"displayName\":\"AWS Secret\",\"path\":\"s3.awsSecret\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes:Secret\"]}],\"statusDescriptors\":[{\"description\":\"Indicates if the backup was successful.\",\"displayName\":\"Succeeded\",\"path\":\"succeeded\",\"x-descriptors\":[\"urn:alm:descriptor:text\"]},{\"description\":\"Indicates the reason for any backup related failures.\",\"displayName\":\"Reason\",\"path\":\"reason\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes.phase:reason\"]}],\"version\":\"v1beta2\"},{\"description\":\"Represents the intent to restore an etcd cluster from a backup.\",\"displayName\":\"etcd Restore\",\"kind\":\"EtcdRestore\",\"name\":\"etcdrestores.etcd.database.coreos.com\",\"specDescriptors\":[{\"description\":\"References the EtcdCluster which should be restored,\",\"displayName\":\"etcd Cluster\",\"path\":\"etcdCluster.name\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes:EtcdCluster\",\"urn:alm:descriptor:text\"]},{\"description\":\"The full AWS S3 path where the backup is saved.\",\"displayName\":\"S3 Path\",\"path\":\"s3.path\",\"x-descriptors\":[\"urn:alm:descriptor:aws:s3:path\"]},{\"description\":\"The name of the secret object that stores the AWS credential and config files.\",\"displayName\":\"AWS Secret\",\"path\":\"s3.awsSecret\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes:Secret\"]}],\"statusDescriptors\":[{\"description\":\"Indicates if the restore was successful.\",\"displayName\":\"Succeeded\",\"path\":\"succeeded\",\"x-descriptors\":[\"urn:alm:descriptor:text\"]},{\"description\":\"Indicates the reason for any restore related failures.\",\"displayName\":\"Reason\",\"path\":\"reason\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes.phase:reason\"]}],\"version\":\"v1beta2\"}],\"required\":[{\"description\":\"Represents a cluster of etcd nodes.\",\"displayName\":\"etcd Cluster\",\"kind\":\"EtcdCluster\",\"name\":\"etcdclusters.etcd.database.coreos.com\",\"resources\":[{\"kind\":\"Service\",\"version\":\"v1\"},{\"kind\":\"Pod\",\"version\":\"v1\"}],\"specDescriptors\":[{\"description\":\"The desired number of member Pods for the etcd cluster.\",\"displayName\":\"Size\",\"path\":\"size\",\"x-descriptors\":[\"urn:alm:descriptor:com.tectonic.ui:podCount\"]}],\"version\":\"v1beta2\"}]},\"description\":\"etcd is a distributed key value store that provides a reliable way to store data across a cluster of machines. It’s open-source and available on GitHub. etcd gracefully handles leader elections during network partitions and will tolerate machine failure, including the leader. Your applications can read and write data into etcd.\\nA simple use-case is to store database connection details or feature flags within etcd as key value pairs. These values can be watched, allowing your app to reconfigure itself when they change. Advanced uses take advantage of the consistency guarantees to implement database leader elections or do distributed locking across a cluster of workers.\\n\\n_The etcd Open Cloud Service is Public Alpha. The goal before Beta is to fully implement backup features._\\n\\n### Reading and writing to etcd\\n\\nCommunicate with etcd though its command line utility `etcdctl` or with the API using the Kubernetes Service.\\n\\n[Read the complete guide to using the etcd Open Cloud Service](https://coreos.com/tectonic/docs/latest/alm/etcd-ocs.html)\\n\\n### Supported Features\\n\\n\\n**High availability**\\n\\n\\nMultiple instances of etcd are networked together and secured. Individual failures or networking issues are transparently handled to keep your cluster up and running.\\n\\n\\n**Automated updates**\\n\\n\\nRolling out a new etcd version works like all Kubernetes rolling updates. Simply declare the desired version, and the etcd service starts a safe rolling update to the new version automatically.\\n\\n\\n**Backups included**\\n\\n\\nComing soon, the ability to schedule backups to happen on or off cluster.\\n\",\"displayName\":\"etcd\",\"icon\":[{\"base64data\":\" \",\"mediatype\":\"image/png\"}],\"install\":{\"spec\":{\"deployments\":[{\"name\":\"etcd-operator\",\"spec\":{\"replicas\":1,\"selector\":{\"matchLabels\":{\"name\":\"etcd-operator-alm-owned\"}},\"template\":{\"metadata\":{\"labels\":{\"name\":\"etcd-operator-alm-owned\"},\"name\":\"etcd-operator-alm-owned\"},\"spec\":{\"containers\":[{\"command\":[\"etcd-operator\",\"--create-crd=false\"],\"env\":[{\"name\":\"MY_POD_NAMESPACE\",\"valueFrom\":{\"fieldRef\":{\"fieldPath\":\"metadata.namespace\"}}},{\"name\":\"MY_POD_NAME\",\"valueFrom\":{\"fieldRef\":{\"fieldPath\":\"metadata.name\"}}}],\"image\":\"quay.io/coreos/etcd-operator@sha256:c0301e4686c3ed4206e370b42de5a3bd2229b9fb4906cf85f3f30650424abec2\",\"name\":\"etcd-operator\"},{\"command\":[\"etcd-backup-operator\",\"--create-crd=false\"],\"env\":[{\"name\":\"MY_POD_NAMESPACE\",\"valueFrom\":{\"fieldRef\":{\"fieldPath\":\"metadata.namespace\"}}},{\"name\":\"MY_POD_NAME\",\"valueFrom\":{\"fieldRef\":{\"fieldPath\":\"metadata.name\"}}}],\"image\":\"quay.io/coreos/etcd-operator@sha256:c0301e4686c3ed4206e370b42de5a3bd2229b9fb4906cf85f3f30650424abec2\",\"name\":\"etcd-backup-operator\"},{\"command\":[\"etcd-restore-operator\",\"--create-crd=false\"],\"env\":[{\"name\":\"MY_POD_NAMESPACE\",\"valueFrom\":{\"fieldRef\":{\"fieldPath\":\"metadata.namespace\"}}},{\"name\":\"MY_POD_NAME\",\"valueFrom\":{\"fieldRef\":{\"fieldPath\":\"metadata.name\"}}}],\"image\":\"quay.io/coreos/etcd-operator@sha256:c0301e4686c3ed4206e370b42de5a3bd2229b9fb4906cf85f3f30650424abec2\",\"name\":\"etcd-restore-operator\"}],\"serviceAccountName\":\"etcd-operator\"}}}}],\"permissions\":[{\"rules\":[{\"apiGroups\":[\"etcd.database.coreos.com\"],\"resources\":[\"etcdclusters\",\"etcdbackups\",\"etcdrestores\"],\"verbs\":[\"*\"]},{\"apiGroups\":[\"\"],\"resources\":[\"pods\",\"services\",\"endpoints\",\"persistentvolumeclaims\",\"events\"],\"verbs\":[\"*\"]},{\"apiGroups\":[\"apps\"],\"resources\":[\"deployments\"],\"verbs\":[\"*\"]},{\"apiGroups\":[\"\"],\"resources\":[\"secrets\"],\"verbs\":[\"get\"]}],\"serviceAccountName\":\"etcd-operator\"}]},\"strategy\":\"deployment\"},\"keywords\":[\"etcd\",\"key value\",\"database\",\"coreos\",\"open source\"],\"labels\":{\"alm-owner-etcd\":\"etcdoperator\",\"operated-by\":\"etcdoperator\"},\"links\":[{\"name\":\"Blog\",\"url\":\"https://coreos.com/etcd\"},{\"name\":\"Documentation\",\"url\":\"https://coreos.com/operators/etcd/docs/latest/\"},{\"name\":\"etcd Operator Source Code\",\"url\":\"https://github.com/coreos/etcd-operator\"}],\"maintainers\":[{\"email\":\"support@coreos.com\",\"name\":\"CoreOS, Inc\"}],\"maturity\":\"alpha\",\"provider\":{\"name\":\"CoreOS, Inc\"},\"replaces\":\"etcdoperator.v0.9.0\",\"selector\":{\"matchLabels\":{\"alm-owner-etcd\":\"etcdoperator\",\"operated-by\":\"etcdoperator\"}},\"skips\":[\"etcdoperator.v0.9.1\"],\"version\":\"0.9.2\"}}", "{\"apiVersion\":\"apiextensions.k8s.io/v1beta1\",\"kind\":\"CustomResourceDefinition\",\"metadata\":{\"name\":\"etcdrestores.etcd.database.coreos.com\"},\"spec\":{\"group\":\"etcd.database.coreos.com\",\"names\":{\"kind\":\"EtcdRestore\",\"listKind\":\"EtcdRestoreList\",\"plural\":\"etcdrestores\",\"singular\":\"etcdrestore\"},\"scope\":\"Namespaced\",\"version\":\"v1beta2\"}}"}, BundlePath: "quay.io/test/etcd.0.9.2",
		Version:   "0.9.2",
		SkipRange: "",
		Dependencies: []*api.Dependency{
			{
				Type:  "olm.gvk",
				Value: `{"group":"testapi.coreos.com","kind":"testapi","version":"v1"}`,
			},
			{
				Type:  "olm.gvk",
				Value: `{"group":"etcd.database.coreos.com","kind":"EtcdCluster","version":"v1beta2"}`,
			},
			{
				Type:  "olm.constraint",
				Value: `{"cel":{"rule":"properties.exists(p, p.type == \"certified\")"},"failureMessage":"require to have \"certified\""}`,
			},
		},
		Properties: []*api.Property{
			{
				Type:  "olm.package",
				Value: `{"packageName":"etcd","version":"0.9.2"}`,
			},
			{
				Type:  "olm.gvk",
				Value: `{"group":"etcd.database.coreos.com","kind":"EtcdCluster","version":"v1beta2"}`,
			},
			{
				Type:  "olm.gvk",
				Value: `{"group":"etcd.database.coreos.com","kind":"EtcdBackup","version":"v1beta2"}`,
			},
			{
				Type:  "olm.gvk",
				Value: `{"group":"etcd.database.coreos.com","kind":"EtcdRestore","version":"v1beta2"}`,
			},
		},
		ProvidedApis: []*api.GroupVersionKind{
			{Group: "etcd.database.coreos.com", Version: "v1beta2", Kind: "EtcdCluster", Plural: "etcdclusters"},
			{Group: "etcd.database.coreos.com", Version: "v1beta2", Kind: "EtcdBackup", Plural: "etcdbackups"},
			{Group: "etcd.database.coreos.com", Version: "v1beta2", Kind: "EtcdRestore", Plural: "etcdrestores"},
		},
		RequiredApis: []*api.GroupVersionKind{
			{Group: "etcd.database.coreos.com", Version: "v1beta2", Kind: "EtcdCluster", Plural: "etcdclusters"},
			{Group: "testapi.coreos.com", Version: "v1", Kind: "testapi"},
		},
	}
	bareGetBundleForChannelResult := &api.Bundle{
		CsvName: expectedBundle.CsvName,
		CsvJson: expectedBundle.CsvJson + "\n",
	}
	EqualBundles(t, *bareGetBundleForChannelResult, *etcdBundleByChannel)

	etcdBundle, err := store.GetBundle(context.TODO(), "etcd", "alpha", "etcdoperator.v0.9.2")
	require.NoError(t, err)
	EqualBundles(t, *expectedBundle, *etcdBundle)

	etcdChannelEntries, err := store.GetChannelEntriesThatReplace(context.TODO(), "etcdoperator.v0.9.0")
	require.NoError(t, err)
	require.ElementsMatch(t, []*registry.ChannelEntry{{"etcd", "alpha", "etcdoperator.v0.9.2", "etcdoperator.v0.9.0"}, {"etcd", "stable", "etcdoperator.v0.9.2", "etcdoperator.v0.9.0"}}, etcdChannelEntries)

	etcdBundleByReplaces, err := store.GetBundleThatReplaces(context.TODO(), "etcdoperator.v0.9.0", "etcd", "alpha")
	require.NoError(t, err)
	EqualBundles(t, *expectedBundle, *etcdBundleByReplaces)

	etcdChannelEntriesThatProvide, err := store.GetChannelEntriesThatProvide(context.TODO(), "etcd.database.coreos.com", "v1beta2", "EtcdCluster")
	require.NoError(t, err)
	require.ElementsMatch(t, []*registry.ChannelEntry{
		{"etcd", "alpha", "etcdoperator.v0.9.0", ""},
		{"etcd", "alpha", "etcdoperator.v0.9.2", "etcdoperator.v0.9.1"},
		{"etcd", "alpha", "etcdoperator.v0.9.2", "etcdoperator.v0.9.0"},
		{"etcd", "stable", "etcdoperator.v0.9.0", ""},
		{"etcd", "stable", "etcdoperator.v0.9.2", "etcdoperator.v0.9.1"},
		{"etcd", "stable", "etcdoperator.v0.9.2", "etcdoperator.v0.9.0"},
		{"etcd", "beta", "etcdoperator.v0.9.0", ""}}, etcdChannelEntriesThatProvide)

	etcdLatestChannelEntriesThatProvide, err := store.GetLatestChannelEntriesThatProvide(context.TODO(), "etcd.database.coreos.com", "v1beta2", "EtcdCluster")
	require.NoError(t, err)
	require.ElementsMatch(t, []*registry.ChannelEntry{{"etcd", "alpha", "etcdoperator.v0.9.2", "etcdoperator.v0.9.0"},
		{"etcd", "stable", "etcdoperator.v0.9.2", "etcdoperator.v0.9.0"},
		{"etcd", "beta", "etcdoperator.v0.9.0", ""}}, etcdLatestChannelEntriesThatProvide)

	etcdBundleByProvides, err := store.GetBundleThatProvides(context.TODO(), "etcd.database.coreos.com", "v1beta2", "EtcdCluster")
	require.NoError(t, err)
	EqualBundles(t, *expectedBundle, *etcdBundleByProvides)

	expectedEtcdImages := []string{
		"quay.io/test/etcd.0.9.2",
		"quay.io/coreos/etcd-operator@sha256:c0301e4686c3ed4206e370b42de5a3bd2229b9fb4906cf85f3f30650424abec2",
	}
	etcdImages, err := store.GetImagesForBundle(context.TODO(), "etcdoperator.v0.9.2")
	require.NoError(t, err)
	require.ElementsMatch(t, expectedEtcdImages, etcdImages)

	expectedDatabaseImages := []string{
		"quay.io/test/etcd.0.9.0",
		"quay.io/coreos/etcd-operator@sha256:db563baa8194fcfe39d1df744ed70024b0f1f9e9b55b5923c2f3a413c44dc6b8",
		"quay.io/test/etcd.0.9.2",
		"quay.io/coreos/etcd-operator@sha256:c0301e4686c3ed4206e370b42de5a3bd2229b9fb4906cf85f3f30650424abec2",
		"quay.io/test/prometheus.0.14.0",
		"quay.io/coreos/prometheus-operator@sha256:5037b4e90dbb03ebdefaa547ddf6a1f748c8eeebeedf6b9d9f0913ad662b5731",
		"quay.io/test/prometheus.0.15.0",
		"quay.io/coreos/prometheus-operator@sha256:0e92dd9b5789c4b13d53e1319d0a6375bcca4caaf0d698af61198061222a576d",
		"quay.io/test/prometheus.0.22.2",
		"quay.io/coreos/prometheus-operator@sha256:3daa69a8c6c2f1d35dcf1fe48a7cd8b230e55f5229a1ded438f687debade5bcf",
	}
	dbImages, err := store.ListImages(context.TODO())
	require.NoError(t, err)
	require.ElementsMatch(t, expectedDatabaseImages, dbImages)

	version, err := store.GetBundleVersion(context.TODO(), "quay.io/test/etcd.0.9.2")
	require.NoError(t, err)
	require.Equal(t, "0.9.2", version)

	bundlePaths, err := store.GetBundlePathsForPackage(context.TODO(), "etcd")
	require.NoError(t, err)
	expectedBundlePaths := []string{"quay.io/test/etcd.0.9.0", "quay.io/test/etcd.0.9.2"}
	require.ElementsMatch(t, expectedBundlePaths, bundlePaths)

	defaultChannel, err := store.GetDefaultChannelForPackage(context.TODO(), "etcd")
	require.NoError(t, err)
	require.Equal(t, "alpha", defaultChannel)

	listChannels, err := store.ListChannels(context.TODO(), "etcd")
	require.NoError(t, err)
	expectedListChannels := []string{"alpha", "stable", "beta"}
	require.ElementsMatch(t, expectedListChannels, listChannels)

	currentCSVName, err := store.GetCurrentCSVNameForChannel(context.TODO(), "etcd", "alpha")
	require.NoError(t, err)
	require.Equal(t, "etcdoperator.v0.9.2", currentCSVName)
}

func TestImageLoading(t *testing.T) {
	// TODO: remove requirement to have real files
	type img struct {
		ref image.SimpleReference
		dir string
	}
	tests := []struct {
		name         string
		initImages   []img
		addImage     img
		wantPackages []*registry.Package
		wantErr      bool
		err          error
	}{
		{
			name: "OneChannel/AddBundleToTwoChannels",
			initImages: []img{
				{
					// this is in the "preview" channel
					ref: image.SimpleReference("quay.io/prometheus/operator:0.14.0"),
					dir: "../../bundles/prometheus.0.14.0",
				},
			},
			addImage: img{
				// this is in the "preview" and "stable" channels and replaces 0.14.0
				ref: image.SimpleReference("quay.io/prometheus/operator:0.15.0"),
				dir: "../../bundles/prometheus.0.15.0",
			},
			wantPackages: []*registry.Package{
				{
					Name:           "prometheus",
					DefaultChannel: "preview",
					Channels: map[string]registry.Channel{
						"preview": {
							Head: registry.BundleKey{
								BundlePath: "quay.io/prometheus/operator:0.15.0",
								Version:    "0.15.0",
								CsvName:    "prometheusoperator.0.15.0",
							},
							Nodes: map[registry.BundleKey]map[registry.BundleKey]struct{}{
								{BundlePath: "quay.io/prometheus/operator:0.15.0", Version: "0.15.0", CsvName: "prometheusoperator.0.15.0"}: {
									{BundlePath: "quay.io/prometheus/operator:0.14.0", Version: "0.14.0", CsvName: "prometheusoperator.0.14.0"}: struct{}{},
								},
								{BundlePath: "quay.io/prometheus/operator:0.14.0", Version: "0.14.0", CsvName: "prometheusoperator.0.14.0"}: {},
							},
						},
						"stable": {
							Head: registry.BundleKey{
								BundlePath: "quay.io/prometheus/operator:0.15.0",
								Version:    "0.15.0",
								CsvName:    "prometheusoperator.0.15.0",
							},
							Nodes: map[registry.BundleKey]map[registry.BundleKey]struct{}{
								{BundlePath: "quay.io/prometheus/operator:0.15.0", Version: "0.15.0", CsvName: "prometheusoperator.0.15.0"}: {
									{BundlePath: "quay.io/prometheus/operator:0.14.0", Version: "0.14.0", CsvName: "prometheusoperator.0.14.0"}: struct{}{},
								},
								{BundlePath: "quay.io/prometheus/operator:0.14.0", Version: "0.14.0", CsvName: "prometheusoperator.0.14.0"}: {}},
						},
					},
				},
			},
			wantErr: false,
		},
		{
			name: "OneChannel/AddBundleToNewChannel",
			initImages: []img{
				{
					// this is in the "preview" channel
					ref: image.SimpleReference("quay.io/prometheus/operator:0.14.0"),
					dir: "../../bundles/prometheus.0.14.0",
				},
			},
			addImage: img{
				// this is in the "beta" channel
				ref: image.SimpleReference("quay.io/prometheus/operator:0.14.0-beta"),
				dir: "../../bundles/prometheus.0.14.0-beta",
			},
			wantPackages: []*registry.Package{
				{
					Name:           "prometheus",
					DefaultChannel: "preview",
					Channels: map[string]registry.Channel{
						"preview": {
							Head: registry.BundleKey{
								BundlePath: "quay.io/prometheus/operator:0.14.0",
								Version:    "0.14.0",
								CsvName:    "prometheusoperator.0.14.0",
							},
							Nodes: map[registry.BundleKey]map[registry.BundleKey]struct{}{
								{BundlePath: "quay.io/prometheus/operator:0.14.0", Version: "0.14.0", CsvName: "prometheusoperator.0.14.0"}: {},
							},
						},
						"beta": {
							Head: registry.BundleKey{
								BundlePath: "quay.io/prometheus/operator:0.14.0-beta",
								Version:    "0.14.0-beta",
								CsvName:    "prometheusoperator.0.14.0-beta",
							},
							Nodes: map[registry.BundleKey]map[registry.BundleKey]struct{}{
								{BundlePath: "quay.io/prometheus/operator:0.14.0-beta", Version: "0.14.0-beta", CsvName: "prometheusoperator.0.14.0-beta"}: {},
							},
						},
					},
				},
			},
			wantErr: false,
		},
		{
			name: "TwoChannel/OneChannelIsASubset",
			initImages: []img{
				{
					// this is in the "preview" channel
					ref: image.SimpleReference("quay.io/prometheus/operator:0.14.0"),
					dir: "../../bundles/prometheus.0.14.0",
				},
			},
			addImage: img{
				// this is in the "stable" channel and replaces v0.14.0
				ref: image.SimpleReference("quay.io/prometheus/operator:0.15.0-stable"),
				dir: "../../bundles/prometheus.0.15.0-stable",
			},
			wantPackages: []*registry.Package{
				{
					Name:           "prometheus",
					DefaultChannel: "stable",
					Channels: map[string]registry.Channel{
						"preview": {
							Head: registry.BundleKey{
								BundlePath: "quay.io/prometheus/operator:0.14.0",
								Version:    "0.14.0",
								CsvName:    "prometheusoperator.0.14.0",
							},
							Nodes: map[registry.BundleKey]map[registry.BundleKey]struct{}{
								{BundlePath: "quay.io/prometheus/operator:0.14.0", Version: "0.14.0", CsvName: "prometheusoperator.0.14.0"}: {},
							},
						},
						"stable": {
							Head: registry.BundleKey{
								BundlePath: "quay.io/prometheus/operator:0.15.0-stable",
								Version:    "0.15.0",
								CsvName:    "prometheusoperator.0.15.0-stable",
							},
							Nodes: map[registry.BundleKey]map[registry.BundleKey]struct{}{
								{BundlePath: "quay.io/prometheus/operator:0.15.0-stable", Version: "0.15.0", CsvName: "prometheusoperator.0.15.0-stable"}: {
									{BundlePath: "quay.io/prometheus/operator:0.14.0", Version: "0.14.0", CsvName: "prometheusoperator.0.14.0"}: struct{}{},
								},
								{BundlePath: "quay.io/prometheus/operator:0.14.0", Version: "0.14.0", CsvName: "prometheusoperator.0.14.0"}: {}},
						},
					},
				},
			},
			wantErr: false,
		},
		{
			name: "AddBundleAlreadyExists",
			initImages: []img{
				{
					// this is in the "preview" channel
					ref: image.SimpleReference("quay.io/prometheus/operator:0.14.0"),
					dir: "../../bundles/prometheus.0.14.0",
				},
			},
			addImage: img{
				//Adding same bundle different bundle
				ref: image.SimpleReference("quay.io/prometheus/operator-test:testing"),
				dir: "../../bundles/prometheus.0.14.0",
			},
			wantPackages: []*registry.Package{
				{
					Name:           "prometheus",
					DefaultChannel: "stable",
					Channels: map[string]registry.Channel{
						"preview": {
							Head: registry.BundleKey{
								BundlePath: "quay.io/prometheus/operator:0.14.0",
								Version:    "0.14.0",
								CsvName:    "prometheusoperator.0.14.0",
							},
							Nodes: map[registry.BundleKey]map[registry.BundleKey]struct{}{
								{BundlePath: "quay.io/prometheus/operator:0.14.0", Version: "0.14.0", CsvName: "prometheusoperator.0.14.0"}: {},
							},
						},
					},
				},
			},
			wantErr: true,
			err:     registry.PackageVersionAlreadyAddedErr{},
		},
		{
			name: "AddExactBundleAlreadyExists",
			initImages: []img{
				{
					// this is in the "preview" channel
					ref: image.SimpleReference("quay.io/prometheus/operator:0.14.0"),
					dir: "../../bundles/prometheus.0.14.0",
				},
			},
			addImage: img{
				// Add the same package
				ref: image.SimpleReference("quay.io/prometheus/operator:0.14.0"),
				dir: "../../bundles/prometheus.0.14.0",
			},
			wantPackages: []*registry.Package{
				{
					Name:           "prometheus",
					DefaultChannel: "stable",
					Channels: map[string]registry.Channel{
						"preview": {
							Head: registry.BundleKey{
								BundlePath: "quay.io/prometheus/operator:0.14.0",
								Version:    "0.14.0",
								CsvName:    "prometheusoperator.0.14.0",
							},
							Nodes: map[registry.BundleKey]map[registry.BundleKey]struct{}{
								{BundlePath: "quay.io/prometheus/operator:0.14.0", Version: "0.14.0", CsvName: "prometheusoperator.0.14.0"}: {},
							},
						},
					},
				},
			},
			wantErr: true,
			err:     registry.BundleImageAlreadyAddedErr{},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			logrus.SetLevel(logrus.DebugLevel)
			db, cleanup := CreateTestDB(t)
			defer cleanup()
			load, err := sqlite.NewSQLLiteLoader(db)
			require.NoError(t, err)
			require.NoError(t, load.Migrate(context.TODO()))
			query := sqlite.NewSQLLiteQuerierFromDb(db)
			graphLoader, err := sqlite.NewSQLGraphLoaderFromDB(db)
			require.NoError(t, err)
			for _, i := range tt.initImages {
				p := registry.NewDirectoryPopulator(
					load,
					graphLoader,
					query,
					map[image.Reference]string{i.ref: i.dir},
					nil)
				require.NoError(t, p.Populate(registry.ReplacesMode))
			}
			add := registry.NewDirectoryPopulator(
				load,
				graphLoader,
				query,
				map[image.Reference]string{tt.addImage.ref: tt.addImage.dir},
				nil)
			err = add.Populate(registry.ReplacesMode)
			if tt.wantErr {
				require.True(t, checkAggErr(err, tt.err))
				return
			}
			require.NoError(t, err)

			for _, p := range tt.wantPackages {
				graphLoader, err := sqlite.NewSQLGraphLoaderFromDB(db)
				require.NoError(t, err)

				result, err := graphLoader.Generate(p.Name)
				require.NoError(t, err)
				require.Equal(t, p, result)
			}
			CheckInvariants(t, db)
		})
	}
}

func checkAggErr(aggErr, wantErr error) bool {
	var a utilerrors.Aggregate
	if errors.As(aggErr, &a) {
		for _, e := range a.Errors() {
			if reflect.TypeOf(e).String() == reflect.TypeOf(wantErr).String() {
				return true
			}
		}
		return false
	}
	return reflect.TypeOf(aggErr).String() == reflect.TypeOf(wantErr).String()
}

func TestQuerierForDependencies(t *testing.T) {
	logrus.SetLevel(logrus.DebugLevel)
	db, cleanup := CreateTestDB(t)
	defer cleanup()

	store, err := createAndPopulateDB(db)
	require.NoError(t, err)

	expectedDependencies := []*api.Dependency{
		{
			Type:  "olm.package",
			Value: `{"packageName":"testoperator","version":"\u003e 0.2.0"}`,
		},
		{
			Type:  "olm.gvk",
			Value: `{"group":"testapi.coreos.com","kind":"testapi","version":"v1"}`,
		},
		{
			Type:  "olm.gvk",
			Value: `{"group":"etcd.database.coreos.com","kind":"EtcdCluster","version":"v1beta2"}`,
		},
		{
			Type:  "olm.gvk",
			Value: `{"group":"testprometheus.coreos.com","kind":"testtestprometheus","version":"v1"}`,
		},
		{
			Type:  "olm.constraint",
			Value: `{"cel":{"rule":"properties.exists(p, p.type == \"certified\")"},"failureMessage":"require to have \"certified\""}`,
		},
	}

	type operatorbundle struct {
		name    string
		version string
		path    string
	}

	bundlesList := []operatorbundle{
		{
			name:    "etcdoperator.v0.9.2",
			version: "0.9.2",
			path:    "quay.io/test/etcd.0.9.2",
		},
		{
			name:    "prometheusoperator.0.22.2",
			version: "0.22.2",
			path:    "quay.io/test/prometheus.0.22.2",
		},
	}

	dependencies := make([]*api.Dependency, 0, len(bundlesList))
	for _, b := range bundlesList {
		dep, err := store.GetDependenciesForBundle(context.TODO(), b.name, b.version, b.path)
		require.NoError(t, err)
		dependencies = append(dependencies, dep...)
	}
	require.ElementsMatch(t, expectedDependencies, dependencies)
}

func TestListBundles(t *testing.T) {
	logrus.SetLevel(logrus.DebugLevel)
	db, cleanup := CreateTestDB(t)
	defer cleanup()

	store, err := createAndPopulateDB(db)
	require.NoError(t, err)

	expectedDependencies := []*api.Dependency{
		{
			Type:  "olm.package",
			Value: `{"packageName":"testoperator","version":"\u003e 0.2.0"}`,
		},
		{
			Type:  "olm.gvk",
			Value: `{"group":"testapi.coreos.com","kind":"testapi","version":"v1"}`,
		},
		{
			Type:  "olm.gvk",
			Value: `{"group":"etcd.database.coreos.com","kind":"EtcdCluster","version":"v1beta2"}`,
		},
		{
			Type:  "olm.gvk",
			Value: `{"group":"testprometheus.coreos.com","kind":"testtestprometheus","version":"v1"}`,
		},
		{
			Type:  "olm.gvk",
			Value: `{"group":"testapi.coreos.com","kind":"testapi","version":"v1"}`,
		},
		{
			Type:  "olm.gvk",
			Value: `{"group":"etcd.database.coreos.com","kind":"EtcdCluster","version":"v1beta2"}`,
		},
		{
			Type:  "olm.constraint",
			Value: `{"cel":{"rule":"properties.exists(p, p.type == \"certified\")"},"failureMessage":"require to have \"certified\""}`,
		},
		{
			Type:  "olm.constraint",
			Value: `{"cel":{"rule":"properties.exists(p, p.type == \"certified\")"},"failureMessage":"require to have \"certified\""}`,
		},
	}

	dependencies := []*api.Dependency{}
	bundles, err := store.ListBundles(context.TODO())
	require.NoError(t, err)
	for _, b := range bundles {
		for _, d := range b.Dependencies {
			if d.GetType() != "" {
				dependencies = append(dependencies, d)
			}
		}
	}
	require.Len(t, bundles, 10)
	require.ElementsMatch(t, expectedDependencies, dependencies)
}

func EqualBundles(t *testing.T, expected, actual api.Bundle) {
	require.ElementsMatch(t, expected.ProvidedApis, actual.ProvidedApis, "provided apis don't match: %#v\n%#v", expected.ProvidedApis, actual.ProvidedApis)
	require.ElementsMatch(t, expected.RequiredApis, actual.RequiredApis, "required apis don't match: %#v\n%#v", expected.RequiredApis, actual.RequiredApis)
	require.ElementsMatch(t, expected.Dependencies, actual.Dependencies, "dependencies don't match: %#v\n%#v", expected.Dependencies, actual.Dependencies)
	require.ElementsMatch(t, expected.Properties, actual.Properties, "properties don't match %#v\n%#v", expected.Properties, actual.Properties)
	expected.RequiredApis, expected.ProvidedApis, actual.RequiredApis, actual.ProvidedApis = nil, nil, nil, nil
	expected.Dependencies, expected.Properties, actual.Dependencies, actual.Properties = nil, nil, nil, nil
	require.Equal(t, expected, actual)
}

func CheckInvariants(t *testing.T, db *sql.DB) {
	CheckChannelHeadsHaveDescriptions(t, db)
	CheckBundlesHaveContentsIfNoPath(t, db)
}

func CheckChannelHeadsHaveDescriptions(t *testing.T, db *sql.DB) {
	// check channel heads have csv / bundle
	rows, err := db.Query(`
		select operatorbundle.name,length(operatorbundle.csv),length(operatorbundle.bundle) from operatorbundle
		join channel on channel.head_operatorbundle_name = operatorbundle.name`)
	require.NoError(t, err)

	for rows.Next() {
		var name sql.NullString
		var csvlen sql.NullInt64
		var bundlelen sql.NullInt64
		err := rows.Scan(&name, &csvlen, &bundlelen)
		require.NoError(t, err)
		t.Logf("channel head %s has csvlen %d and bundlelen %d", name.String, csvlen.Int64, bundlelen.Int64)
		require.NotZero(t, csvlen.Int64, "length of csv for %s should not be zero, it is a channel head", name.String)
		require.NotZero(t, bundlelen.Int64, "length of bundle for %s should not be zero, it is a channel head", name.String)
	}
}

func CheckBundlesHaveContentsIfNoPath(t *testing.T, db *sql.DB) {
	// check that any bundle entry has csv/bundle content unpacked if there is no bundlepath
	rows, err := db.Query(`
		select name,length(csv),length(bundle) from operatorbundle
		where bundlepath="" or bundlepath=null`)
	require.NoError(t, err)

	for rows.Next() {
		var name sql.NullString
		var csvlen sql.NullInt64
		var bundlelen sql.NullInt64
		err := rows.Scan(&name, &csvlen, &bundlelen)
		require.NoError(t, err)
		t.Logf("bundle %s has csvlen %d and bundlelen %d", name.String, csvlen.Int64, bundlelen.Int64)
		require.NotZero(t, csvlen.Int64, "length of csv for %s should not be zero, it has no bundle path", name.String)
		require.NotZero(t, bundlelen.Int64, "length of bundle for %s should not be zero, it has no bundle path", name.String)
	}
}

func TestDirectoryPopulator(t *testing.T) {
	db, cleanup := CreateTestDB(t)
	defer cleanup()

	loader, err := sqlite.NewSQLLiteLoader(db)
	require.NoError(t, err)
	require.NoError(t, loader.Migrate(context.TODO()))

	graphLoader, err := sqlite.NewSQLGraphLoaderFromDB(db)
	require.NoError(t, err)

	query := sqlite.NewSQLLiteQuerierFromDb(db)

	populate := func(bundles map[image.Reference]string) error {
		return registry.NewDirectoryPopulator(
			loader,
			graphLoader,
			query,
			bundles,
			nil).Populate(registry.ReplacesMode)
	}
	add := map[image.Reference]string{
		image.SimpleReference("quay.io/test/etcd.0.9.2"):        "../../bundles/etcd.0.9.2",
		image.SimpleReference("quay.io/test/prometheus.0.22.2"): "../../bundles/prometheus.0.22.2",
	}

	err = populate(add)
	require.Error(t, err)
	require.Contains(t, err.Error(), fmt.Sprintf("Invalid bundle %s, replaces nonexistent bundle %s", "etcdoperator.v0.9.2", "etcdoperator.v0.9.0"))
	require.Contains(t, err.Error(), fmt.Sprintf("Invalid bundle %s, replaces nonexistent bundle %s", "prometheusoperator.0.22.2", "prometheusoperator.0.15.0"))
}

func TestDeprecateBundle(t *testing.T) {
	type args struct {
		bundles []string
	}
	type pkgChannel map[string][]string
	type expected struct {
		err                  error
		remainingBundles     []string
		deprecatedBundles    []string
		remainingPkgChannels pkgChannel
	}
	tests := []struct {
		description string
		args        args
		expected    expected
	}{
		{
			description: "IgnoreIfNotInIndex",
			args: args{
				bundles: []string{
					"quay.io/test/etcd.0.6.0",
				},
			},
			expected: expected{
				err: nil,
				remainingBundles: []string{
					"quay.io/test/etcd.0.9.0/alpha",
					"quay.io/test/etcd.0.9.0/beta",
					"quay.io/test/etcd.0.9.0/stable",
					"quay.io/test/etcd.0.9.2/stable",
					"quay.io/test/etcd.0.9.2/alpha",
					"quay.io/test/prometheus.0.22.2/preview",
					"quay.io/test/prometheus.0.15.0/preview",
					"quay.io/test/prometheus.0.15.0/stable",
					"quay.io/test/prometheus.0.14.0/preview",
					"quay.io/test/prometheus.0.14.0/stable",
				},
				deprecatedBundles: []string{},
				remainingPkgChannels: pkgChannel{
					"etcd": []string{
						"beta",
						"alpha",
						"stable",
					},
					"prometheus": []string{
						"preview",
						"stable",
					},
				},
			},
		},
		{
			description: "ChannelRemoved",
			args: args{
				bundles: []string{
					"quay.io/test/prometheus.0.15.0",
				},
			},
			expected: expected{
				err: nil,
				remainingBundles: []string{
					"quay.io/test/etcd.0.9.0/alpha",
					"quay.io/test/etcd.0.9.0/beta",
					"quay.io/test/etcd.0.9.0/stable",
					"quay.io/test/etcd.0.9.2/stable",
					"quay.io/test/etcd.0.9.2/alpha",
					"quay.io/test/prometheus.0.22.2/preview",
					"quay.io/test/prometheus.0.15.0/preview",
				},
				deprecatedBundles: []string{
					"quay.io/test/prometheus.0.15.0/preview",
				},
				remainingPkgChannels: pkgChannel{
					"etcd": []string{
						"beta",
						"alpha",
						"stable",
					},
					"prometheus": []string{
						"preview",
					},
				},
			},
		},
		{
			description: "ChannelRemoved/ErrorOnDefault",
			args: args{
				bundles: []string{
					"quay.io/test/prometheus.0.22.2",
				},
			},
			expected: expected{
				err: utilerrors.NewAggregate([]error{fmt.Errorf("error deprecating bundle quay.io/test/prometheus.0.22.2: %s", registry.ErrRemovingDefaultChannelDuringDeprecation)}),
				remainingBundles: []string{
					"quay.io/test/etcd.0.9.0/alpha",
					"quay.io/test/etcd.0.9.0/beta",
					"quay.io/test/etcd.0.9.0/stable",
					"quay.io/test/etcd.0.9.2/stable",
					"quay.io/test/etcd.0.9.2/alpha",
					"quay.io/test/prometheus.0.22.2/preview",
					"quay.io/test/prometheus.0.15.0/preview",
					"quay.io/test/prometheus.0.15.0/stable",
					"quay.io/test/prometheus.0.14.0/preview",
					"quay.io/test/prometheus.0.14.0/stable",
				},
				deprecatedBundles: []string{},
				remainingPkgChannels: pkgChannel{
					"etcd": []string{
						"beta",
						"alpha",
						"stable",
					},
					"prometheus": []string{
						"preview",
						"stable",
					},
				},
			},
		},
		{
			description: "OutOfOrderDeprecate",
			args: args{
				bundles: []string{
					"quay.io/test/prometheus.0.15.0",
					"quay.io/test/prometheus.0.14.0",
				},
			},
			expected: expected{
				err: nil,
				remainingBundles: []string{
					"quay.io/test/etcd.0.9.0/alpha",
					"quay.io/test/etcd.0.9.0/beta",
					"quay.io/test/etcd.0.9.0/stable",
					"quay.io/test/etcd.0.9.2/stable",
					"quay.io/test/etcd.0.9.2/alpha",
					"quay.io/test/prometheus.0.22.2/preview",
					"quay.io/test/prometheus.0.15.0/preview",
				},
				deprecatedBundles: []string{
					"quay.io/test/prometheus.0.15.0/preview",
				},
				remainingPkgChannels: pkgChannel{
					"etcd": []string{
						"alpha",
						"beta",
						"stable",
					},
					"prometheus": []string{
						"preview",
					},
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.description, func(t *testing.T) {
			logrus.SetLevel(logrus.DebugLevel)
			db, cleanup := CreateTestDB(t)
			defer cleanup()

			querier, err := createAndPopulateDB(db)
			require.NoError(t, err)

			store, err := sqlite.NewSQLLiteLoader(db)
			require.NoError(t, err)

			deprecator := sqlite.NewSQLDeprecatorForBundles(store, tt.args.bundles)
			require.Equal(t, tt.expected.err, deprecator.Deprecate())

			// Ensure remaining bundlePaths in db match
			bundles, err := querier.ListBundles(context.Background())
			require.NoError(t, err)
			bundlePaths := make([]string, 0, len(bundles))
			for _, bundle := range bundles {
				bundlePaths = append(bundlePaths, strings.Join([]string{bundle.BundlePath, bundle.ChannelName}, "/"))
			}
			require.ElementsMatch(t, tt.expected.remainingBundles, bundlePaths)

			// Ensure deprecated bundles match
			var deprecatedBundles []string
			deprecatedProperty, err := json.Marshal(registry.DeprecatedProperty{})
			require.NoError(t, err)
			for _, bundle := range bundles {
				for _, prop := range bundle.Properties {
					if prop.Type == registry.DeprecatedType && prop.Value == string(deprecatedProperty) {
						deprecatedBundles = append(deprecatedBundles, strings.Join([]string{bundle.BundlePath, bundle.ChannelName}, "/"))
					}
				}
			}

			require.ElementsMatch(t, tt.expected.deprecatedBundles, deprecatedBundles)

			// Ensure remaining channels match
			packages, err := querier.ListPackages(context.Background())
			require.NoError(t, err)

			for _, pkg := range packages {
				channelEntries, err := querier.GetChannelEntriesFromPackage(context.Background(), pkg)
				require.NoError(t, err)

				uniqueChannels := make(map[string]struct{})
				channels := make([]string, 0, len(uniqueChannels))
				for _, ch := range channelEntries {
					uniqueChannels[ch.ChannelName] = struct{}{}
				}
				for k := range uniqueChannels {
					channels = append(channels, k)
				}
				require.ElementsMatch(t, tt.expected.remainingPkgChannels[pkg], channels)
			}
		})
	}
}

func TestDeprecatePackage(t *testing.T) {
	type args struct {
		bundles []string
	}
	type pkgChannel map[string][]string
	type expected struct {
		err                  error
		remainingBundles     []string
		deprecatedBundles    []string
		remainingPkgChannels pkgChannel
	}
	tests := []struct {
		description string
		args        args
		expected    expected
	}{
		{
			description: "RemoveEntirePackage/BundlesAreAllHeadsOfChannels",
			args: args{
				bundles: []string{
					"quay.io/test/etcd.0.9.2",
					"quay.io/test/etcd.0.9.0",
				},
			},
			expected: expected{
				err: nil,
				remainingBundles: []string{
					"quay.io/test/prometheus.0.22.2/preview",
					"quay.io/test/prometheus.0.15.0/preview",
					"quay.io/test/prometheus.0.15.0/stable",
					"quay.io/test/prometheus.0.14.0/preview",
					"quay.io/test/prometheus.0.14.0/stable",
				},
				deprecatedBundles: []string{},
				remainingPkgChannels: pkgChannel{
					"prometheus": []string{
						"preview",
						"stable",
					},
				},
			},
		},
		{
			description: "RemoveHeadOfDefaultChannelWithoutAllChannelHeads/Error",
			args: args{
				bundles: []string{
					"quay.io/test/etcd.0.9.2",
				},
			},
			expected: expected{
				err: utilerrors.NewAggregate([]error{fmt.Errorf("cannot deprecate default channel head from package without removing all other channel heads in package %s: must deprecate %s, head of channel %s", "etcd", "quay.io/test/etcd.0.9.0", "beta")}),
				remainingBundles: []string{
					"quay.io/test/etcd.0.9.0/alpha",
					"quay.io/test/etcd.0.9.0/beta",
					"quay.io/test/etcd.0.9.0/stable",
					"quay.io/test/etcd.0.9.2/stable",
					"quay.io/test/etcd.0.9.2/alpha",
					"quay.io/test/prometheus.0.14.0/preview",
					"quay.io/test/prometheus.0.14.0/stable",
					"quay.io/test/prometheus.0.15.0/preview",
					"quay.io/test/prometheus.0.15.0/stable",
					"quay.io/test/prometheus.0.22.2/preview",
				},
				deprecatedBundles: []string{},
				remainingPkgChannels: pkgChannel{
					"etcd": []string{
						"alpha",
						"beta",
						"stable",
					},
					"prometheus": []string{
						"preview",
						"stable",
					},
					"apicurio-registry": []string{
						"2.x",
						"alpha",
					},
				},
			},
		},
		{
			description: "RemoveHeadOfDefaultChannelWithoutAllChannelHeads/Success",
			args: args{
				bundles: []string{
					"quay.io/test/apicurio-registry.v0.0.1",
					"quay.io/test/apicurio-registry.v0.0.3-v1.2.3.final",
					"quay.io/test/apicurio-registry.v0.0.4-v1.3.2.final",
				},
			},
			expected: expected{
				remainingBundles: []string{
					"quay.io/test/etcd.0.9.0/alpha",
					"quay.io/test/etcd.0.9.0/beta",
					"quay.io/test/etcd.0.9.0/stable",
					"quay.io/test/etcd.0.9.2/stable",
					"quay.io/test/etcd.0.9.2/alpha",
					"quay.io/test/prometheus.0.14.0/preview",
					"quay.io/test/prometheus.0.14.0/stable",
					"quay.io/test/prometheus.0.15.0/preview",
					"quay.io/test/prometheus.0.15.0/stable",
					"quay.io/test/prometheus.0.22.2/preview",
				},
				deprecatedBundles: []string{},
				remainingPkgChannels: pkgChannel{
					"etcd": []string{
						"alpha",
						"beta",
						"stable",
					},
					"prometheus": []string{
						"preview",
						"stable",
					},
					"apicurio-registry": []string{
						"2.x",
					},
				},
			},
		},
		{
			description: "RemoveEntirePackage/AndDeprecateAdditionalBundle",
			args: args{
				bundles: []string{
					"quay.io/test/etcd.0.9.2",
					"quay.io/test/etcd.0.9.0",
					"quay.io/test/prometheus.0.14.0",
				},
			},
			expected: expected{
				err: nil,
				remainingBundles: []string{
					"quay.io/test/prometheus.0.22.2/preview",
					"quay.io/test/prometheus.0.15.0/preview",
					"quay.io/test/prometheus.0.15.0/stable",
					"quay.io/test/prometheus.0.14.0/preview",
					"quay.io/test/prometheus.0.14.0/stable",
				},
				deprecatedBundles: []string{
					"quay.io/test/prometheus.0.14.0/preview",
					"quay.io/test/prometheus.0.14.0/stable",
				},
				remainingPkgChannels: pkgChannel{
					"prometheus": []string{
						"preview",
						"stable",
					},
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.description, func(t *testing.T) {
			logrus.SetLevel(logrus.DebugLevel)
			db, cleanup := CreateTestDB(t)
			defer cleanup()

			querier, err := createAndPopulateDB(db)
			require.NoError(t, err)

			store, err := sqlite.NewSQLLiteLoader(db)
			require.NoError(t, err)

			deprecator := sqlite.NewSQLDeprecatorForBundles(store, tt.args.bundles)
			packageDeprecator := sqlite.NewSQLDeprecatorForBundlesAndPackages(deprecator, querier)
			require.Equal(t, tt.expected.err, packageDeprecator.MaybeRemovePackages())
			if len(tt.expected.deprecatedBundles) > 0 {
				require.Equal(t, tt.expected.err, deprecator.Deprecate())
			}

			// Ensure remaining bundlePaths in db match
			bundles, err := querier.ListBundles(context.Background())
			require.NoError(t, err)
			bundlePaths := make([]string, 0, len(bundles))
			for _, bundle := range bundles {
				bundlePaths = append(bundlePaths, strings.Join([]string{bundle.BundlePath, bundle.ChannelName}, "/"))
			}
			require.ElementsMatch(t, tt.expected.remainingBundles, bundlePaths)

			// Ensure deprecated bundles match
			var deprecatedBundles []string
			deprecatedProperty, err := json.Marshal(registry.DeprecatedProperty{})
			require.NoError(t, err)
			for _, bundle := range bundles {
				for _, prop := range bundle.Properties {
					if prop.Type == registry.DeprecatedType && prop.Value == string(deprecatedProperty) {
						deprecatedBundles = append(deprecatedBundles, strings.Join([]string{bundle.BundlePath, bundle.ChannelName}, "/"))
					}
				}
			}

			require.ElementsMatch(t, tt.expected.deprecatedBundles, deprecatedBundles)

			// Ensure remaining channels match
			packages, err := querier.ListPackages(context.Background())
			require.NoError(t, err)

			for _, pkg := range packages {
				channelEntries, err := querier.GetChannelEntriesFromPackage(context.Background(), pkg)
				require.NoError(t, err)

				uniqueChannels := make(map[string]struct{})
				channels := make([]string, 0, len(uniqueChannels))
				for _, ch := range channelEntries {
					uniqueChannels[ch.ChannelName] = struct{}{}
				}
				for k := range uniqueChannels {
					channels = append(channels, k)
				}
				require.ElementsMatch(t, tt.expected.remainingPkgChannels[pkg], channels)
			}
		})
	}
}

func TestAddAfterDeprecate(t *testing.T) {
	tmpdir := t.TempDir()

	/*
	                     (0.1) 0.1.2 <- 0.1.1 <- 0.1.0
	                                               |
	                 (0.2) 0.2.2 <- 0.2.1 <- 0.2.0<|
	                                  |
	   (0.3) 0.3.2 <- 0.3.1 <- 0.3.0 <|
	*/
	testBundles := []struct {
		suffix         string
		channels       string
		defaultChannel string
		csvName        string
		csvSpec        json.RawMessage
	}{
		{
			csvName:        "testpkg.v0.1.0",
			channels:       "0.1",
			csvSpec:        json.RawMessage(`{"version":"0.1.0","replaces":""}`),
			defaultChannel: "0.1",
		},
		{
			csvName:        "testpkg.v0.1.1",
			csvSpec:        json.RawMessage(`{"version":"0.1.1","replaces":"testpkg.v0.1.0"}`),
			channels:       "0.1",
			defaultChannel: "0.1",
		},
		{
			csvName:        "testpkg.v0.1.2",
			csvSpec:        json.RawMessage(`{"version":"0.1.2","replaces":"testpkg.v0.1.1"}`),
			channels:       "0.1",
			defaultChannel: "0.1",
		},
		{
			csvName:        "testpkg.v0.2.0",
			csvSpec:        json.RawMessage(`{"version":"0.2.0","replaces":"testpkg.v0.1.0"}`),
			channels:       "0.2",
			defaultChannel: "0.1",
		},
		{
			csvName:        "testpkg.v0.2.1",
			csvSpec:        json.RawMessage(`{"version":"0.2.1","replaces":"testpkg.v0.2.0"}`),
			channels:       "0.2",
			defaultChannel: "0.1",
		},
		{
			csvName:        "testpkg.v0.2.2",
			csvSpec:        json.RawMessage(`{"version":"0.2.2","replaces":"testpkg.v0.2.1"}`),
			channels:       "0.2",
			defaultChannel: "0.1",
		},
		{
			csvName:        "testpkg.v0.3.0",
			csvSpec:        json.RawMessage(`{"version":"0.3.0","replaces":"testpkg.v0.2.1"}`),
			channels:       "0.3",
			defaultChannel: "0.1",
		},
		{
			csvName:        "testpkg.v0.3.0",
			suffix:         "overwrite",
			csvSpec:        json.RawMessage(`{"version":"0.3.0","replaces":""}`),
			channels:       "0.3",
			defaultChannel: "0.1",
		},
		{
			csvName:        "testpkg.v0.3.0",
			csvSpec:        json.RawMessage(`{"version":"0.3.0","replaces":"testpkg.v0.2.0"}`),
			suffix:         "overwrite-replaces-0.2.0",
			channels:       "0.3",
			defaultChannel: "0.1",
		},
		{
			csvName:        "testpkg.v0.3.1",
			csvSpec:        json.RawMessage(`{"version":"0.3.1","replaces":"testpkg.v0.3.0"}`),
			channels:       "0.3",
			defaultChannel: "0.1",
		},
		{
			csvName:        "testpkg.v0.3.1",
			suffix:         "overwrite",
			csvSpec:        json.RawMessage(`{"version":"0.3.1","replaces":"testpkg.v0.3.0"}`),
			channels:       "0.3",
			defaultChannel: "0.1",
		},
		{
			csvName:        "testpkg.v0.3.2",
			csvSpec:        json.RawMessage(`{"version":"0.3.2","replaces":"testpkg.v0.3.1"}`),
			channels:       "0.3",
			defaultChannel: "0.1",
		},
	}
	for _, b := range testBundles {
		dir := b.csvName
		if len(b.suffix) > 0 {
			dir += "-" + b.suffix
		}
		_, _, err := newUnpackedTestBundle(tmpdir, dir, b.csvName, b.csvSpec, registry.Annotations{PackageName: "testpkg", Channels: b.channels, DefaultChannelName: b.defaultChannel})
		require.NoError(t, err)
	}

	type args struct {
		dir       string //directory to find the bundles
		existing  []string
		deprecate []string
		add       []string
		overwrite map[string][]string
	}
	type pkgChannel map[string][]string
	type expected struct {
		err         error
		remaining   []string
		deprecated  []string
		pkgChannels pkgChannel
	}
	tests := []struct {
		description string
		args        args
		expected    expected
	}{
		{
			description: "SimpleAdd",
			args: args{
				dir: "../../bundles/",
				existing: []string{
					"prometheus.0.14.0",
					"prometheus.0.15.0",
				},
				deprecate: []string{
					"quay.io/test/prometheus.0.14.0",
				},
				add: []string{
					"prometheus.0.22.2",
				},
			},
			expected: expected{
				err: nil,
				remaining: []string{
					"quay.io/test/prometheus.0.22.2/preview",
					"quay.io/test/prometheus.0.15.0/preview",
					"quay.io/test/prometheus.0.14.0/preview",
					"quay.io/test/prometheus.0.15.0/stable",
					"quay.io/test/prometheus.0.14.0/stable",
				},
				deprecated: []string{
					"quay.io/test/prometheus.0.14.0/preview",
					"quay.io/test/prometheus.0.14.0/stable",
				},
				pkgChannels: pkgChannel{
					"prometheus": []string{
						"preview",
						"stable",
					},
				},
			},
		},
		{
			description: "OverwriteLatest",
			args: args{
				dir: "../../bundles/",
				existing: []string{
					"prometheus.0.14.0",
					"prometheus.0.15.0",
					"prometheus.0.22.2",
				},
				deprecate: []string{
					"quay.io/test/prometheus.0.15.0",
				},
				overwrite: map[string][]string{
					"prometheus": {
						"prometheus.0.15.0",
						"prometheus.0.22.2",
					},
				},
			},
			expected: expected{
				err: nil,
				remaining: []string{
					"quay.io/test/prometheus.0.22.2/preview",
					"quay.io/test/prometheus.0.15.0/preview",
				},
				deprecated: []string{
					"quay.io/test/prometheus.0.15.0/preview",
				},
				pkgChannels: pkgChannel{
					"prometheus": []string{
						"preview",
					},
				},
			},
		},
		{
			description: "TruncateTillBranchPoint",
			args: args{
				dir: tmpdir,
				existing: []string{
					"testpkg.v0.1.0",
					"testpkg.v0.1.1",
					"testpkg.v0.2.0",
					"testpkg.v0.2.1",
					"testpkg.v0.2.2",
					"testpkg.v0.3.0",
					"testpkg.v0.3.1",
					"testpkg.v0.3.2",
				},
				deprecate: []string{
					"quay.io/test/testpkg.v0.3.1",
				},
				add: []string{
					"testpkg.v0.1.2",
				},
				overwrite: nil,
			},
			expected: expected{
				err: nil,
				remaining: []string{
					"quay.io/test/testpkg.v0.1.0/0.1",
					"quay.io/test/testpkg.v0.1.0/0.2",
					"quay.io/test/testpkg.v0.1.1/0.1",
					"quay.io/test/testpkg.v0.1.2/0.1",
					"quay.io/test/testpkg.v0.2.0/0.2",
					"quay.io/test/testpkg.v0.2.1/0.2",
					"quay.io/test/testpkg.v0.2.2/0.2",
					"quay.io/test/testpkg.v0.3.1/0.3",
					"quay.io/test/testpkg.v0.3.2/0.3",
				},
				deprecated: []string{
					"quay.io/test/testpkg.v0.3.1/0.3",
				},
				pkgChannels: pkgChannel{
					"testpkg": []string{"0.1", "0.2", "0.3"},
				},
			},
		},
		{
			description: "DeprecateAboveBranchPoint",
			args: args{
				dir: tmpdir,
				existing: []string{
					"testpkg.v0.1.0",
					"testpkg.v0.1.1",
					"testpkg.v0.2.0",
					"testpkg.v0.2.1",
					"testpkg.v0.2.2",
					"testpkg.v0.3.0",
					"testpkg.v0.3.1",
				},
				deprecate: []string{
					"quay.io/test/testpkg.v0.3.0",
				},
				add: []string{
					"testpkg.v0.1.2",
				},
				overwrite: nil,
			},
			expected: expected{
				err: nil,
				remaining: []string{
					"quay.io/test/testpkg.v0.1.0/0.1",
					"quay.io/test/testpkg.v0.1.0/0.2",
					"quay.io/test/testpkg.v0.1.1/0.1",
					"quay.io/test/testpkg.v0.1.2/0.1",
					"quay.io/test/testpkg.v0.2.0/0.2",
					"quay.io/test/testpkg.v0.2.1/0.2",
					"quay.io/test/testpkg.v0.2.2/0.2",
					"quay.io/test/testpkg.v0.3.0/0.3",
					"quay.io/test/testpkg.v0.3.1/0.3",
				},
				deprecated: []string{
					"quay.io/test/testpkg.v0.3.0/0.3",
				},
				pkgChannels: pkgChannel{
					"testpkg": []string{"0.1", "0.2", "0.3"},
				},
			},
		},
		{
			description: "ReplaceDeprecatedChannelHead",
			args: args{
				dir: tmpdir,
				existing: []string{
					"testpkg.v0.1.0",
					"testpkg.v0.1.1",
					"testpkg.v0.2.0",
					"testpkg.v0.2.1",
					"testpkg.v0.2.2",
					"testpkg.v0.3.0",
				},
				deprecate: []string{
					"quay.io/test/testpkg.v0.3.0",
				},
				add: []string{
					"testpkg.v0.3.1",
					"testpkg.v0.3.2",
				},
				overwrite: nil,
			},
			expected: expected{
				err: nil,
				remaining: []string{
					"quay.io/test/testpkg.v0.1.0/0.1",
					"quay.io/test/testpkg.v0.1.0/0.2",
					"quay.io/test/testpkg.v0.1.1/0.1",
					"quay.io/test/testpkg.v0.2.0/0.2",
					"quay.io/test/testpkg.v0.2.1/0.2",
					"quay.io/test/testpkg.v0.2.2/0.2",
					"quay.io/test/testpkg.v0.3.0/0.3",
					"quay.io/test/testpkg.v0.3.1/0.3",
					"quay.io/test/testpkg.v0.3.2/0.3",
				},
				deprecated: []string{
					"quay.io/test/testpkg.v0.3.0/0.3",
				},
				pkgChannels: pkgChannel{
					"testpkg": []string{"0.1", "0.2", "0.3"},
				},
			},
		},
		{
			description: "OverwriteDeprecatedChannelHead",
			args: args{
				dir: tmpdir,
				existing: []string{
					"testpkg.v0.1.0",
					"testpkg.v0.1.1",
					"testpkg.v0.2.0",
					"testpkg.v0.2.1",
					"testpkg.v0.2.2",
					"testpkg.v0.3.0",
				},
				deprecate: []string{
					"quay.io/test/testpkg.v0.3.0",
				},
				add: []string{
					"testpkg.v0.3.0-overwrite",
				},
				overwrite: map[string][]string{
					"testpkg": {"testpkg.v0.3.0"},
				},
			},
			expected: expected{
				err: nil,
				remaining: []string{
					"quay.io/test/testpkg.v0.1.0/0.1",
					"quay.io/test/testpkg.v0.1.0/0.2",
					"quay.io/test/testpkg.v0.1.1/0.1",
					"quay.io/test/testpkg.v0.2.0/0.2",
					"quay.io/test/testpkg.v0.2.1/0.2",
					"quay.io/test/testpkg.v0.2.2/0.2",
					"quay.io/test/testpkg.v0.3.0-overwrite/0.3",
				},
				deprecated: []string{},
				pkgChannels: pkgChannel{
					"testpkg": []string{"0.1", "0.2", "0.3"},
				},
			},
		},
		{
			description: "OverwriteDeprecatedChannelHeadWithReplaces",
			args: args{
				dir: tmpdir,
				existing: []string{
					"testpkg.v0.1.0",
					"testpkg.v0.1.1",
					"testpkg.v0.2.0",
					"testpkg.v0.2.1",
					"testpkg.v0.2.2",
					"testpkg.v0.3.0",
				},
				deprecate: []string{
					"quay.io/test/testpkg.v0.3.0",
				},
				add: []string{
					"testpkg.v0.3.0-overwrite-replaces-0.2.0",
				},
				overwrite: map[string][]string{
					"testpkg": {"testpkg.v0.3.0"},
				},
			},
			expected: expected{
				err: nil,
				remaining: []string{
					"quay.io/test/testpkg.v0.1.0/0.1",
					"quay.io/test/testpkg.v0.1.0/0.2",
					"quay.io/test/testpkg.v0.1.0/0.3",
					"quay.io/test/testpkg.v0.1.1/0.1",
					"quay.io/test/testpkg.v0.2.0/0.2",
					"quay.io/test/testpkg.v0.2.0/0.3",
					"quay.io/test/testpkg.v0.2.1/0.2",
					"quay.io/test/testpkg.v0.2.2/0.2",
					"quay.io/test/testpkg.v0.3.0-overwrite-replaces-0.2.0/0.3",
				},
				deprecated: []string{},
				pkgChannels: pkgChannel{
					"testpkg": []string{"0.1", "0.2", "0.3"},
				},
			},
		},
		{
			description: "OverwriteAboveDeprecated",
			args: args{
				dir: tmpdir,
				existing: []string{
					"testpkg.v0.1.0",
					"testpkg.v0.1.1",
					"testpkg.v0.2.0",
					"testpkg.v0.2.1",
					"testpkg.v0.2.2",
					"testpkg.v0.3.0",
					"testpkg.v0.3.1",
				},
				deprecate: []string{
					"quay.io/test/testpkg.v0.3.0",
				},
				add: []string{
					"testpkg.v0.3.1-overwrite",
				},
				overwrite: map[string][]string{
					"testpkg": {"testpkg.v0.3.1"},
				},
			},
			expected: expected{
				err: nil,
				remaining: []string{
					"quay.io/test/testpkg.v0.1.0/0.1",
					"quay.io/test/testpkg.v0.1.0/0.2",
					"quay.io/test/testpkg.v0.1.1/0.1",
					"quay.io/test/testpkg.v0.2.0/0.2",
					"quay.io/test/testpkg.v0.2.1/0.2",
					"quay.io/test/testpkg.v0.2.2/0.2",
					"quay.io/test/testpkg.v0.3.0/0.3",
					"quay.io/test/testpkg.v0.3.1-overwrite/0.3",
				},
				deprecated: []string{
					"quay.io/test/testpkg.v0.3.0/0.3",
				},
				pkgChannels: pkgChannel{
					"testpkg": []string{"0.1", "0.2", "0.3"},
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.description, func(t *testing.T) {
			logrus.SetLevel(logrus.DebugLevel)
			db, cleanup := CreateTestDB(t)
			defer cleanup()

			load, err := sqlite.NewSQLLiteLoader(db, sqlite.WithEnableAlpha(true))
			require.NoError(t, err)
			err = load.Migrate(context.TODO())
			require.NoError(t, err)
			query := sqlite.NewSQLLiteQuerierFromDb(db)

			graphLoader, err := sqlite.NewSQLGraphLoaderFromDB(db)
			require.NoError(t, err)

			populate := func(add []string, overwrite map[string][]string) error {
				addRefs := map[image.Reference]string{}
				for _, a := range add {
					addRefs[image.SimpleReference("quay.io/test/"+a)] = filepath.Join(tt.args.dir, a)
				}

				return registry.NewDirectoryPopulator(
					load,
					graphLoader,
					query,
					addRefs,
					overwrite).Populate(registry.ReplacesMode)
			}
			// Initialize index with some bundles
			require.NoError(t, populate(tt.args.existing, nil))

			deprecator := sqlite.NewSQLDeprecatorForBundles(load, tt.args.deprecate)
			require.Equal(t, tt.expected.err, deprecator.Deprecate())

			require.NoError(t, populate(tt.args.add, tt.args.overwrite))

			// Ensure remaining bundlePaths in db match
			bundles, err := query.ListBundles(context.Background())
			require.NoError(t, err)
			remaining := make([]string, 0, len(bundles))
			for _, bundle := range bundles {
				remaining = append(remaining, strings.Join([]string{bundle.BundlePath, bundle.ChannelName}, "/"))
			}
			require.ElementsMatch(t, tt.expected.remaining, remaining)

			// Ensure deprecated bundles match
			var deprecated []string
			deprecatedProperty, err := json.Marshal(registry.DeprecatedProperty{})
			require.NoError(t, err)
			for _, bundle := range bundles {
				for _, prop := range bundle.Properties {
					if prop.Type == registry.DeprecatedType && prop.Value == string(deprecatedProperty) {
						deprecated = append(deprecated, strings.Join([]string{bundle.BundlePath, bundle.ChannelName}, "/"))
					}
				}
			}

			require.ElementsMatch(t, tt.expected.deprecated, deprecated)

			// Ensure remaining channels match
			packages, err := query.ListPackages(context.Background())
			require.NoError(t, err)

			for _, pkg := range packages {
				channelEntries, err := query.GetChannelEntriesFromPackage(context.Background(), pkg)
				require.NoError(t, err)

				uniqueChannels := make(map[string]struct{})
				channels := make([]string, 0, len(uniqueChannels))
				for _, ch := range channelEntries {
					uniqueChannels[ch.ChannelName] = struct{}{}
				}
				for k := range uniqueChannels {
					channels = append(channels, k)
				}
				require.ElementsMatch(t, tt.expected.pkgChannels[pkg], channels)
			}
		})
	}
}

func TestOverwrite(t *testing.T) {
	type args struct {
		firstAdd   map[image.Reference]string
		secondAdd  map[image.Reference]string
		overwrites map[string][]string
	}
	type pkgChannel map[string][]string
	type expected struct {
		errs                     []error
		remainingBundles         []string
		remainingPkgChannels     pkgChannel
		remainingDefaultChannels map[string]string
	}
	getBundleRefs := func(names []string) map[image.Reference]string {
		refs := map[image.Reference]string{}
		for _, name := range names {
			refs[image.SimpleReference("quay.io/test/"+name)] = "../../bundles/" + name
		}
		return refs
	}

	tests := []struct {
		description string
		args        args
		expected    expected
	}{
		{
			description: "DefaultBehavior",
			args: args{
				firstAdd: getBundleRefs([]string{"prometheus.0.14.0"}),
				secondAdd: map[image.Reference]string{
					image.SimpleReference("quay.io/test/etcd.0.9.2"):        "../../bundles/etcd.0.9.2",
					image.SimpleReference("quay.io/test/prometheus.0.22.2"): "../../bundles/prometheus.0.22.2",
				},
				overwrites: nil,
			},
			expected: expected{
				errs: []error{
					fmt.Errorf("Invalid bundle %s, replaces nonexistent bundle %s", "etcdoperator.v0.9.2", "etcdoperator.v0.9.0"),
					fmt.Errorf("Invalid bundle %s, replaces nonexistent bundle %s", "prometheusoperator.0.22.2", "prometheusoperator.0.15.0"),
				},
				remainingBundles: []string{
					"quay.io/test/prometheus.0.14.0/preview",
				},
				remainingPkgChannels: pkgChannel{
					"prometheus": []string{
						"preview",
					},
				},
				remainingDefaultChannels: map[string]string{
					"prometheus": "preview",
				},
			},
		},
		{
			description: "SimpleCsvChange",
			args: args{
				firstAdd: getBundleRefs([]string{"etcd.0.9.0", "prometheus.0.14.0", "prometheus.0.15.0"}),
				secondAdd: map[image.Reference]string{
					image.SimpleReference("quay.io/test/new-etcd.0.9.0"):    "testdata/overwrite/etcd.0.9.0",
					image.SimpleReference("quay.io/test/prometheus.0.22.2"): "../../bundles/prometheus.0.22.2",
				},
				overwrites: map[string][]string{"etcd": {"etcdoperator.v0.9.0"}},
			},
			expected: expected{
				errs: nil,
				remainingBundles: []string{
					"quay.io/test/new-etcd.0.9.0/alpha",
					"quay.io/test/new-etcd.0.9.0/beta",
					"quay.io/test/new-etcd.0.9.0/stable",
					"quay.io/test/prometheus.0.22.2/preview",
					"quay.io/test/prometheus.0.15.0/preview",
					"quay.io/test/prometheus.0.15.0/stable",
					"quay.io/test/prometheus.0.14.0/preview",
					"quay.io/test/prometheus.0.14.0/stable",
				},
				remainingPkgChannels: pkgChannel{
					"etcd": []string{
						"beta",
						"alpha",
						"stable",
					},
					"prometheus": []string{
						"preview",
						"stable",
					},
				},
				remainingDefaultChannels: map[string]string{
					"etcd":       "stable",
					"prometheus": "preview",
				},
			},
		},
		{
			description: "ChannelRemove",
			args: args{
				firstAdd: getBundleRefs([]string{"etcd.0.9.0", "etcd.0.9.2", "prometheus.0.14.0", "prometheus.0.15.0"}),
				secondAdd: map[image.Reference]string{
					image.SimpleReference("quay.io/test/new-etcd.0.9.2"):    "testdata/overwrite/etcd.0.9.2",
					image.SimpleReference("quay.io/test/prometheus.0.22.2"): "../../bundles/prometheus.0.22.2",
				},
				overwrites: map[string][]string{"etcd": {"etcdoperator.v0.9.2"}},
			},
			expected: expected{
				errs: nil,
				remainingBundles: []string{
					"quay.io/test/etcd.0.9.0/alpha",
					"quay.io/test/etcd.0.9.0/beta",
					"quay.io/test/etcd.0.9.0/stable",
					"quay.io/test/new-etcd.0.9.2/alpha",
					"quay.io/test/prometheus.0.14.0/preview",
					"quay.io/test/prometheus.0.14.0/stable",
					"quay.io/test/prometheus.0.15.0/preview",
					"quay.io/test/prometheus.0.15.0/stable",
					"quay.io/test/prometheus.0.22.2/preview",
				},
				remainingPkgChannels: pkgChannel{
					"etcd": []string{
						"alpha",
						"beta",
						"stable",
					},
					"prometheus": []string{
						"preview",
						"stable",
					},
				},
				remainingDefaultChannels: map[string]string{
					"etcd":       "alpha",
					"prometheus": "preview",
				},
			},
		},
		{
			description: "OverwriteBundle/ChannelSwitch",
			args: args{
				firstAdd: getBundleRefs([]string{"prometheus.0.14.0", "prometheus.0.15.0", "prometheus.0.22.2"}),
				secondAdd: map[image.Reference]string{
					image.SimpleReference("quay.io/test/etcd.0.9.0"):            "../../bundles/etcd.0.9.0",
					image.SimpleReference("quay.io/test/etcd.0.9.2"):            "../../bundles/etcd.0.9.2",
					image.SimpleReference("quay.io/test/new-prometheus.0.22.2"): "testdata/overwrite/prometheus.0.22.2",
				},
				overwrites: map[string][]string{"prometheus": {"prometheusoperator.0.22.2"}},
			},
			expected: expected{
				errs: nil,
				remainingBundles: []string{
					"quay.io/test/etcd.0.9.0/alpha",
					"quay.io/test/etcd.0.9.0/beta",
					"quay.io/test/etcd.0.9.0/stable",
					"quay.io/test/etcd.0.9.2/alpha",
					"quay.io/test/etcd.0.9.2/stable",
					"quay.io/test/prometheus.0.14.0/preview",
					"quay.io/test/prometheus.0.14.0/stable",
					"quay.io/test/prometheus.0.15.0/preview",
					"quay.io/test/prometheus.0.15.0/stable",
					"quay.io/test/new-prometheus.0.22.2/alpha",
					"quay.io/test/prometheus.0.15.0/alpha",
					"quay.io/test/prometheus.0.14.0/alpha",
				},
				remainingPkgChannels: pkgChannel{
					"etcd": []string{
						"beta",
						"alpha",
						"stable",
					},
					"prometheus": []string{
						"preview",
						"stable",
						"alpha",
					},
				},
				remainingDefaultChannels: map[string]string{
					"etcd":       "alpha",
					"prometheus": "preview",
				},
			},
		},
		{
			description: "OverwriteBundle/DefaultChannelChange",
			args: args{
				firstAdd: getBundleRefs([]string{"prometheus.0.14.0", "prometheus.0.15.0"}),
				secondAdd: map[image.Reference]string{
					image.SimpleReference("quay.io/test/etcd.0.9.0"):            "../../bundles/etcd.0.9.0",
					image.SimpleReference("quay.io/test/etcd.0.9.2"):            "../../bundles/etcd.0.9.2",
					image.SimpleReference("quay.io/test/new-prometheus.0.15.0"): "testdata/overwrite/prometheus.0.15.0",
				},
				overwrites: map[string][]string{"prometheus": {"prometheusoperator.0.15.0"}},
			},
			expected: expected{
				errs: nil,
				remainingBundles: []string{
					"quay.io/test/etcd.0.9.0/alpha",
					"quay.io/test/etcd.0.9.0/beta",
					"quay.io/test/etcd.0.9.0/stable",
					"quay.io/test/etcd.0.9.2/alpha",
					"quay.io/test/etcd.0.9.2/stable",
					"quay.io/test/prometheus.0.14.0/preview",
					"quay.io/test/prometheus.0.14.0/stable",
					"quay.io/test/new-prometheus.0.15.0/preview",
					"quay.io/test/new-prometheus.0.15.0/stable",
				},
				remainingPkgChannels: pkgChannel{
					"etcd": []string{
						"alpha",
						"beta",
						"stable",
					},
					"prometheus": []string{
						"preview",
						"stable",
					},
				},
				remainingDefaultChannels: map[string]string{
					"etcd":       "alpha",
					"prometheus": "stable",
				},
			},
		},
		{
			description: "OverwriteBundle/NonLatestOverwrite",
			args: args{
				firstAdd: getBundleRefs([]string{"prometheus.0.14.0", "prometheus.0.15.0", "prometheus.0.22.2"}),
				secondAdd: map[image.Reference]string{
					image.SimpleReference("quay.io/test/etcd.0.9.0"):            "../../bundles/etcd.0.9.0",
					image.SimpleReference("quay.io/test/etcd.0.9.2"):            "../../bundles/etcd.0.9.2",
					image.SimpleReference("quay.io/test/new-prometheus.0.15.0"): "testdata/overwrite/prometheus.0.15.0",
				},
				overwrites: map[string][]string{"prometheus": {"prometheus.0.14.0"}},
			},
			expected: expected{
				errs: []error{registry.OverwriteErr{ErrorString: "Cannot overwrite a bundle that is not at the head of a channel using --overwrite-latest"}},
				remainingBundles: []string{
					"quay.io/test/prometheus.0.14.0/preview",
					"quay.io/test/prometheus.0.14.0/stable",
					"quay.io/test/prometheus.0.15.0/preview",
					"quay.io/test/prometheus.0.15.0/stable",
					"quay.io/test/prometheus.0.22.2/preview",
				},
				remainingPkgChannels: pkgChannel{
					"prometheus": []string{
						"preview",
						"stable",
					},
				},
				remainingDefaultChannels: map[string]string{
					"prometheus": "preview",
				},
			},
		},
		{
			description: "OverwriteBundle/MultipleOverwrites",
			args: args{
				firstAdd: getBundleRefs([]string{"etcd.0.9.0", "etcd.0.9.2", "prometheus.0.14.0", "prometheus.0.15.0", "prometheus.0.22.2"}),
				secondAdd: map[image.Reference]string{
					image.SimpleReference("quay.io/test/new-etcd.0.9.2"):        "testdata/overwrite/etcd.0.9.2",
					image.SimpleReference("quay.io/test/new-prometheus.0.22.2"): "testdata/overwrite/prometheus.0.22.2",
				},
				overwrites: map[string][]string{
					"prometheus": {"prometheusoperator.0.22.2"},
					"etcd":       {"etcdoperator.v0.9.2"},
				},
			},
			expected: expected{
				errs: nil,
				remainingBundles: []string{
					"quay.io/test/etcd.0.9.0/alpha",
					"quay.io/test/etcd.0.9.0/beta",
					"quay.io/test/etcd.0.9.0/stable",
					"quay.io/test/new-etcd.0.9.2/alpha",
					"quay.io/test/prometheus.0.14.0/preview",
					"quay.io/test/prometheus.0.14.0/stable",
					"quay.io/test/prometheus.0.15.0/preview",
					"quay.io/test/prometheus.0.15.0/stable",
					"quay.io/test/new-prometheus.0.22.2/alpha",
					"quay.io/test/prometheus.0.15.0/alpha",
					"quay.io/test/prometheus.0.14.0/alpha",
				},
				remainingPkgChannels: pkgChannel{
					"etcd": []string{
						"beta",
						"alpha",
						"stable",
					},
					"prometheus": []string{
						"preview",
						"stable",
						"alpha",
					},
				},
				remainingDefaultChannels: map[string]string{
					"etcd":       "alpha",
					"prometheus": "preview",
				},
			},
		},
		{
			description: "OverwriteBundle/MultipleOverwritesPerPackage",
			args: args{
				firstAdd: getBundleRefs([]string{"etcd.0.9.0", "etcd.0.9.2", "prometheus.0.14.0", "prometheus.0.15.0", "prometheus.0.22.2"}),
				secondAdd: map[image.Reference]string{
					image.SimpleReference("quay.io/test/new-etcd.0.9.2"):     "testdata/overwrite/etcd.0.9.2",
					image.SimpleReference("quay.io/test/new-new-etcd.0.9.2"): "testdata/overwrite/etcd.0.9.2",
				},
				overwrites: map[string][]string{"etcd": {"etcd.0.9.0"}},
			},
			expected: expected{
				errs: []error{registry.OverwriteErr{ErrorString: "Cannot overwrite more than one bundle at a time for a given package using --overwrite-latest"}},
				remainingBundles: []string{
					"quay.io/test/etcd.0.9.0/alpha",
					"quay.io/test/etcd.0.9.0/beta",
					"quay.io/test/etcd.0.9.0/stable",
					"quay.io/test/etcd.0.9.2/alpha",
					"quay.io/test/etcd.0.9.2/stable",
					"quay.io/test/prometheus.0.22.2/preview",
					"quay.io/test/prometheus.0.14.0/preview",
					"quay.io/test/prometheus.0.14.0/stable",
					"quay.io/test/prometheus.0.15.0/preview",
					"quay.io/test/prometheus.0.15.0/stable",
				},
				remainingPkgChannels: pkgChannel{
					"etcd": []string{
						"alpha",
						"beta",
						"stable",
					},
					"prometheus": []string{
						"preview",
						"stable",
					},
				},
				remainingDefaultChannels: map[string]string{
					"etcd":       "alpha",
					"prometheus": "preview",
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.description, func(t *testing.T) {
			logrus.SetLevel(logrus.DebugLevel)
			db, cleanup := CreateTestDB(t)
			defer cleanup()

			store, err := sqlite.NewSQLLiteLoader(db)
			require.NoError(t, err)
			require.NoError(t, store.Migrate(context.TODO()))

			graphLoader, err := sqlite.NewSQLGraphLoaderFromDB(db)
			require.NoError(t, err)

			query := sqlite.NewSQLLiteQuerierFromDb(db)

			populate := func(bundles map[image.Reference]string, overwrites map[string][]string) error {
				return registry.NewDirectoryPopulator(
					store,
					graphLoader,
					query,
					bundles,
					overwrites).Populate(registry.ReplacesMode)
			}
			require.NoError(t, populate(tt.args.firstAdd, nil))

			err = populate(tt.args.secondAdd, tt.args.overwrites)
			if len(tt.expected.errs) < 1 {
				require.NoError(t, err)
			}
			for _, e := range tt.expected.errs {
				require.Contains(t, err.Error(), e.Error())
			}

			// Ensure remaining bundlePaths in db match
			bundles, err := query.ListBundles(context.Background())
			require.NoError(t, err)
			bundlePaths := make([]string, 0, len(bundles))
			for _, bundle := range bundles {
				bundlePaths = append(bundlePaths, strings.Join([]string{bundle.BundlePath, bundle.ChannelName}, "/"))
			}
			require.ElementsMatch(t, tt.expected.remainingBundles, bundlePaths)

			// Ensure remaining channels and default channel match
			packages, err := query.ListPackages(context.Background())
			require.NoError(t, err)

			for _, pkg := range packages {
				channelEntries, err := query.GetChannelEntriesFromPackage(context.Background(), pkg)
				require.NoError(t, err)

				uniqueChannels := make(map[string]struct{})
				channels := make([]string, 0, len(uniqueChannels))
				for _, ch := range channelEntries {
					uniqueChannels[ch.ChannelName] = struct{}{}
				}
				for k := range uniqueChannels {
					channels = append(channels, k)
				}
				defaultChannel, err := query.GetDefaultChannelForPackage(context.Background(), pkg)
				require.NoError(t, err)
				require.ElementsMatch(t, tt.expected.remainingPkgChannels[pkg], channels)
				require.Equal(t, tt.expected.remainingDefaultChannels[pkg], defaultChannel)
			}
		})
	}
}

func TestSemverPackageManifest(t *testing.T) {
	bundle := func(name, version, pkg, defaultChannel, channels string) *registry.Bundle {
		b, err := registry.NewBundleFromStrings(name, version, pkg, defaultChannel, channels, "")
		require.NoError(t, err)
		return b
	}
	type args struct {
		bundles []*registry.Bundle
	}
	type expect struct {
		packageManifest *registry.PackageManifest
		hasError        bool
	}
	for _, tt := range []struct {
		description string
		args        args
		expect      expect
	}{
		{
			description: "OneUnversioned",
			args: args{
				bundles: []*registry.Bundle{
					bundle("operator", "", "package", "stable", "stable"), // version "" is interpreted as 0.0.0-z
				},
			},
			expect: expect{
				packageManifest: &registry.PackageManifest{
					PackageName:        "package",
					DefaultChannelName: "stable",
					Channels: []registry.PackageChannel{
						{
							Name:           "stable",
							CurrentCSVName: "operator",
						},
					},
				},
			},
		},
		{
			description: "TwoUnversioned",
			args: args{
				bundles: []*registry.Bundle{
					bundle("operator-1", "", "package", "stable", "stable"),
					bundle("operator-2", "", "package", "stable", "stable"),
				},
			},
			expect: expect{
				hasError: true,
			},
		},
		{
			description: "UnversionedAndVersioned",
			args: args{
				bundles: []*registry.Bundle{
					bundle("operator-1", "", "package", "", "stable"),
					bundle("operator-2", "", "package", "", "stable"),
					bundle("operator-3", "0.0.1", "package", "", "stable"), // As long as there is one version, we should be good
				},
			},
			expect: expect{
				packageManifest: &registry.PackageManifest{
					PackageName:        "package",
					DefaultChannelName: "stable",
					Channels: []registry.PackageChannel{
						{
							Name:           "stable",
							CurrentCSVName: "operator-3",
						},
					},
				},
			},
		},
		{
			description: "MaxVersionsAreChannelHeads",
			args: args{
				bundles: []*registry.Bundle{
					bundle("operator-1", "1.0.0", "package", "slow", "slow"),
					bundle("operator-2", "1.1.0", "package", "stable", "slow,stable"),
					bundle("operator-3", "2.1.0", "package", "stable", "edge"),
				},
			},
			expect: expect{
				packageManifest: &registry.PackageManifest{
					PackageName:        "package",
					DefaultChannelName: "stable",
					Channels: []registry.PackageChannel{
						{
							Name:           "slow",
							CurrentCSVName: "operator-2",
						},
						{
							Name:           "stable",
							CurrentCSVName: "operator-2",
						},
						{
							Name:           "edge",
							CurrentCSVName: "operator-3",
						},
					},
				},
			},
		},
		{
			description: "DuplicateVersionsNotTolerated",
			args: args{
				bundles: []*registry.Bundle{
					bundle("operator-1", "1.0.0", "package", "slow", "slow"),
					bundle("operator-2", "1.0.0", "package", "stable", "slow,stable"),
					bundle("operator-3", "2.1.0", "package", "stable", "edge"),
				},
			},
			expect: expect{
				hasError: true,
			},
		},
		{
			description: "DuplicateVersionsInSeparateChannelsAreTolerated",
			args: args{
				bundles: []*registry.Bundle{
					bundle("operator-1", "1.0.0", "package", "slow", "slow"),
					bundle("operator-2", "1.0.0", "package", "stable", "stable"),
					bundle("operator-3", "2.1.0", "package", "edge", "edge"), // Should only be tolerated if we have a global max
				},
			},
			expect: expect{
				packageManifest: &registry.PackageManifest{
					PackageName:        "package",
					DefaultChannelName: "edge",
					Channels: []registry.PackageChannel{
						{
							Name:           "slow",
							CurrentCSVName: "operator-1",
						},
						{
							Name:           "stable",
							CurrentCSVName: "operator-2",
						},
						{
							Name:           "edge",
							CurrentCSVName: "operator-3",
						},
					},
				},
			},
		},
		{
			description: "DuplicateMaxVersionsAreNotTolerated",
			args: args{
				bundles: []*registry.Bundle{
					bundle("operator-1", "1.0.0", "package", "slow", "slow"),
					bundle("operator-2", "1.0.0", "package", "stable", "stable"),
				},
			},
			expect: expect{
				hasError: true,
			},
		},
		{
			description: "UnknownDefaultChannel",
			args: args{
				bundles: []*registry.Bundle{
					bundle("operator-1", "1.0.0", "package", "stable", "stable"),
					bundle("operator-2", "2.0.0", "package", "edge", "stable"),
				},
			},
			expect: expect{
				hasError: true,
			},
		},
		{
			description: "BuildIDAndPreReleaseHeads",
			args: args{
				bundles: []*registry.Bundle{
					bundle("operator-1", "1.0.0", "package", "stable", "stable"),
					bundle("operator-2", "1.0.0+1", "package", "stable", "stable"),
					bundle("operator-3", "1.0.0+2", "package", "stable", "stable"),
					bundle("operator-4", "2.0.0-pre", "package", "stable", "edge"),
				},
			},
			expect: expect{
				packageManifest: &registry.PackageManifest{
					PackageName:        "package",
					DefaultChannelName: "stable",
					Channels: []registry.PackageChannel{
						{
							Name:           "stable",
							CurrentCSVName: "operator-3",
						},
						{
							Name:           "edge",
							CurrentCSVName: "operator-4",
						},
					},
				},
			},
		},
	} {
		t.Run(tt.description, func(t *testing.T) {
			packageManifest, err := registry.SemverPackageManifest(tt.args.bundles)
			if tt.expect.hasError {
				require.Error(t, err)
				return
			}

			require.NoError(t, err)
			require.NotNil(t, packageManifest)

			expected := tt.expect.packageManifest
			require.Equal(t, expected.PackageName, packageManifest.PackageName)
			require.Equal(t, expected.DefaultChannelName, packageManifest.DefaultChannelName)
			require.ElementsMatch(t, expected.Channels, packageManifest.Channels)
		})
	}
}

func TestSubstitutesFor(t *testing.T) {
	type args struct {
		bundles  []string
		rebuilds []string
	}
	type replaces map[string]string
	type expected struct {
		bundles        []string
		substitutions  map[string]string
		whatReplaces   map[string]replaces
		defaultChannel string
	}
	tests := []struct {
		description string
		args        args
		expected    expected
	}{
		{
			description: "FirstSubstitutionReplaces",
			args: args{
				bundles: []string{
					"prometheus.0.22.2",
					"prometheus.0.14.0",
					"prometheus.0.15.0",
				},
				rebuilds: []string{
					"prometheus.0.15.0.substitutesfor",
				},
			},
			expected: expected{
				bundles: []string{
					"quay.io/test/prometheus.0.22.2",
					"quay.io/test/prometheus.0.14.0",
					"quay.io/test/prometheus.0.15.0.substitutesfor",
					"quay.io/test/prometheus.0.15.0.substitutesfor",
					"quay.io/test/prometheus.0.15.0",
					"quay.io/test/prometheus.0.15.0",
					"quay.io/test/prometheus.0.14.0",
				},
				substitutions: map[string]string{
					"prometheusoperator.0.22.2":                      "",
					"prometheusoperator.0.14.0":                      "",
					"prometheusoperator.0.15.0":                      "",
					"prometheusoperator.0.15.0+1-freshmaker-rebuild": "prometheusoperator.0.15.0",
				},
				whatReplaces: map[string]replaces{
					"prometheusoperator.0.22.2": {
						"preview": "",
					},
					"prometheusoperator.0.14.0": {
						"preview": "prometheusoperator.0.15.0+1-freshmaker-rebuild",
						"stable":  "prometheusoperator.0.15.0+1-freshmaker-rebuild",
					},
					"prometheusoperator.0.15.0": {
						"preview": "prometheusoperator.0.15.0+1-freshmaker-rebuild",
						"stable":  "prometheusoperator.0.15.0+1-freshmaker-rebuild",
					},
					"prometheusoperator.0.15.0+1-freshmaker-rebuild": {
						"preview": "prometheusoperator.0.22.2",
						"stable":  "",
					},
				},
				defaultChannel: "preview",
			},
		},
		{
			description: "FirstSubstitutionReplacesNonHead",
			args: args{
				bundles: []string{
					"prometheus.0.22.2",
					"prometheus.0.14.0",
					"prometheus.0.15.0",
				},
				rebuilds: []string{
					"prometheus.0.14.0.substitutesfor",
				},
			},
			expected: expected{
				bundles: []string{
					"quay.io/test/prometheus.0.22.2",
					"quay.io/test/prometheus.0.14.0",
					"quay.io/test/prometheus.0.14.0.substitutesfor",
					"quay.io/test/prometheus.0.14.0.substitutesfor",
					"quay.io/test/prometheus.0.15.0",
					"quay.io/test/prometheus.0.15.0",
					"quay.io/test/prometheus.0.14.0",
				},
				substitutions: map[string]string{
					"prometheusoperator.0.22.2":                "",
					"prometheusoperator.0.14.0":                "",
					"prometheusoperator.0.15.0":                "",
					"prometheusoperator.0.14.0+0.1234-rebuild": "prometheusoperator.0.14.0",
				},
				whatReplaces: map[string]replaces{
					"prometheusoperator.0.22.2": {
						"preview": "",
					},
					"prometheusoperator.0.14.0": {
						"preview": "prometheusoperator.0.14.0+0.1234-rebuild",
						"stable":  "prometheusoperator.0.14.0+0.1234-rebuild",
					},
					"prometheusoperator.0.15.0": {
						"preview": "prometheusoperator.0.22.2",
						"stable":  "",
					},
					"prometheusoperator.0.14.0+0.1234-rebuild": {
						"preview": "prometheusoperator.0.15.0",
						"stable":  "prometheusoperator.0.15.0",
					},
				},
				defaultChannel: "preview",
			},
		},
		{
			description: "SecondSubstitutionReplaces",
			args: args{
				bundles: []string{
					"prometheus.0.22.2",
					"prometheus.0.14.0",
					"prometheus.0.15.0",
				},
				rebuilds: []string{
					"prometheus.0.15.0.substitutesfor",
					"prometheus.0.15.0.substitutesfor2",
				},
			},
			expected: expected{
				bundles: []string{
					"quay.io/test/prometheus.0.22.2",
					"quay.io/test/prometheus.0.14.0",
					"quay.io/test/prometheus.0.15.0.substitutesfor",
					"quay.io/test/prometheus.0.15.0.substitutesfor",
					"quay.io/test/prometheus.0.15.0.substitutesfor2",
					"quay.io/test/prometheus.0.15.0.substitutesfor2",
					"quay.io/test/prometheus.0.15.0",
					"quay.io/test/prometheus.0.15.0",
					"quay.io/test/prometheus.0.14.0",
				},
				substitutions: map[string]string{
					"prometheusoperator.0.22.2":                      "",
					"prometheusoperator.0.14.0":                      "",
					"prometheusoperator.0.15.0":                      "",
					"prometheusoperator.0.15.0+1-freshmaker-rebuild": "prometheusoperator.0.15.0",
					"prometheusoperator.0.15.0+2-freshmaker-rebuild": "prometheusoperator.0.15.0+1-freshmaker-rebuild",
				},
				whatReplaces: map[string]replaces{
					"prometheusoperator.0.22.2": {
						"preview": "",
					},
					"prometheusoperator.0.14.0": {
						"preview": "prometheusoperator.0.15.0+2-freshmaker-rebuild",
						"stable":  "prometheusoperator.0.15.0+2-freshmaker-rebuild",
					},
					"prometheusoperator.0.15.0": {
						"preview": "prometheusoperator.0.15.0+2-freshmaker-rebuild",
						"stable":  "prometheusoperator.0.15.0+2-freshmaker-rebuild",
					},
					"prometheusoperator.0.15.0+2-freshmaker-rebuild": {
						"preview": "prometheusoperator.0.22.2",
						"stable":  "",
					},
					"prometheusoperator.0.15.0+1-freshmaker-rebuild": {
						"preview": "prometheusoperator.0.15.0+2-freshmaker-rebuild",
						"stable":  "prometheusoperator.0.15.0+2-freshmaker-rebuild",
					},
				},
				defaultChannel: "preview",
			},
		},
		{
			description: "SecondSubstitutionReplacesWithNonHead",
			args: args{
				bundles: []string{
					"prometheus.0.22.2",
					"prometheus.0.14.0",
					"prometheus.0.15.0",
				},
				rebuilds: []string{
					"prometheus.0.14.0.substitutesfor",
					"prometheus.0.15.0.substitutesfor",
					"prometheus.0.15.0.substitutesfor2",
				},
			},
			expected: expected{
				bundles: []string{
					"quay.io/test/prometheus.0.22.2",
					"quay.io/test/prometheus.0.14.0",
					"quay.io/test/prometheus.0.15.0.substitutesfor",
					"quay.io/test/prometheus.0.15.0.substitutesfor",
					"quay.io/test/prometheus.0.15.0.substitutesfor2",
					"quay.io/test/prometheus.0.15.0.substitutesfor2",
					"quay.io/test/prometheus.0.14.0.substitutesfor",
					"quay.io/test/prometheus.0.14.0.substitutesfor",
					"quay.io/test/prometheus.0.15.0",
					"quay.io/test/prometheus.0.15.0",
					"quay.io/test/prometheus.0.14.0",
				},
				substitutions: map[string]string{
					"prometheusoperator.0.22.2":                      "",
					"prometheusoperator.0.14.0":                      "",
					"prometheusoperator.0.15.0":                      "",
					"prometheusoperator.0.14.0+0.1234-rebuild":       "prometheusoperator.0.14.0",
					"prometheusoperator.0.15.0+1-freshmaker-rebuild": "prometheusoperator.0.15.0",
					"prometheusoperator.0.15.0+2-freshmaker-rebuild": "prometheusoperator.0.15.0+1-freshmaker-rebuild",
				},
				whatReplaces: map[string]replaces{
					"prometheusoperator.0.22.2": {
						"preview": "",
					},
					"prometheusoperator.0.15.0": {
						"preview": "prometheusoperator.0.15.0+2-freshmaker-rebuild",
						"stable":  "prometheusoperator.0.15.0+2-freshmaker-rebuild",
					},
					"prometheusoperator.0.15.0+2-freshmaker-rebuild": {
						"preview": "prometheusoperator.0.22.2",
						"stable":  "",
					},
					"prometheusoperator.0.15.0+1-freshmaker-rebuild": {
						"preview": "prometheusoperator.0.15.0+2-freshmaker-rebuild",
						"stable":  "prometheusoperator.0.15.0+2-freshmaker-rebuild",
					},
					"prometheusoperator.0.14.0": {
						"preview": "prometheusoperator.0.14.0+0.1234-rebuild",
						"stable":  "prometheusoperator.0.14.0+0.1234-rebuild",
					},
					"prometheusoperator.0.14.0+0.1234-rebuild": {
						"preview": "prometheusoperator.0.15.0+2-freshmaker-rebuild",
						"stable":  "prometheusoperator.0.15.0+2-freshmaker-rebuild",
					},
				},
				defaultChannel: "preview",
			},
		},
		{
			description: "CanBatchAddSubstitutesFor",
			args: args{
				bundles: []string{
					"prometheus.0.14.0",
					"prometheus.0.15.0.substitutesfor",
					"prometheus.0.15.0.substitutesfor2",
					"prometheus.0.15.0",
				},
				rebuilds: []string{},
			},
			expected: expected{
				bundles: []string{
					"quay.io/test/prometheus.0.14.0",
					"quay.io/test/prometheus.0.15.0.substitutesfor",
					"quay.io/test/prometheus.0.15.0.substitutesfor",
					"quay.io/test/prometheus.0.15.0.substitutesfor2",
					"quay.io/test/prometheus.0.15.0.substitutesfor2",
					"quay.io/test/prometheus.0.15.0",
					"quay.io/test/prometheus.0.15.0",
					"quay.io/test/prometheus.0.14.0",
				},
				substitutions: map[string]string{
					"prometheusoperator.0.14.0":                      "",
					"prometheusoperator.0.15.0":                      "",
					"prometheusoperator.0.15.0+1-freshmaker-rebuild": "prometheusoperator.0.15.0",
					"prometheusoperator.0.15.0+2-freshmaker-rebuild": "prometheusoperator.0.15.0+1-freshmaker-rebuild",
				},
				whatReplaces: map[string]replaces{
					"prometheusoperator.0.15.0": {
						"preview": "prometheusoperator.0.15.0+2-freshmaker-rebuild",
						"stable":  "prometheusoperator.0.15.0+2-freshmaker-rebuild",
					},
					"prometheusoperator.0.15.0+2-freshmaker-rebuild": {
						"preview": "",
						"stable":  "",
					},
					"prometheusoperator.0.15.0+1-freshmaker-rebuild": {
						"preview": "prometheusoperator.0.15.0+2-freshmaker-rebuild",
						"stable":  "prometheusoperator.0.15.0+2-freshmaker-rebuild",
					},
					"prometheusoperator.0.14.0": {
						"preview": "prometheusoperator.0.15.0+2-freshmaker-rebuild",
						"stable":  "prometheusoperator.0.15.0+2-freshmaker-rebuild",
					},
				},
				defaultChannel: "preview",
			},
		},
		{
			description: "CanBatchAddSubstitutesForWithNonHead",
			args: args{
				bundles: []string{
					"prometheus.0.14.0",
					"prometheus.0.14.0.substitutesfor",
					"prometheus.0.15.0.substitutesfor",
					"prometheus.0.15.0.substitutesfor2",
					"prometheus.0.15.0",
				},
				rebuilds: []string{},
			},
			expected: expected{
				bundles: []string{
					"quay.io/test/prometheus.0.14.0",
					"quay.io/test/prometheus.0.15.0.substitutesfor",
					"quay.io/test/prometheus.0.15.0.substitutesfor",
					"quay.io/test/prometheus.0.15.0.substitutesfor2",
					"quay.io/test/prometheus.0.15.0.substitutesfor2",
					"quay.io/test/prometheus.0.14.0.substitutesfor",
					"quay.io/test/prometheus.0.14.0.substitutesfor",
					"quay.io/test/prometheus.0.15.0",
					"quay.io/test/prometheus.0.15.0",
					"quay.io/test/prometheus.0.14.0",
				},
				substitutions: map[string]string{
					"prometheusoperator.0.14.0":                      "",
					"prometheusoperator.0.15.0":                      "",
					"prometheusoperator.0.14.0+0.1234-rebuild":       "prometheusoperator.0.14.0",
					"prometheusoperator.0.15.0+1-freshmaker-rebuild": "prometheusoperator.0.15.0",
					"prometheusoperator.0.15.0+2-freshmaker-rebuild": "prometheusoperator.0.15.0+1-freshmaker-rebuild",
				},
				whatReplaces: map[string]replaces{
					"prometheusoperator.0.15.0": {
						"preview": "prometheusoperator.0.15.0+2-freshmaker-rebuild",
						"stable":  "prometheusoperator.0.15.0+2-freshmaker-rebuild",
					},
					"prometheusoperator.0.15.0+2-freshmaker-rebuild": {
						"preview": "",
						"stable":  "",
					},
					"prometheusoperator.0.15.0+1-freshmaker-rebuild": {
						"preview": "prometheusoperator.0.15.0+2-freshmaker-rebuild",
						"stable":  "prometheusoperator.0.15.0+2-freshmaker-rebuild",
					},
					"prometheusoperator.0.14.0": {
						"preview": "prometheusoperator.0.14.0+0.1234-rebuild",
						"stable":  "prometheusoperator.0.14.0+0.1234-rebuild",
					},
					"prometheusoperator.0.14.0+0.1234-rebuild": {
						"preview": "prometheusoperator.0.15.0+2-freshmaker-rebuild",
						"stable":  "prometheusoperator.0.15.0+2-freshmaker-rebuild",
					},
				},
				defaultChannel: "preview",
			},
		},
		{
			description: "CanAddABundleThatReplacesSubstitutedOne",
			args: args{
				bundles: []string{
					"prometheus.0.14.0",
					"prometheus.0.15.0",
					"prometheus.0.15.0.substitutesfor",
					"prometheus.0.15.0.substitutesfor2",
				},
				rebuilds: []string{
					"prometheus.0.22.2",
				},
			},
			expected: expected{
				bundles: []string{
					"quay.io/test/prometheus.0.22.2",
					"quay.io/test/prometheus.0.14.0",
					"quay.io/test/prometheus.0.15.0.substitutesfor",
					"quay.io/test/prometheus.0.15.0.substitutesfor",
					"quay.io/test/prometheus.0.15.0.substitutesfor2",
					"quay.io/test/prometheus.0.15.0.substitutesfor2",
					"quay.io/test/prometheus.0.15.0",
					"quay.io/test/prometheus.0.15.0",
					"quay.io/test/prometheus.0.14.0",
				},
				substitutions: map[string]string{
					"prometheusoperator.0.22.2":                      "",
					"prometheusoperator.0.14.0":                      "",
					"prometheusoperator.0.15.0":                      "",
					"prometheusoperator.0.15.0+1-freshmaker-rebuild": "prometheusoperator.0.15.0",
					"prometheusoperator.0.15.0+2-freshmaker-rebuild": "prometheusoperator.0.15.0+1-freshmaker-rebuild",
				},
				whatReplaces: map[string]replaces{
					"prometheusoperator.0.22.2": {
						"preview": "",
					},
					"prometheusoperator.0.14.0": {
						"preview": "prometheusoperator.0.15.0+2-freshmaker-rebuild",
						"stable":  "prometheusoperator.0.15.0+2-freshmaker-rebuild",
					},
					"prometheusoperator.0.15.0": {
						"preview": "prometheusoperator.0.15.0+2-freshmaker-rebuild",
						"stable":  "prometheusoperator.0.15.0+2-freshmaker-rebuild",
					},
					"prometheusoperator.0.15.0+2-freshmaker-rebuild": {
						"preview": "prometheusoperator.0.22.2",
						"stable":  "",
					},
					"prometheusoperator.0.15.0+1-freshmaker-rebuild": {
						"preview": "prometheusoperator.0.15.0+2-freshmaker-rebuild",
						"stable":  "prometheusoperator.0.15.0+2-freshmaker-rebuild",
					},
				},
				defaultChannel: "preview",
			},
		},
		{
			description: "CanAddABundleThatReplacesSubstitutedOneWithNonHead",
			args: args{
				bundles: []string{
					"prometheus.0.14.0",
					"prometheus.0.15.0",
					"prometheus.0.15.0.substitutesfor",
					"prometheus.0.15.0.substitutesfor2",
					"prometheus.0.14.0.substitutesfor",
				},
				rebuilds: []string{
					"prometheus.0.22.2",
				},
			},
			expected: expected{
				bundles: []string{
					"quay.io/test/prometheus.0.22.2",
					"quay.io/test/prometheus.0.14.0",
					"quay.io/test/prometheus.0.15.0.substitutesfor",
					"quay.io/test/prometheus.0.15.0.substitutesfor",
					"quay.io/test/prometheus.0.15.0.substitutesfor2",
					"quay.io/test/prometheus.0.15.0.substitutesfor2",
					"quay.io/test/prometheus.0.14.0.substitutesfor",
					"quay.io/test/prometheus.0.14.0.substitutesfor",
					"quay.io/test/prometheus.0.15.0",
					"quay.io/test/prometheus.0.15.0",
					"quay.io/test/prometheus.0.14.0",
				},
				substitutions: map[string]string{
					"prometheusoperator.0.22.2":                      "",
					"prometheusoperator.0.14.0":                      "",
					"prometheusoperator.0.15.0":                      "",
					"prometheusoperator.0.14.0+0.1234-rebuild":       "prometheusoperator.0.14.0",
					"prometheusoperator.0.15.0+1-freshmaker-rebuild": "prometheusoperator.0.15.0",
					"prometheusoperator.0.15.0+2-freshmaker-rebuild": "prometheusoperator.0.15.0+1-freshmaker-rebuild",
				},
				whatReplaces: map[string]replaces{
					"prometheusoperator.0.22.2": {
						"preview": "",
					},
					"prometheusoperator.0.15.0": {
						"preview": "prometheusoperator.0.15.0+2-freshmaker-rebuild",
						"stable":  "prometheusoperator.0.15.0+2-freshmaker-rebuild",
					},
					"prometheusoperator.0.15.0+2-freshmaker-rebuild": {
						"preview": "prometheusoperator.0.22.2",
						"stable":  "",
					},
					"prometheusoperator.0.15.0+1-freshmaker-rebuild": {
						"preview": "prometheusoperator.0.15.0+2-freshmaker-rebuild",
						"stable":  "prometheusoperator.0.15.0+2-freshmaker-rebuild",
					},
					"prometheusoperator.0.14.0": {
						"preview": "prometheusoperator.0.14.0+0.1234-rebuild",
						"stable":  "prometheusoperator.0.14.0+0.1234-rebuild",
					},
					"prometheusoperator.0.14.0+0.1234-rebuild": {
						"preview": "prometheusoperator.0.15.0+2-freshmaker-rebuild",
						"stable":  "prometheusoperator.0.15.0+2-freshmaker-rebuild",
					},
				},
				defaultChannel: "preview",
			},
		},
		{
			description: "MultipleBundlesSubstitutingForTheSameBundleAddedInWrongOrder",
			args: args{
				bundles: []string{
					"prometheus.0.14.0",
				},
				rebuilds: []string{
					"prometheus.0.14.0.substitutesfor",
					"prometheus.0.14.0.substitutesfor3",
					"prometheus.0.14.0.substitutesfor2",
				},
			},
			expected: expected{
				bundles: []string{
					"quay.io/test/prometheus.0.14.0",
					"quay.io/test/prometheus.0.14.0.substitutesfor2",
					"quay.io/test/prometheus.0.14.0.substitutesfor3",
					"quay.io/test/prometheus.0.14.0.substitutesfor",
				},
				substitutions: map[string]string{
					"prometheusoperator.0.14.0":                "",
					"prometheusoperator.0.14.0+0.1234-rebuild": "prometheusoperator.0.14.0",
					"prometheusoperator.0.14.0+2-rebuild":      "prometheusoperator.0.14.0+0.1234-rebuild",
					"prometheusoperator.0.14.0+3-rebuild":      "prometheusoperator.0.14.0+2-rebuild",
				},
				whatReplaces: map[string]replaces{
					"prometheusoperator.0.14.0": {
						"preview": "prometheusoperator.0.14.0+3-rebuild",
					},
					"prometheusoperator.0.14.0+0.1234-rebuild": {
						"preview": "prometheusoperator.0.14.0+3-rebuild",
					},
					"prometheusoperator.0.14.0+2-rebuild": {
						"preview": "prometheusoperator.0.14.0+3-rebuild",
					},
					"prometheusoperator.0.14.0+3-rebuild": {
						"preview": "",
					},
				},
				defaultChannel: "preview",
			},
		},
		{
			description: "BatchAddWithMultipleBundlesSubstitutingForTheSameBundle",
			args: args{
				bundles: []string{
					"prometheus.0.14.0",
					"prometheus.0.14.0.substitutesfor",
					"prometheus.0.14.0.substitutesfor3",
					"prometheus.0.14.0.substitutesfor2",
				},
				rebuilds: []string{},
			},
			expected: expected{
				bundles: []string{
					"quay.io/test/prometheus.0.14.0",
					"quay.io/test/prometheus.0.14.0.substitutesfor3",
					"quay.io/test/prometheus.0.14.0.substitutesfor2",
					"quay.io/test/prometheus.0.14.0.substitutesfor",
				},
				substitutions: map[string]string{
					"prometheusoperator.0.14.0":                "",
					"prometheusoperator.0.14.0+0.1234-rebuild": "prometheusoperator.0.14.0",
					"prometheusoperator.0.14.0+2-rebuild":      "prometheusoperator.0.14.0+0.1234-rebuild",
					"prometheusoperator.0.14.0+3-rebuild":      "prometheusoperator.0.14.0+2-rebuild",
				},
				whatReplaces: map[string]replaces{
					"prometheusoperator.0.14.0": {
						"preview": "prometheusoperator.0.14.0+3-rebuild",
					},
					"prometheusoperator.0.14.0+0.1234-rebuild": {
						"preview": "prometheusoperator.0.14.0+3-rebuild",
					},
					"prometheusoperator.0.14.0+2-rebuild": {
						"preview": "prometheusoperator.0.14.0+3-rebuild",
					},
					"prometheusoperator.0.14.0+3-rebuild": {
						"preview": "",
					},
				},
				defaultChannel: "preview",
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.description, func(t *testing.T) {
			logrus.SetLevel(logrus.DebugLevel)
			db, cleanup := CreateTestDB(t)
			defer cleanup()

			load, err := sqlite.NewSQLLiteLoader(db, sqlite.WithEnableAlpha(true))
			require.NoError(t, err)
			err = load.Migrate(context.TODO())
			require.NoError(t, err)
			query := sqlite.NewSQLLiteQuerierFromDb(db)

			graphLoader, err := sqlite.NewSQLGraphLoaderFromDB(db)
			require.NoError(t, err)

			populate := func(names []string) error {
				refMap := make(map[image.Reference]string, 0)
				for _, name := range names {
					refMap[image.SimpleReference("quay.io/test/"+name)] = "../../bundles/" + name
				}
				return registry.NewDirectoryPopulator(
					load,
					graphLoader,
					query,
					refMap,
					nil).Populate(registry.ReplacesMode)
			}
			// Initialize index with some bundles
			require.NoError(t, populate(tt.args.bundles))

			// Add the rebuilds one at a time
			for _, rb := range tt.args.rebuilds {
				require.NoError(t, populate([]string{rb}))
			}

			// Check graph is unchanged but has new csv name + version
			// Ensure bundlePaths in db match
			bundles, err := query.ListBundles(context.Background())
			require.NoError(t, err)
			bundlePaths := make([]string, 0, len(bundles))
			for _, bundle := range bundles {
				bundlePaths = append(bundlePaths, bundle.BundlePath)
				bundleThatReplaces, _ := query.GetBundleThatReplaces(context.Background(), bundle.CsvName, bundle.PackageName, bundle.ChannelName)
				if bundleThatReplaces != nil {
					require.Equal(t, tt.expected.whatReplaces[bundle.CsvName][bundle.ChannelName], bundleThatReplaces.CsvName)
				} else {
					require.Empty(t, tt.expected.whatReplaces[bundle.CsvName][bundle.ChannelName])
				}
				substitution, err := getBundleSubstitution(context.Background(), db, bundle.CsvName)
				require.NoError(t, err)
				require.Equal(t, tt.expected.substitutions[bundle.CsvName], substitution)
			}
			require.ElementsMatch(t, tt.expected.bundles, bundlePaths)

			// check default channel
			defaultChannel, err := query.GetDefaultChannelForPackage(context.Background(), "prometheus")
			require.NoError(t, err)
			require.Equal(t, tt.expected.defaultChannel, defaultChannel)
		})
	}
}

func getBundleSubstitution(ctx context.Context, db *sql.DB, name string) (string, error) {
	query := `SELECT substitutesfor FROM operatorbundle WHERE name=?`
	rows, err := db.QueryContext(ctx, query, name)
	if err != nil {
		return "", err
	}
	defer rows.Close()

	var substitutesFor sql.NullString
	if rows.Next() {
		if err := rows.Scan(&substitutesFor); err != nil {
			return "", err
		}
	}
	return substitutesFor.String, nil
}

func TestEnableAlpha(t *testing.T) {
	type args struct {
		bundles     []string
		enableAlpha bool
	}
	type expected struct {
		err error
	}
	tests := []struct {
		description string
		args        args
		expected    expected
	}{
		{
			description: "SubstitutesForTrue",
			args: args{
				bundles: []string{
					"prometheus.0.22.2",
					"prometheus.0.14.0",
					"prometheus.0.15.0",
					"prometheus.0.15.0.substitutesfor",
				},
				enableAlpha: true,
			},
			expected: expected{
				err: nil,
			},
		},
		{
			description: "SubstitutesForFalse",
			args: args{
				bundles: []string{
					"prometheus.0.22.2",
					"prometheus.0.14.0",
					"prometheus.0.15.0",
					"prometheus.0.15.0.substitutesfor",
				},
				enableAlpha: false,
			},
			expected: expected{
				err: utilerrors.NewAggregate([]error{fmt.Errorf("SubstitutesFor is an alpha-only feature. You must enable alpha features with the flag --enable-alpha in order to use this feature.")}),
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.description, func(t *testing.T) {
			logrus.SetLevel(logrus.DebugLevel)
			db, cleanup := CreateTestDB(t)
			defer cleanup()

			load, err := sqlite.NewSQLLiteLoader(db, sqlite.WithEnableAlpha(tt.args.enableAlpha))
			require.NoError(t, err)
			err = load.Migrate(context.TODO())
			require.NoError(t, err)
			query := sqlite.NewSQLLiteQuerierFromDb(db)

			graphLoader, err := sqlite.NewSQLGraphLoaderFromDB(db)
			require.NoError(t, err)

			populate := func(names []string) error {
				refMap := make(map[image.Reference]string, 0)
				for _, name := range names {
					refMap[image.SimpleReference("quay.io/test/"+name)] = "../../bundles/" + name
				}
				return registry.NewDirectoryPopulator(
					load,
					graphLoader,
					query,
					refMap,
					nil).Populate(registry.ReplacesMode)
			}
			require.Equal(t, tt.expected.err, populate(tt.args.bundles))
		})
	}
}

func newUnpackedTestBundle(root, dir, name string, csvSpec json.RawMessage, annotations registry.Annotations) (string, func(), error) {
	bundleDir := filepath.Join(root, dir)
	cleanup := func() {
		os.RemoveAll(bundleDir)
	}
	if err := os.Mkdir(bundleDir, 0755); err != nil {
		return bundleDir, cleanup, err
	}
	if err := os.Mkdir(filepath.Join(bundleDir, bundle.ManifestsDir), 0755); err != nil {
		return bundleDir, cleanup, err
	}
	if err := os.Mkdir(filepath.Join(bundleDir, bundle.MetadataDir), 0755); err != nil {
		return bundleDir, cleanup, err
	}
	if len(csvSpec) == 0 {
		csvSpec = json.RawMessage(`{}`)
	}

	rawCSV, err := json.Marshal(registry.ClusterServiceVersion{
		TypeMeta: metav1.TypeMeta{
			Kind: sqlite.ClusterServiceVersionKind,
		},
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
		Spec: csvSpec,
	})
	if err != nil {
		return bundleDir, cleanup, err
	}

	rawObj := unstructured.Unstructured{}
	if err := json.Unmarshal(rawCSV, &rawObj); err != nil {
		return bundleDir, cleanup, err
	}
	rawObj.SetCreationTimestamp(metav1.Time{})

	jsonout, err := rawObj.MarshalJSON()
	if err != nil {
		return bundleDir, cleanup, err
	}
	out, err := yaml.JSONToYAML(jsonout)
	if err != nil {
		return bundleDir, cleanup, err
	}
	if err := os.WriteFile(filepath.Join(bundleDir, bundle.ManifestsDir, "csv.yaml"), out, 0600); err != nil {
		return bundleDir, cleanup, err
	}

	out, err = yaml.Marshal(registry.AnnotationsFile{Annotations: annotations})
	if err != nil {
		return bundleDir, cleanup, err
	}
	if err := os.WriteFile(filepath.Join(bundleDir, bundle.MetadataDir, "annotations.yaml"), out, 0600); err != nil {
		return bundleDir, cleanup, err
	}
	return bundleDir, cleanup, nil
}

func TestValidateEdgeBundlePackage(t *testing.T) {
	newInput := func(name, versionString, pkg, defaultChannel, channels, replaces string, skips []string) *registry.ImageInput {
		v, err := semver.Parse(versionString)
		require.NoError(t, err)

		spec := v1alpha1.ClusterServiceVersionSpec{
			Replaces: replaces,
			Skips:    skips,
			Version:  version.OperatorVersion{Version: v},
		}
		specJSON, err := json.Marshal(&spec)
		require.NoError(t, err)

		rawCSV, err := json.Marshal(registry.ClusterServiceVersion{
			TypeMeta: metav1.TypeMeta{
				Kind: sqlite.ClusterServiceVersionKind,
			},
			ObjectMeta: metav1.ObjectMeta{
				Name: name,
			},
			Spec: specJSON,
		})
		require.NoError(t, err)

		rawObj := unstructured.Unstructured{}
		require.NoError(t, json.Unmarshal(rawCSV, &rawObj))
		rawObj.SetCreationTimestamp(metav1.Time{})

		jsonout, err := rawObj.MarshalJSON()
		require.NoError(t, err)

		b, err := registry.NewBundleFromStrings(name, versionString, pkg, defaultChannel, channels, string(jsonout))
		require.NoError(t, err)
		return &registry.ImageInput{Bundle: b}
	}

	logrus.SetLevel(logrus.DebugLevel)
	db, cleanup := CreateTestDB(t)
	defer cleanup()

	store, err := createAndPopulateDB(db)
	require.NoError(t, err)

	r := registry.NewDirectoryPopulator(nil, nil, store, nil, nil)

	tests := []struct {
		name    string
		args    []*registry.ImageInput
		wantErr error
	}{
		{
			name: "conflictInAdded",
			args: []*registry.ImageInput{
				newInput("b1", "0.0.1", "p1", "a", "a", "", []string{}),
				newInput("b2", "0.0.2", "p2", "a", "a", "", []string{"b1"}),
			},
			wantErr: fmt.Errorf("bundle b1 must belong to exactly one package, found on: [p1 p2]"),
		},
		{
			name: "conflictWithExisting",
			args: []*registry.ImageInput{
				newInput("b1", "0.0.1", "p1", "a", "a", "", []string{"etcdoperator.v0.9.0"}),
			},
			wantErr: fmt.Errorf("bundle etcdoperator.v0.9.0 belongs to package etcd on index, cannot be added as an edge for package p1"),
		},
		{
			name: "passForNonConflicting",
			args: []*registry.ImageInput{
				newInput("b1", "0.0.1", "etcd", "a", "a", "", []string{"etcdoperator.v0.9.0"}),
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := r.ValidateEdgeBundlePackage(tt.args)
			if tt.wantErr == nil {
				require.NoError(t, err)
				return
			}
			require.EqualError(t, err, tt.wantErr.Error())
		})
	}
}
