# inst/tinytest/test_cbf.R
#
# Run with:  tinytest::test_package("ROI.format.cbf")
#
# Tests are organised in four groups:
#   1. Writer output  – known OP  → CBF text, check the file content directly.
#   2. Reader output  – hand-written CBF string → OP, check structure/values.
#   3. Round-trip     – OP → CBF → OP, compare OP fields.
#   4. Solver         – OP → CBF → OP → solve (requires ROI.plugin.ecos).
#
# Every test uses a temporary file cleaned up by on.exit().
# The ECOS group is guarded by requireNamespace() so the package suite
# passes even when ROI.plugin.ecos is not installed.

Sys.setenv(ROI_LOAD_PLUGINS = "FALSE")
library(ROI.format.cbf)
library(ROI)
library(slam)

# Helper: write a CBF string to a temp file and return the path.
.cbf_tmp <- function(text) {
    f <- tempfile(fileext = ".cbf")
    writeLines(text, f)
    f
}

# Helper: read all non-comment, non-empty lines from a CBF file into a
# named list so tests can query individual sections.
.read_sections <- function(path) {
    lines <- readLines(path, warn = FALSE)
    lines <- trimws(lines)
    lines <- lines[!startsWith(lines, "#") & nzchar(lines)]
    kws   <- c("VER", "OBJSENSE", "POWCONES", "POW*CONES",
               "PSDVAR", "VAR", "INT", "PSDCON", "CON",
               "OBJFCOORD", "OBJACOORD", "OBJBCOORD",
               "FCOORD", "ACOORD", "BCOORD", "HCOORD", "DCOORD", "CHANGE")
    is_kw  <- lines %in% kws
    kw_pos <- which(is_kw)
    result <- list()
    for (k in seq_along(kw_pos)) {
        s <- kw_pos[k] + 1L
        e <- if (k < length(kw_pos)) kw_pos[k + 1L] - 1L else length(lines)
        result[[lines[kw_pos[k]]]] <- if (s <= e) lines[s:e] else character(0L)
    }
    result
}

# Helper that writes op to CBF, reads it back, and returns the re-read OP.
.round_trip <- function(op) {
    f <- tempfile(fileext = ".cbf")
    on.exit(unlink(f), add = TRUE)
    write_cbf(op, f)
    read_cbf(f)
}

# ===========================================================================
# 1. WRITER TESTS
# ===========================================================================

# ---------------------------------------------------------------------------
# 1a. Minimal LP – spec §1.1 example
#     minimize  5.1 x0
#     s.t.      6.2 x1 + 7.3 x2 - 8.4 = 0    (L= equality)
#               (x0, x1, x2) ∈ Q^3            (SOC via VAR)
#               x0 ∈ Z
# ---------------------------------------------------------------------------

op_lp_minimal <- OP(
    objective   = L_objective(c(5.1, 0, 0)),
    constraints = L_constraint(
        L   = matrix(c(0, 6.2, 7.3), nrow = 1),
        dir = "==",
        rhs = 8.4          # rhs_roi = -b_cbf  =>  b_cbf = -8.4 in BCOORD
    ),
    types  = c("I", "C", "C"),
    bounds = V_bound(li = 1:3, ui = 1:3,
                     lb = c(-Inf, -Inf, -Inf),
                     ub = c( Inf,  Inf,  Inf),
                     nobj = 3L),
    maximum = FALSE
)

f_min <- tempfile(fileext = ".cbf")
write_cbf(op_lp_minimal, f_min)

sec_min <- .read_sections(f_min)

expect_equal(sec_min[["VER"]], "4", info = "VER section must contain '4'")

expect_equal(toupper(sec_min[["OBJSENSE"]]), "MIN", info = "OBJSENSE must be MIN")

# VAR: 3 variables, 1 chunk (all free given -Inf/+Inf bounds)
var_hdr <- strsplit(sec_min[["VAR"]][1L], "\\s+")[[1L]]
expect_equal(as.integer(var_hdr[1L]), 3L, info = "VAR header: total variables = 3")
expect_equal(as.integer(var_hdr[2L]), 1L, info = "VAR header: 1 cone chunk")
expect_equal(trimws(sec_min[["VAR"]][2L]), "F 3", info = "VAR body: 3 free variables")

# INT: 1 integer variable, index 0
int_lines <- sec_min[["INT"]]
expect_equal(as.integer(int_lines[1L]), 1L, info = "INT header: 1 integer variable")
expect_equal(as.integer(int_lines[2L]), 0L, info = "INT body: variable index 0 (x0)")

# CON: 1 constraint, 1 chunk, L=
con_hdr   <- strsplit(sec_min[["CON"]][1L], "\\s+")[[1L]]
expect_equal(as.integer(con_hdr[1L]), 1L, info = "CON header: 1 constraint")
expect_equal(trimws(sec_min[["CON"]][2L]), "L= 1", info = "CON body: equality cone chunk")

# ACOORD: coefficients for (x1, x2) at constraint 0
# Expect entries for (0 1 6.2) and (0 2 7.3) in some order.
a_entries <- sec_min[["ACOORD"]][-1L]   # drop count header
a_mat     <- do.call(rbind, lapply(a_entries, function(ln)
    as.numeric(strsplit(trimws(ln), "\\s+")[[1L]])))
# column 1 = constraint idx (0-based), col 2 = var idx (0-based), col 3 = val
expect_true(any(abs(a_mat[, 2L] - 1) < 1e-9 & abs(a_mat[, 3L] - 6.2) < 1e-9), info = "ACOORD: coefficient 6.2 for variable index 1")
expect_true(any(abs(a_mat[, 2L] - 2) < 1e-9 & abs(a_mat[, 3L] - 7.3) < 1e-9), info = "ACOORD: coefficient 7.3 for variable index 2")

# BCOORD: b_cbf = -rhs_roi = -8.4 for constraint 0
b_entries <- sec_min[["BCOORD"]][-1L]
b_mat     <- do.call(rbind, lapply(b_entries, function(ln)
    as.numeric(strsplit(trimws(ln), "\\s+")[[1L]])))
expect_true(any(b_mat[, 1L] == 0 & abs(b_mat[, 2L] - (-8.4)) < 1e-9), info = "BCOORD: b = -8.4 for constraint 0 (= -rhs_roi)")

# OBJACOORD: coefficient 5.1 for variable 0
obj_entries <- sec_min[["OBJACOORD"]][-1L]
obj_mat     <- do.call(rbind, lapply(obj_entries, function(ln)
    as.numeric(strsplit(trimws(ln), "\\s+")[[1L]])))
expect_true(any(obj_mat[, 1L] == 0 & abs(obj_mat[, 2L] - 5.1) < 1e-9), info = "OBJACOORD: coefficient 5.1 for variable 0")


# ---------------------------------------------------------------------------
# 1b. LP writer – Example C.6 from spec
#     maximize  x0 + 0.64 x1
#     s.t.      50 x0 + 31 x1 <= 250    (L- constraint, b_cbf = 250 - shifted)
#               3  x0 -  2 x1 >= -4     (L+ constraint)
#               x0, x1 >= 0
# ---------------------------------------------------------------------------

op_c6 <- OP(
    objective   = L_objective(c(1, 0.64)),
    constraints = L_constraint(
        L   = rbind(c(50, 31), c(3, -2)),
        dir = c("<=", ">="),
        rhs = c(250, -4)
    ),
    bounds  = V_bound(lb = c(0, 0), ub = c(Inf, Inf), nobj = 2L),
    maximum = TRUE
)

f_c6 <- tempfile(fileext = ".cbf")
write_cbf(op_c6, f_c6)

sec_c6 <- .read_sections(f_c6)

expect_equal(toupper(sec_c6[["OBJSENSE"]]), "MAX", info = "C.6 OBJSENSE = MAX")

# Variables declared as L+ (nonneg bounds)
expect_true(grepl("^L\\+", sec_c6[["VAR"]][2L]), info = "C.6 VAR: nonneg cone (L+)")

# CON should have two chunks in order: L- then L+
con_chunks <- sec_c6[["CON"]][-1L]
expect_equal(trimws(con_chunks[1L]), "L- 1", info = "C.6 CON: first chunk is L- (<=)")
expect_equal(trimws(con_chunks[2L]), "L+ 1", info = "C.6 CON: second chunk is L+ (>=)")

# BCOORD: constraint 0 gets b = -rhs = -250; constraint 1 gets b = -(-4) = 4
b_c6 <- sec_c6[["BCOORD"]][-1L]
b_c6_mat <- do.call(rbind, lapply(b_c6, function(ln)
    as.numeric(strsplit(trimws(ln), "\\s+")[[1L]])))
expect_true(any(b_c6_mat[, 1L] == 0 & abs(b_c6_mat[, 2L] - (-250)) < 1e-9), info = "C.6 BCOORD: b=-250 for constraint 0 (<= 250)")
expect_true(any(b_c6_mat[, 1L] == 1 & abs(b_c6_mat[, 2L] - 4) < 1e-9), info = "C.6 BCOORD: b=4 for constraint 1 (>= -4)")


# ===========================================================================
# 2. READER TESTS
# ===========================================================================

# ---------------------------------------------------------------------------
# 2a. Read the spec §1.1 minimal working example directly from CBF text.
#     minimize  5.1 x0
#     s.t.      6.2 x1 + 7.3 x2 - 8.4 = 0
#               x ∈ Q^3,  x0 ∈ Z
# ---------------------------------------------------------------------------

cbf_minimal <- c(
    "VER",
    "4",
    "OBJSENSE",
    "MIN",
    "VAR", 
    "3 1", 
    "Q 3",
    "INT",
    "1",
    "0",
    "CON",
    "1 1",
    "L= 1",
    "OBJACOORD",
    "1",
    "0 5.1",
    "ACOORD",
    "2",
    "0 1 6.2",
    "0 2 7.3",
    "BCOORD",
    "1",
    "0 -8.4"
)

f_read_min <- .cbf_tmp(cbf_minimal)
op_read_min <- read_cbf(f_read_min)

expect_equal(op_read_min$maximum, FALSE,
             info = "Reader minimal: maximum = FALSE (MIN)")

# Objective: coefficient 5.1 on x0
obj_coef <- as.vector(as.matrix(terms(objective(op_read_min))$L))
expect_equal(length(obj_coef), 3L, info = "Reader minimal: 3 objective coefficients")
expect_equal(obj_coef[1L], 5.1, info = "Reader minimal: obj coef[1] = 5.1")
expect_equal(obj_coef[2L], 0, info = "Reader minimal: obj coef[2] = 0")

# Integer types: x0 is I, x1 and x2 are C
types_read <- types(op_read_min)
expect_equal(types_read[1L], "I", info = "Reader minimal: x0 is integer")
expect_equal(types_read[2L], "C", info = "Reader minimal: x1 is continuous")

# The equality constraint: rhs = -b = -(-8.4) = 8.4
# It is represented as a C_constraint (Q cone) or L_constraint (L= chunk).
# CON declares one L= chunk, so we get an L_constraint with dir "==".
con_obj <- constraints(op_read_min)
expect_true(inherits(con_obj, "L_constraint") ||
            inherits(con_obj, "constraint"),
            info = "Reader minimal: constraint object is present")


# ---------------------------------------------------------------------------
# 2b. Read Example C.6 LP from CBF text.
#     maximize  x0 + 0.64 x1
#     s.t.      50 x0 + 31 x1 <= 250   (b_cbf = -250 means BCOORD = -250)
#               3  x0 -  2 x1 >= -4    (b_cbf = 4)
#               x0, x1 >= 0            (VAR: L+)
# ---------------------------------------------------------------------------

cbf_c6 <- c(
    "VER",
    "4",
    "OBJSENSE",
    "MAX",
    "VAR",
    "2 1",
    "L+ 2",
    "CON",
    "2 2",
    "L- 1",
    "L+ 1",
    "OBJACOORD",
    "2",
    "0 1.0",
    "1 0.64",
    "ACOORD",
    "4",
    "0 0 50.0",
    "0 1 31.0",
    "1 0 3.0",
    "1 1 -2.0",
    "BCOORD",
    "2",
    "0 -250.0",
    "1 4.0"
)

f_read_c6 <- .cbf_tmp(cbf_c6)
op_read_c6 <- read_cbf(f_read_c6)

expect_equal(op_read_c6$maximum, TRUE, info = "Reader C.6: maximum = TRUE (MAX)")

# Objective coefficients
obj_c6 <- as.vector(as.matrix(terms(objective(op_read_c6))$L))
expect_equal(obj_c6[1L], 1.0, info = "Reader C.6: obj coef[1] = 1.0")
expect_equal(obj_c6[2L], 0.64, info = "Reader C.6: obj coef[2] = 0.64")

# Constraints: the two L_constraint chunks are combined.
# Check that the constraint matrix has the right dimensions: 2 rows × 2 cols.
con_c6 <- constraints(op_read_c6)
L_c6   <- as.matrix(con_c6$L)
expect_equal(nrow(L_c6), 2L, info = "Reader C.6: constraint matrix has 2 rows")
expect_equal(ncol(L_c6), 2L, info = "Reader C.6: constraint matrix has 2 columns")

# RHS: rhs = -b_cbf → row 0 rhs = 250, row 1 rhs = -4
rhs_c6 <- con_c6$rhs
expect_equal(rhs_c6[1L], 250, info = "Reader C.6: rhs[1] = 250 (from b_cbf = -250)")
expect_equal(rhs_c6[2L], -4, info = "Reader C.6: rhs[2] = -4 (from b_cbf = 4)")


# ---------------------------------------------------------------------------
# 2c. Reader: comment lines and empty lines are ignored.
# ---------------------------------------------------------------------------

cbf_with_comments <- c(
    "# This is a comment",
    "VER",
    "# Another comment",
    "4",
    "",
    "OBJSENSE",
    "MIN",
    "",
    "VAR",
    "2 1",
    "# inline comment",
    "L+ 2",
    "",
    "OBJACOORD",
    "2",
    "0 1.0",
    "1 2.0"
)

f_comments <- .cbf_tmp(cbf_with_comments)
op_comments <- read_cbf(f_comments)

expect_equal(ncol(terms(objective(op_comments))$L), 2L, info = "Reader: comment lines do not corrupt parsing")
expect_equal(as.vector(as.matrix(terms(objective(op_comments))$L))[2L], 2.0, info = "Reader: obj coef[2] = 2.0 despite comments")


# ---------------------------------------------------------------------------
# 2d. Reader: CHANGE keyword treated as end-of-file.
# ---------------------------------------------------------------------------

cbf_change <- c(
    "VER",
    "4",
    "OBJSENSE",
    "MIN",
    "VAR",
    "1 1",
    "L+ 1",
    "OBJACOORD",
    "1",
    "0 3.0",
    "CHANGE",
    # Everything below the CHANGE keyword must be ignored.
    "OBJACOORD", "1", "0 99.0"
)

f_change <- .cbf_tmp(cbf_change)
op_change <- read_cbf(f_change)

coef_change <- as.vector(as.matrix(terms(objective(op_change))$L))[1L]
expect_equal(coef_change, 3.0, info = "Reader: CHANGE stops parsing; coefficient after CHANGE is ignored")


# ---------------------------------------------------------------------------
# 2e. Reader: nonlinear VAR chunk EXP becomes an ROI exponential-cone constraint.
# ---------------------------------------------------------------------------

cbf_var_exp <- c(
    "VER",
    "4",
    "OBJSENSE",
    "MIN",
    "VAR",
    "3 1",
    "EXP 3"
)

op_var_exp <- read_cbf(.cbf_tmp(cbf_var_exp))
con_var_exp <- constraints(op_var_exp)

expect_true(inherits(con_var_exp, "C_constraint"),
            info = "Reader VAR EXP: produces a conic constraint")
expect_equal(con_var_exp$cones$cone[1L], 5L,
             info = "Reader VAR EXP: cone type is ROI primal exponential")
expect_equal(as.matrix(terms(con_var_exp)$L), -diag(3)[,3:1], tolerance = 1e-10,
             info = "Reader VAR EXP: primal exponential cone is mapped to ROI form by negation and swapping coordinates 1 and 3")
expect_equal(con_var_exp$rhs, c(0, 0, 0), tolerance = 1e-10,
             info = "Reader VAR EXP: rhs is zero")


# ---------------------------------------------------------------------------
# 2f. Reader: nonlinear VAR chunk EXP* becomes an ROI dual exponential-cone constraint.
# ---------------------------------------------------------------------------

cbf_var_expd <- c(
    "VER",
    "4",
    "OBJSENSE",
    "MIN",
    "VAR",
    "3 1",
    "EXP* 3"
)

op_var_expd <- read_cbf(.cbf_tmp(cbf_var_expd))
con_var_expd <- constraints(op_var_expd)

expect_true(inherits(con_var_expd, "C_constraint"),
            info = "Reader VAR EXP*: produces a conic constraint")
expect_equal(con_var_expd$cones$cone[1L], 6L,
             info = "Reader VAR EXP*: cone type is ROI dual exponential")
expect_equal(as.matrix(terms(con_var_expd)$L), -diag(3)[,3:1], tolerance = 1e-10,
             info = "Reader VAR EXP*: dual exponential cone is mapped to ROI form by negation and swapping coordinates 1 and 3")
expect_equal(con_var_expd$rhs, c(0, 0, 0), tolerance = 1e-10,
             info = "Reader VAR EXP*: rhs is zero")


# ===========================================================================
# 3. ROUND-TRIP TESTS (write then read back)
# ===========================================================================

# ---------------------------------------------------------------------------
# 3a. LP round-trip (Example C.6)
# ---------------------------------------------------------------------------

op_rt_lp <- .round_trip(op_c6)

expect_equal(op_rt_lp$maximum, op_c6$maximum, info = "Round-trip LP: maximum flag preserved")

obj_rt <- as.vector(as.matrix(terms(objective(op_rt_lp))$L))
obj_or <- as.vector(as.matrix(terms(objective(op_c6))$L))
expect_equal(obj_rt, obj_or, tolerance = 1e-10, info = "Round-trip LP: objective coefficients preserved")

rhs_rt <- constraints(op_rt_lp)$rhs
rhs_or <- constraints(op_c6)$rhs
expect_equal(sort(rhs_rt), sort(rhs_or), tolerance = 1e-10, info = "Round-trip LP: RHS values preserved (after sorting for order independence)")

# ---------------------------------------------------------------------------
# 3b. Mixed-integer LP round-trip
# ---------------------------------------------------------------------------

op_milp <- OP(
    objective   = L_objective(c(1, 2, 3)),
    constraints = L_constraint(
        L   = rbind(c(1, 1, 1), c(2, 0, 1)),
        dir = c("<=", ">="),
        rhs = c(10, 3)
    ),
    types  = c("I", "C", "I"),
    bounds = V_bound(lb = c(0, 0, 0), ub = c(Inf, Inf, Inf), nobj = 3L),
    maximum = FALSE
)

op_rt_milp <- .round_trip(op_milp)

types_rt <- types(op_rt_milp)
expect_equal(types_rt[1L], "I", info = "Round-trip MILP: x0 is integer")
expect_equal(types_rt[2L], "C", info = "Round-trip MILP: x1 is continuous")
expect_equal(types_rt[3L], "I", info = "Round-trip MILP: x2 is integer")


# ---------------------------------------------------------------------------
# 3c. Objective coefficients round-trip (negative coefficients)
# NOTE: L_objective() in this ROI version does not accept a 'constant'
#       argument, so the OBJBCOORD round-trip is not tested here.
# ---------------------------------------------------------------------------

op_const <- OP(
    objective = L_objective(c(2, -1)),
    bounds    = V_bound(lb = c(-Inf, -Inf), ub = c(Inf, Inf), nobj = 2L),
    maximum   = FALSE
)

op_rt_const <- .round_trip(op_const)
coef_rt <- as.vector(as.matrix(terms(objective(op_rt_const))$L))
expect_equal(coef_rt[1L], 2,  tolerance = 1e-10, info = "Round-trip 3c: objective coef[1] = 2")
expect_equal(coef_rt[2L], -1, tolerance = 1e-10, info = "Round-trip 3c: objective coef[2] = -1")


# ---------------------------------------------------------------------------
# 2c. Read a rotated quadratic cone (QR) by bridging it to a standard SOC.
#     CBF cone: (u, v, w) in QR  <=>  (u+v, u-v, sqrt(2) w) in Q
# ---------------------------------------------------------------------------

cbf_qr <- c(
    "VER",
    "4",
    "OBJSENSE",
    "MIN",
    "VAR",
    "2 1",
    "F 2",
    "CON",
    "3 1",
    "QR 3",
    "ACOORD",
    "3",
    "0 0 1",
    "1 1 1",
    "2 0 1",
    "BCOORD",
    "0"
)

f_read_qr <- .cbf_tmp(cbf_qr)
op_read_qr <- read_cbf(f_read_qr)

qr_con <- constraints(op_read_qr)
expect_true(inherits(qr_con, "C_constraint"), info = "Reader QR bridge: produces a conic constraint")
expect_equal(qr_con$cones$cone[1L], 3L, info = "Reader QR bridge: rotated cone is rewritten to ROI SOC")

qr_L <- as.matrix(qr_con$L)
expect_equal(qr_L[1L, ], c(-1, -1), tolerance = 1e-10, info = "Reader QR bridge: first row is -(u + v) in ROI conic form")
expect_equal(qr_L[2L, ], c(-1, 1), tolerance = 1e-10, info = "Reader QR bridge: second row is -(u - v) in ROI conic form")
expect_equal(qr_L[3L, ], c(-sqrt(2), 0), tolerance = 1e-10, info = "Reader QR bridge: tail rows are scaled and negated for ROI conic form")
expect_equal(qr_con$rhs, c(0, 0, 0), tolerance = 1e-10, info = "Reader QR bridge: rhs is transformed consistently")


# ===========================================================================
# 4. SOLVER TESTS
# ===========================================================================

if (!requireNamespace("ROI.plugin.ecos", quietly = TRUE)) {
    message("NOTE: ROI.plugin.ecos not installed — skipping ECOS solver tests.")
} else {
    library(ROI.plugin.ecos)

    # -------------------------------------------------------------------------
    # 4a. LP via CBF round-trip: Example C.6
    # -------------------------------------------------------------------------

    op_c6 <- OP(
        objective   = L_objective(c(1, 0.64)),
        constraints = L_constraint(
            L   = rbind(c(50, 31), 
                        c(3, -2)),
            dir = c("<=", ">="),
            rhs = c(250, -4)
        ),
        bounds  = V_bound(lb = c(0, 0), ub = c(Inf, Inf), nobj = 2L),
        maximum = TRUE
    )
    op_lp_solve <- .round_trip(op_c6)
    res_lp <- ROI_solve(op_lp_solve)

    expect_equal(res_lp$status$code, 0L, info = "Solver LP (C.6): solver reports optimal (code=0)")

    sol_lp <- solution(res_lp)
    expect_true(all(sol_lp >= -1e-6), info = "Solver LP (C.6): solution is nonneg (variable bounds)")

    x <- sol_lp
    expect_true(50 * x[1L] + 31 * x[2L] <= 250 + 1e-6, info = "Solver LP (C.6): 50 x0 + 31 x1 <= 250")
    expect_true(3 * x[1L] - 2 * x[2L] >= -4 - 1e-6, info = "Solver LP (C.6): 3 x0 - 2 x1 >= -4")

    obj_val_lp <- as.vector(as.matrix(terms(objective(op_lp_solve))$L)) %*% x
    expect_true(obj_val_lp >= 5 - 1e-6, info = "Solver LP (C.6): objective >= 5 (feasible vertex lower bound)")

    # -------------------------------------------------------------------------
    # 4b. SOCP via CBF round-trip
    # -------------------------------------------------------------------------

    A_socp <- slam::simple_triplet_matrix(
        i = 1:3, j = 1:3, v = c(-1, -1, -1), nrow = 3L, ncol = 3L
    )

    op_socp <- OP(
        objective   = L_objective(c(1, 0, 0)),
        constraints = c(
            C_constraint(L = A_socp, cone = K_soc(3L), rhs = c(0, 0, 0)),
            L_constraint(L = matrix(c(0, 1, 0), nrow = 1), dir = "==", rhs = 3),
            L_constraint(L = matrix(c(0, 0, 1), nrow = 1), dir = "==", rhs = 4)
        ),
        bounds  = V_bound(lb = rep(-Inf, 3L), ub = rep(Inf, 3L), nobj = 3L),
        maximum = FALSE
    )

    op_socp_rt <- .round_trip(op_socp)
    res_socp   <- ROI_solve(op_socp_rt)

    expect_equal(res_socp$status$code, 0L, info = "Solver SOCP: solver reports optimal (code=0)")

    sol_socp <- solution(res_socp)
    expect_equal(sol_socp[1L], 5, tolerance = 1e-4, info = "Solver SOCP: t = sqrt(3^2 + 4^2) = 5")
    expect_equal(sol_socp[2L], 3, tolerance = 1e-4, info = "Solver SOCP: x0 = 3")
    expect_equal(sol_socp[3L], 4, tolerance = 1e-4, info = "Solver SOCP: x1 = 4")

    # -----------------------------------------------------------------------
    # 4c. Primal exponential-cone example with hardcoded reference solution.
    # -----------------------------------------------------------------------

    cbf_exp_primal <- c(
        "VER", "4",
        "OBJSENSE", "MIN",
        "VAR", "3 1", "EXP 3",
        "CON", "2 1", "L= 2",
        "ACOORD", "2", "0 1 1", "1 2 1",
        "BCOORD", "2", "0 -1", "1 -1",
        "OBJACOORD", "1", "0 1"
    )

    sol_exp_primal <- ROI_solve(read_cbf(.cbf_tmp(cbf_exp_primal)))
    expect_equal(sol_exp_primal$status$code, 0L, info = "Solver EXP primal example: solver reports optimal")
    expect_equal(solution(sol_exp_primal), c(exp(1), 1, 1), tolerance = 1e-6, info = "Solver EXP primal example: matches hardcoded reference")

}

if (!requireNamespace("ROI.plugin.scs", quietly = TRUE)) {
    message("NOTE: ROI.plugin.scs not installed — skipping SCS solver tests.")
} else {
    library(ROI.plugin.scs)

    # -----------------------------------------------------------------------
    # 4d. Dual exponential-cone example with hardcoded reference solution.
    # -----------------------------------------------------------------------

    cbf_exp_dual <- c(
        "VER", "4",
        "OBJSENSE", "MIN",
        "VAR", "3 1", "EXP* 3",
        "CON", "2 1", "L= 2",
        "ACOORD", "2", "0 1 1", "1 2 1",
        "BCOORD", "2", "0 0", "1 1",
        "OBJACOORD", "1", "0 1"
    )

    sol_exp_dual <- ROI_solve(read_cbf(.cbf_tmp(cbf_exp_dual)), solver="scs")
    expect_equal(sol_exp_dual$status$code, 0L, info = "Solver EXP dual example: solver reports optimal")
    expect_equal(solution(sol_exp_dual), c(exp(-1), 0, -1), tolerance = 1e-5, info = "Solver EXP dual example: matches hardcoded reference")
}
