test_that("arcopt solves simple sphere function", {
  # Sphere function: f(x) = sum(x^2)
  # Minimum at x = 0, f = 0
  sphere <- function(x) sum(x^2)
  sphere_gr <- function(x) 2 * x
  sphere_hess <- function(x) 2 * diag(length(x))

  result <- arcopt(
    x0 = c(5, 5),
    fn = sphere,
    gr = sphere_gr,
    hess = sphere_hess
  )

  expect_true(result$converged)
  expect_equal(result$par, c(0, 0), tolerance = 1e-6)
  expect_equal(result$value, 0, tolerance = 1e-8)
})

test_that("arcopt solves Rosenbrock function", {
  # Rosenbrock: f(x) = (1-x1)^2 + 100*(x2-x1^2)^2
  # Minimum at (1, 1), f = 0
  rosenbrock <- function(x) {
    (1 - x[1])^2 + 100 * (x[2] - x[1]^2)^2
  }

  rosenbrock_gr <- function(x) {
    c(-2 * (1 - x[1]) - 400 * x[1] * (x[2] - x[1]^2),
      200 * (x[2] - x[1]^2))
  }

  rosenbrock_hess <- function(x) {
    matrix(c(
      1200 * x[1]^2 - 400 * x[2] + 2, -400 * x[1],
      -400 * x[1], 200
    ), 2, 2)
  }

  result <- arcopt(
    x0 = c(-1.2, 1),
    fn = rosenbrock,
    gr = rosenbrock_gr,
    hess = rosenbrock_hess
  )

  expect_true(result$converged)
  expect_equal(result$par, c(1, 1), tolerance = 1e-4)
  expect_equal(result$value, 0, tolerance = 1e-6)
})

test_that("arcopt handles quadratic function correctly", {
  # Quadratic: f(x) = 1/2 x^T A x - b^T x
  # where A is PD
  A <- matrix(c(2, 0.5, 0.5, 3), 2, 2)
  b <- c(1, 2)

  quad_fn <- function(x) 0.5 * sum(x * (A %*% x)) - sum(b * x)
  quad_gr <- function(x) as.vector(A %*% x) - b
  quad_hess <- function(x) A

  result <- arcopt(
    x0 = c(0, 0),
    fn = quad_fn,
    gr = quad_gr,
    hess = quad_hess
  )

  # Solution: A * x = b
  expected_par <- solve(A, b)

  expect_true(result$converged)
  expect_equal(result$par, expected_par, tolerance = 1e-6)
})

test_that("arcopt returns evaluation counts", {
  sphere <- function(x) sum(x^2)
  sphere_gr <- function(x) 2 * x
  sphere_hess <- function(x) 2 * diag(length(x))

  result <- arcopt(
    x0 = c(1, 1),
    fn = sphere,
    gr = sphere_gr,
    hess = sphere_hess
  )

  expect_true(result$evaluations$fn > 0)
  expect_true(result$evaluations$gr > 0)
  expect_true(result$evaluations$hess > 0)
  expect_equal(result$evaluations$fn, result$evaluations$gr)
})

test_that("arcopt respects max_iter control parameter", {
  # Use Rosenbrock which needs many iterations
  rosenbrock <- function(x) {
    (1 - x[1])^2 + 100 * (x[2] - x[1]^2)^2
  }

  rosenbrock_gr <- function(x) {
    c(-2 * (1 - x[1]) - 400 * x[1] * (x[2] - x[1]^2),
      200 * (x[2] - x[1]^2))
  }

  rosenbrock_hess <- function(x) {
    matrix(c(
      1200 * x[1]^2 - 400 * x[2] + 2, -400 * x[1],
      -400 * x[1], 200
    ), 2, 2)
  }

  result <- arcopt(
    x0 = c(-1.2, 1),
    fn = rosenbrock,
    gr = rosenbrock_gr,
    hess = rosenbrock_hess,
    control = list(maxit = 3)  # Stop early
  )

  expect_true(result$iterations <= 3)
  expect_equal(result$message, "max_iter")
})

test_that("arcopt works on higher dimensional problems", {
  # 5-dimensional sphere
  n <- 5
  sphere <- function(x) sum(x^2)
  sphere_gr <- function(x) 2 * x
  sphere_hess <- function(x) 2 * diag(length(x))

  result <- arcopt(
    x0 = rep(3, n),
    fn = sphere,
    gr = sphere_gr,
    hess = sphere_hess
  )

  expect_true(result$converged)
  expect_equal(result$par, rep(0, n), tolerance = 1e-5)
})

test_that("arcopt requires hess", {
  sphere <- function(x) sum(x^2)
  sphere_gr <- function(x) 2 * x

  expect_error(
    arcopt(x0 = c(1, 1), fn = sphere, gr = sphere_gr, hess = NULL),
    "Hessian function 'hess' must be provided"
  )
})

test_that("arcopt handles function with saddle point", {
  # Function with saddle: f(x) = x1^2 - x2^2
  # Has saddle at origin, indefinite Hessian
  # Starting away from origin should still work
  saddle_fn <- function(x) x[1]^2 - x[2]^2 + 10
  saddle_gr <- function(x) c(2 * x[1], -2 * x[2])
  saddle_hess <- function(x) matrix(c(2, 0, 0, -2), 2, 2)

  # Start at point where we can descend
  result <- arcopt(
    x0 = c(2, 2),
    fn = saddle_fn,
    gr = saddle_gr,
    hess = saddle_hess,
    control = list(maxit = 100)
  )

  # Should converge somewhere (behavior with indefinite Hessian)
  expect_true(result$converged || result$iterations == 100)
})

test_that("arcopt validates input", {
  sphere <- function(x) sum(x^2)
  sphere_gr <- function(x) 2 * x
  sphere_hess <- function(x) 2 * diag(length(x))

  # Non-numeric x0
  expect_error(
    arcopt(x0 = "not numeric", fn = sphere, gr = sphere_gr, hess = sphere_hess),
    "x0 must be a finite numeric vector"
  )

  # Non-finite x0
  expect_error(
    arcopt(x0 = c(1, Inf), fn = sphere, gr = sphere_gr, hess = sphere_hess),
    "x0 must be a finite numeric vector"
  )

  # fn not a function
  expect_error(
    arcopt(x0 = c(1, 1), fn = "not a function", gr = sphere_gr, hess = sphere_hess),
    "fn must be a function"
  )

  # gr not provided
  expect_error(
    arcopt(x0 = c(1, 1), fn = sphere, gr = "not a function", hess = sphere_hess),
    "gr .* is required"
  )
})

test_that("arcopt uses custom control parameters", {
  sphere <- function(x) sum(x^2)
  sphere_gr <- function(x) 2 * x
  sphere_hess <- function(x) 2 * diag(length(x))

  result <- arcopt(
    x0 = c(1, 1),
    fn = sphere,
    gr = sphere_gr,
    hess = sphere_hess,
    control = list(
      gtol_abs = 1e-10,  # Very strict gradient tolerance
      sigma0 = 10.0       # Large initial sigma
    )
  )

  expect_true(result$converged)
  # With stricter tolerance, should get very close to optimum
  expect_true(max(abs(result$gradient)) < 1e-9)
})

test_that("arcopt detects NaN in initial point", {
  # Function that returns NaN at initial point
  nan_fn <- function(x) {
    if (all(x == c(1, 1))) return(NaN)
    sum(x^2)
  }
  nan_gr <- function(x) 2 * x
  nan_hess <- function(x) 2 * diag(length(x))

  expect_error(
    arcopt(x0 = c(1, 1), fn = nan_fn, gr = nan_gr, hess = nan_hess),
    "Initial point yields NaN"
  )
})

test_that("arcopt detects NaN in gradient at initial point", {
  # Function with NaN gradient at initial point
  sphere <- function(x) sum(x^2)
  nan_gr <- function(x) {
    if (all(x == c(1, 1))) return(c(NaN, NaN))
    2 * x
  }
  sphere_hess <- function(x) 2 * diag(length(x))

  expect_error(
    arcopt(x0 = c(1, 1), fn = sphere, gr = nan_gr, hess = sphere_hess),
    "Initial point yields NaN"
  )
})

test_that("arcopt handles NaN during optimization by rejecting step", {
  # Function that returns NaN at specific point
  eval_count <- 0
  nan_fn <- function(x) {
    eval_count <<- eval_count + 1
    # Return NaN on second evaluation (trial point)
    if (eval_count == 2) return(NaN)
    sum(x^2)
  }
  sphere_gr <- function(x) 2 * x
  sphere_hess <- function(x) 2 * diag(length(x))

  # Reset counter
  eval_count <- 0

  result <- arcopt(
    x0 = c(5, 5),
    fn = nan_fn,
    gr = sphere_gr,
    hess = sphere_hess,
    control = list(maxit = 10)
  )

  # Should handle NaN by increasing sigma and continuing
  # Eventually converges or hits max iterations
  expect_true(result$converged || result$iterations == 10)
})

test_that("arcopt respects box constraints", {
  # Sphere function with box constraints
  sphere <- function(x) sum(x^2)
  sphere_gr <- function(x) 2 * x
  sphere_hess <- function(x) 2 * diag(length(x))

  # Constrain to [0.5, 1] x [0.5, 1]
  # Optimum without constraints is (0, 0)
  # With constraints, optimum should be (0.5, 0.5)
  expect_warning(
    result <- arcopt(
      x0 = c(2, 2),
      fn = sphere,
      gr = sphere_gr,
      hess = sphere_hess,
      lower = c(0.5, 0.5),
      upper = c(1, 1)
    ),
    "Initial point infeasible"
  )

  expect_true(result$converged)
  expect_equal(result$par, c(0.5, 0.5), tolerance = 1e-5)
  # All iterates should respect bounds
  expect_true(all(result$par >= c(0.5, 0.5) - 1e-10))
  expect_true(all(result$par <= c(1, 1) + 1e-10))
})

test_that("arcopt projects infeasible initial point", {
  sphere <- function(x) sum(x^2)
  sphere_gr <- function(x) 2 * x
  sphere_hess <- function(x) 2 * diag(length(x))

  # Start outside feasible region
  expect_warning(
    result <- arcopt(
      x0 = c(5, 5),  # Outside [0, 1] x [0, 1]
      fn = sphere,
      gr = sphere_gr,
      hess = sphere_hess,
      lower = c(0, 0),
      upper = c(1, 1)
    ),
    "Initial point infeasible"
  )

  expect_true(result$converged)
  expect_equal(result$par, c(0, 0), tolerance = 1e-5)
})

test_that("arcopt handles one-sided bounds", {
  # Function with minimum at (-1, -1)
  offset_sphere <- function(x) sum((x + 1)^2)
  offset_gr <- function(x) 2 * (x + 1)
  offset_hess <- function(x) 2 * diag(length(x))

  # Only lower bound at 0
  result <- arcopt(
    x0 = c(1, 1),
    fn = offset_sphere,
    gr = offset_gr,
    hess = offset_hess,
    lower = c(0, 0),
    upper = c(Inf, Inf)
  )

  expect_true(result$converged)
  # Optimum should be at boundary (0, 0) since unconstrained optimum (-1, -1) is infeasible
  expect_equal(result$par, c(0, 0), tolerance = 1e-5)
})

test_that("arcopt handles asymmetric bounds", {
  sphere <- function(x) sum(x^2)
  sphere_gr <- function(x) 2 * x
  sphere_hess <- function(x) 2 * diag(length(x))

  # Different bounds for each dimension
  result <- arcopt(
    x0 = c(1, 1),
    fn = sphere,
    gr = sphere_gr,
    hess = sphere_hess,
    lower = c(-10, 0.5),
    upper = c(10, 2)
  )

  expect_true(result$converged)
  # Check bounds are respected
  expect_true(result$par[1] >= -10 && result$par[1] <= 10)
  expect_true(result$par[2] >= 0.5 - 1e-10 && result$par[2] <= 2 + 1e-10)
  # Function value should be close to 0.25 (minimum is at (0, 0.5), f = 0.25)
  # but we allow for some numerical error and possible convergence to nearby point
  expect_true(result$value <= 1.0)
})

# Integration tests for Newton-rho behavior

test_that("arcopt uses rho-based acceptance for Newton steps", {
  Q <- matrix(c(1, 0, 0, 100), 2, 2)
  b <- c(1, 10)

  fn <- function(x) 0.5 * sum(x * (Q %*% x)) - sum(b * x)
  gr <- function(x) as.vector(Q %*% x - b)
  hess <- function(x) Q

  x0 <- c(5, 5)

  result <- arcopt(x0, fn, gr, hess, control = list(maxit = 50, trace = FALSE))

  expect_true(result$converged)
  expect_equal(result$par, c(1, 0.1), tolerance = 1e-6)
})

test_that("arcopt rejects poor Newton steps and falls back to cubic", {
  rosenbrock_fn <- function(x) {
    (1 - x[1])^2 + 100 * (x[2] - x[1]^2)^2
  }

  rosenbrock_gr <- function(x) {
    c(
      -2 * (1 - x[1]) - 400 * x[1] * (x[2] - x[1]^2),
      200 * (x[2] - x[1]^2)
    )
  }

  rosenbrock_hess <- function(x) {
    matrix(c(
      1200 * x[1]^2 - 400 * x[2] + 2, -400 * x[1],
      -400 * x[1], 200
    ), 2, 2)
  }

  x0 <- c(0.5, 0.5)

  result <- arcopt(
    x0,
    rosenbrock_fn,
    rosenbrock_gr,
    rosenbrock_hess,
    control = list(maxit = 100, eta1 = 0.1, trace = FALSE)
  )

  expect_true(result$converged)
  expect_equal(result$par, c(1, 1), tolerance = 1e-5)
})

test_that("Newton and cubic steps use same sigma update mechanism", {
  sphere_fn <- function(x) sum(x^2)
  sphere_gr <- function(x) 2 * x
  sphere_hess <- function(x) 2 * diag(length(x))

  x0 <- c(1, 1, 1)

  result <- arcopt(
    x0,
    sphere_fn,
    sphere_gr,
    sphere_hess,
    control = list(maxit = 50, trace = FALSE)
  )

  expect_true(result$converged)
  expect_equal(result$par, c(0, 0, 0), tolerance = 1e-8)
})

test_that("arcopt forwards ... to fn, gr, and hess", {
  # Scaled sphere: f(x; s) = s * sum(x^2)
  scaled_fn <- function(x, scale) scale * sum(x^2)
  scaled_gr <- function(x, scale) scale * 2 * x
  scaled_hess <- function(x, scale) scale * 2 * diag(length(x))

  result <- arcopt(
    x0 = c(5, 5),
    fn = scaled_fn,
    gr = scaled_gr,
    hess = scaled_hess,
    scale = 2
  )

  expect_true(result$converged)
  expect_equal(result$par, c(0, 0), tolerance = 1e-6)
  expect_equal(result$value, 0, tolerance = 1e-8)
})

test_that("arcopt forwards ... through use_qn dispatch", {
  # Verify ... reaches arcopt_qn when use_qn = TRUE
  scaled_fn <- function(x, scale) scale * sum(x^2)
  scaled_gr <- function(x, scale) scale * 2 * x
  scaled_hess <- function(x, scale) scale * 2 * diag(length(x))

  result <- arcopt(
    x0 = c(5, 5),
    fn = scaled_fn,
    gr = scaled_gr,
    hess = scaled_hess,
    scale = 2,
    control = list(use_qn = TRUE, qn_method = "sr1", trace = 0)
  )

  expect_true(result$converged)
  expect_equal(result$par, c(0, 0), tolerance = 1e-4)
})
