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

package integration

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

	"forgejo.org/models/db"
	"forgejo.org/models/packages"
	"forgejo.org/models/unittest"
	user_model "forgejo.org/models/user"
	"forgejo.org/modules/base"
	debian_module "forgejo.org/modules/packages/debian"
	"forgejo.org/modules/setting"
	"forgejo.org/tests"

	"github.com/blakesmith/ar"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func createDebianArchive(name, version, architecture, packageDescription string) io.Reader {
	var cbuf bytes.Buffer
	zw := gzip.NewWriter(&cbuf)
	tw := tar.NewWriter(zw)
	tw.WriteHeader(&tar.Header{
		Name: "control",
		Mode: 0o600,
		Size: 50,
	})
	fmt.Fprintf(tw, "Package: %s\nVersion: %s\nArchitecture: %s\nDescription: %s\n", name, version, architecture, packageDescription)
	tw.Close()
	zw.Close()

	var buf bytes.Buffer
	aw := ar.NewWriter(&buf)
	aw.WriteGlobalHeader()
	hdr := &ar.Header{
		Name: "control.tar.gz",
		Mode: 0o600,
		Size: int64(cbuf.Len()),
	}
	aw.WriteHeader(hdr)
	aw.Write(cbuf.Bytes())
	return &buf
}

func TestPackageDebian(t *testing.T) {
	defer tests.PrepareTestEnv(t)()
	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})

	packageName := "gitea"
	packageVersion := "1.0.3"
	packageVersion2 := "1.0.4"
	packageDescription := "Package Description"

	distributions := []string{"test", "gitea"}
	components := []string{"main", "stable"}
	architectures := []string{"all", "amd64"}

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

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

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

		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-----")
	})

	for _, distribution := range distributions {
		t.Run(fmt.Sprintf("[Distribution:%s]", distribution), func(t *testing.T) {
			for _, component := range components {
				for _, architecture := range architectures {
					t.Run(fmt.Sprintf("[Component:%s,Architecture:%s]", component, architecture), func(t *testing.T) {
						uploadURL := fmt.Sprintf("%s/pool/%s/%s/upload", rootURL, distribution, component)

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

							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, createDebianArchive("", "", "", packageDescription)).
								AddBasicAuth(user.Name)
							MakeRequest(t, req, http.StatusBadRequest)

							req = NewRequestWithBody(t, "PUT", uploadURL, createDebianArchive(packageName, packageVersion, architecture, packageDescription)).
								AddBasicAuth(user.Name).
								SetHeader("content-type", "multipart/form-data")
							MakeRequest(t, req, http.StatusBadRequest)

							req = NewRequestWithBody(t, "PUT", uploadURL, createDebianArchive(packageName, packageVersion, architecture, packageDescription)).
								AddBasicAuth(user.Name)
							MakeRequest(t, req, http.StatusCreated)

							pv, err := packages.GetVersionByNameAndVersion(db.DefaultContext, user.ID, packages.TypeDebian, packageName, packageVersion)
							require.NoError(t, err)

							pd, err := packages.GetPackageDescriptor(db.DefaultContext, pv)
							require.NoError(t, err)
							assert.Nil(t, pd.SemVer)
							assert.IsType(t, &debian_module.Metadata{}, pd.Metadata)
							assert.Equal(t, packageName, pd.Package.Name)
							assert.Equal(t, packageVersion, pd.Version.Version)

							pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pv.ID)
							require.NoError(t, err)
							assert.NotEmpty(t, pfs)
							assert.Condition(t, func() bool {
								seen := false
								expectedFilename := fmt.Sprintf("%s_%s_%s.deb", packageName, packageVersion, architecture)
								expectedCompositeKey := fmt.Sprintf("%s|%s", distribution, component)
								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)
										require.NoError(t, err)

										for _, pfp := range pfps {
											switch pfp.Name {
											case debian_module.PropertyDistribution:
												assert.Equal(t, distribution, pfp.Value)
											case debian_module.PropertyComponent:
												assert.Equal(t, component, pfp.Value)
											case debian_module.PropertyArchitecture:
												assert.Equal(t, architecture, pfp.Value)
											}
										}
									}
								}
								return seen
							})

							req = NewRequestWithBody(t, "PUT", uploadURL, createDebianArchive(packageName, packageVersion, architecture, packageDescription)).
								AddBasicAuth(user.Name)
							MakeRequest(t, req, http.StatusConflict)
						})

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

							req := NewRequest(t, "GET", fmt.Sprintf("%s/pool/%s/%s/%s_%s_%s.deb", rootURL, distribution, component, packageName, packageVersion, architecture))
							resp := MakeRequest(t, req, http.StatusOK)

							assert.Equal(t, "application/vnd.debian.binary-package", resp.Header().Get("Content-Type"))
						})

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

							req := NewRequestWithBody(t, "PUT", uploadURL, createDebianArchive(packageName, packageVersion2, architecture, packageDescription)).
								AddBasicAuth(user.Name)
							MakeRequest(t, req, http.StatusCreated)

							url := fmt.Sprintf("%s/dists/%s/%s/binary-%s/Packages", rootURL, distribution, component, architecture)

							req = NewRequest(t, "GET", url)
							resp := MakeRequest(t, req, http.StatusOK)

							body := resp.Body.String()

							assert.Contains(t, body, "Package: "+packageName+"\n")
							assert.Contains(t, body, "Version: "+packageVersion+"\n")
							assert.Contains(t, body, "Version: "+packageVersion2+"\n")
							assert.Contains(t, body, "Architecture: "+architecture+"\n")
							assert.Contains(t, body, fmt.Sprintf("Filename: pool/%s/%s/%s_%s_%s.deb\n", distribution, component, packageName, packageVersion, architecture))
							assert.Contains(t, body, fmt.Sprintf("Filename: pool/%s/%s/%s_%s_%s.deb\n", distribution, component, packageName, packageVersion2, architecture))

							req = NewRequest(t, "GET", url+".gz")
							MakeRequest(t, req, http.StatusOK)

							req = NewRequest(t, "GET", url+".xz")
							MakeRequest(t, req, http.StatusOK)

							url = fmt.Sprintf("%s/dists/%s/%s/%s/by-hash/SHA256/%s", rootURL, distribution, component, architecture, base.EncodeSha256(body))
							req = NewRequest(t, "GET", url)
							resp = MakeRequest(t, req, http.StatusOK)

							assert.Equal(t, body, resp.Body.String())
						})
					})
				}
			}

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

				req := NewRequest(t, "HEAD", fmt.Sprintf("%s/dists/%s/Release", rootURL, distribution))
				MakeRequest(t, req, http.StatusOK)

				req = NewRequest(t, "GET", fmt.Sprintf("%s/dists/%s/Release", rootURL, distribution))
				resp := MakeRequest(t, req, http.StatusOK)

				body := resp.Body.String()

				assert.Contains(t, body, "Components: "+strings.Join(components, " ")+"\n")
				assert.Contains(t, body, "Architectures: "+strings.Join(architectures, " ")+"\n")

				for _, component := range components {
					for _, architecture := range architectures {
						assert.Contains(t, body, fmt.Sprintf("%s/binary-%s/Packages\n", component, architecture))
						assert.Contains(t, body, fmt.Sprintf("%s/binary-%s/Packages.gz\n", component, architecture))
						assert.Contains(t, body, fmt.Sprintf("%s/binary-%s/Packages.xz\n", component, architecture))
					}
				}

				req = NewRequest(t, "GET", fmt.Sprintf("%s/dists/%s/by-hash/SHA256/%s", rootURL, distribution, base.EncodeSha256(body)))
				resp = MakeRequest(t, req, http.StatusOK)

				assert.Equal(t, body, resp.Body.String())

				req = NewRequest(t, "GET", fmt.Sprintf("%s/dists/%s/Release.gpg", rootURL, distribution))
				resp = MakeRequest(t, req, http.StatusOK)

				assert.Contains(t, resp.Body.String(), "-----BEGIN PGP SIGNATURE-----")

				req = NewRequest(t, "HEAD", fmt.Sprintf("%s/dists/%s/InRelease", rootURL, distribution))
				MakeRequest(t, req, http.StatusOK)

				req = NewRequest(t, "GET", fmt.Sprintf("%s/dists/%s/InRelease", rootURL, distribution))
				resp = MakeRequest(t, req, http.StatusOK)

				assert.Contains(t, resp.Body.String(), "-----BEGIN PGP SIGNED MESSAGE-----")
			})
		})
	}

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

		distribution := distributions[0]
		architecture := architectures[0]

		for _, component := range components {
			req := NewRequest(t, "DELETE", fmt.Sprintf("%s/pool/%s/%s/%s/%s/%s", rootURL, distribution, component, packageName, packageVersion, architecture))
			MakeRequest(t, req, http.StatusUnauthorized)

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

			req = NewRequest(t, "DELETE", fmt.Sprintf("%s/pool/%s/%s/%s/%s/%s", rootURL, distribution, component, packageName, packageVersion2, architecture)).
				AddBasicAuth(user.Name)
			MakeRequest(t, req, http.StatusNoContent)

			req = NewRequest(t, "GET", fmt.Sprintf("%s/dists/%s/%s/binary-%s/Packages", rootURL, distribution, component, architecture))
			MakeRequest(t, req, http.StatusNotFound)
		}

		req := NewRequest(t, "GET", fmt.Sprintf("%s/dists/%s/Release", rootURL, distribution))
		resp := MakeRequest(t, req, http.StatusOK)

		body := resp.Body.String()

		assert.Contains(t, body, "Components: "+strings.Join(components, " ")+"\n")
		assert.Contains(t, body, "Architectures: "+architectures[1]+"\n")
	})

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

		// Test precondition -- ensure that the packageVersion & packageVersion2 are listed in the index
		indexURL := fmt.Sprintf("%s/dists/%s/%s/binary-%s/Packages", rootURL, distributions[1], components[0], architectures[0])
		req := NewRequest(t, "GET", indexURL)
		resp := MakeRequest(t, req, http.StatusOK)
		body := resp.Body.String()
		require.Contains(t, body, fmt.Sprintf("Version: %s", packageVersion))
		require.Contains(t, body, fmt.Sprintf("Version: %s", packageVersion2))

		// Perform backend request that simulates the "Delete package" UI option, which is a generic codepath without
		// debian-specific package management awareness...
		user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
		session := loginUser(t, user.Name)
		settingsURL := fmt.Sprintf("/user2/-/packages/debian/%s/%s/settings", packageName, packageVersion)
		req = NewRequestWithValues(t, "POST", settingsURL, map[string]string{
			"action": "delete",
		})
		session.MakeRequest(t, req, http.StatusSeeOther)

		// Ensure that the package index has been rebuilt without the deleted package, handled by debianPackageNotifier
		req = NewRequest(t, "GET", indexURL)
		resp = MakeRequest(t, req, http.StatusOK)
		body = resp.Body.String()
		assert.NotContains(t, body, fmt.Sprintf("Version: %s", packageVersion))
		require.Contains(t, body, fmt.Sprintf("Version: %s", packageVersion2))
	})
}

func TestPackageDebianConcurrent(t *testing.T) {
	if setting.Database.Type.IsSQLite3() {
		// Concurrency test fails on SQLite w/ "database is locked"
		t.Skip()
	}

	defer tests.PrepareTestEnv(t)()

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

	distribution := "test"
	component := "main"
	architecture := "amd64"
	packageName := "gitea"
	packageDescription := "Package Description"

	rootURL := fmt.Sprintf("/api/packages/%s/debian", user.Name)
	uploadURL := fmt.Sprintf("%s/pool/%s/%s/upload", rootURL, distribution, component)

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

		var wg sync.WaitGroup
		packageCount := 10
		for i := range packageCount {
			wg.Go(func() {
				req := NewRequestWithBody(t, "PUT", uploadURL,
					createDebianArchive(packageName, fmt.Sprintf("1.0.%d", i), architecture, packageDescription)).
					AddBasicAuth(user.Name)
				MakeRequest(t, req, http.StatusCreated)
			})
		}
		wg.Wait()

		url := fmt.Sprintf("%s/dists/%s/%s/binary-%s/Packages", rootURL, distribution, component, architecture)

		req := NewRequest(t, "GET", url)
		resp := MakeRequest(t, req, http.StatusOK)
		body := resp.Body.String()

		assert.Contains(t, body, fmt.Sprintf("Package: %s\n", packageName))
		for i := range packageCount {
			assert.Contains(t, body, fmt.Sprintf("Version: 1.0.%d\n", i))
		}
	})
}
