package storage

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

import (
	"bytes"
	"crypto/md5"
	"encoding/base64"
	"io"

	chk "gopkg.in/check.v1"
)

type StorageFileSuite struct{}

var _ = chk.Suite(&StorageFileSuite{})

func (s *StorageFileSuite) TestCreateFile(c *chk.C) {
	cli := getFileClient(c)
	rec := cli.client.appendRecorder(c)
	defer rec.Stop()
	cli.deleteAllShares()

	// create share
	share := cli.GetShareReference(shareName(c))
	c.Assert(share.Create(nil), chk.IsNil)
	defer share.Delete(nil)
	root := share.GetRootDirectoryReference()

	// create directory structure
	dir1 := root.GetDirectoryReference("one")
	c.Assert(dir1.Create(nil), chk.IsNil)
	dir2 := dir1.GetDirectoryReference("two")
	c.Assert(dir2.Create(nil), chk.IsNil)

	// verify file doesn't exist
	file := dir2.GetFileReference("some.file")
	exists, err := file.Exists()
	c.Assert(err, chk.IsNil)
	c.Assert(exists, chk.Equals, false)

	// create file
	c.Assert(file.Create(1024, nil), chk.IsNil)

	// delete file and verify
	c.Assert(file.Delete(nil), chk.IsNil)
	exists, err = file.Exists()
	c.Assert(err, chk.IsNil)
	c.Assert(exists, chk.Equals, false)
}

func (s *StorageFileSuite) TestGetFile(c *chk.C) {
	cli := getFileClient(c)
	rec := cli.client.appendRecorder(c)
	defer rec.Stop()

	// create share
	share := cli.GetShareReference(shareName(c))
	c.Assert(share.Create(nil), chk.IsNil)
	defer share.Delete(nil)
	root := share.GetRootDirectoryReference()

	// create file
	const size = uint64(1024)
	byteStream, _ := newByteStream(size)
	file := root.GetFileReference("some.file")
	c.Assert(file.Create(size, nil), chk.IsNil)

	// fill file with some data
	c.Assert(file.WriteRange(byteStream, FileRange{End: size - 1}, nil), chk.IsNil)

	// set some metadata
	md := map[string]string{
		"something": "somethingvalue",
		"another":   "anothervalue",
	}
	file.Metadata = md
	c.Assert(file.SetMetadata(nil), chk.IsNil)

	options := GetFileOptions{
		GetContentMD5: false,
	}
	// retrieve full file content and verify
	stream, err := file.DownloadRangeToStream(FileRange{Start: 0, End: size - 1}, &options)
	c.Assert(err, chk.IsNil)
	defer stream.Body.Close()
	var b1 [size]byte
	count, _ := stream.Body.Read(b1[:])
	c.Assert(count, chk.Equals, int(size))
	var c1 [size]byte
	bs, _ := newByteStream(size)
	bs.Read(c1[:])
	c.Assert(b1, chk.DeepEquals, c1)

	// retrieve partial file content and verify
	stream, err = file.DownloadRangeToStream(FileRange{Start: size / 2, End: size - 1}, &options)
	c.Assert(err, chk.IsNil)
	defer stream.Body.Close()
	var b2 [size / 2]byte
	count, _ = stream.Body.Read(b2[:])
	c.Assert(count, chk.Equals, int(size)/2)
	var c2 [size / 2]byte
	bs, _ = newByteStream(size / 2)
	bs.Read(c2[:])
	c.Assert(b2, chk.DeepEquals, c2)
}

func (s *StorageFileSuite) TestFileRanges(c *chk.C) {
	cli := getFileClient(c)
	rec := cli.client.appendRecorder(c)
	defer rec.Stop()

	share := cli.GetShareReference(shareName(c))
	c.Assert(share.Create(nil), chk.IsNil)
	defer share.Delete(nil)
	root := share.GetRootDirectoryReference()

	fileSize := uint64(4096)
	contentBytes := content(int(fileSize))

	// --- File with no valid ranges
	file1 := root.GetFileReference("file1.txt")
	c.Assert(file1.Create(fileSize, nil), chk.IsNil)

	ranges, err := file1.ListRanges(nil)
	c.Assert(err, chk.IsNil)
	c.Assert(ranges.ContentLength, chk.Equals, fileSize)
	c.Assert(ranges.FileRanges, chk.IsNil)

	// --- File after writing a range
	file2 := root.GetFileReference("file2.txt")
	c.Assert(file2.Create(fileSize, nil), chk.IsNil)
	c.Assert(file2.WriteRange(bytes.NewReader(contentBytes), FileRange{End: fileSize - 1}, nil), chk.IsNil)

	ranges, err = file2.ListRanges(nil)
	c.Assert(err, chk.IsNil)
	c.Assert(len(ranges.FileRanges), chk.Equals, 1)
	c.Assert((ranges.FileRanges[0].End-ranges.FileRanges[0].Start)+1, chk.Equals, fileSize)

	// --- File after writing and clearing
	file3 := root.GetFileReference("file3.txt")
	c.Assert(file3.Create(fileSize, nil), chk.IsNil)
	c.Assert(file3.WriteRange(bytes.NewReader(contentBytes), FileRange{End: fileSize - 1}, nil), chk.IsNil)
	c.Assert(file3.ClearRange(FileRange{End: fileSize - 1}, nil), chk.IsNil)

	ranges, err = file3.ListRanges(nil)
	c.Assert(err, chk.IsNil)
	c.Assert(ranges.FileRanges, chk.IsNil)

	// --- File with ranges and subranges
	file4 := root.GetFileReference("file4.txt")
	c.Assert(file4.Create(fileSize, nil), chk.IsNil)
	putRanges := []FileRange{
		{End: 511},
		{Start: 1024, End: 1535},
		{Start: 2048, End: 2559},
		{Start: 3072, End: 3583},
	}

	for _, r := range putRanges {
		err = file4.WriteRange(bytes.NewReader(contentBytes[:512]), r, nil)
		c.Assert(err, chk.IsNil)
	}

	// validate all ranges
	ranges, err = file4.ListRanges(nil)
	c.Assert(err, chk.IsNil)
	c.Assert(ranges.FileRanges, chk.DeepEquals, putRanges)

	options := ListRangesOptions{
		ListRange: &FileRange{
			Start: 1000,
			End:   3000,
		},
	}
	// validate sub-ranges
	ranges, err = file4.ListRanges(&options)
	c.Assert(err, chk.IsNil)
	c.Assert(ranges.FileRanges, chk.DeepEquals, putRanges[1:3])

	// --- clear partial range and validate
	file5 := root.GetFileReference("file5.txt")
	c.Assert(file5.Create(fileSize, nil), chk.IsNil)
	c.Assert(file5.WriteRange(bytes.NewReader(contentBytes), FileRange{End: fileSize - 1}, nil), chk.IsNil)
	c.Assert(file5.ClearRange(putRanges[0], nil), chk.IsNil)
	c.Assert(file5.ClearRange(putRanges[2], nil), chk.IsNil)

	ranges, err = file5.ListRanges(nil)
	c.Assert(err, chk.IsNil)
	expectedtRanges := []FileRange{
		{Start: 512, End: 2047},
		{Start: 2560, End: 4095},
	}
	c.Assert(ranges.FileRanges, chk.HasLen, 2)
	c.Assert(ranges.FileRanges[0], chk.DeepEquals, expectedtRanges[0])
	c.Assert(ranges.FileRanges[1], chk.DeepEquals, expectedtRanges[1])
}

func (s *StorageFileSuite) TestFileProperties(c *chk.C) {
	cli := getFileClient(c)
	rec := cli.client.appendRecorder(c)
	defer rec.Stop()

	// create share
	share := cli.GetShareReference(shareName(c))
	c.Assert(share.Create(nil), chk.IsNil)
	defer share.Delete(nil)
	root := share.GetRootDirectoryReference()

	fileSize := uint64(512)
	file := root.GetFileReference("test.dat")
	c.Assert(file.Create(fileSize, nil), chk.IsNil)

	// get initial set of properties
	c.Assert(file.Properties.Length, chk.Equals, fileSize)
	c.Assert(file.Properties.Etag, chk.NotNil)

	// set some file properties
	cc := "cachecontrol"
	ct := "mytype"
	enc := "noencoding"
	lang := "neutral"
	disp := "friendly"
	file.Properties.CacheControl = cc
	file.Properties.Type = ct
	file.Properties.Disposition = disp
	file.Properties.Encoding = enc
	file.Properties.Language = lang
	c.Assert(file.SetProperties(nil), chk.IsNil)

	// retrieve and verify
	c.Assert(file.FetchAttributes(nil), chk.IsNil)
	c.Assert(file.Properties.CacheControl, chk.Equals, cc)
	c.Assert(file.Properties.Type, chk.Equals, ct)
	c.Assert(file.Properties.Disposition, chk.Equals, disp)
	c.Assert(file.Properties.Encoding, chk.Equals, enc)
	c.Assert(file.Properties.Language, chk.Equals, lang)
}

func (s *StorageFileSuite) TestFileMetadata(c *chk.C) {
	cli := getFileClient(c)
	rec := cli.client.appendRecorder(c)
	defer rec.Stop()

	// create share
	share := cli.GetShareReference(shareName(c))
	c.Assert(share.Create(nil), chk.IsNil)
	defer share.Delete(nil)
	root := share.GetRootDirectoryReference()

	fileSize := uint64(512)
	file := root.GetFileReference("test.dat")
	c.Assert(file.Create(fileSize, nil), chk.IsNil)

	// get metadata, shouldn't be any
	c.Assert(file.Metadata, chk.HasLen, 0)

	// set some custom metadata
	md := map[string]string{
		"something": "somethingvalue",
		"another":   "anothervalue",
	}
	file.Metadata = md
	c.Assert(file.SetMetadata(nil), chk.IsNil)

	// retrieve and verify
	c.Assert(file.FetchAttributes(nil), chk.IsNil)
	c.Assert(file.Metadata, chk.DeepEquals, md)
}

func (s *StorageFileSuite) TestFileMD5(c *chk.C) {
	cli := getFileClient(c)
	rec := cli.client.appendRecorder(c)
	defer rec.Stop()

	// create share
	share := cli.GetShareReference(shareName(c))
	c.Assert(share.Create(nil), chk.IsNil)
	defer share.Delete(nil)
	root := share.GetRootDirectoryReference()

	// create file
	const size = uint64(1024)
	fileSize := uint64(size)
	file := root.GetFileReference("test.dat")
	c.Assert(file.Create(fileSize, nil), chk.IsNil)

	// fill file with some data and MD5 hash
	byteStream, contentMD5 := newByteStream(size)
	options := WriteRangeOptions{
		ContentMD5: contentMD5,
	}
	c.Assert(file.WriteRange(byteStream, FileRange{End: size - 1}, &options), chk.IsNil)

	// download file and verify
	downloadOptions := GetFileOptions{
		GetContentMD5: true,
	}
	stream, err := file.DownloadRangeToStream(FileRange{Start: 0, End: size - 1}, &downloadOptions)
	c.Assert(err, chk.IsNil)
	defer stream.Body.Close()
	c.Assert(stream.ContentMD5, chk.Equals, contentMD5)
}

// returns a byte stream along with a base-64 encoded MD5 hash of its contents
func newByteStream(count uint64) (io.Reader, string) {
	b := make([]uint8, count)
	for i := uint64(0); i < count; i++ {
		b[i] = 0xff
	}

	// create an MD5 hash of the array
	hash := md5.Sum(b)

	return bytes.NewReader(b), base64.StdEncoding.EncodeToString(hash[:])
}

func (s *StorageFileSuite) TestCopyFileSameAccountNoMetaData(c *chk.C) {
	cli := getFileClient(c)
	rec := cli.client.appendRecorder(c)
	defer rec.Stop()

	// create share
	share := cli.GetShareReference(shareName(c))
	c.Assert(share.Create(nil), chk.IsNil)
	defer share.Delete(nil)
	root := share.GetRootDirectoryReference()

	// create directory structure
	dir1 := root.GetDirectoryReference("one")
	c.Assert(dir1.Create(nil), chk.IsNil)
	dir2 := dir1.GetDirectoryReference("two")
	c.Assert(dir2.Create(nil), chk.IsNil)

	// create file
	file := dir2.GetFileReference("some.file")
	c.Assert(file.Create(1024, nil), chk.IsNil)
	exists, err := file.Exists()
	c.Assert(err, chk.IsNil)
	c.Assert(exists, chk.Equals, true)

	otherFile := dir2.GetFileReference("someother.file")

	// copy the file, no timeout parameter
	err = otherFile.CopyFile(file.URL(), nil)
	c.Assert(err, chk.IsNil)

	// delete files
	c.Assert(file.Delete(nil), chk.IsNil)
	c.Assert(otherFile.Delete(nil), chk.IsNil)
}

func (s *StorageFileSuite) TestCopyFileSameAccountTimeout(c *chk.C) {
	cli := getFileClient(c)
	rec := cli.client.appendRecorder(c)
	defer rec.Stop()

	// create share
	share := cli.GetShareReference(shareName(c))
	c.Assert(share.Create(nil), chk.IsNil)
	defer share.Delete(nil)
	root := share.GetRootDirectoryReference()

	// create directory structure
	dir1 := root.GetDirectoryReference("one")
	c.Assert(dir1.Create(nil), chk.IsNil)
	dir2 := dir1.GetDirectoryReference("two")
	c.Assert(dir2.Create(nil), chk.IsNil)

	// create file
	file := dir2.GetFileReference("some.file")
	c.Assert(file.Create(1024, nil), chk.IsNil)

	// copy the file, 60 second timeout.
	otherFile := dir2.GetFileReference("someother.file")
	options := FileRequestOptions{}
	options.Timeout = 60
	c.Assert(otherFile.CopyFile(file.URL(), &options), chk.IsNil)

	// delete files
	c.Assert(file.Delete(nil), chk.IsNil)
	c.Assert(otherFile.Delete(nil), chk.IsNil)
}

func (s *StorageFileSuite) TestCopyFileMissingFile(c *chk.C) {
	cli := getFileClient(c)
	rec := cli.client.appendRecorder(c)
	defer rec.Stop()

	// create share
	share := cli.GetShareReference(shareName(c))
	c.Assert(share.Create(nil), chk.IsNil)
	defer share.Delete(nil)
	root := share.GetRootDirectoryReference()

	// create directory structure
	dir1 := root.GetDirectoryReference("one")
	c.Assert(dir1.Create(nil), chk.IsNil)

	otherFile := dir1.GetFileReference("someother.file")

	// copy the file, no timeout parameter
	err := otherFile.CopyFile("", nil)
	c.Assert(err, chk.NotNil)
}
