package fs

import (
	"fmt"
	"net/http"
	"testing"

	"github.com/rclone/rclone/fs/hash"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestParseRangeOption(t *testing.T) {
	for _, test := range []struct {
		in   string
		want RangeOption
		err  string
	}{
		{in: "", err: "doesn't start with bytes="},
		{in: "bytes=1-2,3-4", err: "contains multiple ranges"},
		{in: "bytes=100", err: "contains no '-'"},
		{in: "bytes=x-8", err: "bad start"},
		{in: "bytes=8-x", err: "bad end"},
		{in: "bytes=1-2", want: RangeOption{Start: 1, End: 2}},
		{in: "bytes=-123456789123456789", want: RangeOption{Start: -1, End: 123456789123456789}},
		{in: "bytes=123456789123456789-", want: RangeOption{Start: 123456789123456789, End: -1}},
		{in: "bytes=  1  -  2  ", want: RangeOption{Start: 1, End: 2}},
		{in: "bytes=-", want: RangeOption{Start: -1, End: -1}},
		{in: "bytes=  -  ", want: RangeOption{Start: -1, End: -1}},
	} {
		got, err := ParseRangeOption(test.in)
		what := fmt.Sprintf("parsing %q", test.in)
		if test.err != "" {
			require.Contains(t, err.Error(), test.err)
			require.Nil(t, got, what)
		} else {
			require.NoError(t, err, what)
			assert.Equal(t, test.want, *got, what)
		}
	}
}

func TestRangeOptionDecode(t *testing.T) {
	for _, test := range []struct {
		in         RangeOption
		size       int64
		wantOffset int64
		wantLimit  int64
	}{
		{in: RangeOption{Start: 1, End: 10}, size: 100, wantOffset: 1, wantLimit: 10},
		{in: RangeOption{Start: 10, End: 10}, size: 100, wantOffset: 10, wantLimit: 1},
		{in: RangeOption{Start: 10, End: 9}, size: 100, wantOffset: 10, wantLimit: 0},
		{in: RangeOption{Start: 1, End: -1}, size: 100, wantOffset: 1, wantLimit: -1},
		{in: RangeOption{Start: -1, End: 90}, size: 100, wantOffset: 10, wantLimit: -1},
		{in: RangeOption{Start: -1, End: -1}, size: 100, wantOffset: 0, wantLimit: -1},
	} {
		gotOffset, gotLimit := test.in.Decode(test.size)
		what := fmt.Sprintf("%+v size=%d", test.in, test.size)
		assert.Equal(t, test.wantOffset, gotOffset, "offset "+what)
		assert.Equal(t, test.wantLimit, gotLimit, "limit "+what)
	}
}

func TestRangeOption(t *testing.T) {
	opt := &RangeOption{Start: 1, End: 10}
	var _ OpenOption = opt // check interface
	assert.Equal(t, "RangeOption(1,10)", opt.String())
	key, value := opt.Header()
	assert.Equal(t, "Range", key)
	assert.Equal(t, "bytes=1-10", value)
	assert.Equal(t, true, opt.Mandatory())

	opt = &RangeOption{Start: -1, End: 10}
	assert.Equal(t, "RangeOption(-1,10)", opt.String())
	key, value = opt.Header()
	assert.Equal(t, "Range", key)
	assert.Equal(t, "bytes=-10", value)
	assert.Equal(t, true, opt.Mandatory())

	opt = &RangeOption{Start: 1, End: -1}
	assert.Equal(t, "RangeOption(1,-1)", opt.String())
	key, value = opt.Header()
	assert.Equal(t, "Range", key)
	assert.Equal(t, "bytes=1-", value)
	assert.Equal(t, true, opt.Mandatory())

	opt = &RangeOption{Start: -1, End: -1}
	assert.Equal(t, "RangeOption(-1,-1)", opt.String())
	key, value = opt.Header()
	assert.Equal(t, "Range", key)
	assert.Equal(t, "bytes=-", value)
	assert.Equal(t, true, opt.Mandatory())
}

func TestSeekOption(t *testing.T) {
	opt := &SeekOption{Offset: 1}
	var _ OpenOption = opt // check interface
	assert.Equal(t, "SeekOption(1)", opt.String())
	key, value := opt.Header()
	assert.Equal(t, "Range", key)
	assert.Equal(t, "bytes=1-", value)
	assert.Equal(t, true, opt.Mandatory())
}

func TestHTTPOption(t *testing.T) {
	opt := &HTTPOption{Key: "k", Value: "v"}
	var _ OpenOption = opt // check interface
	assert.Equal(t, `HTTPOption("k","v")`, opt.String())
	key, value := opt.Header()
	assert.Equal(t, "k", key)
	assert.Equal(t, "v", value)
	assert.Equal(t, false, opt.Mandatory())
}

func TestHashesOption(t *testing.T) {
	opt := &HashesOption{hash.Set(hash.MD5 | hash.SHA1)}
	var _ OpenOption = opt // check interface
	assert.Equal(t, `HashesOption([md5, sha1])`, opt.String())
	key, value := opt.Header()
	assert.Equal(t, "", key)
	assert.Equal(t, "", value)
	assert.Equal(t, false, opt.Mandatory())
}

func TestNullOption(t *testing.T) {
	opt := NullOption{}
	var _ OpenOption = opt // check interface
	assert.Equal(t, "NullOption()", opt.String())
	key, value := opt.Header()
	assert.Equal(t, "", key)
	assert.Equal(t, "", value)
	assert.Equal(t, false, opt.Mandatory())
}

func TestMetadataOption(t *testing.T) {
	opt := MetadataOption{"onion": "ice cream"}
	var _ OpenOption = opt // check interface
	assert.Equal(t, "MetadataOption(map[onion:ice cream])", opt.String())
	key, value := opt.Header()
	assert.Equal(t, "", key)
	assert.Equal(t, "", value)
	assert.Equal(t, false, opt.Mandatory())
}

func TestFixRangeOptions(t *testing.T) {
	for _, test := range []struct {
		name string
		in   []OpenOption
		size int64
		want []OpenOption
	}{
		{
			name: "Nil options",
			in:   nil,
			want: nil,
		},
		{
			name: "Empty options",
			in:   []OpenOption{},
			want: []OpenOption{},
		},
		{
			name: "Unknown size -1",
			in: []OpenOption{
				&RangeOption{Start: 1, End: -1},
			},
			want: []OpenOption{
				&RangeOption{Start: 1, End: -1},
			},
			size: -1,
		},
		{
			name: "Fetch a range with size=0",
			in: []OpenOption{
				&HTTPOption{Key: "a", Value: "1"},
				&RangeOption{Start: 1, End: 10},
				&HTTPOption{Key: "b", Value: "2"},
			},
			want: []OpenOption{
				&HTTPOption{Key: "a", Value: "1"},
				NullOption{},
				&HTTPOption{Key: "b", Value: "2"},
			},
			size: 0,
		},
		{
			name: "Fetch a range",
			in: []OpenOption{
				&HTTPOption{Key: "a", Value: "1"},
				&RangeOption{Start: 1, End: 10},
				&HTTPOption{Key: "b", Value: "2"},
			},
			want: []OpenOption{
				&HTTPOption{Key: "a", Value: "1"},
				&RangeOption{Start: 1, End: 10},
				&HTTPOption{Key: "b", Value: "2"},
			},
			size: 100,
		},
		{
			name: "Fetch to end",
			in: []OpenOption{
				&RangeOption{Start: 1, End: -1},
			},
			want: []OpenOption{
				&RangeOption{Start: 1, End: 99},
			},
			size: 100,
		},
		{
			name: "Fetch the last 10 bytes",
			in: []OpenOption{
				&RangeOption{Start: -1, End: 10},
			},
			want: []OpenOption{
				&RangeOption{Start: 90, End: 99},
			},
			size: 100,
		},
		{
			name: "Fetch with end bigger than size",
			in: []OpenOption{
				&RangeOption{Start: 10, End: 200},
			},
			want: []OpenOption{
				&RangeOption{Start: 10, End: 99},
			},
			size: 100,
		},
		{
			name: "SeekOption",
			in: []OpenOption{
				&HTTPOption{Key: "a", Value: "1"},
				&SeekOption{Offset: 10},
				&HTTPOption{Key: "b", Value: "2"},
			},
			want: []OpenOption{
				&HTTPOption{Key: "a", Value: "1"},
				&RangeOption{Start: 10, End: 99},
				&HTTPOption{Key: "b", Value: "2"},
			},
			size: 100,
		},
	} {
		FixRangeOption(test.in, test.size)
		assert.Equal(t, test.want, test.in, test.name)
	}
}

var testOpenOptions = []OpenOption{
	&HTTPOption{Key: "a", Value: "1"},
	&RangeOption{Start: 1, End: 10},
	&HTTPOption{Key: "b", Value: "2"},
	NullOption{},
	&HashesOption{hash.Set(hash.MD5 | hash.SHA1)},
}

func TestOpenOptionAddHeaders(t *testing.T) {
	m := map[string]string{}
	want := map[string]string{
		"a":     "1",
		"Range": "bytes=1-10",
		"b":     "2",
	}
	OpenOptionAddHeaders(testOpenOptions, m)
	assert.Equal(t, want, m)
}

func TestOpenOptionHeaders(t *testing.T) {
	want := map[string]string{
		"a":     "1",
		"Range": "bytes=1-10",
		"b":     "2",
	}
	m := OpenOptionHeaders(testOpenOptions)
	assert.Equal(t, want, m)
	assert.Nil(t, OpenOptionHeaders([]OpenOption{}))
}

func TestOpenOptionAddHTTPHeaders(t *testing.T) {
	headers := http.Header{}
	want := http.Header{
		"A":     {"1"},
		"Range": {"bytes=1-10"},
		"B":     {"2"},
	}
	OpenOptionAddHTTPHeaders(headers, testOpenOptions)
	assert.Equal(t, want, headers)

}