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

package coordinate

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

func TestPerformance_Line(t *testing.T) {
	const spacing = 10 * time.Millisecond
	const nodes, cycles = 10, 1000
	config := DefaultConfig()
	clients, err := GenerateClients(nodes, config)
	if err != nil {
		t.Fatal(err)
	}
	truth := GenerateLine(nodes, spacing)
	Simulate(clients, truth, cycles)
	stats := Evaluate(clients, truth)
	if stats.ErrorAvg > 0.0018 || stats.ErrorMax > 0.0092 {
		t.Fatalf("performance stats are out of spec: %v", stats)
	}
}

func TestPerformance_Grid(t *testing.T) {
	const spacing = 10 * time.Millisecond
	const nodes, cycles = 25, 1000
	config := DefaultConfig()
	clients, err := GenerateClients(nodes, config)
	if err != nil {
		t.Fatal(err)
	}
	truth := GenerateGrid(nodes, spacing)
	Simulate(clients, truth, cycles)
	stats := Evaluate(clients, truth)
	if stats.ErrorAvg > 0.0015 || stats.ErrorMax > 0.022 {
		t.Fatalf("performance stats are out of spec: %v", stats)
	}
}

func TestPerformance_Split(t *testing.T) {
	const lan, wan = 1 * time.Millisecond, 10 * time.Millisecond
	const nodes, cycles = 25, 1000
	config := DefaultConfig()
	clients, err := GenerateClients(nodes, config)
	if err != nil {
		t.Fatal(err)
	}
	truth := GenerateSplit(nodes, lan, wan)
	Simulate(clients, truth, cycles)
	stats := Evaluate(clients, truth)
	if stats.ErrorAvg > 0.000060 || stats.ErrorMax > 0.00048 {
		t.Fatalf("performance stats are out of spec: %v", stats)
	}
}

func TestPerformance_Height(t *testing.T) {
	const radius = 100 * time.Millisecond
	const nodes, cycles = 25, 1000

	// Constrain us to two dimensions so that we can just exactly represent
	// the circle.
	config := DefaultConfig()
	config.Dimensionality = 2
	clients, err := GenerateClients(nodes, config)
	if err != nil {
		t.Fatal(err)
	}

	// Generate truth where the first coordinate is in the "middle" because
	// it's equidistant from all the nodes, but it will have an extra radius
	// added to the distance, so it should come out above all the others.
	truth := GenerateCircle(nodes, radius)
	Simulate(clients, truth, cycles)

	// Make sure the height looks reasonable with the regular nodes all in a
	// plane, and the center node up above.
	for i := range clients {
		coord := clients[i].GetCoordinate()
		if i == 0 {
			if coord.Height < 0.97*radius.Seconds() {
				t.Fatalf("height is out of spec: %9.6f", coord.Height)
			}
		} else {
			if coord.Height > 0.03*radius.Seconds() {
				t.Fatalf("height is out of spec: %9.6f", coord.Height)
			}
		}
	}
	stats := Evaluate(clients, truth)
	if stats.ErrorAvg > 0.0025 || stats.ErrorMax > 0.064 {
		t.Fatalf("performance stats are out of spec: %v", stats)
	}
}

func TestPerformance_Drift(t *testing.T) {
	const dist = 500 * time.Millisecond
	const nodes = 4
	config := DefaultConfig()
	config.Dimensionality = 2
	clients, err := GenerateClients(nodes, config)
	if err != nil {
		t.Fatal(err)
	}

	// Do some icky surgery on the clients to put them into a square, up in
	// the first quadrant.
	clients[0].coord.Vec = []float64{0.0, 0.0}
	clients[1].coord.Vec = []float64{0.0, dist.Seconds()}
	clients[2].coord.Vec = []float64{dist.Seconds(), dist.Seconds()}
	clients[3].coord.Vec = []float64{dist.Seconds(), dist.Seconds()}

	// Make a corresponding truth matrix. The nodes are laid out like this
	// so the distances are all equal, except for the diagonal:
	//
	// (1)  <- dist ->  (2)
	//
	//  | <- dist        |
	//  |                |
	//  |        dist -> |
	//
	// (0)  <- dist ->  (3)
	//
	truth := make([][]time.Duration, nodes)
	for i := range truth {
		truth[i] = make([]time.Duration, nodes)
	}
	for i := 0; i < nodes; i++ {
		for j := i + 1; j < nodes; j++ {
			rtt := dist
			if (i%2 == 0) && (j%2 == 0) {
				rtt = time.Duration(math.Sqrt2 * float64(rtt))
			}
			truth[i][j], truth[j][i] = rtt, rtt
		}
	}

	calcCenterError := func() float64 {
		min, max := clients[0].GetCoordinate(), clients[0].GetCoordinate()
		for i := 1; i < nodes; i++ {
			coord := clients[i].GetCoordinate()
			for j, v := range coord.Vec {
				min.Vec[j] = math.Min(min.Vec[j], v)
				max.Vec[j] = math.Max(max.Vec[j], v)
			}
		}

		mid := make([]float64, config.Dimensionality)
		for i := range mid {
			mid[i] = min.Vec[i] + (max.Vec[i]-min.Vec[i])/2
		}
		return magnitude(mid)
	}

	// Let the simulation run for a while to stabilize, then snap a baseline
	// for the center error.
	Simulate(clients, truth, 1000)
	baseline := calcCenterError()

	// Now run for a bunch more cycles and see if gravity pulls the center
	// in the right direction.
	Simulate(clients, truth, 10000)
	if error := calcCenterError(); error > 0.8*baseline {
		t.Fatalf("drift performance out of spec: %9.6f -> %9.6f", baseline, error)
	}
}

func TestPerformance_Random(t *testing.T) {
	const mean, deviation = 100 * time.Millisecond, 10 * time.Millisecond
	const nodes, cycles = 25, 1000
	config := DefaultConfig()
	clients, err := GenerateClients(nodes, config)
	if err != nil {
		t.Fatal(err)
	}
	truth := GenerateRandom(nodes, mean, deviation)
	Simulate(clients, truth, cycles)
	stats := Evaluate(clients, truth)
	if stats.ErrorAvg > 0.075 || stats.ErrorMax > 0.33 {
		t.Fatalf("performance stats are out of spec: %v", stats)
	}
}
