test_that("as_polygon2d()", {
	# from Coord2D
	v <- as_coord2d(x = c(0, 1, 1, 0), y = c(0, 0, 1, 1))
	p <- as_polygon2d(v)
	expect_r6_class(p, "Polygon2D")
	expect_true(is_polygon2d(p))
	expect_true(is_coord2d(p)) # Polygon2D is a Coord2D subclass
	expect_false(is_polygon2d(v))
	expect_true(p$is_convex)
	expect_equal(length(p), 4L)

	# from numeric
	p2 <- as_polygon2d(x = c(0, 1, 1, 0), y = c(0, 0, 1, 1))
	expect_r6_class(p2, "Polygon2D")

	# explicit convex override
	p3 <- as_polygon2d(v, convex = TRUE)
	expect_true(p3$is_convex)
	p4 <- as_polygon2d(v, convex = FALSE)
	expect_false(p4$is_convex)

	# print
	expect_snapshot(print(p))
})

test_that("as_polygon2d.Polygon2D()", {
	p <- as_polygon2d(as_coord2d(x = c(0, 1, 1, 0), y = c(0, 0, 1, 1)))
	expect_identical(as_polygon2d(p), p)
	expect_false(as_polygon2d(p, convex = FALSE)$is_convex)
})

test_that("Polygon2D normals", {
	# Unit square vertices CCW
	v <- as_coord2d(x = c(0, 1, 1, 0), y = c(0, 0, 1, 1))
	p <- as_polygon2d(v)
	norms <- p$normals
	expect_r6_class(norms, "Coord2D")
	expect_false(is_polygon2d(norms)) # normals are plain Coord2D, not Polygon2D
	expect_equal(length(norms), 4L)
	expect_equal(abs(norms), rep(1, 4L))

	# Cached: second access returns same object
	expect_identical(p$normals, norms)

	# Cache invalidated after transform
	p$rotate(degrees(45))
	expect_false(identical(p$normals, norms))
})

test_that("Polygon2D$convex_hull", {
	# Convex polygon: convex_hull returns itself
	v <- as_coord2d(x = c(0, 1, 1, 0), y = c(0, 0, 1, 1))
	p <- as_polygon2d(v, convex = TRUE)
	expect_identical(p$convex_hull, p)

	# Concave polygon (star-like): hull is a different polygon
	# Simple concave: a "C" shape or indented square
	vv <- as_coord2d(
		x = c(0, 0.5, 0.5, 1, 1, 0.5, 0.5, 0),
		y = c(0, 0, 0.4, 0.4, 0.6, 0.6, 1, 1)
	)
	pc <- as_polygon2d(vv, convex = FALSE)
	hull <- pc$convex_hull
	expect_r6_class(hull, "Polygon2D")
	expect_true(hull$is_convex)
	expect_true(length(hull) <= length(pc))

	# Cached: second call returns same object
	expect_identical(pc$convex_hull, hull)

	# Cache invalidated after transform
	pc$rotate(degrees(45))
	expect_false(identical(pc$convex_hull, hull))
})

test_that("convex_hull2d() returns Polygon2D", {
	# From Coord2D
	pts <- as_coord2d(x = rnorm(20), y = rnorm(20))
	hull <- convex_hull2d(pts)
	expect_r6_class(hull, "Polygon2D")
	expect_true(hull$is_convex)
	expect_true(is_coord2d(hull)) # backwards compatible

	# From convex Polygon2D: returns itself
	v <- as_coord2d(x = c(0, 1, 1, 0), y = c(0, 0, 1, 1))
	p <- as_polygon2d(v, convex = TRUE)
	expect_identical(convex_hull2d(p), p)
})

test_that("Polygon2D convex auto-detection", {
	# Convex (CCW square)
	expect_true(
		as_polygon2d(as_coord2d(
			x = c(0, 1, 1, 0),
			y = c(0, 0, 1, 1)
		))$is_convex
	)

	# Convex (CW square)
	expect_true(
		as_polygon2d(as_coord2d(
			x = c(0, 0, 1, 1),
			y = c(0, 1, 1, 0)
		))$is_convex
	)

	# Degenerate: fewer than 3 vertices → FALSE
	expect_false(as_polygon2d(as_coord2d(x = 1:2, y = 1:2))$is_convex)

	# Concave C-shape
	expect_false(
		as_polygon2d(as_coord2d(
			x = c(0, 0.5, 0.5, 1, 1, 0.5, 0.5, 0),
			y = c(0, 0, 0.4, 0.4, 0.6, 0.6, 1, 1)
		))$is_convex
	)
})

test_that("has_overlap2d() polygon-polygon", {
	sq1 <- as_polygon2d(as_coord2d(
		x = c(0, 1, 1, 0),
		y = c(0, 0, 1, 1)
	))
	sq2 <- as_polygon2d(as_coord2d(
		x = c(0.5, 1.5, 1.5, 0.5),
		y = c(0.5, 0.5, 1.5, 1.5)
	))
	sq3 <- as_polygon2d(as_coord2d(
		x = c(2, 3, 3, 2),
		y = c(2, 2, 3, 3)
	))

	expect_true(has_overlap2d(sq1, sq2))
	expect_false(has_overlap2d(sq1, sq3))

	# Touching corners: strictly no non-zero-area overlap
	sq4 <- as_polygon2d(as_coord2d(
		x = c(1, 2, 2, 1),
		y = c(1, 1, 2, 2)
	))
	expect_false(has_overlap2d(sq1, sq4))
})

test_that("has_overlap2d() polygon-circle", {
	sq <- as_polygon2d(as_coord2d(
		x = c(0, 1, 1, 0),
		y = c(0, 0, 1, 1)
	))
	circ_in <- as_ellipse2d(as_coord2d(0.5, 0.5), r = 0.3)
	circ_out <- as_ellipse2d(as_coord2d(3, 3), r = 0.3)
	circ_corner <- as_ellipse2d(as_coord2d(1.5, 1.5), r = 0.3)

	expect_true(has_overlap2d(sq, circ_in))
	expect_false(has_overlap2d(sq, circ_out))
	expect_false(has_overlap2d(sq, circ_corner))

	# Dispatching from Circle2D side
	expect_true(has_overlap2d(circ_in, sq))
	expect_false(has_overlap2d(circ_out, sq))

	# Vectorized circles
	circs <- as_ellipse2d(
		x = c(0.5, 3),
		y = c(0.5, 3),
		r = c(0.3, 0.3)
	)
	expect_equal(has_overlap2d(sq, circs), c(TRUE, FALSE))
})

test_that("has_overlap2d() concave polygon returns NA with warning", {
	# Concave polygon (C-shape)
	vc <- as_coord2d(
		x = c(0, 0.5, 0.5, 1, 1, 0.5, 0.5, 0),
		y = c(0, 0, 0.4, 0.4, 0.6, 0.6, 1, 1)
	)
	pc <- as_polygon2d(vc, convex = FALSE)
	sq <- as_polygon2d(as_coord2d(
		x = c(0.3, 0.7, 0.7, 0.3),
		y = c(0.3, 0.3, 0.7, 0.7)
	))

	# Overlapping bounding boxes + convex hulls → NA with warning
	expect_warning(res <- has_overlap2d(pc, sq), "concave")
	expect_true(is.na(res))

	# Clearly disjoint → FALSE without warning
	sq_far <- as_polygon2d(as_coord2d(
		x = c(5, 6, 6, 5),
		y = c(5, 5, 6, 6)
	))
	expect_false(has_overlap2d(pc, sq_far))
})

test_that("has_overlap2d() concave polygon and circle (bbox_overlap_pc)", {
	pc <- as_polygon2d(
		as_coord2d(
			x = c(0, 0.5, 0.5, 1, 1, 0.5, 0.5, 0),
			y = c(0, 0, 0.4, 0.4, 0.6, 0.6, 1, 1)
		),
		convex = FALSE
	)
	# Circle outside bbox → FALSE via bbox_overlap_pc
	expect_false(has_overlap2d(pc, as_ellipse2d(as_coord2d(5, 5), r = 0.3)))
	# Circle inside bbox, overlapping convex hull → NA with warning
	expect_warning(
		res <- has_overlap2d(pc, as_ellipse2d(as_coord2d(0.5, 0.5), r = 0.3)),
		"concave"
	)
	expect_true(is.na(res))
})

test_that("regular_ngon_polygon2d()", {
	p6 <- regular_ngon_polygon2d(6)
	expect_r6_class(p6, "Polygon2D")
	expect_true(p6$is_convex)
	expect_equal(length(p6), 6L)

	# center and radius are respected
	p <- regular_ngon_polygon2d(4, x = 1, y = 2, radius = 2)
	expect_equal(mean(p)$x, 1, tolerance = 1e-10)
	expect_equal(mean(p)$y, 2, tolerance = 1e-10)
	expect_equal(distance2d(as_coord2d(1, 2), p), rep(2, 4L), tolerance = 1e-10)

	# theta shifts the first vertex
	p0 <- regular_ngon_polygon2d(4, theta = degrees(0))
	expect_equal(p0$x[1], 0.5, tolerance = 1e-10)
	expect_equal(p0$y[1], 0, tolerance = 1e-10)
})

test_that("rectangle_polygon2d()", {
	p <- rectangle_polygon2d()
	expect_r6_class(p, "Polygon2D")
	expect_true(p$is_convex)
	expect_equal(length(p), 4L)
	expect_equal(range(p)$x, c(-0.5, 0.5), tolerance = 1e-10)
	expect_equal(range(p)$y, c(-0.5, 0.5), tolerance = 1e-10)

	# width, height, center respected
	p2 <- rectangle_polygon2d(width = 2, height = 3, x = 1, y = -1)
	expect_equal(range(p2)$x, c(0, 2), tolerance = 1e-10)
	expect_equal(range(p2)$y, c(-2.5, 0.5), tolerance = 1e-10)

	# rotation: a 45-degree square has equal x and y extents
	p3 <- rectangle_polygon2d(width = 1, height = 1, theta = degrees(45))
	expect_equal(range(p3)$x, range(p3)$y, tolerance = 1e-10)
})

test_that("isotoxal_2ngon_polygon2d()", {
	p <- isotoxal_2ngon_polygon2d(5, d = 2)
	expect_r6_class(p, "Polygon2D")
	expect_equal(length(p), 10L)
	expect_false(p$is_convex)

	# alpha and beta_ext routes give same result as d
	s <- isotoxal_2ngon_inner_radius(5, d = 2)
	p_alpha <- isotoxal_2ngon_polygon2d(5, alpha = 180 * (1 - 4 / 5))
	p_scale <- isotoxal_2ngon_polygon2d(5, radial_scale = s)
	expect_equal(p$x, p_alpha$x, tolerance = 1e-10)
	expect_equal(p$x, p_scale$x, tolerance = 1e-10)

	# star_polygon2d() is an alias
	p2 <- star_polygon2d(5, d = 2)
	expect_equal(p$x, p2$x)
	expect_equal(p$y, p2$y)

	# outer vertices sit on circumradius circle
	outer_idx <- seq(1, 10, by = 2)
	center <- as_coord2d(0, 0)
	expect_equal(
		distance2d(center, p)[outer_idx],
		rep(0.5, 5L),
		tolerance = 1e-10
	)

	# convex when radial_scale >= cos(pi/n)
	p_convex <- isotoxal_2ngon_polygon2d(5, radial_scale = cospi(1 / 5))
	expect_true(p_convex$is_convex)
	p_wide <- isotoxal_2ngon_polygon2d(5, radial_scale = 0.9)
	expect_true(p_wide$is_convex)
})

test_that("has_overlap2d.Polygon2D() errors on unsupported y class", {
	sq <- as_polygon2d(as_coord2d(x = c(0, 1, 1, 0), y = c(0, 0, 1, 1)))
	expect_error(has_overlap2d(sq, "foo"), "Don't know how to check overlap with")
})
