test_that("as_ellipse2d()", {
	# from Coord2D
	e <- as_ellipse2d(as_coord2d(0, 0), rx = 2, ry = 1, theta = degrees(45))
	expect_r6_class(e, "Ellipse2D")
	expect_true(is_ellipse2d(e))
	expect_true(is_coord2d(e))
	expect_equal(length(e), 1L)
	expect_equal(e$rx, 2)
	expect_equal(e$ry, 1)
	expect_true(is_angle(e$theta))
	expect_equal(as.numeric(e$theta, "degrees"), 45)
	expect_false(e$is_circle)

	# from numeric
	e2 <- as_ellipse2d(x = c(0, 1), y = c(0, 1), rx = c(1, 2), ry = c(0.5, 1))
	expect_equal(length(e2), 2L)

	# recycling: one center, two radii
	e3 <- as_ellipse2d(as_coord2d(0, 0), rx = c(1, 2), ry = c(0.5, 1))
	expect_equal(length(e3), 2L)

	# [.angle preserves class when indexing theta
	expect_true(is_angle(e3$theta[1L]))

	# print
	expect_snapshot(print(e))
})

test_that("mixed circle/non-circle ellipses()", {
	e1 <- as_ellipse2d(as_coord2d(0, 0), rx = 2, ry = 1)
	expect_true(is_ellipse2d(e1))
	# mixed vector: not all circles → not all is_circle
	mixed <- as_ellipse2d(x = c(0, 1), y = c(0, 0), rx = c(1, 1), ry = c(1, 0.5))
	expect_false(all(mixed$is_circle))
})

test_that("Ellipse2D$is_circle active binding", {
	e <- as_ellipse2d(x = c(0, 1), y = c(0, 0), rx = c(1, 2), ry = c(1, 1))
	expect_equal(e$is_circle, c(TRUE, FALSE))
})

test_that("Ellipse2D transform() updates shape parameters", {
	# Uniform scaling: rx and ry both scaled
	c1 <- as_ellipse2d(as_coord2d(0, 0), r = 1)
	c1$scale(2)
	expect_equal(c1$rx, 2)
	expect_equal(c1$ry, 2)
	expect_true(c1$is_circle)

	# Non-uniform scaling turns circle into ellipse
	c2 <- as_ellipse2d(as_coord2d(0, 0), r = 1)
	c2$scale(2, 1)
	expect_equal(c2$rx, 2)
	expect_equal(c2$ry, 1)
	expect_false(c2$is_circle)

	# Rotation of a circle stays a circle; theta is still an angle
	c3 <- as_ellipse2d(as_coord2d(1, 0), r = 1)
	c3$rotate(degrees(45))
	expect_true(c3$is_circle)
	expect_equal(c3$rx, 1, tolerance = 1e-10)
	expect_equal(c3$ry, 1, tolerance = 1e-10)
	expect_true(is_angle(c3$theta))

	# Translation only moves center, not radii
	c4 <- as_ellipse2d(as_coord2d(0, 0), r = 1)
	c4$translate(as_coord2d(3, 4))
	expect_equal(c4$x, 3)
	expect_equal(c4$y, 4)
	expect_equal(c4$rx, 1)
})

test_that("as.list.Ellipse2D() and as.data.frame.Ellipse2D()", {
	e <- as_ellipse2d(as_coord2d(1, 2), rx = 3, ry = 2, theta = degrees(30))
	lst <- as.list(e)
	expect_equal(lst$x, 1)
	expect_equal(lst$y, 2)
	expect_equal(lst$rx, 3)
	expect_equal(lst$ry, 2)
	expect_true(is_angle(lst$theta))
	expect_equal(as.numeric(lst$theta, "degrees"), 30)

	df <- as.data.frame(e)
	expect_equal(names(df), c("x", "y", "rx", "ry", "theta"))
	expect_true(is_angle(df$theta))
})

test_that("as_polygon2d.Ellipse2D()", {
	c1 <- as_ellipse2d(as_coord2d(0, 0), r = 1)

	inner <- as_polygon2d(c1, n = 8L, type = "inner")
	expect_r6_class(inner, "Polygon2D")
	expect_true(inner$is_convex)
	expect_equal(length(inner), 8L)
	# inner polygon vertices lie on the circle: abs should equal 1
	expect_equal(abs(inner - as_coord2d(0, 0)), rep(1, 8L), tolerance = 1e-10)

	outer <- as_polygon2d(c1, n = 8L, type = "outer")
	expect_r6_class(outer, "Polygon2D")
	expect_true(outer$is_convex)
	expect_equal(length(outer), 8L)
	# outer polygon vertices lie outside the circle
	expect_true(all(abs(outer - as_coord2d(0, 0)) > 1))

	# inner contains circle, outer contains inner
	# (all inner vertices closer than all outer vertices to center)
	expect_true(
		all(abs(inner - as_coord2d(0, 0)) < abs(outer - as_coord2d(0, 0)))
	)

	# only length-1 Ellipse2D supported
	e2 <- as_ellipse2d(x = c(0, 1), y = c(0, 0), rx = c(1, 2), ry = c(1, 1))
	expect_error(as_polygon2d(e2))
})

test_that("has_overlap2d() circle-circle", {
	c1 <- as_ellipse2d(as_coord2d(0, 0), r = 1)
	c2 <- as_ellipse2d(as_coord2d(1.5, 0), r = 1)
	c3 <- as_ellipse2d(as_coord2d(3, 0), r = 1)

	expect_true(has_overlap2d(c1, c2))
	expect_false(has_overlap2d(c1, c3))

	# Tangent circles: no non-zero-area overlap
	c4 <- as_ellipse2d(as_coord2d(2, 0), r = 1)
	expect_false(has_overlap2d(c1, c4))

	# Vectorized
	c_many <- as_ellipse2d(x = c(1.5, 3), y = c(0, 0), r = c(1, 1))
	res <- has_overlap2d(c1, c_many)
	expect_equal(res, c(TRUE, FALSE))
})

test_that("has_overlap2d() ellipse-ellipse (non-circle branches)", {
	e <- as_ellipse2d(as_coord2d(0, 0), rx = 1, ry = 0.5)
	c_near <- as_ellipse2d(as_coord2d(0.5, 0), r = 0.4)
	c_far <- as_ellipse2d(as_coord2d(5, 0), r = 0.3)

	# e1 non-circle, e2 circle
	expect_true(has_overlap2d(e, c_near))
	expect_false(has_overlap2d(e, c_far))

	# e1 circle, e2 non-circle
	expect_true(has_overlap2d(c_near, e))
	expect_false(has_overlap2d(c_far, e))

	# both non-circle
	e_near <- as_ellipse2d(as_coord2d(0.5, 0), rx = 1, ry = 0.5)
	e_far <- as_ellipse2d(as_coord2d(5, 0), rx = 1, ry = 0.5)
	expect_true(has_overlap2d(e, e_near))
	expect_false(has_overlap2d(e, e_far))
})

test_that("has_overlap2d() ellipse-polygon (circle case uses SAT)", {
	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)

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

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

	# Vectorized
	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() non-circle ellipse uses bracketing", {
	sq <- as_polygon2d(as_coord2d(
		x = c(0, 1, 1, 0),
		y = c(0, 0, 1, 1)
	))
	# Clearly inside
	e_in <- as_ellipse2d(as_coord2d(0.5, 0.5), rx = 0.3, ry = 0.1)
	expect_true(has_overlap2d(e_in, sq))

	# Clearly outside
	e_out <- as_ellipse2d(as_coord2d(5, 5), rx = 0.3, ry = 0.1)
	expect_false(has_overlap2d(e_out, sq))
})

test_that("has_overlap2d.Ellipse2D() errors on unsupported y class", {
	circ <- as_ellipse2d(as_coord2d(0, 0), r = 1)
	expect_error(has_overlap2d(circ, "foo"), "Don't know how to check overlap with")
})
