// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package integration

import (
	"archive/tar"
	"bytes"
	"compress/gzip"
	"fmt"
	"io"
	"net/http"
	"testing"

	"code.gitea.io/gitea/models/db"
	"code.gitea.io/gitea/models/packages"
	"code.gitea.io/gitea/models/unittest"
	user_model "code.gitea.io/gitea/models/user"
	arch_module "code.gitea.io/gitea/modules/packages/arch"
	arch_service "code.gitea.io/gitea/services/packages/arch"
	"code.gitea.io/gitea/tests"

	"github.com/klauspost/compress/zstd"
	"github.com/stretchr/testify/assert"
	"github.com/ulikunitz/xz"
)

func TestPackageArch(t *testing.T) {
	defer tests.PrepareTestEnv(t)()

	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})

	packageName := "gitea-test"
	packageVersion := "1.4.1-r3"

	createPackage := func(compression, name, version, architecture string) []byte {
		var buf bytes.Buffer
		var cw io.WriteCloser
		switch compression {
		case "zst":
			cw, _ = zstd.NewWriter(&buf)
		case "xz":
			cw, _ = xz.NewWriter(&buf)
		case "gz":
			cw = gzip.NewWriter(&buf)
		}
		tw := tar.NewWriter(cw)

		info := []byte(`pkgname = ` + name + `
pkgbase = ` + name + `
pkgver = ` + version + `
pkgdesc = Description
# comment
builddate = 1678834800
size = 8
arch = ` + architecture + `
license = MIT`)

		hdr := &tar.Header{
			Name: ".PKGINFO",
			Mode: 0o600,
			Size: int64(len(info)),
		}
		tw.WriteHeader(hdr)
		tw.Write(info)

		for _, file := range []string{"etc/dummy", "opt/file/bin"} {
			hdr := &tar.Header{
				Name: file,
				Mode: 0o600,
				Size: 4,
			}
			tw.WriteHeader(hdr)
			tw.Write([]byte("test"))
		}

		tw.Close()
		cw.Close()

		return buf.Bytes()
	}

	compressions := []string{"gz", "xz", "zst"}
	repositories := []string{"main", "testing", "with/slash", ""}

	rootURL := fmt.Sprintf("/api/packages/%s/arch", user.Name)

	t.Run("RepositoryKey", func(t *testing.T) {
		defer tests.PrintCurrentTest(t)()

		req := NewRequest(t, "GET", rootURL+"/repository.key")
		resp := MakeRequest(t, req, http.StatusOK)

		assert.Equal(t, "application/pgp-keys", resp.Header().Get("Content-Type"))
		assert.Contains(t, resp.Body.String(), "-----BEGIN PGP PUBLIC KEY BLOCK-----")
	})

	contentAarch64Gz := createPackage("gz", packageName, packageVersion, "aarch64")
	for _, compression := range compressions {
		contentAarch64 := createPackage(compression, packageName, packageVersion, "aarch64")
		contentAny := createPackage(compression, packageName+"_"+arch_module.AnyArch, packageVersion, arch_module.AnyArch)

		for _, repository := range repositories {
			t.Run(fmt.Sprintf("[%s,%s]", repository, compression), func(t *testing.T) {
				t.Run("Upload", func(t *testing.T) {
					defer tests.PrintCurrentTest(t)()

					uploadURL := fmt.Sprintf("%s/%s", rootURL, repository)

					req := NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader([]byte{}))
					MakeRequest(t, req, http.StatusUnauthorized)

					req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader([]byte{})).
						AddBasicAuth(user.Name)
					MakeRequest(t, req, http.StatusBadRequest)

					req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(contentAarch64)).
						AddBasicAuth(user.Name)
					MakeRequest(t, req, http.StatusCreated)

					pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeArch)
					assert.NoError(t, err)
					assert.Len(t, pvs, 1)

					pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
					assert.NoError(t, err)
					assert.Nil(t, pd.SemVer)
					assert.IsType(t, &arch_module.VersionMetadata{}, pd.Metadata)
					assert.Equal(t, packageName, pd.Package.Name)
					assert.Equal(t, packageVersion, pd.Version.Version)

					pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
					assert.NoError(t, err)
					assert.NotEmpty(t, pfs)
					assert.Condition(t, func() bool {
						seen := false
						expectedFilename := fmt.Sprintf("%s-%s-aarch64.pkg.tar.%s", packageName, packageVersion, compression)
						expectedCompositeKey := fmt.Sprintf("%s|aarch64", repository)
						for _, pf := range pfs {
							if pf.Name == expectedFilename && pf.CompositeKey == expectedCompositeKey {
								if seen {
									return false
								}
								seen = true

								assert.True(t, pf.IsLead)

								pfps, err := packages.GetProperties(db.DefaultContext, packages.PropertyTypeFile, pf.ID)
								assert.NoError(t, err)

								for _, pfp := range pfps {
									switch pfp.Name {
									case arch_module.PropertyRepository:
										assert.Equal(t, repository, pfp.Value)
									case arch_module.PropertyArchitecture:
										assert.Equal(t, "aarch64", pfp.Value)
									}
								}
							}
						}
						return seen
					})

					req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(contentAarch64)).
						AddBasicAuth(user.Name)
					MakeRequest(t, req, http.StatusConflict)

					// Add same package with different compression leads to conflict
					req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(contentAarch64Gz)).
						AddBasicAuth(user.Name)
					MakeRequest(t, req, http.StatusConflict)
				})

				readIndexContent := func(r io.Reader) (map[string]string, error) {
					gzr, err := gzip.NewReader(r)
					if err != nil {
						return nil, err
					}

					content := make(map[string]string)

					tr := tar.NewReader(gzr)
					for {
						hd, err := tr.Next()
						if err == io.EOF {
							break
						}
						if err != nil {
							return nil, err
						}

						buf, err := io.ReadAll(tr)
						if err != nil {
							return nil, err
						}

						content[hd.Name] = string(buf)
					}

					return content, nil
				}

				t.Run("Index", func(t *testing.T) {
					defer tests.PrintCurrentTest(t)()

					req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s", rootURL, repository, arch_service.IndexArchiveFilename))
					resp := MakeRequest(t, req, http.StatusOK)

					content, err := readIndexContent(resp.Body)
					assert.NoError(t, err)

					desc, has := content[fmt.Sprintf("%s-%s/desc", packageName, packageVersion)]
					assert.True(t, has)
					assert.Contains(t, desc, "%FILENAME%\n"+fmt.Sprintf("%s-%s-aarch64.pkg.tar.%s", packageName, packageVersion, compression)+"\n\n")
					assert.Contains(t, desc, "%NAME%\n"+packageName+"\n\n")
					assert.Contains(t, desc, "%VERSION%\n"+packageVersion+"\n\n")
					assert.Contains(t, desc, "%ARCH%\naarch64\n")
					assert.NotContains(t, desc, "%ARCH%\n"+arch_module.AnyArch+"\n")
					assert.Contains(t, desc, "%LICENSE%\nMIT\n")

					files, has := content[fmt.Sprintf("%s-%s/files", packageName, packageVersion)]
					assert.True(t, has)
					assert.Contains(t, files, "%FILES%\netc/dummy\nopt/file/bin\n\n")

					for _, indexFile := range []string{
						arch_service.IndexArchiveFilename,
						arch_service.IndexArchiveFilename + ".tar.gz",
						"index.db",
						"index.db.tar.gz",
						"index.files",
						"index.files.tar.gz",
					} {
						req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s", rootURL, repository, indexFile))
						MakeRequest(t, req, http.StatusOK)

						req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s.sig", rootURL, repository, indexFile))
						MakeRequest(t, req, http.StatusOK)
					}
				})

				t.Run("Download", func(t *testing.T) {
					defer tests.PrintCurrentTest(t)()

					req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s-%s-aarch64.pkg.tar.%s", rootURL, repository, packageName, packageVersion, compression))
					MakeRequest(t, req, http.StatusOK)

					req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s-%s-aarch64.pkg.tar.%s.sig", rootURL, repository, packageName, packageVersion, compression))
					MakeRequest(t, req, http.StatusOK)
				})

				t.Run("Any", func(t *testing.T) {
					defer tests.PrintCurrentTest(t)()

					req := NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/%s", rootURL, repository), bytes.NewReader(contentAny)).
						AddBasicAuth(user.Name)
					MakeRequest(t, req, http.StatusCreated)

					req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s", rootURL, repository, arch_service.IndexArchiveFilename))
					resp := MakeRequest(t, req, http.StatusOK)

					content, err := readIndexContent(resp.Body)
					assert.NoError(t, err)

					desc, has := content[fmt.Sprintf("%s-%s/desc", packageName, packageVersion)]
					assert.True(t, has)
					assert.Contains(t, desc, "%NAME%\n"+packageName+"\n\n")
					assert.Contains(t, desc, "%ARCH%\naarch64\n")

					desc, has = content[fmt.Sprintf("%s-%s/desc", packageName+"_"+arch_module.AnyArch, packageVersion)]
					assert.True(t, has)
					assert.Contains(t, desc, "%NAME%\n"+packageName+"_any\n\n")
					assert.Contains(t, desc, "%ARCH%\n"+arch_module.AnyArch+"\n")

					// "any" architecture package should be available with every architecture requested
					for _, arch := range []string{arch_module.AnyArch, "aarch64", "myarch"} {
						req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/%s-%s-any.pkg.tar.%s", rootURL, repository, arch, packageName+"_"+arch_module.AnyArch, packageVersion, compression))
						MakeRequest(t, req, http.StatusOK)
					}

					req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s/%s/any", rootURL, repository, packageName+"_"+arch_module.AnyArch, packageVersion)).
						AddBasicAuth(user.Name)
					MakeRequest(t, req, http.StatusNoContent)
				})

				t.Run("Delete", func(t *testing.T) {
					defer tests.PrintCurrentTest(t)()

					req := NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s/%s/aarch64", rootURL, repository, packageName, packageVersion))
					MakeRequest(t, req, http.StatusUnauthorized)

					req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s/%s/aarch64", rootURL, repository, packageName, packageVersion)).
						AddBasicAuth(user.Name)
					MakeRequest(t, req, http.StatusNoContent)

					// Deleting the last file of an architecture should remove that index
					req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s", rootURL, repository, arch_service.IndexArchiveFilename))
					MakeRequest(t, req, http.StatusNotFound)
				})
			})
		}
	}
}