package fixedpoint

import (
	"encoding/json"
	"math/big"
	"testing"

	"github.com/stretchr/testify/assert"
	"gopkg.in/yaml.v3"
)

const Delta = 1e-9

func BenchmarkFloat64(b *testing.B) {
	b.ResetTimer()

	b.Run("convert-float64", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			x := NewFromFloat(20.0)
			_ = x.Float64() // nolint
		}
	})
	b.Run("convert-float64-large-numbers", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			x := NewFromFloat(88.12345678)
			_ = x.Float64() // nolint
		}
	})
}

func BenchmarkDiv(b *testing.B) {
	b.ResetTimer()

	b.Run("div-float64", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			x := NewFromFloat(20.0)
			y := NewFromFloat(20.0)
			x = x.Div(y) // nolint
		}
	})
	b.Run("div-float64-large-numbers", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			x := NewFromFloat(88.12345678)
			y := NewFromFloat(888.12345678)
			x = x.Div(y) // nolint
		}
	})
	b.Run("div-big-small-numbers", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			x := big.NewFloat(20.0)
			y := big.NewFloat(20.0)
			x = new(big.Float).Quo(x, y) // nolint
		}
	})
	b.Run("div-big-large-numbers", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			x := big.NewFloat(88.12345678)
			y := big.NewFloat(888.12345678)
			x = new(big.Float).Quo(x, y) // nolint
		}
	})
	b.Run("div-native-float64", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			x := 20.0
			y := 20.0
			x = x / y // nolint
		}
	})
	b.Run("div-native-float64-large-numbers", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			x := 88.12345678
			y := 888.12345678
			x = x / y // nolint
		}
	})
}

func BenchmarkMul(b *testing.B) {
	b.ResetTimer()

	b.Run("mul-float64", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			x := NewFromFloat(20.0)
			y := NewFromFloat(20.0)
			x = x.Mul(y) // nolint
		}
	})

	b.Run("mul-float64-large-numbers", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			x := NewFromFloat(88.12345678)
			y := NewFromFloat(88.12345678)
			x = x.Mul(y) // nolint
		}
	})

	b.Run("mul-big-small-numbers", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			x := big.NewFloat(20.0)
			y := big.NewFloat(20.0)
			x = new(big.Float).Mul(x, y) // nolint
		}
	})

	b.Run("mul-big-large-numbers", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			x := big.NewFloat(88.12345678)
			y := big.NewFloat(88.12345678)
			x = new(big.Float).Mul(x, y) // nolint
		}
	})

	b.Run("mul native float64", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			x := 20.0
			y := 20.0
			x = x * y // nolint
		}
	})
}

func TestMulString(t *testing.T) {
	x := NewFromFloat(10.55)
	assert.Equal(t, "10.55", x.String())
	y := NewFromFloat(10.55)
	x = x.Mul(y)
	assert.Equal(t, "111.3025", x.String())
	assert.Equal(t, "111.30", x.FormatString(2))
	assert.InDelta(t, 111.3025, x.Float64(), Delta)
}

func TestMulExp(t *testing.T) {
	x, _ := NewFromString("166")
	digits := x.NumIntDigits()
	assert.Equal(t, digits, 3)
	step := x.MulExp(-digits + 1)
	assert.Equal(t, "1.66", step.String())
}

func TestNew(t *testing.T) {
	f := NewFromFloat(0.001)
	assert.Equal(t, "0.001", f.String())
	assert.Equal(t, "0.0010", f.FormatString(4))
	assert.Equal(t, "0.1%", f.Percentage())
	assert.Equal(t, "0.10%", f.FormatPercentage(2))
	f = NewFromFloat(0.1)
	assert.Equal(t, "10%", f.Percentage())
	assert.Equal(t, "10%", f.FormatPercentage(0))
	f = NewFromFloat(0.01)
	assert.Equal(t, "1%", f.Percentage())
	assert.Equal(t, "1%", f.FormatPercentage(0))
	f = NewFromFloat(0.111)
	assert.Equal(t, "11.1%", f.Percentage())
	assert.Equal(t, "11.1%", f.FormatPercentage(1))
}

func TestFormatString(t *testing.T) {
	testCases := []struct {
		value Value
		prec  int
		out   string
	}{
		{
			value: NewFromFloat(0.001),
			prec:  8,
			out:   "0.00100000",
		},
		{
			value: NewFromFloat(0.123456789),
			prec:  4,
			out:   "0.1234",
		},
		{
			value: NewFromFloat(0.123456789),
			prec:  5,
			out:   "0.12345",
		},
		{
			value: NewFromFloat(20.0),
			prec:  0,
			out:   "20",
		},
	}
	for _, testCase := range testCases {
		assert.Equal(t, testCase.out, testCase.value.FormatString(testCase.prec))
	}
}

func TestRound(t *testing.T) {
	f := NewFromFloat(1.2345)
	f = f.Round(0, Down)
	assert.Equal(t, "1", f.String())
	w := NewFromFloat(1.2345)
	w = w.Trunc()
	assert.Equal(t, "1", w.String())
	s := NewFromFloat(1.2345)
	assert.Equal(t, "1.23", s.Round(2, Down).String())
}

func TestNewFromString(t *testing.T) {
	f, err := NewFromString("0.00000003")
	assert.NoError(t, err)
	assert.Equal(t, "0.00000003", f.String())
}

func TestFromString(t *testing.T) {
	f := MustNewFromString("0.004075")
	assert.Equal(t, "0.004075", f.String())
	f = MustNewFromString("0.03")
	assert.Equal(t, "0.03", f.String())

	f = MustNewFromString("0.75%")
	assert.Equal(t, "0.0075", f.String())
	f = MustNewFromString("1.1e-7")
	assert.Equal(t, "0.00000011", f.String())
	f = MustNewFromString(".0%")
	assert.Equal(t, Zero, f)
	f = MustNewFromString("")
	assert.Equal(t, Zero, f)

	for _, s := range []string{"inf", "Inf", "INF", "iNF"} {
		f = MustNewFromString(s)
		assert.Equal(t, PosInf, f)
		f = MustNewFromString("+" + s)
		assert.Equal(t, PosInf, f)
		f = MustNewFromString("-" + s)
		assert.Equal(t, NegInf, f)
	}
}

func TestJson(t *testing.T) {
	p := MustNewFromString("0")
	e, err := json.Marshal(p)
	assert.NoError(t, err)
	assert.Equal(t, "0.00000000", string(e))
	p = MustNewFromString("1.00000003")
	e, err = json.Marshal(p)
	assert.NoError(t, err)
	assert.Equal(t, "1.00000003", string(e))
	p = MustNewFromString("1.000000003")
	e, err = json.Marshal(p)
	assert.NoError(t, err)
	assert.Equal(t, "1.00000000", string(e))
	p = MustNewFromString("1.000000008")
	e, err = json.Marshal(p)
	assert.NoError(t, err)
	assert.Equal(t, "1.00000000", string(e))
	p = MustNewFromString("0.999999999")
	e, err = json.Marshal(p)
	assert.NoError(t, err)
	assert.Equal(t, "0.99999999", string(e))

	p = MustNewFromString("1.2e-9")
	e, err = json.Marshal(p)
	assert.NoError(t, err)
	assert.Equal(t, "0.00000000", p.FormatString(8))
	assert.Equal(t, "0.00000000", string(e))

	_ = json.Unmarshal([]byte("0.00153917575"), &p)
	assert.Equal(t, "0.00153917", p.FormatString(8))

	q := NewFromFloat(0.00153917575)
	assert.Equal(t, p, q)
	_ = json.Unmarshal([]byte("6e-8"), &p)
	_ = json.Unmarshal([]byte("0.000062"), &q)
	assert.Equal(t, "0.00006194", q.Sub(p).String())

	assert.NoError(t, json.Unmarshal([]byte(`"inf"`), &p))
	assert.NoError(t, json.Unmarshal([]byte(`"+Inf"`), &q))
	assert.Equal(t, PosInf, p)
	assert.Equal(t, p, q)
}

func TestYaml(t *testing.T) {
	p := MustNewFromString("0")
	e, err := yaml.Marshal(p)
	assert.NoError(t, err)
	assert.Equal(t, "\"0.00000000\"\n", string(e))
	p = MustNewFromString("1.00000003")
	e, err = yaml.Marshal(p)
	assert.NoError(t, err)
	assert.Equal(t, "\"1.00000003\"\n", string(e))
	p = MustNewFromString("1.000000003")
	e, err = yaml.Marshal(p)
	assert.NoError(t, err)
	assert.Equal(t, "\"1.00000000\"\n", string(e))
	p = MustNewFromString("1.000000008")
	e, err = yaml.Marshal(p)
	assert.NoError(t, err)
	assert.Equal(t, "\"1.00000000\"\n", string(e))
	p = MustNewFromString("0.999999999")
	e, err = yaml.Marshal(p)
	assert.NoError(t, err)
	assert.Equal(t, "\"0.99999999\"\n", string(e))

	p = MustNewFromString("1.2e-9")
	e, err = yaml.Marshal(p)
	assert.NoError(t, err)
	assert.Equal(t, "0.00000000", p.FormatString(8))
	assert.Equal(t, "\"0.00000000\"\n", string(e))

	_ = yaml.Unmarshal([]byte("0.00153917575"), &p)
	assert.Equal(t, "0.00153917", p.FormatString(8))

	q := NewFromFloat(0.00153917575)
	assert.Equal(t, p, q)
	_ = yaml.Unmarshal([]byte("6e-8"), &p)
	_ = yaml.Unmarshal([]byte("0.000062"), &q)
	assert.Equal(t, "0.00006194", q.Sub(p).String())

	assert.NoError(t, json.Unmarshal([]byte(`"inf"`), &p))
	assert.NoError(t, json.Unmarshal([]byte(`"+Inf"`), &q))
	assert.Equal(t, PosInf, p)
	assert.Equal(t, p, q)
}

func TestNumFractionalDigits(t *testing.T) {
	tests := []struct {
		name string
		v    Value
		want int
	}{
		{
			name: "ignore the integer part",
			v:    MustNewFromString("123.4567"),
			want: 4,
		},
		{
			name: "ignore the sign",
			v:    MustNewFromString("-123.4567"),
			want: 4,
		},
		{
			name: "ignore the trailing zero",
			v:    MustNewFromString("-123.45000000"),
			want: 2,
		},
		{
			name: "no fractional parts",
			v:    MustNewFromString("-1"),
			want: 0,
		},
		{
			name: "no fractional parts",
			v:    MustNewFromString("-1.0"),
			want: 0,
		},
		{
			name: "only fractional part",
			v:    MustNewFromString(".123456"),
			want: 6,
		},
		{
			name: "percentage",
			v:    MustNewFromString("0.075%"), // 0.075 * 0.01
			want: 5,
		},
		{
			name: "scientific notation",
			v:    MustNewFromString("1.1e-7"),
			want: 8,
		},
		{
			name: "zero",
			v:    MustNewFromString("0"),
			want: 0,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := tt.v.NumFractionalDigits(); got != tt.want {
				t.Errorf("NumFractionalDigits() = %v, want %v", got, tt.want)
			}
		})
	}
}
