package artifacts

import (
	"archive/zip"
	"bytes"
	"crypto/md5"
	"encoding/hex"
	"fmt"
	"io/ioutil"
	"mime/multipart"
	"net/http"
	"net/http/httptest"
	"os"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"gitlab.com/gitlab-org/gitlab-workhorse/internal/api"
	"gitlab.com/gitlab-org/gitlab-workhorse/internal/objectstore/test"
	"gitlab.com/gitlab-org/gitlab-workhorse/internal/testhelper"
)

func createTestZipArchive(t *testing.T) (data []byte, md5Hash string) {
	var buffer bytes.Buffer
	archive := zip.NewWriter(&buffer)
	fileInArchive, err := archive.Create("test.file")
	require.NoError(t, err)
	fmt.Fprint(fileInArchive, "test")
	archive.Close()
	data = buffer.Bytes()

	hasher := md5.New()
	hasher.Write(data)
	hexHash := hasher.Sum(nil)
	md5Hash = hex.EncodeToString(hexHash)

	return data, md5Hash
}

func createTestMultipartForm(t *testing.T, data []byte) (bytes.Buffer, string) {
	var buffer bytes.Buffer
	writer := multipart.NewWriter(&buffer)
	file, err := writer.CreateFormFile("file", "my.file")
	require.NoError(t, err)
	file.Write(data)
	writer.Close()
	return buffer, writer.FormDataContentType()
}

func testUploadArtifactsFromTestZip(t *testing.T, ts *httptest.Server) *httptest.ResponseRecorder {
	archiveData, _ := createTestZipArchive(t)
	contentBuffer, contentType := createTestMultipartForm(t, archiveData)

	return testUploadArtifacts(t, contentType, ts.URL+Path, &contentBuffer)
}

func TestUploadHandlerSendingToExternalStorage(t *testing.T) {
	tempPath, err := ioutil.TempDir("", "uploads")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(tempPath)

	archiveData, md5 := createTestZipArchive(t)
	archiveFile, err := ioutil.TempFile("", "artifact.zip")
	require.NoError(t, err)
	defer os.Remove(archiveFile.Name())
	_, err = archiveFile.Write(archiveData)
	require.NoError(t, err)
	archiveFile.Close()

	storeServerCalled := 0
	storeServerMux := http.NewServeMux()
	storeServerMux.HandleFunc("/url/put", func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, "PUT", r.Method)

		receivedData, err := ioutil.ReadAll(r.Body)
		require.NoError(t, err)
		require.Equal(t, archiveData, receivedData)

		storeServerCalled++
		w.Header().Set("ETag", md5)
		w.WriteHeader(200)
	})
	storeServerMux.HandleFunc("/store-id", func(w http.ResponseWriter, r *http.Request) {
		http.ServeFile(w, r, archiveFile.Name())
	})

	responseProcessorCalled := 0
	responseProcessor := func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, "store-id", r.FormValue("file.remote_id"))
		require.NotEmpty(t, r.FormValue("file.remote_url"))
		w.WriteHeader(200)
		responseProcessorCalled++
	}

	storeServer := httptest.NewServer(storeServerMux)
	defer storeServer.Close()

	qs := fmt.Sprintf("?%s=%s", ArtifactFormatKey, ArtifactFormatZip)

	tests := []struct {
		name    string
		preauth api.Response
	}{
		{
			name: "ObjectStore Upload",
			preauth: api.Response{
				RemoteObject: api.RemoteObject{
					StoreURL: storeServer.URL + "/url/put" + qs,
					ID:       "store-id",
					GetURL:   storeServer.URL + "/store-id",
				},
			},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			storeServerCalled = 0
			responseProcessorCalled = 0

			ts := testArtifactsUploadServer(t, test.preauth, responseProcessor)
			defer ts.Close()

			contentBuffer, contentType := createTestMultipartForm(t, archiveData)
			response := testUploadArtifacts(t, contentType, ts.URL+Path+qs, &contentBuffer)
			require.Equal(t, http.StatusOK, response.Code)
			testhelper.RequireResponseHeader(t, response, MetadataHeaderKey, MetadataHeaderPresent)
			require.Equal(t, 1, storeServerCalled, "store should be called only once")
			require.Equal(t, 1, responseProcessorCalled, "response processor should be called only once")
		})
	}
}

func TestUploadHandlerSendingToExternalStorageAndStorageServerUnreachable(t *testing.T) {
	tempPath, err := ioutil.TempDir("", "uploads")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(tempPath)

	responseProcessor := func(w http.ResponseWriter, r *http.Request) {
		t.Fatal("it should not be called")
	}

	authResponse := api.Response{
		TempPath: tempPath,
		RemoteObject: api.RemoteObject{
			StoreURL: "http://localhost:12323/invalid/url",
			ID:       "store-id",
		},
	}

	ts := testArtifactsUploadServer(t, authResponse, responseProcessor)
	defer ts.Close()

	response := testUploadArtifactsFromTestZip(t, ts)
	require.Equal(t, http.StatusInternalServerError, response.Code)
}

func TestUploadHandlerSendingToExternalStorageAndInvalidURLIsUsed(t *testing.T) {
	tempPath, err := ioutil.TempDir("", "uploads")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(tempPath)

	responseProcessor := func(w http.ResponseWriter, r *http.Request) {
		t.Fatal("it should not be called")
	}

	authResponse := api.Response{
		TempPath: tempPath,
		RemoteObject: api.RemoteObject{
			StoreURL: "htt:////invalid-url",
			ID:       "store-id",
		},
	}

	ts := testArtifactsUploadServer(t, authResponse, responseProcessor)
	defer ts.Close()

	response := testUploadArtifactsFromTestZip(t, ts)
	require.Equal(t, http.StatusInternalServerError, response.Code)
}

func TestUploadHandlerSendingToExternalStorageAndItReturnsAnError(t *testing.T) {
	putCalledTimes := 0

	storeServerMux := http.NewServeMux()
	storeServerMux.HandleFunc("/url/put", func(w http.ResponseWriter, r *http.Request) {
		putCalledTimes++
		require.Equal(t, "PUT", r.Method)
		w.WriteHeader(510)
	})

	responseProcessor := func(w http.ResponseWriter, r *http.Request) {
		t.Fatal("it should not be called")
	}

	storeServer := httptest.NewServer(storeServerMux)
	defer storeServer.Close()

	authResponse := api.Response{
		RemoteObject: api.RemoteObject{
			StoreURL: storeServer.URL + "/url/put",
			ID:       "store-id",
		},
	}

	ts := testArtifactsUploadServer(t, authResponse, responseProcessor)
	defer ts.Close()

	response := testUploadArtifactsFromTestZip(t, ts)
	require.Equal(t, http.StatusInternalServerError, response.Code)
	require.Equal(t, 1, putCalledTimes, "upload should be called only once")
}

func TestUploadHandlerSendingToExternalStorageAndSupportRequestTimeout(t *testing.T) {
	putCalledTimes := 0

	storeServerMux := http.NewServeMux()
	storeServerMux.HandleFunc("/url/put", func(w http.ResponseWriter, r *http.Request) {
		putCalledTimes++
		require.Equal(t, "PUT", r.Method)
		time.Sleep(10 * time.Second)
		w.WriteHeader(510)
	})

	responseProcessor := func(w http.ResponseWriter, r *http.Request) {
		t.Fatal("it should not be called")
	}

	storeServer := httptest.NewServer(storeServerMux)
	defer storeServer.Close()

	authResponse := api.Response{
		RemoteObject: api.RemoteObject{
			StoreURL: storeServer.URL + "/url/put",
			ID:       "store-id",
			Timeout:  1,
		},
	}

	ts := testArtifactsUploadServer(t, authResponse, responseProcessor)
	defer ts.Close()

	response := testUploadArtifactsFromTestZip(t, ts)
	require.Equal(t, http.StatusInternalServerError, response.Code)
	require.Equal(t, 1, putCalledTimes, "upload should be called only once")
}

func TestUploadHandlerMultipartUploadSizeLimit(t *testing.T) {
	os, server := test.StartObjectStore()
	defer server.Close()

	err := os.InitiateMultipartUpload(test.ObjectPath)
	require.NoError(t, err)

	objectURL := server.URL + test.ObjectPath

	uploadSize := 10
	preauth := api.Response{
		RemoteObject: api.RemoteObject{
			ID: "store-id",
			MultipartUpload: &api.MultipartUploadParams{
				PartSize:    1,
				PartURLs:    []string{objectURL + "?partNumber=1"},
				AbortURL:    objectURL, // DELETE
				CompleteURL: objectURL, // POST
			},
		},
	}

	responseProcessor := func(w http.ResponseWriter, r *http.Request) {
		t.Fatal("it should not be called")
	}

	ts := testArtifactsUploadServer(t, preauth, responseProcessor)
	defer ts.Close()

	contentBuffer, contentType := createTestMultipartForm(t, make([]byte, uploadSize))
	response := testUploadArtifacts(t, contentType, ts.URL+Path, &contentBuffer)
	require.Equal(t, http.StatusRequestEntityTooLarge, response.Code)

	// Poll because AbortMultipartUpload is async
	for i := 0; os.IsMultipartUpload(test.ObjectPath) && i < 100; i++ {
		time.Sleep(10 * time.Millisecond)
	}
	require.False(t, os.IsMultipartUpload(test.ObjectPath), "MultipartUpload should not be in progress anymore")
	require.Empty(t, os.GetObjectMD5(test.ObjectPath), "upload should have failed, so the object should not exists")
}

func TestUploadHandlerMultipartUploadMaximumSizeFromApi(t *testing.T) {
	os, server := test.StartObjectStore()
	defer server.Close()

	err := os.InitiateMultipartUpload(test.ObjectPath)
	require.NoError(t, err)

	objectURL := server.URL + test.ObjectPath

	uploadSize := int64(10)
	maxSize := uploadSize - 1
	preauth := api.Response{
		MaximumSize: maxSize,
		RemoteObject: api.RemoteObject{
			ID: "store-id",
			MultipartUpload: &api.MultipartUploadParams{
				PartSize:    uploadSize,
				PartURLs:    []string{objectURL + "?partNumber=1"},
				AbortURL:    objectURL, // DELETE
				CompleteURL: objectURL, // POST
			},
		},
	}

	responseProcessor := func(w http.ResponseWriter, r *http.Request) {
		t.Fatal("it should not be called")
	}

	ts := testArtifactsUploadServer(t, preauth, responseProcessor)
	defer ts.Close()

	contentBuffer, contentType := createTestMultipartForm(t, make([]byte, uploadSize))
	response := testUploadArtifacts(t, contentType, ts.URL+Path, &contentBuffer)
	require.Equal(t, http.StatusRequestEntityTooLarge, response.Code)

	testhelper.Retry(t, 5*time.Second, func() error {
		if os.GetObjectMD5(test.ObjectPath) == "" {
			return nil
		}

		return fmt.Errorf("file is still present")
	})
}
