package repo

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"strconv"
	"strings"
	"testing"

	"github.com/artifacthub/hub/internal/authz"
	"github.com/artifacthub/hub/internal/httpw"
	"github.com/artifacthub/hub/internal/hub"
	"github.com/artifacthub/hub/internal/oci"
	"github.com/artifacthub/hub/internal/tests"
	"github.com/artifacthub/hub/internal/util"
	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
	"github.com/spf13/viper"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
	"gopkg.in/yaml.v2"
)

const repoID = "00000000-0000-0000-0000-000000000001"

var cfg = viper.New()

func TestAdd(t *testing.T) {
	ctx := context.WithValue(context.Background(), hub.UserIDKey, "userID")

	t.Run("user id not found in ctx", func(t *testing.T) {
		t.Parallel()
		m := NewManager(cfg, nil, nil, nil)
		assert.Panics(t, func() {
			_ = m.Add(context.Background(), "orgName", nil)
		})
	})

	t.Run("invalid input", func(t *testing.T) {
		testCases := []struct {
			errMsg  string
			orgName string
			r       *hub.Repository
			lErr    error
		}{
			{
				"invalid kind",
				"org1",
				&hub.Repository{
					Kind: hub.RepositoryKind(100),
				},
				nil,
			},
			{
				"name not provided",
				"org1",
				&hub.Repository{
					Kind: hub.Helm,
					Name: "",
				},
				nil,
			},
			{
				"invalid name",
				"org1",
				&hub.Repository{
					Kind: hub.Helm,
					Name: "1repo",
					URL:  "https://repo1.com",
				},
				nil,
			},
			{
				"invalid name",
				"org1",
				&hub.Repository{
					Kind: hub.Helm,
					Name: "repo_underscore",
					URL:  "https://repo1.com",
				},
				nil,
			},
			{
				"invalid name",
				"org1",
				&hub.Repository{
					Kind: hub.Helm,
					Name: "REPO_UPPERCASE",
					URL:  "https://repo1.com",
				},
				nil,
			},
			{
				"url not provided",
				"org1",
				&hub.Repository{
					Kind: hub.Helm,
					Name: "repo1",
					URL:  "",
				},
				nil,
			},
			{
				"missing protocol scheme",
				"org1",
				&hub.Repository{
					Kind: hub.Helm,
					Name: "repo1",
					URL:  ":::///",
				},
				nil,
			},
			{
				"scheme not supported",
				"org1",
				&hub.Repository{
					Kind: hub.Helm,
					Name: "repo1",
					URL:  "other://repo1.com",
				},
				nil,
			},
			{
				"urls with credentials not allowed",
				"org1",
				&hub.Repository{
					Kind: hub.Helm,
					Name: "repo1",
					URL:  "https://user:pass@repo1.com",
				},
				nil,
			},
			{
				"invalid url format",
				"org1",
				&hub.Repository{
					Kind: hub.OLM,
					Name: "repo1",
					URL:  "https://repo1.com",
				},
				nil,
			},
			{
				"invalid url format",
				"org1",
				&hub.Repository{
					Kind: hub.OLM,
					Name: "repo1",
					URL:  "https://github.com/incomplete",
				},
				nil,
			},
			{
				"invalid url format",
				"org1",
				&hub.Repository{
					Kind: hub.Container,
					Name: "repo1",
					URL:  "https://repo1.url",
				},
				nil,
			},
			{
				"the url provided does not point to a valid Helm repository",
				"org1",
				&hub.Repository{
					Kind: hub.Helm,
					Name: "repo1",
					URL:  "https://repo1.com",
				},
				errors.New("error loading index file"),
			},
			{
				"private repositories not allowed",
				"org1",
				&hub.Repository{
					Kind:     hub.Helm,
					Name:     "repo1",
					URL:      "https://repo1.com",
					AuthUser: "user1",
					AuthPass: "pass1",
				},
				nil,
			},
			{
				"invalid container image data",
				"org1",
				&hub.Repository{
					Kind: hub.Container,
					Name: "repo1",
					URL:  "oci://registry.io/namespace/repo",
					Data: json.RawMessage("{["),
				},
				nil,
			},
			{
				"too many tags",
				"org1",
				&hub.Repository{
					Kind: hub.Container,
					Name: "repo1",
					URL:  "oci://registry.io/namespace/repo",
					Data: json.RawMessage(`{
						"tags": [
							{"name": "tag1"},
							{"name": "tag2"},
							{"name": "tag3"},
							{"name": "tag4"},
							{"name": "tag5"},
							{"name": "tag6"},
							{"name": "tag7"},
							{"name": "tag8"},
							{"name": "tag9"},
							{"name": "tag10"},
							{"name": "tag11"}
						]
					}`),
				},
				nil,
			},
		}
		for _, tc := range testCases {
			t.Run(tc.errMsg, func(t *testing.T) {
				t.Parallel()
				l := &HelmIndexLoaderMock{}
				if tc.lErr != nil {
					l.On("LoadIndex", tc.r).Return(nil, "", tc.lErr)
				} else {
					l.On("LoadIndex", tc.r).Return(nil, "", nil).Maybe()
				}
				m := NewManager(cfg, nil, nil, nil, WithHelmIndexLoader(l))

				err := m.Add(ctx, tc.orgName, tc.r)
				assert.True(t, errors.Is(err, hub.ErrInvalidInput))
				assert.Contains(t, err.Error(), tc.errMsg)
				l.AssertExpectations(t)
			})
		}
	})

	t.Run("authorization failed", func(t *testing.T) {
		t.Parallel()
		r := &hub.Repository{
			Name:        "repo1",
			DisplayName: "Repository 1",
			URL:         "https://repo1.com",
			Kind:        hub.Helm,
		}
		az := &authz.AuthorizerMock{}
		az.On("Authorize", ctx, &hub.AuthorizeInput{
			OrganizationName: "orgName",
			UserID:           "userID",
			Action:           hub.AddOrganizationRepository,
		}).Return(tests.ErrFake)
		l := &HelmIndexLoaderMock{}
		l.On("LoadIndex", r).Return(nil, "", nil)
		m := NewManager(cfg, nil, az, nil, WithHelmIndexLoader(l))

		err := m.Add(ctx, "orgName", r)
		assert.Equal(t, tests.ErrFake, err)
		l.AssertExpectations(t)
		az.AssertExpectations(t)
	})

	t.Run("database error", func(t *testing.T) {
		testCases := []struct {
			r             *hub.Repository
			dbErr         error
			expectedError error
		}{
			{
				&hub.Repository{
					Name:        "repo1",
					DisplayName: "Repository 1",
					URL:         "https://repo1.com",
					Kind:        hub.Helm,
				},
				tests.ErrFakeDB,
				tests.ErrFakeDB,
			},
			{
				&hub.Repository{
					Name:        "repo1",
					DisplayName: "Repository 1",
					URL:         "https://repo1.com",
					Kind:        hub.Helm,
				},
				util.ErrDBInsufficientPrivilege,
				hub.ErrInsufficientPrivilege,
			},
		}
		for _, tc := range testCases {
			t.Run(tc.dbErr.Error(), func(t *testing.T) {
				t.Parallel()
				db := &tests.DBMock{}
				db.On("Exec", ctx, addRepoDBQ, "userID", "orgName", mock.Anything).Return(tc.dbErr)
				az := &authz.AuthorizerMock{}
				az.On("Authorize", ctx, &hub.AuthorizeInput{
					OrganizationName: "orgName",
					UserID:           "userID",
					Action:           hub.AddOrganizationRepository,
				}).Return(nil)
				l := &HelmIndexLoaderMock{}
				l.On("LoadIndex", tc.r).Return(nil, "", nil)
				m := NewManager(cfg, db, az, nil, WithHelmIndexLoader(l))

				err := m.Add(ctx, "orgName", tc.r)
				assert.Equal(t, tc.expectedError, err)
				db.AssertExpectations(t)
				az.AssertExpectations(t)
				l.AssertExpectations(t)
			})
		}
	})

	t.Run("add repository succeeded", func(t *testing.T) {
		testCases := []struct {
			r *hub.Repository
		}{
			{
				&hub.Repository{
					Name:        "repo1",
					DisplayName: "Repository 1",
					URL:         "https://repo1.com",
					Kind:        hub.Helm,
				},
			},
			{
				&hub.Repository{
					Name:        "repo2",
					DisplayName: "Repository 2",
					URL:         "https://github.com/org2/repo2/path",
					Kind:        hub.OLM,
				},
			},
		}
		for i, tc := range testCases {
			t.Run(strconv.Itoa(i), func(t *testing.T) {
				t.Parallel()
				db := &tests.DBMock{}
				db.On("Exec", ctx, addRepoDBQ, "userID", "orgName", mock.Anything).Return(nil)
				az := &authz.AuthorizerMock{}
				az.On("Authorize", ctx, &hub.AuthorizeInput{
					OrganizationName: "orgName",
					UserID:           "userID",
					Action:           hub.AddOrganizationRepository,
				}).Return(nil)
				l := &HelmIndexLoaderMock{}
				if tc.r.Kind == hub.Helm {
					l.On("LoadIndex", tc.r).Return(nil, "", nil)
				}
				m := NewManager(cfg, db, az, nil, WithHelmIndexLoader(l))

				err := m.Add(ctx, "orgName", tc.r)
				assert.NoError(t, err)
				db.AssertExpectations(t)
				az.AssertExpectations(t)
				l.AssertExpectations(t)
			})
		}
	})
}

func TestCheckAvailability(t *testing.T) {
	ctx := context.Background()

	t.Run("invalid input", func(t *testing.T) {
		testCases := []struct {
			errMsg       string
			resourceKind string
			value        string
		}{
			{
				"invalid resource kind",
				"invalid",
				"value",
			},
			{
				"invalid value",
				"repositoryName",
				"",
			},
		}
		for _, tc := range testCases {
			t.Run(tc.errMsg, func(t *testing.T) {
				t.Parallel()
				m := NewManager(cfg, nil, nil, nil)
				_, err := m.CheckAvailability(context.Background(), tc.resourceKind, tc.value)
				assert.True(t, errors.Is(err, hub.ErrInvalidInput))
				assert.Contains(t, err.Error(), tc.errMsg)
			})
		}
	})

	t.Run("database query succeeded", func(t *testing.T) {
		testCases := []struct {
			resourceKind string
			dbQuery      string
			available    bool
		}{
			{
				"repositoryName",
				checkRepoNameAvailDBQ,
				true,
			},
			{
				"repositoryURL",
				checkRepoURLAvailDBQ,
				false,
			},
		}
		for _, tc := range testCases {
			t.Run(fmt.Sprintf("resource kind: %s", tc.resourceKind), func(t *testing.T) {
				t.Parallel()
				tc.dbQuery = fmt.Sprintf("select not exists (%s)", tc.dbQuery)
				db := &tests.DBMock{}
				db.On("QueryRow", ctx, tc.dbQuery, "value").Return(tc.available, nil)
				m := NewManager(cfg, db, nil, nil)

				available, err := m.CheckAvailability(ctx, tc.resourceKind, "value/")
				assert.NoError(t, err)
				assert.Equal(t, tc.available, available)
				db.AssertExpectations(t)
			})
		}
	})

	t.Run("database error", func(t *testing.T) {
		t.Parallel()
		db := &tests.DBMock{}
		dbQuery := fmt.Sprintf(`select not exists (%s)`, checkRepoNameAvailDBQ)
		db.On("QueryRow", ctx, dbQuery, "value").Return(false, tests.ErrFakeDB)
		m := NewManager(cfg, db, nil, nil)

		available, err := m.CheckAvailability(ctx, "repositoryName", "value")
		assert.Equal(t, tests.ErrFakeDB, err)
		assert.False(t, available)
		db.AssertExpectations(t)
	})
}

func TestClaimOwnership(t *testing.T) {
	userID := "userID"
	userIDP := &userID
	org := "org1"
	orgP := &org
	helmRepoJSON := []byte(`{"kind": 0, "url": "http://repo.url"}`)
	opaRepoJSON := []byte(`{"kind": 2, "url": "http://repo.url"}`)
	olmRepoJSON := []byte(`{"kind": 3, "url": "oci://repo.url"}`)
	ctx := context.WithValue(context.Background(), hub.UserIDKey, userID)
	mdYmlReq, _ := httpw.NewRequest("GET", "http://repo.url/artifacthub-repo.yml", nil)
	mdYamlReq, _ := httpw.NewRequest("GET", "http://repo.url/artifacthub-repo.yaml", nil)

	t.Run("user id not found in ctx", func(t *testing.T) {
		t.Parallel()
		m := NewManager(cfg, nil, nil, nil)
		assert.Panics(t, func() {
			_ = m.ClaimOwnership(context.Background(), "repo1", "")
		})
	})

	t.Run("invalid input", func(t *testing.T) {
		testCases := []struct {
			errMsg   string
			repoName string
		}{
			{
				"repository name not provided",
				"",
			},
		}
		for _, tc := range testCases {
			t.Run(tc.errMsg, func(t *testing.T) {
				t.Parallel()
				m := NewManager(cfg, nil, nil, nil)

				err := m.ClaimOwnership(ctx, tc.repoName, "")
				assert.True(t, errors.Is(err, hub.ErrInvalidInput))
			})
		}
	})

	t.Run("ownership claim failed: database error getting repository", func(t *testing.T) {
		t.Parallel()
		db := &tests.DBMock{}
		db.On("QueryRow", ctx, getRepoByNameDBQ, "repo1", true).Return(nil, tests.ErrFakeDB)
		m := NewManager(cfg, db, nil, nil)

		err := m.ClaimOwnership(ctx, "repo1", org)
		assert.Equal(t, tests.ErrFakeDB, err)
		db.AssertExpectations(t)
	})

	t.Run("ownership claim failed: error getting repository metadata", func(t *testing.T) {
		t.Parallel()
		db := &tests.DBMock{}
		db.On("QueryRow", ctx, getRepoByNameDBQ, "repo1", true).Return(helmRepoJSON, nil)
		hc := &tests.HTTPClientMock{}
		hc.On("Do", mdYmlReq).Return(&http.Response{
			Body:       io.NopCloser(strings.NewReader("")),
			StatusCode: http.StatusNotFound,
		}, nil)
		hc.On("Do", mdYamlReq).Return(&http.Response{
			Body:       io.NopCloser(strings.NewReader("")),
			StatusCode: http.StatusNotFound,
		}, nil)
		m := NewManager(cfg, db, nil, hc)

		err := m.ClaimOwnership(ctx, "repo1", org)
		assert.Error(t, err)
		db.AssertExpectations(t)
		hc.AssertExpectations(t)
	})

	t.Run("ownership claim failed: database error getting user email", func(t *testing.T) {
		t.Parallel()
		db := &tests.DBMock{}
		db.On("QueryRow", ctx, getRepoByNameDBQ, "repo1", true).Return(helmRepoJSON, nil)
		db.On("QueryRow", ctx, getUserEmailDBQ, userID).Return("", tests.ErrFakeDB)
		mdFile, _ := os.Open("testdata/artifacthub-repo.yml")
		hc := &tests.HTTPClientMock{}
		hc.On("Do", mdYmlReq).Return(&http.Response{
			Body:       mdFile,
			StatusCode: http.StatusOK,
		}, nil)
		m := NewManager(cfg, db, nil, hc)

		err := m.ClaimOwnership(ctx, "repo1", org)
		assert.Equal(t, tests.ErrFakeDB, err)
		db.AssertExpectations(t)
		hc.AssertExpectations(t)
	})

	t.Run("ownership claim failed: user not in repository owners list", func(t *testing.T) {
		t.Parallel()
		db := &tests.DBMock{}
		db.On("QueryRow", ctx, getRepoByNameDBQ, "repo1", true).Return(helmRepoJSON, nil)
		db.On("QueryRow", ctx, getUserEmailDBQ, userID).Return("user1@email.com", nil)
		mdFile, _ := os.Open("testdata/artifacthub-repo.yml")
		hc := &tests.HTTPClientMock{}
		hc.On("Do", mdYmlReq).Return(&http.Response{
			Body:       mdFile,
			StatusCode: http.StatusOK,
		}, nil)
		m := NewManager(cfg, db, nil, hc)

		err := m.ClaimOwnership(ctx, "repo1", org)
		assert.Equal(t, hub.ErrInsufficientPrivilege, err)
		db.AssertExpectations(t)
		hc.AssertExpectations(t)
	})

	t.Run("ownership claim failed: olm oci repo", func(t *testing.T) {
		t.Parallel()
		db := &tests.DBMock{}
		db.On("QueryRow", ctx, getRepoByNameDBQ, "repo1", true).Return(olmRepoJSON, nil)
		m := NewManager(cfg, db, nil, nil)

		err := m.ClaimOwnership(ctx, "repo1", org)
		assert.True(t, errors.Is(err, hub.ErrInvalidInput))
		db.AssertExpectations(t)
	})

	t.Run("ownership claim succeeded (helm)", func(t *testing.T) {
		t.Parallel()
		db := &tests.DBMock{}
		db.On("QueryRow", ctx, getRepoByNameDBQ, "repo1", true).Return(helmRepoJSON, nil)
		db.On("QueryRow", ctx, getUserEmailDBQ, userID).Return("owner1@email.com", nil)
		db.On("Exec", ctx, transferRepoDBQ, "repo1", userIDP, orgP, true).Return(nil)
		mdFile, _ := os.Open("testdata/artifacthub-repo.yml")
		hc := &tests.HTTPClientMock{}
		hc.On("Do", mdYmlReq).Return(&http.Response{
			Body:       mdFile,
			StatusCode: http.StatusOK,
		}, nil)
		m := NewManager(cfg, db, nil, hc)

		err := m.ClaimOwnership(ctx, "repo1", org)
		assert.Nil(t, err)
		db.AssertExpectations(t)
		hc.AssertExpectations(t)
	})

	t.Run("ownership claim failed (opa): error cloning repository", func(t *testing.T) {
		t.Parallel()
		db := &tests.DBMock{}
		db.On("QueryRow", ctx, getRepoByNameDBQ, "repo1", true).Return(opaRepoJSON, nil)
		rc := &ClonerMock{}
		var r *hub.Repository
		_ = json.Unmarshal(opaRepoJSON, &r)
		rc.On("CloneRepository", ctx, r).Return("", "", tests.ErrFake)
		m := NewManager(cfg, db, nil, nil, withRepositoryCloner(rc))

		err := m.ClaimOwnership(ctx, "repo1", org)
		assert.Equal(t, tests.ErrFake, err)
		db.AssertExpectations(t)
	})

	t.Run("ownership claim succeeded (opa)", func(t *testing.T) {
		t.Parallel()
		db := &tests.DBMock{}
		db.On("QueryRow", ctx, getRepoByNameDBQ, "repo1", true).Return(opaRepoJSON, nil)
		db.On("QueryRow", ctx, getUserEmailDBQ, userID).Return("owner1@email.com", nil)
		db.On("Exec", ctx, transferRepoDBQ, "repo1", userIDP, orgP, true).Return(nil)
		rc := &ClonerMock{}
		var r *hub.Repository
		_ = json.Unmarshal(opaRepoJSON, &r)
		rc.On("CloneRepository", ctx, r).Return(".", "testdata", nil)
		m := NewManager(cfg, db, nil, nil, withRepositoryCloner(rc))

		err := m.ClaimOwnership(ctx, "repo1", org)
		assert.Nil(t, err)
		db.AssertExpectations(t)
		rc.AssertExpectations(t)
	})
}

func TestDelete(t *testing.T) {
	ctx := context.WithValue(context.Background(), hub.UserIDKey, "userID")

	t.Run("user id not found in ctx", func(t *testing.T) {
		t.Parallel()
		m := NewManager(cfg, nil, nil, nil)
		assert.Panics(t, func() {
			_ = m.Delete(context.Background(), "repo1")
		})
	})

	t.Run("invalid input", func(t *testing.T) {
		t.Parallel()
		m := NewManager(cfg, nil, nil, nil)
		err := m.Delete(ctx, "")
		assert.True(t, errors.Is(err, hub.ErrInvalidInput))
	})

	t.Run("authorization failed", func(t *testing.T) {
		t.Parallel()
		db := &tests.DBMock{}
		db.On("QueryRow", ctx, getRepoByNameDBQ, "repo1", false).Return([]byte(`
		{
			"repository_id": "00000000-0000-0000-0000-000000000001",
			"name": "repo1",
			"organization_name": "orgName"
		}
		`), nil)
		az := &authz.AuthorizerMock{}
		az.On("Authorize", ctx, &hub.AuthorizeInput{
			OrganizationName: "orgName",
			UserID:           "userID",
			Action:           hub.DeleteOrganizationRepository,
		}).Return(tests.ErrFake)
		m := NewManager(cfg, db, az, nil)

		err := m.Delete(ctx, "repo1")
		assert.Equal(t, tests.ErrFake, err)
		db.AssertExpectations(t)
		az.AssertExpectations(t)
	})

	t.Run("database error", func(t *testing.T) {
		testCases := []struct {
			dbErr         error
			expectedError error
		}{
			{
				tests.ErrFakeDB,
				tests.ErrFakeDB,
			},
			{
				util.ErrDBInsufficientPrivilege,
				hub.ErrInsufficientPrivilege,
			},
		}
		for _, tc := range testCases {
			t.Run(tc.dbErr.Error(), func(t *testing.T) {
				t.Parallel()
				db := &tests.DBMock{}
				db.On("QueryRow", ctx, getRepoByNameDBQ, "repo1", false).Return([]byte(`
				{
					"repository_id": "00000000-0000-0000-0000-000000000001",
					"name": "repo1",
					"organization_name": "orgName"
				}
				`), nil)
				db.On("Exec", ctx, deleteRepoDBQ, "userID", "repo1").Return(tc.dbErr)
				az := &authz.AuthorizerMock{}
				az.On("Authorize", ctx, &hub.AuthorizeInput{
					OrganizationName: "orgName",
					UserID:           "userID",
					Action:           hub.DeleteOrganizationRepository,
				}).Return(nil)
				m := NewManager(cfg, db, az, nil)

				err := m.Delete(ctx, "repo1")
				assert.Equal(t, tc.expectedError, err)
				db.AssertExpectations(t)
				az.AssertExpectations(t)
			})
		}
	})

	t.Run("delete repository succeeded", func(t *testing.T) {
		t.Parallel()
		db := &tests.DBMock{}
		db.On("QueryRow", ctx, getRepoByNameDBQ, "repo1", false).Return([]byte(`
		{
			"repository_id": "00000000-0000-0000-0000-000000000001",
			"name": "repo1",
			"user_alias": "user1"
		}
		`), nil)
		db.On("Exec", ctx, deleteRepoDBQ, "userID", "repo1").Return(nil)
		m := NewManager(cfg, db, nil, nil)

		err := m.Delete(ctx, "repo1")
		assert.NoError(t, err)
		db.AssertExpectations(t)
	})
}

func TestGetByID(t *testing.T) {
	ctx := context.Background()

	t.Run("invalid input", func(t *testing.T) {
		testCases := []struct {
			repoID string
			errStr string
		}{
			{
				"",
				"repository id not provided",
			},
			{
				"invalid",
				"invalid repository id",
			},
		}
		for _, tc := range testCases {
			t.Run(tc.errStr, func(t *testing.T) {
				t.Parallel()
				m := NewManager(cfg, nil, nil, nil)

				_, err := m.GetByID(ctx, tc.repoID, false)
				assert.True(t, errors.Is(err, hub.ErrInvalidInput))
				assert.Contains(t, err.Error(), tc.errStr)
			})
		}
	})

	t.Run("database error", func(t *testing.T) {
		t.Parallel()
		db := &tests.DBMock{}
		db.On("QueryRow", ctx, getRepoByIDDBQ, repoID, false).Return(nil, tests.ErrFakeDB)
		m := NewManager(cfg, db, nil, nil)

		r, err := m.GetByID(context.Background(), repoID, false)
		assert.Equal(t, tests.ErrFakeDB, err)
		assert.Nil(t, r)
		db.AssertExpectations(t)
	})

	t.Run("database query succeeded", func(t *testing.T) {
		t.Parallel()
		db := &tests.DBMock{}
		db.On("QueryRow", ctx, getRepoByIDDBQ, repoID, false).Return([]byte(`
		{
			"repository_id": "00000000-0000-0000-0000-000000000001",
			"name": "repo1",
			"display_name": "Repo 1",
			"url": "https://repo1.com",
			"kind": 0,
			"verified_publisher": true,
			"official": true,
			"cncf": true
		}
		`), nil)
		m := NewManager(cfg, db, nil, nil)

		r, err := m.GetByID(context.Background(), repoID, false)
		require.NoError(t, err)
		assert.Equal(t, repoID, r.RepositoryID)
		assert.Equal(t, "repo1", r.Name)
		assert.Equal(t, "Repo 1", r.DisplayName)
		assert.Equal(t, "https://repo1.com", r.URL)
		assert.Equal(t, hub.Helm, r.Kind)
		assert.True(t, r.VerifiedPublisher)
		assert.True(t, r.Official)
		assert.True(t, r.CNCF)
		db.AssertExpectations(t)
	})
}

func TestGetByName(t *testing.T) {
	ctx := context.Background()

	t.Run("invalid input", func(t *testing.T) {
		t.Parallel()
		m := NewManager(cfg, nil, nil, nil)
		_, err := m.GetByName(ctx, "", false)
		assert.True(t, errors.Is(err, hub.ErrInvalidInput))
	})

	t.Run("get existing repository by name", func(t *testing.T) {
		t.Parallel()
		db := &tests.DBMock{}
		db.On("QueryRow", ctx, getRepoByNameDBQ, "repo1", false).Return([]byte(`
		{
			"repository_id": "00000000-0000-0000-0000-000000000001",
			"name": "repo1",
			"display_name": "Repo 1",
			"url": "https://repo1.com",
			"kind": 0,
			"verified_publisher": true,
			"official": true,
			"cncf": true,
			"user_alias": "user",
			"organization_name": ""
		}
		`), nil)
		m := NewManager(cfg, db, nil, nil)

		r, err := m.GetByName(context.Background(), "repo1", false)
		require.NoError(t, err)
		assert.Equal(t, "00000000-0000-0000-0000-000000000001", r.RepositoryID)
		assert.Equal(t, "repo1", r.Name)
		assert.Equal(t, "Repo 1", r.DisplayName)
		assert.Equal(t, "https://repo1.com", r.URL)
		assert.Equal(t, hub.Helm, r.Kind)
		assert.True(t, r.VerifiedPublisher)
		assert.True(t, r.Official)
		assert.True(t, r.CNCF)
		db.AssertExpectations(t)
	})

	t.Run("database error", func(t *testing.T) {
		t.Parallel()
		db := &tests.DBMock{}
		db.On("QueryRow", ctx, getRepoByNameDBQ, "repo1", false).Return(nil, tests.ErrFakeDB)
		m := NewManager(cfg, db, nil, nil)

		r, err := m.GetByName(context.Background(), "repo1", false)
		assert.Equal(t, tests.ErrFakeDB, err)
		assert.Nil(t, r)
		db.AssertExpectations(t)
	})

	t.Run("invalid json data returned from database", func(t *testing.T) {
		t.Parallel()
		db := &tests.DBMock{}
		db.On("QueryRow", ctx, getRepoByNameDBQ, "repo1", false).Return([]byte("invalid json"), nil)
		m := NewManager(cfg, db, nil, nil)

		r, err := m.GetByName(context.Background(), "repo1", false)
		assert.Error(t, err)
		assert.Nil(t, r)
		db.AssertExpectations(t)
	})
}

func TestGetMetadata(t *testing.T) {
	repoURL := "http://url.test"
	ymlReq, _ := httpw.NewRequest("GET", "http://url.test/artifacthub-repo.yml", nil)
	yamlReq, _ := httpw.NewRequest("GET", "http://url.test/artifacthub-repo.yaml", nil)
	ociRepoURL := "oci://registry/namespace/repo"
	ociRef := fmt.Sprintf("%s:%s", strings.TrimPrefix(ociRepoURL, hub.RepositoryOCIPrefix), artifacthubTag)
	expectedMetadata := &hub.RepositoryMetadata{
		RepositoryID: "00000000-0000-0000-0000-000000000001",
		Owners: []*hub.Owner{
			{
				Name:  "owner1",
				Email: "owner1@email.com",
			},
		},
	}
	expectedMetadataYaml, _ := yaml.Marshal(expectedMetadata)

	t.Run("local file: metadata file not found", func(t *testing.T) {
		t.Parallel()
		m := NewManager(cfg, nil, nil, nil)

		r := &hub.Repository{
			Kind: hub.OPA,
		}
		_, err := m.GetMetadata(r, "testdata/not-exists")
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "metadata not found")
	})

	t.Run("local file: error unmarshaling repository metadata file", func(t *testing.T) {
		t.Parallel()
		m := NewManager(cfg, nil, nil, nil)

		r := &hub.Repository{
			Kind: hub.OPA,
		}
		_, err := m.GetMetadata(r, "testdata/invalid")
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "error unmarshaling repository metadata file")
	})

	t.Run("local file: invalid repository id", func(t *testing.T) {
		t.Parallel()
		m := NewManager(cfg, nil, nil, nil)

		r := &hub.Repository{
			Kind: hub.OPA,
		}
		_, err := m.GetMetadata(r, "testdata/invalid-repo-id")
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "invalid repository id")
	})

	t.Run("local file: success fetching .yml", func(t *testing.T) {
		t.Parallel()
		m := NewManager(cfg, nil, nil, nil)

		r := &hub.Repository{
			Kind: hub.OPA,
		}
		md, err := m.GetMetadata(r, "testdata/valid-yml")
		assert.Equal(t, expectedMetadata, md)
		assert.NoError(t, err)
	})

	t.Run("local file: success .yaml", func(t *testing.T) {
		t.Parallel()
		m := NewManager(cfg, nil, nil, nil)

		r := &hub.Repository{
			Kind: hub.OPA,
		}
		md, err := m.GetMetadata(r, "testdata/valid-yaml")
		assert.Equal(t, expectedMetadata, md)
		assert.NoError(t, err)
	})

	t.Run("remote file (http): error downloading repository metadata file", func(t *testing.T) {
		t.Parallel()
		hc := &tests.HTTPClientMock{}
		hc.On("Do", ymlReq).Return(nil, tests.ErrFake)
		hc.On("Do", yamlReq).Return(nil, tests.ErrFake)
		m := NewManager(cfg, nil, nil, hc)

		r := &hub.Repository{
			Kind: hub.Helm,
			URL:  repoURL,
		}
		_, err := m.GetMetadata(r, "")
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "error downloading repository metadata file")
		hc.AssertExpectations(t)
	})

	t.Run("remote file (http): metadata file not found", func(t *testing.T) {
		t.Parallel()
		hc := &tests.HTTPClientMock{}
		hc.On("Do", ymlReq).Return(&http.Response{
			Body:       io.NopCloser(strings.NewReader("")),
			StatusCode: http.StatusNotFound,
		}, nil)
		hc.On("Do", yamlReq).Return(&http.Response{
			Body:       io.NopCloser(strings.NewReader("")),
			StatusCode: http.StatusNotFound,
		}, nil)
		m := NewManager(cfg, nil, nil, hc)

		r := &hub.Repository{
			Kind: hub.Helm,
			URL:  repoURL,
		}
		_, err := m.GetMetadata(r, "")
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "metadata not found")
		hc.AssertExpectations(t)
	})

	t.Run("remote file (http): unexpected status code received", func(t *testing.T) {
		t.Parallel()
		hc := &tests.HTTPClientMock{}
		hc.On("Do", ymlReq).Return(&http.Response{
			Body:       io.NopCloser(strings.NewReader("")),
			StatusCode: http.StatusForbidden,
		}, nil)
		hc.On("Do", yamlReq).Return(&http.Response{
			Body:       io.NopCloser(strings.NewReader("")),
			StatusCode: http.StatusForbidden,
		}, nil)
		m := NewManager(cfg, nil, nil, hc)

		r := &hub.Repository{
			Kind: hub.Helm,
			URL:  repoURL,
		}
		_, err := m.GetMetadata(r, "")
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "unexpected status code received")
		hc.AssertExpectations(t)
	})

	t.Run("remote file (http): error reading repository metadata file", func(t *testing.T) {
		t.Parallel()
		hc := &tests.HTTPClientMock{}
		hc.On("Do", ymlReq).Return(&http.Response{
			Body:       io.NopCloser(tests.ErrReader(0)),
			StatusCode: http.StatusOK,
		}, nil)
		hc.On("Do", yamlReq).Return(&http.Response{
			Body:       io.NopCloser(tests.ErrReader(0)),
			StatusCode: http.StatusOK,
		}, nil)
		m := NewManager(cfg, nil, nil, hc)

		r := &hub.Repository{
			Kind: hub.Helm,
			URL:  repoURL,
		}
		_, err := m.GetMetadata(r, "")
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "error reading repository metadata file")
		hc.AssertExpectations(t)
	})

	t.Run("remote file (http): success", func(t *testing.T) {
		t.Parallel()
		hc := &tests.HTTPClientMock{}
		hc.On("Do", ymlReq).Return(&http.Response{
			Body:       io.NopCloser(bytes.NewReader(expectedMetadataYaml)),
			StatusCode: http.StatusOK,
		}, nil)
		m := NewManager(cfg, nil, nil, hc)

		r := &hub.Repository{
			Kind: hub.Helm,
			URL:  repoURL,
		}
		md, err := m.GetMetadata(r, "")
		assert.Equal(t, expectedMetadata, md)
		assert.NoError(t, err)
		hc.AssertExpectations(t)
	})

	t.Run("remote file (http - with creds): success", func(t *testing.T) {
		t.Parallel()
		hc := &tests.HTTPClientMock{}
		ymlReqWithCreds := ymlReq.Clone(ymlReq.Context())
		ymlReqWithCreds.SetBasicAuth("user", "pass")
		hc.On("Do", ymlReqWithCreds).Return(&http.Response{
			Body:       io.NopCloser(bytes.NewReader(expectedMetadataYaml)),
			StatusCode: http.StatusOK,
		}, nil)
		m := NewManager(cfg, nil, nil, hc)

		r := &hub.Repository{
			Kind:     hub.Helm,
			URL:      repoURL,
			AuthUser: "user",
			AuthPass: "pass",
		}
		md, err := m.GetMetadata(r, "")
		assert.Equal(t, expectedMetadata, md)
		assert.NoError(t, err)
		hc.AssertExpectations(t)
	})

	t.Run("remote file (http): success on yaml", func(t *testing.T) {
		t.Parallel()
		hc := &tests.HTTPClientMock{}
		hc.On("Do", ymlReq).Return(&http.Response{
			Body:       io.NopCloser(strings.NewReader("")),
			StatusCode: http.StatusNotFound,
		}, nil)
		hc.On("Do", yamlReq).Return(&http.Response{
			Body:       io.NopCloser(bytes.NewReader(expectedMetadataYaml)),
			StatusCode: http.StatusOK,
		}, nil)
		m := NewManager(cfg, nil, nil, hc)

		r := &hub.Repository{
			Kind: hub.Helm,
			URL:  repoURL,
		}
		md, err := m.GetMetadata(r, "")
		assert.Equal(t, expectedMetadata, md)
		assert.NoError(t, err)
		hc.AssertExpectations(t)
	})

	t.Run("remote file (oci): error pulling metadata layer", func(t *testing.T) {
		t.Parallel()
		op := &oci.PullerMock{}
		op.On("PullLayer", mock.Anything, ociRef, MetadataLayerMediaType, "", "").
			Return(ocispec.Descriptor{}, nil, tests.ErrFake)
		m := NewManager(cfg, nil, nil, nil, WithOCIPuller(op))

		r := &hub.Repository{
			Kind: hub.Helm,
			URL:  ociRepoURL,
		}
		md, err := m.GetMetadata(r, "")
		assert.Equal(t, tests.ErrFake, err)
		assert.Nil(t, md)
		op.AssertExpectations(t)
	})

	t.Run("remote file (oci): metadata layer not found", func(t *testing.T) {
		t.Parallel()
		op := &oci.PullerMock{}
		op.On("PullLayer", mock.Anything, ociRef, MetadataLayerMediaType, "", "").
			Return(ocispec.Descriptor{}, nil, oci.ErrLayerNotFound)
		m := NewManager(cfg, nil, nil, nil, WithOCIPuller(op))

		r := &hub.Repository{
			Kind: hub.Helm,
			URL:  ociRepoURL,
		}
		md, err := m.GetMetadata(r, "")
		assert.Equal(t, ErrMetadataNotFound, err)
		assert.Nil(t, md)
		op.AssertExpectations(t)
	})

	t.Run("remote file (oci): success", func(t *testing.T) {
		t.Parallel()
		op := &oci.PullerMock{}
		op.On("PullLayer", mock.Anything, ociRef, MetadataLayerMediaType, "", "").
			Return(ocispec.Descriptor{}, expectedMetadataYaml, nil)
		m := NewManager(cfg, nil, nil, nil, WithOCIPuller(op))

		r := &hub.Repository{
			Kind: hub.Helm,
			URL:  ociRepoURL,
		}
		md, err := m.GetMetadata(r, "")
		assert.Equal(t, expectedMetadata, md)
		assert.NoError(t, err)
		op.AssertExpectations(t)
	})

	t.Run("remote file (oci - with creds): success", func(t *testing.T) {
		t.Parallel()
		op := &oci.PullerMock{}
		op.On("PullLayer", mock.Anything, ociRef, MetadataLayerMediaType, "user", "pass").
			Return(ocispec.Descriptor{}, expectedMetadataYaml, nil)
		m := NewManager(cfg, nil, nil, nil, WithOCIPuller(op))

		r := &hub.Repository{
			Kind:     hub.Helm,
			URL:      ociRepoURL,
			AuthUser: "user",
			AuthPass: "pass",
		}
		md, err := m.GetMetadata(r, "")
		assert.Equal(t, expectedMetadata, md)
		assert.NoError(t, err)
		op.AssertExpectations(t)
	})
}

func TestGetPackagesDigest(t *testing.T) {
	ctx := context.Background()

	t.Run("invalid input", func(t *testing.T) {
		t.Parallel()
		m := NewManager(cfg, nil, nil, nil)
		_, err := m.GetPackagesDigest(context.Background(), "invalid")
		assert.True(t, errors.Is(err, hub.ErrInvalidInput))
	})

	t.Run("database query succeeded", func(t *testing.T) {
		t.Parallel()
		db := &tests.DBMock{}
		db.On("QueryRow", ctx, getRepoPkgsDigestDBQ, "00000000-0000-0000-0000-000000000001").Return([]byte(`
		{
			"package1@1.0.0": "digest-package1-1.0.0",
			"package1@0.0.9": "digest-package1-0.0.9",
			"package2@1.0.0": "digest-package2-1.0.0",
			"package2@0.0.9": "digest-package2-0.0.9"
		}
		`), nil)
		m := NewManager(cfg, db, nil, nil)

		pd, err := m.GetPackagesDigest(ctx, "00000000-0000-0000-0000-000000000001")
		require.NoError(t, err)
		assert.Len(t, pd, 4)
		assert.Equal(t, "digest-package1-1.0.0", pd["package1@1.0.0"])
		assert.Equal(t, "digest-package1-0.0.9", pd["package1@0.0.9"])
		assert.Equal(t, "digest-package2-1.0.0", pd["package2@1.0.0"])
		assert.Equal(t, "digest-package2-0.0.9", pd["package2@0.0.9"])
		db.AssertExpectations(t)
	})
}

func TestGetRemoteDigest(t *testing.T) {
	ctx := context.Background()
	helmHTTP := &hub.Repository{
		Kind: hub.Helm,
		Name: "repo1",
		URL:  "https://myrepo.url",
	}
	helmOCI := &hub.Repository{
		Kind: hub.Helm,
		Name: "repo1",
		URL:  "oci://myrepo.url/chart",
	}

	t.Run("helm-http: error loading index", func(t *testing.T) {
		t.Parallel()
		l := &HelmIndexLoaderMock{}
		l.On("LoadIndex", helmHTTP).Return(nil, "", tests.ErrFake)
		m := NewManager(cfg, nil, nil, nil, WithHelmIndexLoader(l))

		digest, err := m.GetRemoteDigest(ctx, helmHTTP)
		assert.Empty(t, digest)
		assert.Equal(t, tests.ErrFake, err)
		l.AssertExpectations(t)
	})

	t.Run("helm-http: success", func(t *testing.T) {
		t.Parallel()
		l := &HelmIndexLoaderMock{}
		l.On("LoadIndex", helmHTTP).Return(nil, "digest", nil)
		m := NewManager(cfg, nil, nil, nil, WithHelmIndexLoader(l))

		digest, err := m.GetRemoteDigest(ctx, helmHTTP)
		assert.Equal(t, "digest", digest)
		assert.Nil(t, err)
		l.AssertExpectations(t)
	})

	t.Run("helm-oci: error getting tags", func(t *testing.T) {
		t.Parallel()
		tg := &oci.TagsGetterMock{}
		tg.On("Tags", ctx, helmOCI, true).Return(nil, tests.ErrFake)
		m := NewManager(cfg, nil, nil, nil, WithOCITagsGetter(tg))

		digest, err := m.GetRemoteDigest(ctx, helmOCI)
		assert.Empty(t, digest)
		assert.Equal(t, tests.ErrFake, err)
		tg.AssertExpectations(t)
	})

	t.Run("helm-oci: success", func(t *testing.T) {
		t.Parallel()
		tg := &oci.TagsGetterMock{}
		tg.On("Tags", ctx, helmOCI, true).Return([]string{"2.0.0", "1.0.0"}, nil)
		m := NewManager(cfg, nil, nil, nil, WithOCITagsGetter(tg))

		digest, err := m.GetRemoteDigest(ctx, helmOCI)
		assert.Equal(t, "32b4478532e3fbd46940cfaa0b288bc328817cec9ef38e81c9d19a803bcff285", digest)
		assert.Nil(t, err)
		tg.AssertExpectations(t)
	})
}

func TestSearch(t *testing.T) {
	t.Parallel()
	ctx := context.Background()
	input := &hub.SearchRepositoryInput{}

	t.Run("invalid input", func(t *testing.T) {
		testCases := []struct {
			errMsg string
			input  *hub.SearchRepositoryInput
		}{
			{
				"invalid user alias",
				&hub.SearchRepositoryInput{
					Users: []string{""},
				},
			},
			{
				"invalid organization name",
				&hub.SearchRepositoryInput{
					Orgs: []string{""},
				},
			},
		}
		for _, tc := range testCases {
			t.Run(tc.errMsg, func(t *testing.T) {
				t.Parallel()
				m := NewManager(cfg, nil, nil, nil)
				result, err := m.Search(ctx, tc.input)
				assert.True(t, errors.Is(err, hub.ErrInvalidInput))
				assert.Contains(t, err.Error(), tc.errMsg)
				assert.Nil(t, result)
			})
		}
	})

	t.Run("database query succeeded", func(t *testing.T) {
		db := &tests.DBMock{}
		db.On("QueryRow", ctx, searchRepositoriesDBQ, mock.Anything).Return([]interface{}{[]byte(`
	[
		{
			"repository_id": "00000000-0000-0000-0000-000000000001",
			"name": "repo1",
			"display_name": "Repo 1",
			"url": "https://repo1.com",
			"kind": 0,
			"verified_publisher": true,
			"official": true
    	},
		{
			"repository_id": "00000000-0000-0000-0000-000000000002",
			"name": "repo2",
			"display_name": "Repo 2",
			"url": "https://repo2.com",
			"kind": 0,
			"verified_publisher": true,
			"official": true
    	},
		{
			"repository_id": "00000000-0000-0000-0000-000000000003",
			"name": "repo3",
			"display_name": "Repo 3",
			"url": "https://repo3.com",
			"kind": 1,
			"verified_publisher": true,
			"official": true
    	}
	]
	`), 3}, nil)
		m := NewManager(cfg, db, nil, nil)

		result, err := m.Search(ctx, input)
		r := result.Repositories

		require.NoError(t, err)
		assert.Equal(t, 3, result.TotalCount)
		assert.Len(t, r, 3)
		assert.Equal(t, "00000000-0000-0000-0000-000000000001", r[0].RepositoryID)
		assert.Equal(t, "repo1", r[0].Name)
		assert.Equal(t, "Repo 1", r[0].DisplayName)
		assert.Equal(t, "https://repo1.com", r[0].URL)
		assert.Equal(t, hub.Helm, r[0].Kind)
		assert.True(t, r[0].VerifiedPublisher)
		assert.True(t, r[0].Official)
		assert.Equal(t, "00000000-0000-0000-0000-000000000002", r[1].RepositoryID)
		assert.Equal(t, "repo2", r[1].Name)
		assert.Equal(t, "Repo 2", r[1].DisplayName)
		assert.Equal(t, "https://repo2.com", r[1].URL)
		assert.Equal(t, hub.Helm, r[1].Kind)
		assert.True(t, r[1].VerifiedPublisher)
		assert.True(t, r[1].Official)
		assert.Equal(t, "00000000-0000-0000-0000-000000000003", r[2].RepositoryID)
		assert.Equal(t, "repo3", r[2].Name)
		assert.Equal(t, "Repo 3", r[2].DisplayName)
		assert.Equal(t, "https://repo3.com", r[2].URL)
		assert.Equal(t, hub.Falco, r[2].Kind)
		assert.True(t, r[2].VerifiedPublisher)
		assert.True(t, r[2].Official)
		db.AssertExpectations(t)
	})

	t.Run("database error", func(t *testing.T) {
		t.Parallel()
		db := &tests.DBMock{}
		db.On("QueryRow", ctx, searchRepositoriesDBQ, mock.Anything).Return(nil, tests.ErrFakeDB)
		m := NewManager(cfg, db, nil, nil)

		result, err := m.Search(ctx, input)
		assert.Equal(t, tests.ErrFakeDB, err)
		assert.Nil(t, result)
		db.AssertExpectations(t)
	})
}

func TestSearchJSON(t *testing.T) {
	ctx := context.Background()
	input := &hub.SearchRepositoryInput{}

	t.Run("invalid input", func(t *testing.T) {
		testCases := []struct {
			errMsg string
			input  *hub.SearchRepositoryInput
		}{
			{
				"invalid user alias",
				&hub.SearchRepositoryInput{
					Users: []string{""},
				},
			},
			{
				"invalid organization name",
				&hub.SearchRepositoryInput{
					Orgs: []string{""},
				},
			},
		}
		for _, tc := range testCases {
			t.Run(tc.errMsg, func(t *testing.T) {
				t.Parallel()
				m := NewManager(cfg, nil, nil, nil)
				result, err := m.SearchJSON(ctx, tc.input)
				assert.True(t, errors.Is(err, hub.ErrInvalidInput))
				assert.Contains(t, err.Error(), tc.errMsg)
				assert.Nil(t, result)
			})
		}
	})

	t.Run("database query succeeded", func(t *testing.T) {
		t.Parallel()
		db := &tests.DBMock{}
		db.On("QueryRow", ctx, searchRepositoriesDBQ, mock.Anything).
			Return([]interface{}{[]byte("dataJSON"), 1}, nil)
		m := NewManager(cfg, db, nil, nil)

		result, err := m.SearchJSON(ctx, input)
		assert.NoError(t, err)
		assert.Equal(t, []byte("dataJSON"), result.Data)
		assert.Equal(t, 1, result.TotalCount)
		db.AssertExpectations(t)
	})

	t.Run("database error", func(t *testing.T) {
		t.Parallel()
		db := &tests.DBMock{}
		db.On("QueryRow", ctx, searchRepositoriesDBQ, mock.Anything).Return(nil, tests.ErrFakeDB)
		m := NewManager(cfg, db, nil, nil)

		result, err := m.SearchJSON(ctx, input)
		assert.Equal(t, tests.ErrFakeDB, err)
		assert.Nil(t, result)
		db.AssertExpectations(t)
	})
}

func TestSetLastScanningResults(t *testing.T) {
	ctx := context.Background()

	t.Run("invalid input", func(t *testing.T) {
		t.Parallel()
		m := NewManager(cfg, nil, nil, nil)
		err := m.SetLastScanningResults(ctx, "invalid", "errors")
		assert.True(t, errors.Is(err, hub.ErrInvalidInput))
	})

	t.Run("database update succeeded", func(t *testing.T) {
		t.Parallel()
		db := &tests.DBMock{}
		db.On("Exec", ctx, setLastScanningResultsDBQ, repoID, "errors", false).Return(nil)
		m := NewManager(cfg, db, nil, nil)

		err := m.SetLastScanningResults(ctx, repoID, "errors")
		assert.NoError(t, err)
		db.AssertExpectations(t)
	})

	t.Run("database error", func(t *testing.T) {
		t.Parallel()
		db := &tests.DBMock{}
		db.On("Exec", ctx, setLastScanningResultsDBQ, repoID, "errors", false).Return(tests.ErrFakeDB)
		m := NewManager(cfg, db, nil, nil)

		err := m.SetLastScanningResults(ctx, repoID, "errors")
		assert.Equal(t, tests.ErrFakeDB, err)
		db.AssertExpectations(t)
	})
}

func TestSetLastTrackingResults(t *testing.T) {
	ctx := context.Background()

	t.Run("invalid input", func(t *testing.T) {
		t.Parallel()
		m := NewManager(cfg, nil, nil, nil)
		err := m.SetLastTrackingResults(ctx, "invalid", "errors")
		assert.True(t, errors.Is(err, hub.ErrInvalidInput))
	})

	t.Run("database update succeeded", func(t *testing.T) {
		t.Parallel()
		db := &tests.DBMock{}
		db.On("Exec", ctx, setLastTrackingResultsDBQ, repoID, "errors", false).Return(nil)
		m := NewManager(cfg, db, nil, nil)

		err := m.SetLastTrackingResults(ctx, repoID, "errors")
		assert.NoError(t, err)
		db.AssertExpectations(t)
	})

	t.Run("database error", func(t *testing.T) {
		t.Parallel()
		db := &tests.DBMock{}
		db.On("Exec", ctx, setLastTrackingResultsDBQ, repoID, "errors", false).Return(tests.ErrFakeDB)
		m := NewManager(cfg, db, nil, nil)

		err := m.SetLastTrackingResults(ctx, repoID, "errors")
		assert.Equal(t, tests.ErrFakeDB, err)
		db.AssertExpectations(t)
	})
}

func TestSetVerifiedPublisher(t *testing.T) {
	ctx := context.Background()

	t.Run("invalid input", func(t *testing.T) {
		t.Parallel()
		m := NewManager(cfg, nil, nil, nil)
		err := m.SetVerifiedPublisher(ctx, "invalid", true)
		assert.True(t, errors.Is(err, hub.ErrInvalidInput))
	})

	t.Run("database update succeeded", func(t *testing.T) {
		t.Parallel()
		db := &tests.DBMock{}
		db.On("Exec", ctx, setVerifiedPublisherDBQ, repoID, true).Return(nil)
		m := NewManager(cfg, db, nil, nil)

		err := m.SetVerifiedPublisher(ctx, repoID, true)
		assert.NoError(t, err)
		db.AssertExpectations(t)
	})

	t.Run("database error", func(t *testing.T) {
		t.Parallel()
		db := &tests.DBMock{}
		db.On("Exec", ctx, setVerifiedPublisherDBQ, repoID, true).Return(tests.ErrFakeDB)
		m := NewManager(cfg, db, nil, nil)

		err := m.SetVerifiedPublisher(ctx, repoID, true)
		assert.Equal(t, tests.ErrFakeDB, err)
		db.AssertExpectations(t)
	})
}

func TestTransfer(t *testing.T) {
	ctx := context.WithValue(context.Background(), hub.UserIDKey, "userID")
	userID := "userID"
	userIDP := &userID
	org := "org1"
	orgP := &org

	t.Run("user id not found in ctx", func(t *testing.T) {
		t.Parallel()
		m := NewManager(cfg, nil, nil, nil)
		assert.Panics(t, func() {
			_ = m.Transfer(context.Background(), "repo1", "", false)
		})
	})

	t.Run("invalid input", func(t *testing.T) {
		testCases := []struct {
			errMsg   string
			repoName string
		}{
			{
				"repository name not provided",
				"",
			},
		}
		for _, tc := range testCases {
			t.Run(tc.errMsg, func(t *testing.T) {
				t.Parallel()
				m := NewManager(cfg, nil, nil, nil)

				err := m.Transfer(ctx, tc.repoName, "", false)
				assert.True(t, errors.Is(err, hub.ErrInvalidInput))
			})
		}
	})

	t.Run("authorization failed", func(t *testing.T) {
		t.Parallel()
		db := &tests.DBMock{}
		db.On("QueryRow", ctx, getRepoByNameDBQ, "repo1", false).Return([]byte(`
		{
			"repository_id": "00000000-0000-0000-0000-000000000001",
			"name": "repo1",
			"organization_name": "orgName"
		}
		`), nil)
		az := &authz.AuthorizerMock{}
		az.On("Authorize", ctx, &hub.AuthorizeInput{
			OrganizationName: "orgName",
			UserID:           "userID",
			Action:           hub.TransferOrganizationRepository,
		}).Return(tests.ErrFake)
		m := NewManager(cfg, db, az, nil)

		err := m.Transfer(ctx, "repo1", "orgDest", false)
		assert.Equal(t, tests.ErrFake, err)
		db.AssertExpectations(t)
		az.AssertExpectations(t)
	})

	t.Run("database error", func(t *testing.T) {
		testCases := []struct {
			dbErr         error
			expectedError error
		}{
			{
				tests.ErrFakeDB,
				tests.ErrFakeDB,
			},
			{
				util.ErrDBInsufficientPrivilege,
				hub.ErrInsufficientPrivilege,
			},
		}
		for _, tc := range testCases {
			t.Run(tc.dbErr.Error(), func(t *testing.T) {
				t.Parallel()
				db := &tests.DBMock{}
				db.On("QueryRow", ctx, getRepoByNameDBQ, "repo1", false).Return([]byte(`
				{
					"repository_id": "00000000-0000-0000-0000-000000000001",
					"name": "repo1",
					"organization_name": "orgName"
				}
				`), nil)
				db.On("Exec", ctx, transferRepoDBQ, "repo1", userIDP, orgP, false).Return(tc.dbErr)
				az := &authz.AuthorizerMock{}
				az.On("Authorize", ctx, &hub.AuthorizeInput{
					OrganizationName: "orgName",
					UserID:           "userID",
					Action:           hub.TransferOrganizationRepository,
				}).Return(nil)
				m := NewManager(cfg, db, az, nil)

				err := m.Transfer(ctx, "repo1", org, false)
				assert.Equal(t, tc.expectedError, err)
				db.AssertExpectations(t)
				az.AssertExpectations(t)
			})
		}
	})

	t.Run("transfer repository succeeded", func(t *testing.T) {
		t.Parallel()
		db := &tests.DBMock{}
		db.On("QueryRow", ctx, getRepoByNameDBQ, "repo1", false).Return([]byte(`
		{
			"repository_id": "00000000-0000-0000-0000-000000000001",
			"name": "repo1",
			"user_alias": "user1"
		}
		`), nil)
		db.On("Exec", ctx, transferRepoDBQ, "repo1", userIDP, orgP, false).Return(nil)
		m := NewManager(cfg, db, nil, nil)

		err := m.Transfer(ctx, "repo1", org, false)
		assert.NoError(t, err)
		db.AssertExpectations(t)
	})
}

func TestUpdate(t *testing.T) {
	ctx := context.WithValue(context.Background(), hub.UserIDKey, "userID")

	t.Run("user id not found in ctx", func(t *testing.T) {
		t.Parallel()
		m := NewManager(cfg, nil, nil, nil)
		assert.Panics(t, func() {
			_ = m.Update(context.Background(), nil)
		})
	})

	t.Run("invalid input", func(t *testing.T) {
		testCases := []struct {
			errMsg string
			r      *hub.Repository
			lErr   error
		}{
			{
				"name not provided",
				&hub.Repository{
					Name: "",
				},
				nil,
			},
			{
				"url not provided",
				&hub.Repository{
					Name: "repo1",
					URL:  "",
				},
				nil,
			},
			{
				"missing protocol scheme",
				&hub.Repository{
					Kind: hub.Helm,
					Name: "repo1",
					URL:  ":::///",
				},
				nil,
			},
			{
				"scheme not supported",
				&hub.Repository{
					Kind: hub.Helm,
					Name: "repo1",
					URL:  "other://repo1.com",
				},
				nil,
			},
			{
				"urls with credentials not allowed",
				&hub.Repository{
					Kind: hub.Helm,
					Name: "repo1",
					URL:  "https://user:pass@repo1.com",
				},
				nil,
			},
			{
				"invalid url format",
				&hub.Repository{
					Kind: hub.OLM,
					Name: "repo1",
					URL:  "https://repo1.com",
				},
				nil,
			},
			{
				"invalid url format",
				&hub.Repository{
					Kind: hub.Container,
					Name: "repo1",
					URL:  "https://repo1.url",
				},
				nil,
			},
			{
				"the url provided does not point to a valid Helm repository",
				&hub.Repository{
					Name: "repo1",
					URL:  "https://repo1.com",
					Kind: hub.Helm,
				},
				errors.New("error loading index file"),
			},
			{
				"private repositories not allowed",
				&hub.Repository{
					Kind:     hub.Helm,
					Name:     "repo1",
					URL:      "https://repo1.com",
					AuthUser: "user1",
					AuthPass: "pass1",
				},
				nil,
			},
			{
				"invalid container image data",
				&hub.Repository{
					Kind: hub.Container,
					Name: "repo1",
					URL:  "oci://registry.io/namespace/repo",
					Data: json.RawMessage("{["),
				},
				nil,
			},
			{
				"too many tags",
				&hub.Repository{
					Kind: hub.Container,
					Name: "repo1",
					URL:  "oci://registry.io/namespace/repo",
					Data: json.RawMessage(`{
						"tags": [
							{"name": "tag1"},
							{"name": "tag2"},
							{"name": "tag3"},
							{"name": "tag4"},
							{"name": "tag5"},
							{"name": "tag6"},
							{"name": "tag7"},
							{"name": "tag8"},
							{"name": "tag9"},
							{"name": "tag10"},
							{"name": "tag11"}
						]
					}`),
				},
				nil,
			},
		}
		for _, tc := range testCases {
			t.Run(tc.errMsg, func(t *testing.T) {
				t.Parallel()
				l := &HelmIndexLoaderMock{}
				if tc.lErr != nil {
					l.On("LoadIndex", tc.r).Return(nil, "", tc.lErr)
				} else {
					l.On("LoadIndex", tc.r).Return(nil, "", nil).Maybe()
				}
				m := NewManager(cfg, nil, nil, nil, WithHelmIndexLoader(l))

				err := m.Update(ctx, tc.r)
				assert.True(t, errors.Is(err, hub.ErrInvalidInput))
				assert.Contains(t, err.Error(), tc.errMsg)
				l.AssertExpectations(t)
			})
		}
	})

	t.Run("authorization failed", func(t *testing.T) {
		t.Parallel()
		r := &hub.Repository{
			Name:        "repo1",
			DisplayName: "Repository 1",
			URL:         "https://repo1.com",
			Kind:        hub.Helm,
		}
		db := &tests.DBMock{}
		db.On("QueryRow", ctx, getRepoByNameDBQ, "repo1", false).Return([]byte(`
		{
			"repository_id": "00000000-0000-0000-0000-000000000001",
			"name": "repo1",
			"organization_name": "orgName"
		}
		`), nil)
		az := &authz.AuthorizerMock{}
		az.On("Authorize", ctx, &hub.AuthorizeInput{
			OrganizationName: "orgName",
			UserID:           "userID",
			Action:           hub.UpdateOrganizationRepository,
		}).Return(tests.ErrFake)
		l := &HelmIndexLoaderMock{}
		l.On("LoadIndex", r).Return(nil, "", nil)
		m := NewManager(cfg, db, az, nil, WithHelmIndexLoader(l))

		err := m.Update(ctx, r)
		assert.Equal(t, tests.ErrFake, err)
		db.AssertExpectations(t)
		az.AssertExpectations(t)
		l.AssertExpectations(t)
	})

	t.Run("database error", func(t *testing.T) {
		testCases := []struct {
			r             *hub.Repository
			dbErr         error
			expectedError error
		}{
			{
				&hub.Repository{
					Name:        "repo1",
					DisplayName: "Repository 1",
					URL:         "https://repo1.com",
					Kind:        hub.Helm,
				},
				tests.ErrFakeDB,
				tests.ErrFakeDB,
			},
			{
				&hub.Repository{
					Name:        "repo1",
					DisplayName: "Repository 1",
					URL:         "https://repo1.com",
					Kind:        hub.Helm,
				},
				util.ErrDBInsufficientPrivilege,
				hub.ErrInsufficientPrivilege,
			},
		}
		for _, tc := range testCases {
			t.Run(tc.dbErr.Error(), func(t *testing.T) {
				t.Parallel()
				db := &tests.DBMock{}
				db.On("QueryRow", ctx, getRepoByNameDBQ, "repo1", false).Return([]byte(`
				{
					"repository_id": "00000000-0000-0000-0000-000000000001",
					"name": "repo1",
					"organization_name": "orgName"
				}
				`), nil)
				db.On("Exec", ctx, updateRepoDBQ, "userID", mock.Anything).Return(tc.dbErr)
				az := &authz.AuthorizerMock{}
				az.On("Authorize", ctx, &hub.AuthorizeInput{
					OrganizationName: "orgName",
					UserID:           "userID",
					Action:           hub.UpdateOrganizationRepository,
				}).Return(nil)

				l := &HelmIndexLoaderMock{}
				l.On("LoadIndex", tc.r).Return(nil, "", nil)
				m := NewManager(cfg, db, az, nil, WithHelmIndexLoader(l))

				err := m.Update(ctx, tc.r)
				assert.Equal(t, tc.expectedError, err)
				db.AssertExpectations(t)
				az.AssertExpectations(t)
				l.AssertExpectations(t)
			})
		}
	})

	t.Run("update repository succeeded", func(t *testing.T) {
		testCases := []struct {
			r *hub.Repository
		}{
			{
				&hub.Repository{
					Name:        "repo1",
					DisplayName: "Repository 1",
					URL:         "https://repo1.com",
					Kind:        hub.Helm,
				},
			},
			{
				&hub.Repository{
					Name:        "repo2",
					DisplayName: "Repository 2",
					URL:         "https://github.com/org2/repo2/path",
					Kind:        hub.OLM,
				},
			},
		}
		for i, tc := range testCases {
			t.Run(strconv.Itoa(i), func(t *testing.T) {
				t.Parallel()
				repoJSON, _ := json.Marshal(tc.r)
				db := &tests.DBMock{}
				db.On("QueryRow", ctx, getRepoByNameDBQ, tc.r.Name, false).Return(repoJSON, nil)
				db.On("Exec", ctx, updateRepoDBQ, "userID", mock.Anything).Return(nil)
				l := &HelmIndexLoaderMock{}
				if tc.r.Kind == hub.Helm {
					l.On("LoadIndex", tc.r).Return(nil, "", nil)
				}
				m := NewManager(cfg, db, nil, nil, WithHelmIndexLoader(l))

				err := m.Update(ctx, tc.r)
				assert.NoError(t, err)
				db.AssertExpectations(t)
				l.AssertExpectations(t)
			})
		}
	})
}

func TestUpdateDigest(t *testing.T) {
	ctx := context.Background()
	repositoryID := "00000000-0000-0000-0000-000000000001"
	digest := "digest"

	t.Run("database update succeeded", func(t *testing.T) {
		t.Parallel()
		db := &tests.DBMock{}
		db.On("Exec", ctx, updateRepoDigestDBQ, repositoryID, digest).Return(nil)
		m := NewManager(cfg, db, nil, nil)

		err := m.UpdateDigest(ctx, repositoryID, digest)
		assert.NoError(t, err)
		db.AssertExpectations(t)
	})

	t.Run("database error", func(t *testing.T) {
		t.Parallel()
		db := &tests.DBMock{}
		db.On("Exec", ctx, updateRepoDigestDBQ, repositoryID, digest).Return(tests.ErrFakeDB)
		m := NewManager(cfg, db, nil, nil)

		err := m.UpdateDigest(ctx, repositoryID, digest)
		assert.Equal(t, tests.ErrFakeDB, err)
		db.AssertExpectations(t)
	})
}

func withRepositoryCloner(rc hub.RepositoryCloner) func(m *Manager) {
	return func(m *Manager) {
		m.rc = rc
	}
}
