// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package coordinate

import (
	"math"
	"reflect"
	"testing"
	"time"
)

// verifyDimensionPanic will run the supplied func and make sure it panics with
// the expected error type.
func verifyDimensionPanic(t *testing.T, f func()) {
	defer func() {
		if r := recover(); r != nil {
			if _, ok := r.(DimensionalityConflictError); !ok {
				t.Fatalf("panic isn't the right type")
			}
		} else {
			t.Fatalf("didn't get expected panic")
		}
	}()
	f()
}

func TestCoordinate_NewCoordinate(t *testing.T) {
	config := DefaultConfig()
	c := NewCoordinate(config)
	if uint(len(c.Vec)) != config.Dimensionality {
		t.Fatalf("dimensionality not set correctly %d != %d",
			len(c.Vec), config.Dimensionality)
	}
}

func TestCoordinate_Clone(t *testing.T) {
	c := NewCoordinate(DefaultConfig())
	c.Vec[0], c.Vec[1], c.Vec[2] = 1.0, 2.0, 3.0
	c.Error = 5.0
	c.Adjustment = 10.0
	c.Height = 4.2

	other := c.Clone()
	if !reflect.DeepEqual(c, other) {
		t.Fatalf("coordinate clone didn't make a proper copy")
	}

	other.Vec[0] = c.Vec[0] + 0.5
	if reflect.DeepEqual(c, other) {
		t.Fatalf("cloned coordinate is still pointing at its ancestor")
	}
}

func TestCoordinate_IsValid(t *testing.T) {
	c := NewCoordinate(DefaultConfig())

	var fields []*float64
	for i := range c.Vec {
		fields = append(fields, &c.Vec[i])
	}
	fields = append(fields, &c.Error)
	fields = append(fields, &c.Adjustment)
	fields = append(fields, &c.Height)

	for i, field := range fields {
		if !c.IsValid() {
			t.Fatalf("field %d should be valid", i)
		}

		*field = math.NaN()
		if c.IsValid() {
			t.Fatalf("field %d should not be valid (NaN)", i)
		}

		*field = 0.0
		if !c.IsValid() {
			t.Fatalf("field %d should be valid", i)
		}

		*field = math.Inf(0)
		if c.IsValid() {
			t.Fatalf("field %d should not be valid (Inf)", i)
		}

		*field = 0.0
		if !c.IsValid() {
			t.Fatalf("field %d should be valid", i)
		}
	}
}

func TestCoordinate_IsCompatibleWith(t *testing.T) {
	config := DefaultConfig()

	config.Dimensionality = 3
	c1 := NewCoordinate(config)
	c2 := NewCoordinate(config)

	config.Dimensionality = 2
	alien := NewCoordinate(config)

	if !c1.IsCompatibleWith(c1) || !c2.IsCompatibleWith(c2) ||
		!alien.IsCompatibleWith(alien) {
		t.Fatalf("coordinates should be compatible with themselves")
	}

	if !c1.IsCompatibleWith(c2) || !c2.IsCompatibleWith(c1) {
		t.Fatalf("coordinates should be compatible with each other")
	}

	if c1.IsCompatibleWith(alien) || c2.IsCompatibleWith(alien) ||
		alien.IsCompatibleWith(c1) || alien.IsCompatibleWith(c2) {
		t.Fatalf("alien should not be compatible with the other coordinates")
	}
}

func TestCoordinate_ApplyForce(t *testing.T) {
	config := DefaultConfig()
	config.Dimensionality = 3
	config.HeightMin = 0

	origin := NewCoordinate(config)

	// This proves that we normalize, get the direction right, and apply the
	// force multiplier correctly.
	above := NewCoordinate(config)
	above.Vec = []float64{0.0, 0.0, 2.9}
	c := origin.ApplyForce(config, 5.3, above)
	verifyEqualVectors(t, c.Vec, []float64{0.0, 0.0, -5.3})

	// Scoot a point not starting at the origin to make sure there's nothing
	// special there.
	right := NewCoordinate(config)
	right.Vec = []float64{3.4, 0.0, -5.3}
	c = c.ApplyForce(config, 2.0, right)
	verifyEqualVectors(t, c.Vec, []float64{-2.0, 0.0, -5.3})

	// If the points are right on top of each other, then we should end up
	// in a random direction, one unit away. This makes sure the unit vector
	// build up doesn't divide by zero.
	c = origin.ApplyForce(config, 1.0, origin)
	verifyEqualFloats(t, origin.DistanceTo(c).Seconds(), 1.0)

	// Enable a minimum height and make sure that gets factored in properly.
	config.HeightMin = 10.0e-6
	origin = NewCoordinate(config)
	c = origin.ApplyForce(config, 5.3, above)
	verifyEqualVectors(t, c.Vec, []float64{0.0, 0.0, -5.3})
	verifyEqualFloats(t, c.Height, config.HeightMin+5.3*config.HeightMin/2.9)

	// Make sure the height minimum is enforced.
	c = origin.ApplyForce(config, -5.3, above)
	verifyEqualVectors(t, c.Vec, []float64{0.0, 0.0, 5.3})
	verifyEqualFloats(t, c.Height, config.HeightMin)

	// Shenanigans should get called if the dimensions don't match.
	bad := c.Clone()
	bad.Vec = make([]float64, len(bad.Vec)+1)
	verifyDimensionPanic(t, func() { c.ApplyForce(config, 1.0, bad) })
}

func TestCoordinate_DistanceTo(t *testing.T) {
	config := DefaultConfig()
	config.Dimensionality = 3
	config.HeightMin = 0

	c1, c2 := NewCoordinate(config), NewCoordinate(config)
	c1.Vec = []float64{-0.5, 1.3, 2.4}
	c2.Vec = []float64{1.2, -2.3, 3.4}

	verifyEqualFloats(t, c1.DistanceTo(c1).Seconds(), 0.0)
	verifyEqualFloats(t, c1.DistanceTo(c2).Seconds(), c2.DistanceTo(c1).Seconds())
	verifyEqualFloats(t, c1.DistanceTo(c2).Seconds(), 4.104875150354758)

	// Make sure negative adjustment factors are ignored.
	c1.Adjustment = -1.0e6
	verifyEqualFloats(t, c1.DistanceTo(c2).Seconds(), 4.104875150354758)

	// Make sure positive adjustment factors affect the distance.
	c1.Adjustment = 0.1
	c2.Adjustment = 0.2
	verifyEqualFloats(t, c1.DistanceTo(c2).Seconds(), 4.104875150354758+0.3)

	// Make sure the heights affect the distance.
	c1.Height = 0.7
	c2.Height = 0.1
	verifyEqualFloats(t, c1.DistanceTo(c2).Seconds(), 4.104875150354758+0.3+0.8)

	// Shenanigans should get called if the dimensions don't match.
	bad := c1.Clone()
	bad.Vec = make([]float64, len(bad.Vec)+1)
	verifyDimensionPanic(t, func() { _ = c1.DistanceTo(bad) })
}

// dist is a self-contained example that appears in documentation.
func dist(a *Coordinate, b *Coordinate) time.Duration {
	// Coordinates will always have the same dimensionality, so this is
	// just a sanity check.
	if len(a.Vec) != len(b.Vec) {
		panic("dimensions aren't compatible")
	}

	// Calculate the Euclidean distance plus the heights.
	sumsq := 0.0
	for i := 0; i < len(a.Vec); i++ {
		diff := a.Vec[i] - b.Vec[i]
		sumsq += diff * diff
	}
	rtt := math.Sqrt(sumsq) + a.Height + b.Height

	// Apply the adjustment components, guarding against negatives.
	adjusted := rtt + a.Adjustment + b.Adjustment
	if adjusted > 0.0 {
		rtt = adjusted
	}

	// Go's times are natively nanoseconds, so we convert from seconds.
	const secondsToNanoseconds = 1.0e9
	return time.Duration(rtt * secondsToNanoseconds)
}

func TestCoordinate_dist_Example(t *testing.T) {
	config := DefaultConfig()
	c1, c2 := NewCoordinate(config), NewCoordinate(config)
	c1.Vec = []float64{-0.5, 1.3, 2.4}
	c2.Vec = []float64{1.2, -2.3, 3.4}
	c1.Adjustment = 0.1
	c2.Adjustment = 0.2
	c1.Height = 0.7
	c2.Height = 0.1
	verifyEqualFloats(t, c1.DistanceTo(c2).Seconds(), dist(c1, c2).Seconds())
}

func TestCoordinate_rawDistanceTo(t *testing.T) {
	config := DefaultConfig()
	config.Dimensionality = 3
	config.HeightMin = 0

	c1, c2 := NewCoordinate(config), NewCoordinate(config)
	c1.Vec = []float64{-0.5, 1.3, 2.4}
	c2.Vec = []float64{1.2, -2.3, 3.4}

	verifyEqualFloats(t, c1.rawDistanceTo(c1), 0.0)
	verifyEqualFloats(t, c1.rawDistanceTo(c2), c2.rawDistanceTo(c1))
	verifyEqualFloats(t, c1.rawDistanceTo(c2), 4.104875150354758)

	// Make sure that the adjustment doesn't factor into the raw
	// distance.
	c1.Adjustment = 1.0e6
	verifyEqualFloats(t, c1.rawDistanceTo(c2), 4.104875150354758)

	// Make sure the heights affect the distance.
	c1.Height = 0.7
	c2.Height = 0.1
	verifyEqualFloats(t, c1.rawDistanceTo(c2), 4.104875150354758+0.8)
}

func TestCoordinate_add(t *testing.T) {
	vec1 := []float64{1.0, -3.0, 3.0}
	vec2 := []float64{-4.0, 5.0, 6.0}
	verifyEqualVectors(t, add(vec1, vec2), []float64{-3.0, 2.0, 9.0})

	zero := []float64{0.0, 0.0, 0.0}
	verifyEqualVectors(t, add(vec1, zero), vec1)
}

func TestCoordinate_diff(t *testing.T) {
	vec1 := []float64{1.0, -3.0, 3.0}
	vec2 := []float64{-4.0, 5.0, 6.0}
	verifyEqualVectors(t, diff(vec1, vec2), []float64{5.0, -8.0, -3.0})

	zero := []float64{0.0, 0.0, 0.0}
	verifyEqualVectors(t, diff(vec1, zero), vec1)
}

func TestCoordinate_magnitude(t *testing.T) {
	zero := []float64{0.0, 0.0, 0.0}
	verifyEqualFloats(t, magnitude(zero), 0.0)

	vec := []float64{1.0, -2.0, 3.0}
	verifyEqualFloats(t, magnitude(vec), 3.7416573867739413)
}

func TestCoordinate_unitVectorAt(t *testing.T) {
	vec1 := []float64{1.0, 2.0, 3.0}
	vec2 := []float64{0.5, 0.6, 0.7}
	u, mag := unitVectorAt(vec1, vec2)
	verifyEqualVectors(t, u, []float64{0.18257418583505536, 0.511207720338155, 0.8398412548412546})
	verifyEqualFloats(t, magnitude(u), 1.0)
	verifyEqualFloats(t, mag, magnitude(diff(vec1, vec2)))

	// If we give positions that are equal we should get a random unit vector
	// returned to us, rather than a divide by zero.
	u, mag = unitVectorAt(vec1, vec1)
	verifyEqualFloats(t, magnitude(u), 1.0)
	verifyEqualFloats(t, mag, 0.0)

	// We can't hit the final clause without heroics so I manually forced it
	// there to verify it works.
}
