package artifacts

import (
	"archive/zip"
	"bytes"
	"compress/gzip"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"mime/multipart"
	"net/http"
	"net/http/httptest"
	"os"
	"testing"

	"github.com/dgrijalva/jwt-go"

	"gitlab.com/gitlab-org/gitlab-workhorse/internal/api"
	"gitlab.com/gitlab-org/gitlab-workhorse/internal/filestore"
	"gitlab.com/gitlab-org/gitlab-workhorse/internal/helper"
	"gitlab.com/gitlab-org/gitlab-workhorse/internal/proxy"
	"gitlab.com/gitlab-org/gitlab-workhorse/internal/testhelper"
	"gitlab.com/gitlab-org/gitlab-workhorse/internal/upload"
	"gitlab.com/gitlab-org/gitlab-workhorse/internal/upstream/roundtripper"
	"gitlab.com/gitlab-org/gitlab-workhorse/internal/zipartifacts"

	"github.com/stretchr/testify/require"
)

const (
	MetadataHeaderKey     = "Metadata-Status"
	MetadataHeaderPresent = "present"
	MetadataHeaderMissing = "missing"
	Path                  = "/url/path"
)

func testArtifactsUploadServer(t *testing.T, authResponse api.Response, bodyProcessor func(w http.ResponseWriter, r *http.Request)) *httptest.Server {
	mux := http.NewServeMux()
	mux.HandleFunc(Path+"/authorize", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != "POST" {
			t.Fatal("Expected POST request")
		}

		w.Header().Set("Content-Type", api.ResponseContentType)

		data, err := json.Marshal(&authResponse)
		if err != nil {
			t.Fatal("Expected to marshal")
		}
		w.Write(data)
	})
	mux.HandleFunc(Path, func(w http.ResponseWriter, r *http.Request) {
		opts, err := filestore.GetOpts(&authResponse)
		require.NoError(t, err)

		if r.Method != "POST" {
			t.Fatal("Expected POST request")
		}
		if opts.IsLocal() {
			if r.FormValue("file.path") == "" {
				t.Fatal("Expected file to be present")
				return
			}

			_, err := ioutil.ReadFile(r.FormValue("file.path"))
			if err != nil {
				t.Fatal("Expected file to be readable")
				return
			}
		} else {
			if r.FormValue("file.remote_url") == "" {
				t.Fatal("Expected file to be remote accessible")
				return
			}
		}

		if r.FormValue("metadata.path") != "" {
			metadata, err := ioutil.ReadFile(r.FormValue("metadata.path"))
			if err != nil {
				t.Fatal("Expected metadata to be readable")
				return
			}
			gz, err := gzip.NewReader(bytes.NewReader(metadata))
			if err != nil {
				t.Fatal("Expected metadata to be valid gzip")
				return
			}
			defer gz.Close()
			metadata, err = ioutil.ReadAll(gz)
			if err != nil {
				t.Fatal("Expected metadata to be valid")
				return
			}
			if !bytes.HasPrefix(metadata, []byte(zipartifacts.MetadataHeaderPrefix+zipartifacts.MetadataHeader)) {
				t.Fatal("Expected metadata to be of valid format")
				return
			}

			w.Header().Set(MetadataHeaderKey, MetadataHeaderPresent)

		} else {
			w.Header().Set(MetadataHeaderKey, MetadataHeaderMissing)
		}

		w.WriteHeader(http.StatusOK)

		if bodyProcessor != nil {
			bodyProcessor(w, r)
		}
	})
	return testhelper.TestServerWithHandler(nil, mux.ServeHTTP)
}

type testServer struct {
	url        string
	writer     *multipart.Writer
	buffer     *bytes.Buffer
	fileWriter io.Writer
	cleanup    func()
}

func setupWithTmpPath(t *testing.T, filename string, includeFormat bool, format string, authResponse *api.Response, bodyProcessor func(w http.ResponseWriter, r *http.Request)) *testServer {
	tempPath, err := ioutil.TempDir("", "uploads")
	require.NoError(t, err)

	if authResponse == nil {
		authResponse = &api.Response{TempPath: tempPath}
	}

	ts := testArtifactsUploadServer(t, *authResponse, bodyProcessor)

	var buffer bytes.Buffer
	writer := multipart.NewWriter(&buffer)
	fileWriter, err := writer.CreateFormFile(filename, "my.file")
	require.NotNil(t, fileWriter)
	require.NoError(t, err)

	cleanup := func() {
		ts.Close()
		require.NoError(t, os.RemoveAll(tempPath))
		require.NoError(t, writer.Close())
	}

	qs := ""

	if includeFormat {
		qs = fmt.Sprintf("?%s=%s", ArtifactFormatKey, format)
	}

	return &testServer{url: ts.URL + Path + qs, writer: writer, buffer: &buffer, fileWriter: fileWriter, cleanup: cleanup}
}

func testUploadArtifacts(t *testing.T, contentType, url string, body io.Reader) *httptest.ResponseRecorder {
	httpRequest, err := http.NewRequest("POST", url, body)
	require.NoError(t, err)

	httpRequest.Header.Set("Content-Type", contentType)
	response := httptest.NewRecorder()
	parsedURL := helper.URLMustParse(url)
	roundTripper := roundtripper.NewTestBackendRoundTripper(parsedURL)
	testhelper.ConfigureSecret()
	apiClient := api.NewAPI(parsedURL, "123", roundTripper)
	proxyClient := proxy.NewProxy(parsedURL, "123", roundTripper)
	UploadArtifacts(apiClient, proxyClient, &upload.DefaultPreparer{}).ServeHTTP(response, httpRequest)
	return response
}

func TestUploadHandlerAddingMetadata(t *testing.T) {
	testCases := []struct {
		desc          string
		format        string
		includeFormat bool
	}{
		{
			desc:          "ZIP format",
			format:        ArtifactFormatZip,
			includeFormat: true,
		},
		{
			desc:          "default format",
			format:        ArtifactFormatDefault,
			includeFormat: true,
		},
		{
			desc:          "default format without artifact_format",
			format:        ArtifactFormatDefault,
			includeFormat: false,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.desc, func(t *testing.T) {
			s := setupWithTmpPath(t, "file", tc.includeFormat, tc.format, nil,
				func(w http.ResponseWriter, r *http.Request) {
					token, err := jwt.ParseWithClaims(r.Header.Get(upload.RewrittenFieldsHeader), &upload.MultipartClaims{}, testhelper.ParseJWT)
					require.NoError(t, err)

					rewrittenFields := token.Claims.(*upload.MultipartClaims).RewrittenFields
					require.Equal(t, 2, len(rewrittenFields))

					require.Contains(t, rewrittenFields, "file")
					require.Contains(t, rewrittenFields, "metadata")
					require.Contains(t, r.PostForm, "file.gitlab-workhorse-upload")
					require.Contains(t, r.PostForm, "metadata.gitlab-workhorse-upload")
				},
			)
			defer s.cleanup()

			archive := zip.NewWriter(s.fileWriter)
			file, err := archive.Create("test.file")
			require.NotNil(t, file)
			require.NoError(t, err)

			require.NoError(t, archive.Close())
			require.NoError(t, s.writer.Close())

			response := testUploadArtifacts(t, s.writer.FormDataContentType(), s.url, s.buffer)
			require.Equal(t, http.StatusOK, response.Code)
			testhelper.RequireResponseHeader(t, response, MetadataHeaderKey, MetadataHeaderPresent)
		})
	}
}

func TestUploadHandlerTarArtifact(t *testing.T) {
	s := setupWithTmpPath(t, "file", true, "tar", nil,
		func(w http.ResponseWriter, r *http.Request) {
			token, err := jwt.ParseWithClaims(r.Header.Get(upload.RewrittenFieldsHeader), &upload.MultipartClaims{}, testhelper.ParseJWT)
			require.NoError(t, err)

			rewrittenFields := token.Claims.(*upload.MultipartClaims).RewrittenFields
			require.Equal(t, 1, len(rewrittenFields))

			require.Contains(t, rewrittenFields, "file")
			require.Contains(t, r.PostForm, "file.gitlab-workhorse-upload")
		},
	)
	defer s.cleanup()

	file, err := os.Open("../../testdata/tarfile.tar")
	require.NoError(t, err)

	_, err = io.Copy(s.fileWriter, file)
	require.NoError(t, err)
	require.NoError(t, file.Close())
	require.NoError(t, s.writer.Close())

	response := testUploadArtifacts(t, s.writer.FormDataContentType(), s.url, s.buffer)
	require.Equal(t, http.StatusOK, response.Code)
	testhelper.RequireResponseHeader(t, response, MetadataHeaderKey, MetadataHeaderMissing)
}

func TestUploadHandlerForUnsupportedArchive(t *testing.T) {
	s := setupWithTmpPath(t, "file", true, "other", nil, nil)
	defer s.cleanup()
	require.NoError(t, s.writer.Close())

	response := testUploadArtifacts(t, s.writer.FormDataContentType(), s.url, s.buffer)
	require.Equal(t, http.StatusOK, response.Code)
	testhelper.RequireResponseHeader(t, response, MetadataHeaderKey, MetadataHeaderMissing)
}

func TestUploadHandlerForMultipleFiles(t *testing.T) {
	s := setupWithTmpPath(t, "file", true, "", nil, nil)
	defer s.cleanup()

	file, err := s.writer.CreateFormFile("file", "my.file")
	require.NotNil(t, file)
	require.NoError(t, err)
	require.NoError(t, s.writer.Close())

	response := testUploadArtifacts(t, s.writer.FormDataContentType(), s.url, s.buffer)
	require.Equal(t, http.StatusInternalServerError, response.Code)
}

func TestUploadFormProcessing(t *testing.T) {
	s := setupWithTmpPath(t, "metadata", true, "", nil, nil)
	defer s.cleanup()
	require.NoError(t, s.writer.Close())

	response := testUploadArtifacts(t, s.writer.FormDataContentType(), s.url, s.buffer)
	require.Equal(t, http.StatusInternalServerError, response.Code)
}

func TestLsifFileProcessing(t *testing.T) {
	tempPath, err := ioutil.TempDir("", "uploads")
	require.NoError(t, err)

	s := setupWithTmpPath(t, "file", true, "zip", &api.Response{TempPath: tempPath, ProcessLsif: true}, nil)
	defer s.cleanup()

	file, err := os.Open("../../testdata/lsif/valid.lsif.zip")
	require.NoError(t, err)

	_, err = io.Copy(s.fileWriter, file)
	require.NoError(t, err)
	require.NoError(t, file.Close())
	require.NoError(t, s.writer.Close())

	response := testUploadArtifacts(t, s.writer.FormDataContentType(), s.url, s.buffer)
	require.Equal(t, http.StatusOK, response.Code)
	testhelper.RequireResponseHeader(t, response, MetadataHeaderKey, MetadataHeaderPresent)
}

func TestInvalidLsifFileProcessing(t *testing.T) {
	tempPath, err := ioutil.TempDir("", "uploads")
	require.NoError(t, err)

	s := setupWithTmpPath(t, "file", true, "zip", &api.Response{TempPath: tempPath, ProcessLsif: true}, nil)
	defer s.cleanup()

	file, err := os.Open("../../testdata/lsif/invalid.lsif.zip")
	require.NoError(t, err)

	_, err = io.Copy(s.fileWriter, file)
	require.NoError(t, err)
	require.NoError(t, file.Close())
	require.NoError(t, s.writer.Close())

	response := testUploadArtifacts(t, s.writer.FormDataContentType(), s.url, s.buffer)
	require.Equal(t, http.StatusInternalServerError, response.Code)
}
