package download

import (
	"crypto/rand"
	"fmt"
	"io/ioutil"
	"math/big"
	"net/http"
	"os"
	"testing"
	"time"

	"github.com/Azure/azure-sdk-for-go/storage"
	"github.com/Azure/custom-script-extension-linux/pkg/blobutil"
	"github.com/go-kit/kit/log"
	"github.com/stretchr/testify/require"
)

var (
	testctx = log.NewContext(log.NewNopLogger())
)

func Test_blobDownload_validateInputs(t *testing.T) {
	type sas interface {
		getURL() (string, error)
	}

	_, err := NewBlobDownload("", "", blobutil.AzureBlobRef{}).GetRequest()
	require.NotNil(t, err)
	require.Contains(t, err.Error(), "failed to initialize azure storage client")
	require.Contains(t, err.Error(), "account name is not valid")

	_, err = NewBlobDownload("account", "", blobutil.AzureBlobRef{}).GetRequest()
	require.NotNil(t, err)
	require.Contains(t, err.Error(), "failed to initialize azure storage client")
	require.Contains(t, err.Error(), "account key required")

	_, err = NewBlobDownload("account", "Zm9vCg==", blobutil.AzureBlobRef{}).GetRequest()
	require.NotNil(t, err)
	require.Contains(t, err.Error(), "failed to initialize azure storage client")

	_, err = NewBlobDownload("account", "Zm9vCg==", blobutil.AzureBlobRef{
		StorageBase: storage.DefaultBaseURL,
	}).GetRequest()
	require.Nil(t, err)
}

func Test_blobDownload_getURL(t *testing.T) {
	type sas interface {
		getURL() (string, error)
	}

	d := NewBlobDownload("account", "Zm9vCg==", blobutil.AzureBlobRef{
		StorageBase: "test.core.windows.net",
		Container:   "",
		Blob:        "blob.txt",
	})

	v, ok := d.(blobDownload)
	require.True(t, ok)

	url, err := v.getURL()
	require.Nil(t, err)
	require.Contains(t, url, "https://", "missing https scheme")
	require.Contains(t, url, "/account.blob.test.core.windows.net/", "missing/wrong host")
	require.Contains(t, url, "/$root/", "missing container in url")
	require.Contains(t, url, "/blob.txt", "missing blob name in url")
	for _, v := range []string{"sig", "se", "sr", "sp", "sv"} { // SAS query parameters
		require.Contains(t, url, v+"=", "missing SAS query '%s' in url", v)
	}
}

func Test_blobDownload_fails_badCreds(t *testing.T) {
	storageAccountName := "deeptivaistorage"
	d := NewBlobDownload(storageAccountName, "Zm9vCg==", blobutil.AzureBlobRef{
		StorageBase: storage.DefaultBaseURL,
		Blob:        "fooBlob.txt",
		Container:   "foocontainer",
	})

	status, _, err := Download(testctx, d)
	require.NotNil(t, err)
	require.Contains(t, err.Error(), "Please verify the machine has network connectivity")
	require.Contains(t, err.Error(), "403")
	require.Equal(t, status, http.StatusForbidden)
}

func Test_blobDownload_fails_urlNotFound(t *testing.T) {
	d := NewBlobDownload("accountname", "Zm9vCg==", blobutil.AzureBlobRef{
		StorageBase: ".example.com",
		Blob:        "fooBlob.txt",
		Container:   "foocontainer",
	})

	_, _, err := Download(testctx, d)
	require.NotNil(t, err)
	require.Contains(t, err.Error(), "http request failed:")
}

func Test_blobDownload_actualBlob(t *testing.T) {
	acct := os.Getenv("AZURE_STORAGE_ACCOUNT")
	key := os.Getenv("AZURE_STORAGE_ACCESS_KEY")
	if acct == "" || key == "" {
		t.Skipf("Skipping: AZURE_STORAGE_ACCOUNT or AZURE_STORAGE_ACCESS_KEY not specified to run this test")
	}
	base := storage.DefaultBaseURL

	// Create a blob first
	cl, err := storage.NewClient(acct, key, base, storage.DefaultAPIVersion, true)
	require.Nil(t, err)
	bs := cl.GetBlobService()
	randInt, _ := rand.Int(rand.Reader, big.NewInt(time.Now().UnixNano()))
	var (
		n         = 1024 * 64
		name      = "blob.txt"
		container = fmt.Sprintf("custom-script-test-%d", randInt)
		chunk     = make([]byte, n)
	)
	containerRef := bs.GetContainerReference(container)
	_, err = containerRef.DeleteIfExists(&storage.DeleteContainerOptions{})
	require.Nil(t, err)
	_, err = containerRef.CreateIfNotExists(&storage.CreateContainerOptions{
		Access: storage.ContainerAccessTypePrivate})
	require.Nil(t, err)
	defer containerRef.Delete(&storage.DeleteContainerOptions{})
	blobRef := containerRef.GetBlobReference(name)
	require.Nil(t, blobRef.PutAppendBlob(&storage.PutBlobOptions{}))
	rand.Read(chunk)
	require.Nil(t, blobRef.AppendBlock(chunk, &storage.AppendBlockOptions{}))

	// Get the blob via downloader
	d := NewBlobDownload(acct, key, blobutil.AzureBlobRef{
		Container:   container,
		Blob:        name,
		StorageBase: base,
	})
	_, body, err := Download(testctx, d)
	require.Nil(t, err)
	defer body.Close()
	b, err := ioutil.ReadAll(body)
	require.Nil(t, err)
	require.EqualValues(t, chunk, b, "retrieved body is different body=%d chunk=%d", len(b), len(chunk))
}

func Test_blobDownload_fails_actualBlob404(t *testing.T) {
	acct := os.Getenv("AZURE_STORAGE_ACCOUNT")
	key := os.Getenv("AZURE_STORAGE_ACCESS_KEY")
	if acct == "" || key == "" {
		t.Skipf("Skipping: AZURE_STORAGE_ACCOUNT or AZURE_STORAGE_ACCESS_KEY not specified to run this test")
	}
	base := storage.DefaultBaseURL

	blobName := "<BLOB THAT DOESN'T EXIST>"
	containerName := "<CONTAINER NAME>"

	// Get the blob via downloader
	d := NewBlobDownload(acct, key, blobutil.AzureBlobRef{
		Container:   containerName,
		Blob:        blobName,
		StorageBase: base,
	})
	code, _, err := Download(testctx, d)
	require.NotNil(t, err)
	require.Equal(t, code, http.StatusNotFound)
	require.Contains(t, err.Error(), "because it does not exist")
	require.Contains(t, err.Error(), "Not Found")
	require.Contains(t, err.Error(), "Service request ID")
}

func Test_blobDownload_fails_actualBlob409(t *testing.T) {
	// before running this test, go to your storage account on portal > Configuration and disable Blob public access
	acct := os.Getenv("AZURE_STORAGE_ACCOUNT")
	key := os.Getenv("AZURE_STORAGE_ACCESS_KEY")
	if acct == "" || key == "" {
		t.Skipf("Skipping: AZURE_STORAGE_ACCOUNT or AZURE_STORAGE_ACCESS_KEY not specified to run this test")
	}
	base := storage.DefaultBaseURL

	blobName := "<BLOB NAME>"
	containerName := "<CONTAINER NAME>"

	// Get the blob via downloader
	d := NewBlobDownload(acct, key, blobutil.AzureBlobRef{
		Container:   containerName,
		Blob:        blobName,
		StorageBase: base,
	})
	code, _, err := Download(testctx, d)
	require.NotNil(t, err)
	require.Equal(t, code, http.StatusConflict)
	require.Contains(t, err.Error(), "Please verify the machine has network connectivity")
	require.Contains(t, err.Error(), "Public access is not permitted on this storage account")
	require.Contains(t, err.Error(), "Service request ID")
}
