// Copyright 2015 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 s2

import (
	"fmt"
	"math"
	"testing"

	"github.com/golang/geo/r1"
	"github.com/golang/geo/r3"
	"github.com/golang/geo/s1"
)

var (
	// The northern hemisphere, defined using two pairs of antipodal points.
	northHemi = LoopFromPoints(parsePoints("0:-180, 0:-90, 0:0, 0:90"))

	// The northern hemisphere, defined using three points 120 degrees apart.
	northHemi3 = LoopFromPoints(parsePoints("0:-180, 0:-60, 0:60"))

	// The southern hemisphere, defined using two pairs of antipodal points.
	southHemi = LoopFromPoints(parsePoints("0:90, 0:0, 0:-90, 0:-180"))

	// The western hemisphere, defined using two pairs of antipodal points.
	westHemi = LoopFromPoints(parsePoints("0:-180, -90:0, 0:0, 90:0"))

	// The eastern hemisphere, defined using two pairs of antipodal points.
	eastHemi = LoopFromPoints(parsePoints("90:0, 0:0, -90:0, 0:-180"))

	// The "near" hemisphere, defined using two pairs of antipodal points.
	nearHemi = LoopFromPoints(parsePoints("0:-90, -90:0, 0:90, 90:0"))

	// The "far" hemisphere, defined using two pairs of antipodal points.
	farHemi = LoopFromPoints(parsePoints("90:0, 0:90, -90:0, 0:-90"))

	// A spiral stripe that slightly over-wraps the equator.
	candyCane = LoopFromPoints(parsePoints("-20:150, -20:-70, 0:70, 10:-150, 10:70, -10:-70"))

	// A small clockwise loop in the northern & eastern hemisperes.
	smallNECW = LoopFromPoints(parsePoints("35:20, 45:20, 40:25"))

	// Loop around the north pole at 80 degrees.
	arctic80 = LoopFromPoints(parsePoints("80:-150, 80:-30, 80:90"))

	// Loop around the south pole at 80 degrees.
	antarctic80 = LoopFromPoints(parsePoints("-80:120, -80:0, -80:-120"))

	// A completely degenerate triangle along the equator that RobustCCW()
	// considers to be CCW.
	lineTriangle = LoopFromPoints(parsePoints("0:1, 0:2, 0:3"))

	// A nearly-degenerate CCW chevron near the equator with very long sides
	// (about 80 degrees).  Its area is less than 1e-640, which is too small
	// to represent in double precision.
	skinnyChevron = LoopFromPoints(parsePoints("0:0, -1e-320:80, 0:1e-320, 1e-320:80"))

	// A diamond-shaped loop around the point 0:180.
	loopA = LoopFromPoints(parsePoints("0:178, -1:180, 0:-179, 1:-180"))

	// Like loopA, but the vertices are at leaf cell centers.
	snappedLoopA = LoopFromPoints([]Point{
		cellIDFromPoint(parsePoint("0:178")).Point(),
		cellIDFromPoint(parsePoint("-1:180")).Point(),
		cellIDFromPoint(parsePoint("0:-179")).Point(),
		cellIDFromPoint(parsePoint("1:-180")).Point(),
	})

	// A different diamond-shaped loop around the point 0:180.
	loopB = LoopFromPoints(parsePoints("0:179, -1:180, 0:-178, 1:-180"))

	// The intersection of A and B.
	aIntersectB = LoopFromPoints(parsePoints("0:179, -1:180, 0:-179, 1:-180"))

	// The union of A and B.
	aUnionB = LoopFromPoints(parsePoints("0:178, -1:180, 0:-178, 1:-180"))

	// A minus B (concave).
	aMinusB = LoopFromPoints(parsePoints("0:178, -1:180, 0:179, 1:-180"))

	// B minus A (concave).
	bMinusA = LoopFromPoints(parsePoints("0:-179, -1:180, 0:-178, 1:-180"))

	// A shape gotten from A by adding a triangle to one edge, and
	// subtracting a triangle from the opposite edge.
	loopC = LoopFromPoints(parsePoints("0:178, 0:180, -1:180, 0:-179, 1:-179, 1:-180"))

	// A shape gotten from A by adding a triangle to one edge, and
	// adding another triangle to the opposite edge.
	loopD = LoopFromPoints(parsePoints("0:178, -1:178, -1:180, 0:-179, 1:-179, 1:-180"))

	//   3------------2
	//   |            |               ^
	//   |  7-8  b-c  |               |
	//   |  | |  | |  |      Latitude |
	//   0--6-9--a-d--1               |
	//   |  | |       |               |
	//   |  f-e       |               +----------->
	//   |            |                 Longitude
	//   4------------5
	//
	// Important: It is not okay to skip over collinear vertices when
	// defining these loops (e.g. to define loop E as "0,1,2,3") because S2
	// uses symbolic perturbations to ensure that no three vertices are
	// *ever* considered collinear (e.g., vertices 0, 6, 9 are not
	// collinear).  In other words, it is unpredictable (modulo knowing the
	// details of the symbolic perturbations) whether 0123 contains 06123
	// for example.

	// Loop E:  0,6,9,a,d,1,2,3
	// Loop F:  0,4,5,1,d,a,9,6
	// Loop G:  0,6,7,8,9,a,b,c,d,1,2,3
	// Loop H:  0,6,f,e,9,a,b,c,d,1,2,3
	// Loop I:  7,6,f,e,9,8
	loopE = LoopFromPoints(parsePoints("0:30, 0:34, 0:36, 0:39, 0:41, 0:44, 30:44, 30:30"))
	loopF = LoopFromPoints(parsePoints("0:30, -30:30, -30:44, 0:44, 0:41, 0:39, 0:36, 0:34"))
	loopG = LoopFromPoints(parsePoints("0:30, 0:34, 10:34, 10:36, 0:36, 0:39, 10:39, 10:41, 0:41, 0:44, 30:44, 30:30"))
	loopH = LoopFromPoints(parsePoints("0:30, 0:34, -10:34, -10:36, 0:36, 0:39, 10:39, 10:41, 0:41, 0:44, 30:44, 30:30"))
	loopI = LoopFromPoints(parsePoints("10:34, 0:34, -10:34, -10:36, 0:36, 10:36"))

	// The set of all test loops.
	allLoops = []*Loop{
		EmptyLoop(),
		FullLoop(),
		northHemi,
		northHemi3,
		southHemi,
		westHemi,
		eastHemi,
		nearHemi,
		farHemi,
		candyCane,
		smallNECW,
		arctic80,
		antarctic80,
		lineTriangle,
		skinnyChevron,
		loopA,
		// snappedLoopA, // Fails TestAreaConsistentWithTurningAngle
		loopB,
		aIntersectB,
		aUnionB,
		aMinusB,
		bMinusA,
		loopC,
		loopD,
		loopE,
		loopF,
		loopG,
		loopH,
		loopI,
	}
)

func TestLoopEmptyLoop(t *testing.T) {
	shape := EmptyLoop()

	if got, want := shape.NumEdges(), 0; got != want {
		t.Errorf("shape.NumEdges() = %v, want %v", got, want)
	}
	if got, want := shape.NumChains(), 0; got != want {
		t.Errorf("shape.NumChains() = %v, want %v", got, want)
	}
	if got, want := shape.Dimension(), 2; got != want {
		t.Errorf("shape.Dimension() = %v, want %v", got, want)
	}
	if !shape.IsEmpty() {
		t.Errorf("shape.IsEmpty() = false, want true")
	}
	if shape.IsFull() {
		t.Errorf("shape.IsFull() = true, want false")
	}
	if !shape.isEmptyOrFull() {
		t.Errorf("shape.isEmptyOrFull = false, want true")
	}
	if shape.ReferencePoint().Contained {
		t.Errorf("shape.ReferencePoint().Contained = true, want false")
	}
}

func TestLoopFullLoop(t *testing.T) {
	shape := FullLoop()

	if got, want := shape.NumEdges(), 0; got != want {
		t.Errorf("shape.NumEdges() = %v, want %v", got, want)
	}
	if got, want := shape.NumChains(), 1; got != want {
		t.Errorf("shape.NumChains() = %v, want %v", got, want)
	}
	if got, want := shape.Dimension(), 2; got != want {
		t.Errorf("shape.Dimension() = %v, want %v", got, want)
	}
	if shape.IsEmpty() {
		t.Errorf("shape.IsEmpty() = true, want false")
	}
	if !shape.IsFull() {
		t.Errorf("shape.IsFull() = false, want true")
	}
	if !shape.isEmptyOrFull() {
		t.Errorf("shape.isEmptyOrFull = false, want true")
	}
	if !shape.ReferencePoint().Contained {
		t.Errorf("shape.ReferencePoint().Contained = false, want true")
	}
}

func TestLoopBasic(t *testing.T) {
	shape := Shape(makeLoop("0:0, 0:1, 1:0"))

	if got, want := shape.NumEdges(), 3; got != want {
		t.Errorf("shape.NumEdges() = %d, want %d", got, want)
	}
	if got, want := shape.NumChains(), 1; got != 1 {
		t.Errorf("shape.NumChains() = %d, want %d", got, want)
	}
	if got, want := shape.Chain(0).Start, 0; got != 0 {
		t.Errorf("shape.Chain(0).Start = %d, want %d", got, want)
	}
	if got, want := shape.Chain(0).Length, 3; got != want {
		t.Errorf("shape.Chain(0).Length = %d, want %d", got, want)
	}

	e := shape.Edge(2)
	if want := PointFromLatLng(LatLngFromDegrees(1, 0)); !e.V0.ApproxEqual(want) {
		t.Errorf("shape.Edge(2) end A = %v, want %v", e.V0, want)
	}
	if want := PointFromLatLng(LatLngFromDegrees(0, 0)); !e.V1.ApproxEqual(want) {
		t.Errorf("shape.Edge(2) end B = %v, want %v", e.V1, want)
	}
	if got, want := shape.Dimension(), 2; got != want {
		t.Errorf("shape.Dimension() = %v, want %v", got, want)
	}
	if shape.IsEmpty() {
		t.Errorf("shape.IsEmpty() = true, want false")
	}
	if shape.IsFull() {
		t.Errorf("shape.IsFull() = true, want false")
	}
	if shape.ReferencePoint().Contained {
		t.Errorf("shape.ReferencePoint().Contained = true, want false")
	}
}

func TestLoopHoleAndSign(t *testing.T) {
	l := LoopFromPoints(parsePoints("0:-180, 0:-90, 0:0, 0:90"))

	if l.IsHole() {
		t.Errorf("loop with default depth should not be a hole")
	}
	if l.Sign() == -1 {
		t.Errorf("loop with default depth should have a sign of +1")
	}

	l.depth = 3
	if !l.IsHole() {
		t.Errorf("loop with odd depth should be a hole")
	}
	if l.Sign() != -1 {
		t.Errorf("loop with odd depth should have a sign of -1")
	}

	l.depth = 2
	if l.IsHole() {
		t.Errorf("loop with even depth should not be a hole")
	}
	if l.Sign() == -1 {
		t.Errorf("loop with even depth should have a sign of +1")
	}

}

func TestLoopRectBound(t *testing.T) {
	rectError := NewRectBounder().maxErrorForTests()

	if !EmptyLoop().RectBound().IsEmpty() {
		t.Errorf("empty loop's RectBound should be empty")
	}
	if !FullLoop().RectBound().IsFull() {
		t.Errorf("full loop's RectBound should be full")
	}
	if !candyCane.RectBound().Lng.IsFull() {
		t.Errorf("candy cane loop's RectBound should have a full longitude range")
	}
	if got := candyCane.RectBound().Lat.Lo; got >= -0.349066 {
		t.Errorf("candy cane loop's RectBound should have a lower latitude (%v) under -0.349066 radians", got)
	}
	if got := candyCane.RectBound().Lat.Hi; got <= 0.174533 {
		t.Errorf("candy cane loop's RectBound should have an upper latitude (%v) over 0.174533 radians", got)
	}
	if !smallNECW.RectBound().IsFull() {
		t.Errorf("small northeast clockwise loop's RectBound should be full")
	}
	if got, want := arctic80.RectBound(), rectFromDegrees(80, -180, 90, 180); !rectsApproxEqual(got, want, rectError.Lat.Radians(), rectError.Lng.Radians()) {
		t.Errorf("arctic 80 loop's RectBound (%v) should be %v", got, want)
	}
	if got, want := antarctic80.RectBound(), rectFromDegrees(-90, -180, -80, 180); !rectsApproxEqual(got, want, rectError.Lat.Radians(), rectError.Lng.Radians()) {
		t.Errorf("antarctic 80 loop's RectBound (%v) should be %v", got, want)
	}

	if !southHemi.RectBound().Lng.IsFull() {
		t.Errorf("south hemi loop's RectBound should have a full longitude range")
	}
	if got, want := southHemi.RectBound().Lat, (r1.Interval{Lo: -math.Pi / 2, Hi: 0}); !r1IntervalsApproxEqual(got, want, rectError.Lat.Radians()) {
		t.Errorf("south hemi loop's RectBound latitude interval (%v) should be %v", got, want)
	}

	// Create a loop that contains the complement of the arctic80 loop.
	arctic80Inv := cloneLoop(arctic80)
	arctic80Inv.Invert()
	// The highest latitude of each edge is attained at its midpoint.
	mid := Point{arctic80Inv.vertices[0].Vector.Add(arctic80Inv.vertices[1].Vector).Mul(.5)}
	if got, want := arctic80Inv.RectBound().Lat.Hi, float64(LatLngFromPoint(mid).Lat); !float64Near(got, want, 10*dblEpsilon) {
		t.Errorf("arctic 80 inverse loop's RectBound should have a latutude hi of %v, got %v", got, want)
	}
}

func TestLoopCapBound(t *testing.T) {
	if !EmptyLoop().CapBound().IsEmpty() {
		t.Errorf("empty loop's CapBound should be empty")
	}
	if !FullLoop().CapBound().IsFull() {
		t.Errorf("full loop's CapBound should be full")
	}
	if !smallNECW.CapBound().IsFull() {
		t.Errorf("small northeast clockwise loop's CapBound should be full")
	}
	if got, want := arctic80.CapBound(), rectFromDegrees(80, -180, 90, 180).CapBound(); !got.ApproxEqual(want) {
		t.Errorf("arctic 80 loop's CapBound (%v) should be %v", got, want)
	}
	if got, want := antarctic80.CapBound(), rectFromDegrees(-90, -180, -80, 180).CapBound(); !got.ApproxEqual(want) {
		t.Errorf("antarctic 80 loop's CapBound (%v) should be %v", got, want)
	}
}

func TestLoopOriginInside(t *testing.T) {
	if !northHemi.originInside {
		t.Errorf("north hemisphere polygon should include origin")
	}
	if !northHemi3.originInside {
		t.Errorf("north hemisphere 3 polygon should include origin")
	}
	if southHemi.originInside {
		t.Errorf("south hemisphere polygon should not include origin")
	}
	if westHemi.originInside {
		t.Errorf("west hemisphere polygon should not include origin")
	}
	if !eastHemi.originInside {
		t.Errorf("east hemisphere polygon should include origin")
	}
	if nearHemi.originInside {
		t.Errorf("near hemisphere polygon should not include origin")
	}
	if !farHemi.originInside {
		t.Errorf("far hemisphere polygon should include origin")
	}
	if candyCane.originInside {
		t.Errorf("candy cane polygon should not include origin")
	}
	if !smallNECW.originInside {
		t.Errorf("smallNECW polygon should include origin")
	}
	if !arctic80.originInside {
		t.Errorf("arctic 80 polygon should include origin")
	}
	if antarctic80.originInside {
		t.Errorf("antarctic 80 polygon should not include origin")
	}
	if loopA.originInside {
		t.Errorf("loop A polygon should not include origin")
	}
}

func rotate(l *Loop) *Loop {
	vertices := make([]Point, 0, len(l.vertices))
	for i := 1; i < len(l.vertices); i++ {
		vertices = append(vertices, l.vertices[i])
	}
	vertices = append(vertices, l.vertices[0])
	return LoopFromPoints(vertices)
}

func TestLoopContainsPoint(t *testing.T) {
	north := Point{r3.Vector{X: 0, Y: 0, Z: 1}}
	south := Point{r3.Vector{X: 0, Y: 0, Z: -1}}
	east := PointFromCoords(0, 1, 0)
	west := PointFromCoords(0, -1, 0)

	if EmptyLoop().ContainsPoint(north) {
		t.Errorf("empty loop should not have any points")
	}
	if !FullLoop().ContainsPoint(south) {
		t.Errorf("full loop should have full point vertex")
	}

	for _, tc := range []struct {
		name string
		l    *Loop
		in   Point
		out  Point
	}{
		{
			"north hemisphere",
			northHemi,
			north,
			south,
		},
		{
			"south hemisphere",
			southHemi,
			south,
			north,
		},
		{
			"west hemisphere",
			westHemi,
			west,
			east,
		},
		{
			"east hemisphere",
			eastHemi,
			east,
			west,
		},
		{
			"candy cane",
			candyCane,
			PointFromLatLng(LatLngFromDegrees(5, 71)),
			PointFromLatLng(LatLngFromDegrees(-8, 71)),
		},
	} {
		l := tc.l
		for i := 0; i < 4; i++ {
			if !l.ContainsPoint(tc.in) {
				t.Errorf("%s loop should contain %v at rotation %d", tc.name, tc.in, i)
			}
			if l.ContainsPoint(tc.out) {
				t.Errorf("%s loop shouldn't contain %v at rotation %d", tc.name, tc.out, i)
			}
			l = rotate(l)
		}
	}

	// This code checks each cell vertex is contained by exactly one of
	// the adjacent cells.
	for level := 0; level < 3; level++ {
		// set of unique points across all loops at this level.
		points := make(map[Point]bool)
		var loops []*Loop
		for id := CellIDFromFace(0).ChildBeginAtLevel(level); id != CellIDFromFace(5).ChildEndAtLevel(level); id = id.Next() {
			var vertices []Point
			cell := CellFromCellID(id)
			points[cell.Center()] = true
			for k := 0; k < 4; k++ {
				vertices = append(vertices, cell.Vertex(k))
				points[cell.Vertex(k)] = true
			}
			loops = append(loops, LoopFromPoints(vertices))
		}

		for point := range points {
			count := 0
			for _, loop := range loops {
				if loop.ContainsPoint(point) {
					count++
				}
			}
			if count != 1 {
				t.Errorf("point %v should only be contained by one loop at level %d, got %d", point, level, count)
			}
		}
	}
}

func TestLoopVertex(t *testing.T) {
	tests := []struct {
		loop   *Loop
		vertex int
		want   Point
	}{
		{EmptyLoop(), 0, Point{r3.Vector{X: 0, Y: 0, Z: 1}}},
		{EmptyLoop(), 1, Point{r3.Vector{X: 0, Y: 0, Z: 1}}},
		{FullLoop(), 0, Point{r3.Vector{X: 0, Y: 0, Z: -1}}},
		{FullLoop(), 1, Point{r3.Vector{X: 0, Y: 0, Z: -1}}},
		{arctic80, 0, parsePoint("80:-150")},
		{arctic80, 1, parsePoint("80:-30")},
		{arctic80, 2, parsePoint("80:90")},
		{arctic80, 3, parsePoint("80:-150")},
	}

	for _, test := range tests {
		if got := test.loop.Vertex(test.vertex); !pointsApproxEqual(got, test.want, epsilon) {
			t.Errorf("%v.Vertex(%d) = %v, want %v", test.loop, test.vertex, got, test.want)
		}
	}

	// Check that wrapping is correct.
	if !pointsApproxEqual(arctic80.Vertex(2), arctic80.Vertex(5), epsilon) {
		t.Errorf("Vertex should wrap values. %v.Vertex(2) = %v != %v.Vertex(5) = %v",
			arctic80, arctic80.Vertex(2), arctic80, arctic80.Vertex(5))
	}

	loopAroundThrice := 2 + 3*len(arctic80.vertices)
	if !pointsApproxEqual(arctic80.Vertex(2), arctic80.Vertex(loopAroundThrice), epsilon) {
		t.Errorf("Vertex should wrap values. %v.Vertex(2) = %v != %v.Vertex(%d) = %v",
			arctic80, arctic80.Vertex(2), arctic80, loopAroundThrice, arctic80.Vertex(loopAroundThrice))
	}
}

func TestLoopNumEdges(t *testing.T) {
	tests := []struct {
		loop *Loop
		want int
	}{
		{EmptyLoop(), 0},
		{FullLoop(), 0},
		{farHemi, 4},
		{candyCane, 6},
		{smallNECW, 3},
		{arctic80, 3},
		{antarctic80, 3},
		{lineTriangle, 3},
		{skinnyChevron, 4},
	}

	for _, test := range tests {
		if got := test.loop.NumEdges(); got != test.want {
			t.Errorf("%v.NumEdges() = %v, want %v", test.loop, got, test.want)
		}
	}
}

func TestLoopEdge(t *testing.T) {
	tests := []struct {
		loop  *Loop
		edge  int
		wantA Point
		wantB Point
	}{
		{
			loop:  farHemi,
			edge:  2,
			wantA: Point{r3.Vector{X: 0, Y: 0, Z: -1}},
			wantB: Point{r3.Vector{X: 0, Y: -1, Z: 0}},
		},
		{
			loop: candyCane,
			edge: 0,

			wantA: parsePoint("-20:150"),
			wantB: parsePoint("-20:-70"),
		},
		{
			loop:  candyCane,
			edge:  1,
			wantA: parsePoint("-20:-70"),
			wantB: parsePoint("0:70"),
		},
		{
			loop:  candyCane,
			edge:  2,
			wantA: parsePoint("0:70"),
			wantB: parsePoint("10:-150"),
		},
		{
			loop:  candyCane,
			edge:  3,
			wantA: parsePoint("10:-150"),
			wantB: parsePoint("10:70"),
		},
		{
			loop:  candyCane,
			edge:  4,
			wantA: parsePoint("10:70"),
			wantB: parsePoint("-10:-70"),
		},
		{
			loop:  candyCane,
			edge:  5,
			wantA: parsePoint("-10:-70"),
			wantB: parsePoint("-20:150"),
		},
		{
			loop:  skinnyChevron,
			edge:  2,
			wantA: parsePoint("0:1e-320"),
			wantB: parsePoint("1e-320:80"),
		},
		{
			loop:  skinnyChevron,
			edge:  3,
			wantA: parsePoint("1e-320:80"),
			wantB: parsePoint("0:0"),
		},
	}

	for _, test := range tests {
		if e := test.loop.Edge(test.edge); !(pointsApproxEqual(e.V0, test.wantA, epsilon) && pointsApproxEqual(e.V1, test.wantB, epsilon)) {
			t.Errorf("%v.Edge(%d) = %v, want (%v, %v)", test.loop, test.edge, e, test.wantA, test.wantB)
		}
	}
}

func TestLoopFromCell(t *testing.T) {
	cell := CellFromCellID(CellIDFromLatLng(LatLng{40.565459 * s1.Degree, -74.645276 * s1.Degree}))
	loopFromCell := LoopFromCell(cell)

	// Demonstrates the reason for this test; the cell bounds are more
	// conservative than the resulting loop bounds.
	if loopFromCell.RectBound().Contains(cell.RectBound()) {
		t.Errorf("loopFromCell's RectBound contains the original cells RectBound, but should not")
	}
}

func TestLoopRegularLoop(t *testing.T) {
	loop := RegularLoop(PointFromLatLng(LatLngFromDegrees(80, 135)), 20*s1.Degree, 4)
	if len(loop.vertices) != 4 {
		t.Errorf("RegularLoop with 4 vertices should have 4 vertices, got %d", len(loop.vertices))
	}
	// The actual Points values are already tested in the s2point_test method TestRegularPoints.
}

// cloneLoop creates a new copy of the given loop including all of its vertices
// so that when tests modify vertices in it, it won't ruin the original loop.
func cloneLoop(l *Loop) *Loop {
	c := &Loop{
		vertices:       make([]Point, len(l.vertices)),
		originInside:   l.originInside,
		bound:          l.bound,
		subregionBound: l.subregionBound,
		index:          NewShapeIndex(),
	}
	copy(c.vertices, l.vertices)
	c.index.Add(c)

	return c
}

func TestLoopEqual(t *testing.T) {
	tests := []struct {
		a, b *Loop
		want bool
	}{
		{
			a:    EmptyLoop(),
			b:    EmptyLoop(),
			want: true,
		},
		{
			a:    FullLoop(),
			b:    FullLoop(),
			want: true,
		},
		{
			a:    EmptyLoop(),
			b:    FullLoop(),
			want: false,
		},
		{
			a:    candyCane,
			b:    candyCane,
			want: true,
		},
		{
			a:    candyCane,
			b:    rotate(candyCane),
			want: false,
		},
		{
			a:    candyCane,
			b:    rotate(rotate(candyCane)),
			want: false,
		},
		{
			a:    candyCane,
			b:    rotate(rotate(rotate(candyCane))),
			want: false,
		},
		{
			a:    candyCane,
			b:    rotate(rotate(rotate(rotate(candyCane)))),
			want: false,
		},
		{
			a:    candyCane,
			b:    rotate(rotate(rotate(rotate(rotate(candyCane))))),
			want: false,
		},
		{
			// candyCane has 6 points, so 6 rotates should line up again.
			a:    candyCane,
			b:    rotate(rotate(rotate(rotate(rotate(rotate(candyCane)))))),
			want: true,
		},
	}

	for _, test := range tests {
		if got := test.a.Equal(test.b); got != test.want {
			t.Errorf("%v.Equal(%v) = %t, want %t", test.a, test.b, got, test.want)
		}
	}
}

func TestLoopContainsMatchesCrossingSign(t *testing.T) {
	// This test demonstrates a former incompatibility between CrossingSign
	// and ContainsPoint. It constructs a Cell-based loop L and
	// an edge E from Origin to a0 that crosses exactly one edge of L.  Yet
	// previously, Contains() returned false for both endpoints of E.
	//
	// The reason for the bug was that the loop bound was sometimes too tight.
	// The Contains() code for a0 bailed out early because a0 was found not to
	// be inside the bound of L.

	// Start with a cell that ends up producing the problem.
	cellID := cellIDFromPoint(Point{r3.Vector{X: 1, Y: 1, Z: 1}}).Parent(21)
	children, ok := CellFromCellID(cellID).Children()
	if !ok {
		t.Fatalf("error subdividing cell")
	}

	points := make([]Point, 4)
	for i := 0; i < 4; i++ {
		// Note extra normalization. Center() is already normalized.
		// The test results will no longer be inconsistent if the extra
		// Normalize() is removed.
		points[i] = Point{children[i].Center().Normalize()}
	}

	// Get a vertex from a grandchild cell.
	// +---------------+---------------+
	// |               |               |
	// |    points[3]  |   points[2]   |
	// |       v       |       v       |
	// |       +-------+------ +       |
	// |       |       |       |       |
	// |       |       |       |       |
	// |       |       |       |       |
	// +-------+-------+-------+-------+
	// |       |       |       |       |
	// |       |    <----------------------- grandchild_cell
	// |       |       |       |       |
	// |       +-------+------ +       |
	// |       ^       |       ^       | <-- cell
	// | points[0]/a0  |     points[1] |
	// |               |               |
	// +---------------+---------------+
	loop := LoopFromPoints(points)
	grandchildren, ok := children[0].Children()
	if !ok {
		t.Fatalf("error subdividing cell")
	}

	grandchildCell := grandchildren[2]

	a0 := grandchildCell.Vertex(0)

	// This test depends on rounding errors that should make a0 slightly different from points[0]
	if points[0] == a0 {
		t.Errorf("%v not different enough from %v to successfully test", points[0], a0)
	}

	// The edge from a0 to the origin crosses one boundary.
	if got, want := NewChainEdgeCrosser(a0, OriginPoint(), loop.Vertex(0)).ChainCrossingSign(loop.Vertex(1)), DoNotCross; got != want {
		t.Errorf("crossingSign(%v, %v, %v, %v) = %v, want %v", a0, OriginPoint(), loop.Vertex(0), loop.Vertex(1), got, want)
	}

	if got, want := NewChainEdgeCrosser(a0, OriginPoint(), loop.Vertex(1)).ChainCrossingSign(loop.Vertex(2)), Cross; got != want {
		t.Errorf("crossingSign(%v, %v, %v, %v) = %v, want %v", a0, OriginPoint(), loop.Vertex(1), loop.Vertex(2), got, want)
	}

	if got, want := NewChainEdgeCrosser(a0, OriginPoint(), loop.Vertex(2)).ChainCrossingSign(loop.Vertex(3)), DoNotCross; got != want {
		t.Errorf("crossingSign(%v, %v, %v, %v) = %v, want %v", a0, OriginPoint(), loop.Vertex(2), loop.Vertex(3), got, want)
	}

	if got, want := NewChainEdgeCrosser(a0, OriginPoint(), loop.Vertex(3)).ChainCrossingSign(loop.Vertex(4)), DoNotCross; got != want {
		t.Errorf("crossingSign(%v, %v, %v, %v) = %v, want %v", a0, OriginPoint(), loop.Vertex(3), loop.Vertex(4), got, want)
	}

	// Contains should return false for the origin, and true for a0.
	if loop.ContainsPoint(OriginPoint()) {
		t.Errorf("%v.ContainsPoint(%v) = true, want false", loop, OriginPoint())
	}
	if !loop.ContainsPoint(a0) {
		t.Errorf("%v.ContainsPoint(%v) = false, want true", loop, a0)
	}

	// Since a0 is inside the loop, it should be inside the bound.
	bound := loop.RectBound()
	if !bound.ContainsPoint(a0) {
		t.Errorf("%v.RectBound().ContainsPoint(%v) = false, want true", loop, a0)
	}
}

func TestLoopRelations(t *testing.T) {
	// this loop is used in the regression test cases below.
	containingLoop := LoopFromPoints([]Point{
		PointFromLatLng(LatLngFromDegrees(-38.0, -135.0)),
		PointFromLatLng(LatLngFromDegrees(-38.0, 149.0)),
		PointFromLatLng(LatLngFromDegrees(77.0, 149.0)),
		PointFromLatLng(LatLngFromDegrees(77.0, -135.0)),
	})

	innerTile := LoopFromPoints([]Point{
		PointFromLatLng(LatLngFromDegrees(37.99616267972809, 13.007812500000002)),
		PointFromLatLng(LatLngFromDegrees(37.99616267972809, 13.359375000000002)),
		PointFromLatLng(LatLngFromDegrees(38.272819658516866, 13.359375000000002)),
		PointFromLatLng(LatLngFromDegrees(38.272819658516866, 13.007812500000002)),
	})

	// +0.2 lat +0.2 lon from innerTile
	extendedTile := LoopFromPoints([]Point{
		PointFromLatLng(LatLngFromDegrees(37.99616267972809, 13.007812500000002)),
		PointFromLatLng(LatLngFromDegrees(37.99616267972809, 13.559375000000002)),
		PointFromLatLng(LatLngFromDegrees(38.472819658516866, 13.559375000000002)),
		PointFromLatLng(LatLngFromDegrees(38.472819658516866, 13.007812500000002)),
	})

	tests := []struct {
		a, b       *Loop
		contains   bool // A contains B
		contained  bool // B contains A
		disjoint   bool // A and B are disjoint (intersection is empty)
		covers     bool // (A union B) covers the entire sphere
		sharedEdge bool // the loops share at least one edge (possibly reversed)
	}{
		// Check full and empty relationships with normal loops and each other.
		{
			a:          FullLoop(),
			b:          FullLoop(),
			contains:   true,
			contained:  true,
			covers:     true,
			sharedEdge: true,
		},
		{
			a:          FullLoop(),
			b:          northHemi,
			contains:   true,
			covers:     true,
			sharedEdge: false,
		},
		{
			a:          FullLoop(),
			b:          EmptyLoop(),
			contains:   true,
			disjoint:   true,
			covers:     true,
			sharedEdge: false,
		},
		{
			a:          northHemi,
			b:          FullLoop(),
			contained:  true,
			covers:     true,
			sharedEdge: false,
		},
		{
			a:          northHemi,
			b:          EmptyLoop(),
			contains:   true,
			disjoint:   true,
			sharedEdge: false,
		},
		{
			a:          EmptyLoop(),
			b:          FullLoop(),
			contained:  true,
			disjoint:   true,
			covers:     true,
			sharedEdge: false,
		},
		{
			a:          EmptyLoop(),
			b:          northHemi,
			contained:  true,
			disjoint:   true,
			sharedEdge: false,
		},
		{
			a:          EmptyLoop(),
			b:          EmptyLoop(),
			contains:   true,
			contained:  true,
			disjoint:   true,
			sharedEdge: false,
		},
		{
			a:          northHemi,
			b:          northHemi,
			contains:   true,
			contained:  true,
			sharedEdge: true,
		},
		{
			a:          northHemi,
			b:          southHemi,
			disjoint:   true,
			covers:     true,
			sharedEdge: true,
		},
		{
			a: northHemi,
			b: eastHemi,
		},
		{
			a:          northHemi,
			b:          arctic80,
			contains:   true,
			sharedEdge: false,
		},
		{
			a:          northHemi,
			b:          antarctic80,
			disjoint:   true,
			sharedEdge: false,
		},
		{
			a: northHemi,
			b: candyCane,
		},
		// We can't compare northHemi3 vs. northHemi or southHemi because the
		// result depends on the "simulation of simplicity" implementation details.
		{
			a:          northHemi3,
			b:          northHemi3,
			contains:   true,
			contained:  true,
			sharedEdge: true,
		},
		{
			a: northHemi3,
			b: eastHemi,
		},
		{
			a:          northHemi3,
			b:          arctic80,
			contains:   true,
			sharedEdge: false,
		},
		{
			a:          northHemi3,
			b:          antarctic80,
			disjoint:   true,
			sharedEdge: false,
		},
		{
			a: northHemi3,
			b: candyCane,
		},
		{
			a:          southHemi,
			b:          northHemi,
			disjoint:   true,
			covers:     true,
			sharedEdge: true,
		},
		{
			a:          southHemi,
			b:          southHemi,
			contains:   true,
			contained:  true,
			sharedEdge: true,
		},
		{
			a: southHemi,
			b: farHemi,
		},
		{
			a:          southHemi,
			b:          arctic80,
			disjoint:   true,
			sharedEdge: false,
		},
		// xxxx?
		{
			a:          southHemi,
			b:          antarctic80,
			contains:   true,
			sharedEdge: false,
		},
		{
			a: southHemi,
			b: candyCane,
		},
		{
			a: candyCane,
			b: northHemi,
		},
		{
			a: candyCane,
			b: southHemi,
		},
		{
			a:          candyCane,
			b:          arctic80,
			disjoint:   true,
			sharedEdge: false,
		},
		{
			a:          candyCane,
			b:          antarctic80,
			disjoint:   true,
			sharedEdge: false,
		},
		{
			a:          candyCane,
			b:          candyCane,
			contains:   true,
			contained:  true,
			sharedEdge: true,
		},
		{
			a: nearHemi,
			b: westHemi,
		},
		{
			a:          smallNECW,
			b:          southHemi,
			contains:   true,
			sharedEdge: false,
		},
		{
			a:          smallNECW,
			b:          westHemi,
			contains:   true,
			sharedEdge: false,
		},
		{
			a:          smallNECW,
			b:          northHemi,
			covers:     true,
			sharedEdge: false,
		},
		{
			a:          smallNECW,
			b:          eastHemi,
			covers:     true,
			sharedEdge: false,
		},
		{
			a:          loopA,
			b:          loopA,
			contains:   true,
			contained:  true,
			sharedEdge: true,
		},
		{
			a: loopA,
			b: loopB,
		},
		{
			a:          loopA,
			b:          aIntersectB,
			contains:   true,
			sharedEdge: true,
		},
		{
			a:          loopA,
			b:          aUnionB,
			contained:  true,
			sharedEdge: true,
		},
		{
			a:          loopA,
			b:          aMinusB,
			contains:   true,
			sharedEdge: true,
		},
		{
			a:          loopA,
			b:          bMinusA,
			disjoint:   true,
			sharedEdge: true,
		},
		{
			a: loopB,
			b: loopA,
		},
		{
			a:          loopB,
			b:          loopB,
			contains:   true,
			contained:  true,
			sharedEdge: true,
		},
		{
			a:          loopB,
			b:          aIntersectB,
			contains:   true,
			sharedEdge: true,
		},
		{
			a:          loopB,
			b:          aUnionB,
			contained:  true,
			sharedEdge: true,
		},
		{
			a:          loopB,
			b:          aMinusB,
			disjoint:   true,
			sharedEdge: true,
		},
		{
			a:          loopB,
			b:          bMinusA,
			contains:   true,
			sharedEdge: true,
		},
		{
			a:          aIntersectB,
			b:          loopA,
			contained:  true,
			sharedEdge: true,
		},
		{
			a:          aIntersectB,
			b:          loopB,
			contained:  true,
			sharedEdge: true,
		},
		{
			a:          aIntersectB,
			b:          aIntersectB,
			contains:   true,
			contained:  true,
			sharedEdge: true,
		},
		{
			a:          aIntersectB,
			b:          aUnionB,
			contained:  true,
			sharedEdge: false,
		},
		{
			a:          aIntersectB,
			b:          aMinusB,
			disjoint:   true,
			sharedEdge: true,
		},
		{
			a:          aIntersectB,
			b:          bMinusA,
			disjoint:   true,
			sharedEdge: true,
		},
		{
			a:          aUnionB,
			b:          loopA,
			contains:   true,
			sharedEdge: true,
		},
		{
			a:          aUnionB,
			b:          loopB,
			contains:   true,
			sharedEdge: true,
		},
		{
			a:          aUnionB,
			b:          aIntersectB,
			contains:   true,
			sharedEdge: false,
		},
		{
			a:          aUnionB,
			b:          aUnionB,
			contains:   true,
			contained:  true,
			sharedEdge: true,
		},
		{
			a:          aUnionB,
			b:          aMinusB,
			contains:   true,
			sharedEdge: true,
		},
		{
			a:          aUnionB,
			b:          bMinusA,
			contains:   true,
			sharedEdge: true,
		},
		{
			a:          aMinusB,
			b:          loopA,
			contained:  true,
			sharedEdge: true,
		},
		{
			a:          aMinusB,
			b:          loopB,
			disjoint:   true,
			sharedEdge: true,
		},
		{
			a:          aMinusB,
			b:          aIntersectB,
			disjoint:   true,
			sharedEdge: true,
		},
		{
			a:          aMinusB,
			b:          aUnionB,
			contained:  true,
			sharedEdge: true,
		},
		{
			a:          aMinusB,
			b:          aMinusB,
			contains:   true,
			contained:  true,
			sharedEdge: true,
		},
		{
			a:          aMinusB,
			b:          bMinusA,
			disjoint:   true,
			sharedEdge: false,
		},
		{
			a:          bMinusA,
			b:          loopA,
			disjoint:   true,
			sharedEdge: true,
		},
		{
			a:          bMinusA,
			b:          loopB,
			contained:  true,
			sharedEdge: true,
		},
		{
			a:          bMinusA,
			b:          aIntersectB,
			disjoint:   true,
			sharedEdge: true,
		},
		{
			a:          bMinusA,
			b:          aUnionB,
			contained:  true,
			sharedEdge: true,
		},
		{
			a:          bMinusA,
			b:          aMinusB,
			disjoint:   true,
			sharedEdge: false,
		},
		{
			a:          bMinusA,
			b:          bMinusA,
			contains:   true,
			contained:  true,
			sharedEdge: true,
		},
		// Make sure the relations are correct if the loop crossing happens on
		// two ends of a shared boundary segment.
		// LoopRelationsWhenSameExceptPiecesStickingOutAndIn
		{
			a:          loopA,
			b:          loopC,
			sharedEdge: true,
		},
		{
			a:          loopC,
			b:          loopA,
			sharedEdge: true,
		},
		{
			a:          loopA,
			b:          loopD,
			contained:  true,
			sharedEdge: true,
		},
		{
			a:          loopD,
			b:          loopA,
			contains:   true,
			sharedEdge: true,
		},
		{
			a:          loopE,
			b:          loopF,
			disjoint:   true,
			sharedEdge: true,
		},
		{
			a:          loopE,
			b:          loopG,
			contains:   true,
			sharedEdge: true,
		},
		{
			a:          loopE,
			b:          loopH,
			sharedEdge: true,
		},
		{
			a:          loopE,
			b:          loopI,
			sharedEdge: false,
		},
		{
			a:          loopF,
			b:          loopG,
			disjoint:   true,
			sharedEdge: true,
		},
		{
			a:          loopF,
			b:          loopH,
			sharedEdge: true,
		},
		{
			a:          loopF,
			b:          loopI,
			sharedEdge: false,
		},
		{
			a:          loopG,
			b:          loopH,
			contained:  true,
			sharedEdge: true,
		},
		{
			a:          loopH,
			b:          loopG,
			contains:   true,
			sharedEdge: true,
		},
		{
			a:          loopG,
			b:          loopI,
			disjoint:   true,
			sharedEdge: true,
		},
		{
			a:          loopH,
			b:          loopI,
			contains:   true,
			sharedEdge: true,
		},
		// Cases below here prevent regressions of bugs which
		// previously appeared in the golang version
		// and which are missing test coverage in the C++ codebase.
		{
			a:        containingLoop,
			b:        innerTile,
			contains: true,
		},
		{
			a:        containingLoop,
			b:        extendedTile,
			contains: true,
		},
	}

	for _, test := range tests {
		if test.contains {
			testLoopNestedPair(t, test.a, test.b)
		}
		if test.contained {
			testLoopNestedPair(t, test.b, test.a)
		}
		if test.covers {
			b1 := cloneLoop(test.b)
			b1.Invert()
			testLoopNestedPair(t, test.a, b1)
		}
		if test.disjoint {
			a1 := cloneLoop(test.a)
			a1.Invert()
			testLoopNestedPair(t, a1, test.b)
		} else if !test.contains && !test.contained && !test.covers {
			// Given loops A and B such that both A and its complement
			// intersect both B and its complement, test various
			// identities involving these four loops.
			a1 := cloneLoop(test.a)
			a1.Invert()
			b1 := cloneLoop(test.b)
			b1.Invert()
			testLoopOneOverlappingPair(t, test.a, test.b)
			testLoopOneOverlappingPair(t, a1, b1)
			testLoopOneOverlappingPair(t, a1, test.b)
			testLoopOneOverlappingPair(t, test.a, b1)
		}
		if !test.sharedEdge && (test.contains || test.contained || test.disjoint) {
			if got, want := test.a.Contains(test.b), test.a.ContainsNested(test.b); got != want {
				t.Errorf("%v.Contains(%v) = %v, but should equal %v.ContainsNested(%v) = %v", test.a, test.b, got, test.a, test.b, want)
			}
		}

		// A contains the boundary of B if either A contains B, or the two loops
		// contain each other's boundaries and there are no shared edges (since at
		// least one such edge must be reversed, and therefore is not considered to
		// be contained according to the rules of compareBoundary).
		comparison := 0
		if test.contains || (test.covers && !test.sharedEdge) {
			comparison = 1
		}

		// Similarly, A excludes the boundary of B if either A and B are disjoint,
		// or B contains A and there are no shared edges (since A is considered to
		// contain such edges according to the rules of compareBoundary).
		if test.disjoint || (test.contained && !test.sharedEdge) {
			comparison = -1
		}

		// compareBoundary requires that neither loop is empty.
		if !test.a.IsEmpty() && !test.b.IsEmpty() {
			if got := test.a.compareBoundary(test.b); got != comparison {
				t.Errorf("%v.compareBoundary(%v) = %v, want %v", test.a, test.b, got, comparison)
			}
		}
	}
}

// Given a pair of loops where A contains B, test various identities
// involving A, B, and their complements.
func testLoopNestedPair(t *testing.T, a, b *Loop) {
	a1 := cloneLoop(a)
	a1.Invert()
	b1 := cloneLoop(b)
	b1.Invert()
	testLoopOneNestedPair(t, a, b)
	testLoopOneNestedPair(t, b1, a1)
	testLoopOneDisjointPair(t, a1, b)
	testLoopOneCoveringPair(t, a, b1)
}

// Given a pair of loops where A contains B, check various identities.
func testLoopOneNestedPair(t *testing.T, a, b *Loop) {
	if !a.Contains(b) {
		t.Errorf("%v.Contains(%v) = false, want true", a, b)
	}
	if got, want := b.Contains(a), a.BoundaryEqual(b); got != want {
		t.Errorf("%v.Contains(%v) = %v, want %v", b, a, got, want)
	}
	if got, want := a.Intersects(b), !b.IsEmpty(); got != want {
		t.Errorf("%v.Intersects(%v) = %v, want %v", a, b, got, want)
	}
	if got, want := b.Intersects(a), !b.IsEmpty(); got != want {
		t.Errorf("%v.Intersects(%v) = %v, want %v", b, a, got, want)
	}
}

// Given a pair of disjoint loops A and B, check various identities.
func testLoopOneDisjointPair(t *testing.T, a, b *Loop) {
	if a.Intersects(b) {
		t.Errorf("%v.Intersects(%v) = true, want false", a, b)
	}
	if b.Intersects(a) {
		t.Errorf("%v.Intersects(%v) = true, want false", b, a)
	}
	if got, want := a.Contains(b), b.IsEmpty(); got != want {
		t.Errorf("%v.Contains(%v) = %v, want %v", a, b, got, want)
	}
	if got, want := b.Contains(a), a.IsEmpty(); got != want {
		t.Errorf("%v.Contains(%v) = %v, want %v", b, a, got, want)
	}
}

// Given loops A and B whose union covers the sphere, check various identities.
func testLoopOneCoveringPair(t *testing.T, a, b *Loop) {
	if got, want := a.Contains(b), a.IsFull(); got != want {
		t.Errorf("%v.Contains(%v) = %v, want %v", a, b, got, want)
	}
	if got, want := b.Contains(a), b.IsFull(); got != want {
		t.Errorf("%v.Contains(%v) = %v, want %v", b, a, got, want)
	}
	a1 := cloneLoop(a)
	a1.Invert()
	complementary := a1.BoundaryEqual(b)
	if got, want := a.Intersects(b), !complementary; got != want {
		t.Errorf("%v.Intersects(%v) = %v, want %v", a, b, got, want)
	}
	if got, want := b.Intersects(a), !complementary; got != want {
		t.Errorf("%v.Intersects(%v) = %v, want %v", b, a, got, want)
	}
}

// Given loops A and B such that both A and its complement intersect both B
// and its complement, check various identities.
func testLoopOneOverlappingPair(t *testing.T, a, b *Loop) {
	if a.Contains(b) {
		t.Errorf("%v.Contains(%v) = true want false", a, b)
	}
	if b.Contains(a) {
		t.Errorf("%v.Contains(%v) = true want false", b, a)
	}
	if !a.Intersects(b) {
		t.Errorf("%v.Intersects(%v) = false, want true", a, b)
	}
	if !b.Intersects(a) {
		t.Errorf("%v.Intersects(%v) = false, want true", b, a)
	}
}

func TestLoopTurningAngle(t *testing.T) {
	tests := []struct {
		loop *Loop
		want float64
	}{
		{EmptyLoop(), 2 * math.Pi},
		{FullLoop(), -2 * math.Pi},
		{northHemi3, 0},
		{westHemi, 0},
		{candyCane, 4.69364376125922},
		{lineTriangle, 2 * math.Pi},
		{skinnyChevron, 2 * math.Pi},
	}

	for _, test := range tests {
		if got := test.loop.TurningAngle(); !float64Near(got, test.want, epsilon) {
			t.Errorf("%v.TurningAngle() = %v, want %v", test.loop, got, test.want)
		}

		// Check that the turning angle is *identical* when the vertex order is
		// rotated, and that the sign is inverted when the vertices are reversed.
		expected := test.loop.TurningAngle()
		loopCopy := cloneLoop(test.loop)
		for i := 0; i < len(test.loop.vertices); i++ {
			loopCopy.Invert()
			if got := loopCopy.TurningAngle(); got != -expected {
				t.Errorf("loop.Invert().TurningAngle() = %v, want %v", got, -expected)
			}
			// Invert it back to normal.
			loopCopy.Invert()

			loopCopy = rotate(loopCopy)
			if got := loopCopy.TurningAngle(); got != expected {
				t.Errorf("loop.TurningAngle() = %v, want %v", got, expected)
			}
		}
	}

	// Build a narrow spiral loop starting at the north pole. This is designed
	// to test that the error in TurningAngle is linear in the number of
	// vertices even when the partial sum of the turning angles gets very large.
	// The spiral consists of two arms defining opposite sides of the loop.
	const armPoints = 10000 // Number of vertices in each "arm"
	const armRadius = 0.01  // Radius of spiral.
	var vertices = make([]Point, 2*armPoints)

	// Set the center point of the spiral.
	vertices[armPoints] = PointFromCoords(0, 0, 1)

	for i := 0; i < armPoints; i++ {
		angle := (2 * math.Pi / 3) * float64(i)
		x := math.Cos(angle)
		y := math.Sin(angle)
		r1 := float64(i) * armRadius / armPoints
		r2 := (float64(i) + 1.5) * armRadius / armPoints
		vertices[armPoints-i-1] = PointFromCoords(r1*x, r1*y, 1)
		vertices[armPoints+i] = PointFromCoords(r2*x, r2*y, 1)
	}
	// This is a pathological loop that contains many long parallel edges.
	spiral := LoopFromPoints(vertices)

	// Check that TurningAngle is consistent with Area to within the
	// error bound of the former. We actually use a tiny fraction of the
	// worst-case error bound, since the worst case only happens when all the
	// roundoff errors happen in the same direction and this test is not
	// designed to achieve that. The error in Area can be ignored for the
	// purposes of this test since it is generally much smaller.
	if got, want := spiral.TurningAngle(), (2*math.Pi - spiral.Area()); !float64Near(got, want, 0.01*spiral.turningAngleMaxError()) {
		t.Errorf("spiral.TurningAngle() = %v, want %v", got, want)
	}
}

func TestLoopAreaAndCentroid(t *testing.T) {
	var p Point

	if got, want := EmptyLoop().Area(), 0.0; got != want {
		t.Errorf("EmptyLoop.Area() = %v, want %v", got, want)
	}
	if got, want := FullLoop().Area(), 4*math.Pi; got != want {
		t.Errorf("FullLoop.Area() = %v, want %v", got, want)
	}
	if got := EmptyLoop().Centroid(); !p.ApproxEqual(got) {
		t.Errorf("EmptyLoop.Centroid() = %v, want %v", got, p)
	}
	if got := FullLoop().Centroid(); !p.ApproxEqual(got) {
		t.Errorf("FullLoop.Centroid() = %v, want %v", got, p)
	}

	if got, want := northHemi.Area(), 2*math.Pi; !float64Eq(got, want) {
		t.Errorf("northHemi.Area() = %v, want %v", got, want)
	}

	eastHemiArea := eastHemi.Area()
	if eastHemiArea < 2*math.Pi-1e-12 || eastHemiArea > 2*math.Pi+1e-12 {
		t.Errorf("eastHemi.Area() = %v, want between [%v, %v]", eastHemiArea, 2*math.Pi-1e-12, 2*math.Pi+1e-12)
	}

	// Construct spherical caps of random height, and approximate their boundary
	// with closely spaces vertices. Then check that the area and centroid are
	// correct.
	for i := 0; i < 50; i++ {
		// Choose a coordinate frame for the spherical cap.
		f := randomFrame()
		x := f.col(0)
		y := f.col(1)
		z := f.col(2)

		// Given two points at latitude phi and whose longitudes differ by dtheta,
		// the geodesic between the two points has a maximum latitude of
		// atan(tan(phi) / cos(dtheta/2)). This can be derived by positioning
		// the two points at (-dtheta/2, phi) and (dtheta/2, phi).
		//
		// We want to position the vertices close enough together so that their
		// maximum distance from the boundary of the spherical cap is maxDist.
		// Thus we want abs(atan(tan(phi) / cos(dtheta/2)) - phi) <= maxDist.
		const maxDist = 1e-6
		height := 2 * randomFloat64()
		phi := math.Asin(1.0 - height)
		maxDtheta := 2 * math.Acos(math.Tan(math.Abs(phi))/math.Tan(math.Abs(phi)+maxDist))
		maxDtheta = math.Min(math.Pi, maxDtheta)

		var vertices []Point
		for theta := 0.0; theta < 2*math.Pi; theta += randomFloat64() * maxDtheta {
			vertices = append(vertices,
				Point{x.Mul(math.Cos(theta) * math.Cos(phi)).Add(
					y.Mul(math.Sin(theta) * math.Cos(phi))).Add(
					z.Mul(math.Sin(phi)))},
			)
		}

		loop := LoopFromPoints(vertices)
		area := loop.Area()
		centroid := loop.Centroid()
		expectedArea := 2 * math.Pi * height
		if delta, want := math.Abs(area-expectedArea), 2*math.Pi*maxDist; delta > want {
			t.Errorf("%v.Area() = %v, want to be within %v of %v, got %v", loop, area, want, expectedArea, delta)
		}
		expectedCentroid := z.Mul(expectedArea * (1 - 0.5*height))
		if delta, want := centroid.Sub(expectedCentroid).Norm(), 2*maxDist; delta > want {
			t.Errorf("%v.Centroid() = %v, want to be within %v of %v, got %v", loop, centroid, want, expectedCentroid, delta)
		}
	}
}

// TODO(roberts): Test that Area() has an accuracy significantly better
// than 1e-15 on loops whose area is small.

func TestLoopAreaConsistentWithTurningAngle(t *testing.T) {
	// Check that the area computed using GetArea() is consistent with the
	// turning angle of the loop computed using GetTurnAngle().  According to
	// the Gauss-Bonnet theorem, the area of the loop should be equal to 2*Pi
	// minus its turning angle.
	for x, loop := range allLoops {
		area := loop.Area()
		gaussArea := 2*math.Pi - loop.TurningAngle()

		// TODO(roberts): The error bound below is much larger than it should be.
		if math.Abs(area-gaussArea) > 1e-9 {
			t.Errorf("%d. %v.Area() = %v want %v", x, loop, area, gaussArea)
		}
	}
}

func TestLoopGetAreaConsistentWithSign(t *testing.T) {
	// TODO(roberts): Uncomment when Loop has Validate
	/*
		// Test that Area() returns an area near 0 for degenerate loops that
		// contain almost no points, and an area near 4*pi for degenerate loops that
		// contain almost all points.
		const maxVertices = 6

		for i := 0; i < 50; i++ {
			numVertices := 3 + randomUniformInt(maxVertices-3+1)
			// Repeatedly choose N vertices that are exactly on the equator until we
			// find some that form a valid loop.
			var loop = Loop{}
			for !loop.Validate() {
				var vertices []Point
				for i := 0; i < numVertices; i++ {
					// We limit longitude to the range [0, 90] to ensure that the loop is
					// degenerate (as opposed to following the entire equator).
					vertices = append(vertices,
						PointFromLatLng(LatLng{0, s1.Angle(randomFloat64()) * math.Pi / 2}))
				}
				loop.vertices = vertices
				break
			}

			ccw := loop.IsNormalized()
			want := 0.0
			if !ccw {
				want = 4 * math.Pi
			}

			// TODO(roberts): The error bound below is much larger than it should be.
			if got := loop.Area(); !float64Near(got, want, 1e-8) {
				t.Errorf("%v.Area() = %v, want %v", loop, got, want)
			}
			p := PointFromCoords(0, 0, 1)
			if got := loop.ContainsPoint(p); got != !ccw {
				t.Errorf("%v.ContainsPoint(%v) = %v, want %v", got, p, !ccw)
			}
		}
	*/
}

func TestLoopNormalizedCompatibleWithContains(t *testing.T) {
	p := parsePoint("40:40")

	tests := []*Loop{
		lineTriangle,
		skinnyChevron,
	}

	// Checks that if a loop is normalized, it doesn't contain a
	// point outside of it, and vice versa.
	for _, loop := range tests {
		flip := cloneLoop(loop)

		flip.Invert()
		if norm, contains := loop.IsNormalized(), loop.ContainsPoint(p); norm == contains {
			t.Errorf("loop.IsNormalized() = %t == loop.ContainsPoint(%v) = %v, want !=", norm, p, contains)
		}
		if norm, contains := flip.IsNormalized(), flip.ContainsPoint(p); norm == contains {
			t.Errorf("flip.IsNormalized() = %t == flip.ContainsPoint(%v) = %v, want !=", norm, p, contains)
		}
		if a, b := loop.IsNormalized(), flip.IsNormalized(); a == b {
			t.Errorf("a loop and it's invert can not both be normalized")
		}
		flip.Normalize()
		if flip.ContainsPoint(p) {
			t.Errorf("%v.ContainsPoint(%v) = true, want false", flip, p)
		}
	}
}

func TestLoopValidateDetectsInvalidLoops(t *testing.T) {
	tests := []struct {
		msg    string
		points []Point
	}{
		// Not enough vertices. Note that all single-vertex loops are valid; they
		// are interpreted as being either "empty" or "full".
		{
			msg:    "loop has no vertices",
			points: parsePoints(""),
		},
		{
			msg:    "loop has too few vertices",
			points: parsePoints("20:20, 21:21"),
		},
		// degenerate edge checks happen in validation before duplicate vertices.
		{
			msg:    "loop has degenerate first edge",
			points: parsePoints("20:20, 20:20, 20:21"),
		},
		{
			msg:    "loop has degenerate third edge",
			points: parsePoints("20:20, 20:21, 20:20"),
		},
		// TODO(roberts): Uncomment these cases when FindAnyCrossings is in.
		/*
			{
				msg:    "loop has duplicate points",
				points: parsePoints("20:20, 21:21, 21:20, 20:20, 20:21"),
			},
			{
				msg:    "loop has crossing edges",
				points: parsePoints("20:20, 21:21, 21:20.5, 21:20, 20:21"),
			},
		*/
		{
			// Ensure points are not normalized.
			msg: "loop with non-normalized vertices",
			points: []Point{
				{r3.Vector{X: 2, Y: 0, Z: 0}},
				{r3.Vector{X: 0, Y: 1, Z: 0}},
				{r3.Vector{X: 0, Y: 0, Z: 1}},
			},
		},
		{
			// Adjacent antipodal vertices
			msg: "loop with antipodal points",
			points: []Point{
				{r3.Vector{X: 1, Y: 0, Z: 0}},
				{r3.Vector{X: -1, Y: 0, Z: 0}},
				{r3.Vector{X: 0, Y: 0, Z: 1}},
			},
		},
	}

	for _, test := range tests {
		loop := LoopFromPoints(test.points)
		if err := loop.Validate(); err == nil {
			t.Errorf("%s. %v.Validate() = %v, want nil", test.msg, loop, err)
		}
		// The C++ tests also tests that the returned error message string contains
		// a specific set of text. That part of the test is skipped here.
	}
}

const (
	// TODO(roberts): Convert these into changeable flags or parameters.
	// A loop with a 10km radius and 4096 vertices has an edge length of 15 meters.
	defaultRadiusKm   = 10.0
	numLoopSamples    = 16
	numQueriesPerLoop = 100
)

func BenchmarkLoopContainsPoint(b *testing.B) {
	// Benchmark ContainsPoint() on regular loops. The query points for a loop are
	// chosen so that they all lie in the loop's bounding rectangle (to avoid the
	// quick-rejection code path).

	// C++ ranges from 4 -> 256k by powers of 2 for number of vertices for benchmarking.
	vertices := 4
	for n := 1; n <= 17; n++ {
		b.Run(fmt.Sprintf("%d", vertices),
			func(b *testing.B) {
				b.StopTimer()
				loops := make([]*Loop, numLoopSamples)
				for i := 0; i < numLoopSamples; i++ {
					loops[i] = RegularLoop(randomPoint(), kmToAngle(10.0), vertices)
				}

				queries := make([][]Point, numLoopSamples)

				for i, loop := range loops {
					queries[i] = make([]Point, numQueriesPerLoop)
					for j := 0; j < numQueriesPerLoop; j++ {
						queries[i][j] = samplePointFromRect(loop.RectBound())
					}
				}

				b.StartTimer()
				for i := 0; i < b.N; i++ {
					loops[i%numLoopSamples].ContainsPoint(queries[i%numLoopSamples][i%numQueriesPerLoop])
				}
			})
		vertices *= 2
	}
}

// TODO(rsned): Differences from C++
// TEST_F(S2LoopTestBase, CompressedEncodedLoopDecodesApproxEqual) {
// TEST_F(S2LoopTestBase, DistanceMethods) {
// TEST_F(S2LoopTestBase, GetAreaAccuracy) {
// TEST_F(S2LoopTestBase, GetAreaConsistentWithSign) {
// TEST_F(S2LoopTestBase, MakeRegularLoop) {
// TEST(S2Loop, BoundaryNear) {
// TEST(S2Loop, BoundsForLoopContainment) {
// TEST(S2LoopDeathTest, IsValidDetectsInvalidLoops) {
// TEST(S2Loop, DefaultLoopIsInvalid) {
// TEST(S2Loop, EmptyFullLossyConversions) {
// TEST(S2Loop, EncodeDecode) {
// TEST(S2Loop, LoopRelations2) {
