/*
Copyright 2014 Google Inc. All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package resource

import (
	//"reflect"
	"encoding/json"
	"testing"

	fuzz "github.com/google/gofuzz"
	"github.com/spf13/pflag"
	"speter.net/go/exp/math/dec/inf"
)

var (
	testQuantityFlag = QuantityFlag("quantityFlag", "1M", "dummy flag for testing the quantity flag mechanism")
)

func dec(i int64, exponent int) *inf.Dec {
	// See the below test-- scale is the negative of an exponent.
	return inf.NewDec(i, inf.Scale(-exponent))
}

func TestDec(t *testing.T) {
	table := []struct {
		got    *inf.Dec
		expect string
	}{
		{dec(1, 0), "1"},
		{dec(1, 1), "10"},
		{dec(5, 2), "500"},
		{dec(8, 3), "8000"},
		{dec(2, 0), "2"},
		{dec(1, -1), "0.1"},
		{dec(3, -2), "0.03"},
		{dec(4, -3), "0.004"},
	}

	for _, item := range table {
		if e, a := item.expect, item.got.String(); e != a {
			t.Errorf("expected %v, got %v", e, a)
		}
	}
}

func TestQuantityParse(t *testing.T) {
	table := []struct {
		input  string
		expect Quantity
	}{
		{"0", Quantity{dec(0, 0), DecimalSI}},
		{"0m", Quantity{dec(0, 0), DecimalSI}},
		{"0Ki", Quantity{dec(0, 0), BinarySI}},
		{"0k", Quantity{dec(0, 0), DecimalSI}},
		{"0Mi", Quantity{dec(0, 0), BinarySI}},
		{"0M", Quantity{dec(0, 0), DecimalSI}},
		{"0Gi", Quantity{dec(0, 0), BinarySI}},
		{"0G", Quantity{dec(0, 0), DecimalSI}},
		{"0Ti", Quantity{dec(0, 0), BinarySI}},
		{"0T", Quantity{dec(0, 0), DecimalSI}},

		// Binary suffixes
		{"1Ki", Quantity{dec(1024, 0), BinarySI}},
		{"8Ki", Quantity{dec(8*1024, 0), BinarySI}},
		{"7Mi", Quantity{dec(7*1024*1024, 0), BinarySI}},
		{"6Gi", Quantity{dec(6*1024*1024*1024, 0), BinarySI}},
		{"5Ti", Quantity{dec(5*1024*1024*1024*1024, 0), BinarySI}},
		{"4Pi", Quantity{dec(4*1024*1024*1024*1024*1024, 0), BinarySI}},
		{"3Ei", Quantity{dec(3*1024*1024*1024*1024*1024*1024, 0), BinarySI}},

		{"10Ti", Quantity{dec(10*1024*1024*1024*1024, 0), BinarySI}},
		{"100Ti", Quantity{dec(100*1024*1024*1024*1024, 0), BinarySI}},

		// Decimal suffixes
		{"3m", Quantity{dec(3, -3), DecimalSI}},
		{"9", Quantity{dec(9, 0), DecimalSI}},
		{"8k", Quantity{dec(8, 3), DecimalSI}},
		{"7M", Quantity{dec(7, 6), DecimalSI}},
		{"6G", Quantity{dec(6, 9), DecimalSI}},
		{"5T", Quantity{dec(5, 12), DecimalSI}},
		{"40T", Quantity{dec(4, 13), DecimalSI}},
		{"300T", Quantity{dec(3, 14), DecimalSI}},
		{"2P", Quantity{dec(2, 15), DecimalSI}},
		{"1E", Quantity{dec(1, 18), DecimalSI}},

		// Decimal exponents
		{"1E-3", Quantity{dec(1, -3), DecimalExponent}},
		{"1e3", Quantity{dec(1, 3), DecimalExponent}},
		{"1E6", Quantity{dec(1, 6), DecimalExponent}},
		{"1e9", Quantity{dec(1, 9), DecimalExponent}},
		{"1E12", Quantity{dec(1, 12), DecimalExponent}},
		{"1e15", Quantity{dec(1, 15), DecimalExponent}},
		{"1E18", Quantity{dec(1, 18), DecimalExponent}},

		// Nonstandard but still parsable
		{"1e14", Quantity{dec(1, 14), DecimalExponent}},
		{"1e13", Quantity{dec(1, 13), DecimalExponent}},
		{"1e3", Quantity{dec(1, 3), DecimalExponent}},
		{"100.035k", Quantity{dec(100035, 0), DecimalSI}},

		// Things that look like floating point
		{"0.001", Quantity{dec(1, -3), DecimalSI}},
		{"0.0005k", Quantity{dec(5, -1), DecimalSI}},
		{"0.005", Quantity{dec(5, -3), DecimalSI}},
		{"0.05", Quantity{dec(5, -2), DecimalSI}},
		{"0.5", Quantity{dec(5, -1), DecimalSI}},
		{"0.00050k", Quantity{dec(5, -1), DecimalSI}},
		{"0.00500", Quantity{dec(5, -3), DecimalSI}},
		{"0.05000", Quantity{dec(5, -2), DecimalSI}},
		{"0.50000", Quantity{dec(5, -1), DecimalSI}},
		{"0.5e0", Quantity{dec(5, -1), DecimalExponent}},
		{"0.5e-1", Quantity{dec(5, -2), DecimalExponent}},
		{"0.5e-2", Quantity{dec(5, -3), DecimalExponent}},
		{"0.5e0", Quantity{dec(5, -1), DecimalExponent}},
		{"10.035M", Quantity{dec(10035, 3), DecimalSI}},

		{"1.2e3", Quantity{dec(12, 2), DecimalExponent}},
		{"1.3E+6", Quantity{dec(13, 5), DecimalExponent}},
		{"1.40e9", Quantity{dec(14, 8), DecimalExponent}},
		{"1.53E12", Quantity{dec(153, 10), DecimalExponent}},
		{"1.6e15", Quantity{dec(16, 14), DecimalExponent}},
		{"1.7E18", Quantity{dec(17, 17), DecimalExponent}},

		{"9.01", Quantity{dec(901, -2), DecimalSI}},
		{"8.1k", Quantity{dec(81, 2), DecimalSI}},
		{"7.123456M", Quantity{dec(7123456, 0), DecimalSI}},
		{"6.987654321G", Quantity{dec(6987654321, 0), DecimalSI}},
		{"5.444T", Quantity{dec(5444, 9), DecimalSI}},
		{"40.1T", Quantity{dec(401, 11), DecimalSI}},
		{"300.2T", Quantity{dec(3002, 11), DecimalSI}},
		{"2.5P", Quantity{dec(25, 14), DecimalSI}},
		{"1.01E", Quantity{dec(101, 16), DecimalSI}},

		// Things that saturate/round
		{"3.001m", Quantity{dec(4, -3), DecimalSI}},
		{"1.1E-3", Quantity{dec(2, -3), DecimalExponent}},
		{"0.0001", Quantity{dec(1, -3), DecimalSI}},
		{"0.0005", Quantity{dec(1, -3), DecimalSI}},
		{"0.00050", Quantity{dec(1, -3), DecimalSI}},
		{"0.5e-3", Quantity{dec(1, -3), DecimalExponent}},
		{"0.9m", Quantity{dec(1, -3), DecimalSI}},
		{"0.12345", Quantity{dec(124, -3), DecimalSI}},
		{"0.12354", Quantity{dec(124, -3), DecimalSI}},
		{"9Ei", Quantity{maxAllowed, BinarySI}},
		{"9223372036854775807Ki", Quantity{maxAllowed, BinarySI}},
		{"12E", Quantity{maxAllowed, DecimalSI}},

		// We'll accept fractional binary stuff, too.
		{"100.035Ki", Quantity{dec(10243584, -2), BinarySI}},
		{"0.5Mi", Quantity{dec(.5*1024*1024, 0), BinarySI}},
		{"0.05Gi", Quantity{dec(536870912, -1), BinarySI}},
		{"0.025Ti", Quantity{dec(274877906944, -1), BinarySI}},

		// Things written by trolls
		{"0.000001Ki", Quantity{dec(2, -3), DecimalSI}}, // rounds up, changes format
		{".001", Quantity{dec(1, -3), DecimalSI}},
		{".0001k", Quantity{dec(100, -3), DecimalSI}},
		{"1.", Quantity{dec(1, 0), DecimalSI}},
		{"1.G", Quantity{dec(1, 9), DecimalSI}},
	}

	for _, item := range table {
		got, err := ParseQuantity(item.input)
		if err != nil {
			t.Errorf("%v: unexpected error: %v", item.input, err)
			continue
		}
		if e, a := item.expect.Amount, got.Amount; e.Cmp(a) != 0 {
			t.Errorf("%v: expected %v, got %v", item.input, e, a)
		}
		if e, a := item.expect.Format, got.Format; e != a {
			t.Errorf("%v: expected %#v, got %#v", item.input, e, a)
		}
	}

	// Try the negative version of everything
	desired := &inf.Dec{}
	for _, item := range table {
		got, err := ParseQuantity("-" + item.input)
		if err != nil {
			t.Errorf("-%v: unexpected error: %v", item.input, err)
			continue
		}
		desired.Neg(item.expect.Amount)
		if e, a := desired, got.Amount; e.Cmp(a) != 0 {
			t.Errorf("%v: expected %v, got %v", item.input, e, a)
		}
		if e, a := item.expect.Format, got.Format; e != a {
			t.Errorf("%v: expected %#v, got %#v", item.input, e, a)
		}
	}

	// Try everything with an explicit +
	for _, item := range table {
		got, err := ParseQuantity("+" + item.input)
		if err != nil {
			t.Errorf("-%v: unexpected error: %v", item.input, err)
			continue
		}
		if e, a := item.expect.Amount, got.Amount; e.Cmp(a) != 0 {
			t.Errorf("%v: expected %v, got %v", item.input, e, a)
		}
		if e, a := item.expect.Format, got.Format; e != a {
			t.Errorf("%v: expected %#v, got %#v", item.input, e, a)
		}
	}

	invalid := []string{
		"1.1.M",
		"1+1.0M",
		"0.1mi",
		"0.1am",
		"aoeu",
		".5i",
		"1i",
		"-3.01i",
	}
	for _, item := range invalid {
		_, err := ParseQuantity(item)
		if err == nil {
			t.Errorf("%v parsed unexpectedly", item)
		}
	}
}

func TestQuantityString(t *testing.T) {
	table := []struct {
		in     Quantity
		expect string
	}{
		{Quantity{dec(1024*1024*1024, 0), BinarySI}, "1Gi"},
		{Quantity{dec(300*1024*1024, 0), BinarySI}, "300Mi"},
		{Quantity{dec(6*1024, 0), BinarySI}, "6Ki"},
		{Quantity{dec(1001*1024*1024*1024, 0), BinarySI}, "1001Gi"},
		{Quantity{dec(1024*1024*1024*1024, 0), BinarySI}, "1Ti"},
		{Quantity{dec(5, 0), BinarySI}, "5"},
		{Quantity{dec(500, -3), BinarySI}, "500m"},
		{Quantity{dec(1, 9), DecimalSI}, "1G"},
		{Quantity{dec(1000, 6), DecimalSI}, "1G"},
		{Quantity{dec(1000000, 3), DecimalSI}, "1G"},
		{Quantity{dec(1000000000, 0), DecimalSI}, "1G"},
		{Quantity{dec(1, -3), DecimalSI}, "1m"},
		{Quantity{dec(80, -3), DecimalSI}, "80m"},
		{Quantity{dec(1080, -3), DecimalSI}, "1080m"},
		{Quantity{dec(108, -2), DecimalSI}, "1080m"},
		{Quantity{dec(10800, -4), DecimalSI}, "1080m"},
		{Quantity{dec(300, 6), DecimalSI}, "300M"},
		{Quantity{dec(1, 12), DecimalSI}, "1T"},
		{Quantity{dec(1234567, 6), DecimalSI}, "1234567M"},
		{Quantity{dec(1234567, -3), BinarySI}, "1234567m"},
		{Quantity{dec(3, 3), DecimalSI}, "3k"},
		{Quantity{dec(1025, 0), BinarySI}, "1025"},
		{Quantity{dec(0, 0), DecimalSI}, "0"},
		{Quantity{dec(0, 0), BinarySI}, "0"},
		{Quantity{dec(1, 9), DecimalExponent}, "1e9"},
		{Quantity{dec(1, -3), DecimalExponent}, "1e-3"},
		{Quantity{dec(80, -3), DecimalExponent}, "80e-3"},
		{Quantity{dec(300, 6), DecimalExponent}, "300e6"},
		{Quantity{dec(1, 12), DecimalExponent}, "1e12"},
		{Quantity{dec(1, 3), DecimalExponent}, "1e3"},
		{Quantity{dec(3, 3), DecimalExponent}, "3e3"},
		{Quantity{dec(3, 3), DecimalSI}, "3k"},
		{Quantity{dec(0, 0), DecimalExponent}, "0"},
	}
	for _, item := range table {
		got := item.in.String()
		if e, a := item.expect, got; e != a {
			t.Errorf("%#v: expected %v, got %v", item.in, e, a)
		}
	}
	desired := &inf.Dec{} // Avoid modifying the values in the table.
	for _, item := range table {
		if item.in.Amount.Cmp(decZero) == 0 {
			// Don't expect it to print "-0" ever
			continue
		}
		q := item.in
		q.Amount = desired.Neg(q.Amount)
		if e, a := "-"+item.expect, q.String(); e != a {
			t.Errorf("%#v: expected %v, got %v", item.in, e, a)
		}
	}
}

func TestQuantityParseEmit(t *testing.T) {
	table := []struct {
		in     string
		expect string
	}{
		{"1Ki", "1Ki"},
		{"1Mi", "1Mi"},
		{"1Gi", "1Gi"},
		{"1024Mi", "1Gi"},
		{"1000M", "1G"},
		{".000001Ki", "2m"},
	}

	for _, item := range table {
		q, err := ParseQuantity(item.in)
		if err != nil {
			t.Errorf("Couldn't parse %v", item.in)
			continue
		}
		if e, a := item.expect, q.String(); e != a {
			t.Errorf("%#v: expected %v, got %v", item.in, e, a)
		}
	}
	for _, item := range table {
		q, err := ParseQuantity("-" + item.in)
		if err != nil {
			t.Errorf("Couldn't parse %v", item.in)
			continue
		}
		if q.Amount.Cmp(decZero) == 0 {
			continue
		}
		if e, a := "-"+item.expect, q.String(); e != a {
			t.Errorf("%#v: expected %v, got %v", item.in, e, a)
		}
	}
}

var fuzzer = fuzz.New().Funcs(
	func(q *Quantity, c fuzz.Continue) {
		q.Amount = &inf.Dec{}
		if c.RandBool() {
			q.Format = BinarySI
			if c.RandBool() {
				q.Amount.SetScale(0)
				q.Amount.SetUnscaled(c.Int63())
				return
			}
			// Be sure to test cases like 1Mi
			q.Amount.SetScale(0)
			q.Amount.SetUnscaled(c.Int63n(1024) << uint(10*c.Intn(5)))
			return
		}
		if c.RandBool() {
			q.Format = DecimalSI
		} else {
			q.Format = DecimalExponent
		}
		if c.RandBool() {
			q.Amount.SetScale(inf.Scale(c.Intn(4)))
			q.Amount.SetUnscaled(c.Int63())
			return
		}
		// Be sure to test cases like 1M
		q.Amount.SetScale(inf.Scale(3 - c.Intn(15)))
		q.Amount.SetUnscaled(c.Int63n(1000))
	},
)

func TestJSON(t *testing.T) {
	for i := 0; i < 500; i++ {
		q := &Quantity{}
		fuzzer.Fuzz(q)
		b, err := json.Marshal(q)
		if err != nil {
			t.Errorf("error encoding %v", q)
		}
		q2 := &Quantity{}
		err = json.Unmarshal(b, q2)
		if err != nil {
			t.Errorf("%v: error decoding %v", q, string(b))
		}
		if q2.Amount.Cmp(q.Amount) != 0 {
			t.Errorf("Expected equal: %v, %v (json was '%v')", q, q2, string(b))
		}
	}
}

func TestMilliNewSet(t *testing.T) {
	table := []struct {
		value  int64
		format Format
		expect string
		exact  bool
	}{
		{1, DecimalSI, "1m", true},
		{1000, DecimalSI, "1", true},
		{1234000, DecimalSI, "1234", true},
		{1024, BinarySI, "1024m", false}, // Format changes
		{1000000, "invalidFormatDefaultsToExponent", "1e3", true},
		{1024 * 1024, BinarySI, "1048576m", false}, // Format changes
	}

	for _, item := range table {
		q := NewMilliQuantity(item.value, item.format)
		if e, a := item.expect, q.String(); e != a {
			t.Errorf("Expected %v, got %v; %#v", e, a, q)
		}
		if !item.exact {
			continue
		}
		q2, err := ParseQuantity(q.String())
		if err != nil {
			t.Errorf("Round trip failed on %v", q)
		}
		if e, a := item.value, q2.MilliValue(); e != a {
			t.Errorf("Expected %v, got %v", e, a)
		}
	}

	for _, item := range table {
		q := NewQuantity(0, item.format)
		q.SetMilli(item.value)
		if e, a := item.expect, q.String(); e != a {
			t.Errorf("Set: Expected %v, got %v; %#v", e, a, q)
		}
	}
}

func TestNewSet(t *testing.T) {
	table := []struct {
		value  int64
		format Format
		expect string
	}{
		{1, DecimalSI, "1"},
		{1000, DecimalSI, "1k"},
		{1234000, DecimalSI, "1234k"},
		{1024, BinarySI, "1Ki"},
		{1000000, "invalidFormatDefaultsToExponent", "1e6"},
		{1024 * 1024, BinarySI, "1Mi"},
	}

	for _, item := range table {
		q := NewQuantity(item.value, item.format)
		if e, a := item.expect, q.String(); e != a {
			t.Errorf("Expected %v, got %v; %#v", e, a, q)
		}
		q2, err := ParseQuantity(q.String())
		if err != nil {
			t.Errorf("Round trip failed on %v", q)
		}
		if e, a := item.value, q2.Value(); e != a {
			t.Errorf("Expected %v, got %v", e, a)
		}
	}

	for _, item := range table {
		q := NewQuantity(0, item.format)
		q.Set(item.value)
		if e, a := item.expect, q.String(); e != a {
			t.Errorf("Set: Expected %v, got %v; %#v", e, a, q)
		}
	}
}

func TestUninitializedNoCrash(t *testing.T) {
	var q Quantity

	q.Value()
	q.MilliValue()
	q.Copy()
	q.String()
	q.MarshalJSON()
}

func TestCopy(t *testing.T) {
	q := NewQuantity(5, DecimalSI)
	c := q.Copy()
	c.Set(6)
	if q.Value() == 6 {
		t.Errorf("Copy didn't")
	}
}

func TestQFlagSet(t *testing.T) {
	qf := qFlag{&Quantity{}}
	qf.Set("1Ki")
	if e, a := "1Ki", qf.String(); e != a {
		t.Errorf("Unexpected result %v != %v", e, a)
	}
}

func TestQFlagIsPFlag(t *testing.T) {
	var pfv pflag.Value = qFlag{}
	if e, a := "quantity", pfv.Type(); e != a {
		t.Errorf("Unexpected result %v != %v", e, a)
	}
}
