# tests/testthat/test-mpm_elasticity.R


test_that("mpm_elasticity: lambda framework input checks", {
  matA <- matrix(
    c(
      0.2, 0.0,
      0.3, 0.4
    ),
    nrow = 2, byrow = TRUE
  )

  # matA required
  expect_error(mpm_elasticity(matA = NULL, framework = "lambda"),
               "`matA` must be provided")

  # components must be NULL in lambda framework
  matF <- matrix(
    c(
      0, 1,
      0, 0
    ),
    nrow = 2, byrow = TRUE
  )

  matU <- matrix(
    c(
      0.2, 0.0,
      0.3, 0.4
    ),
    nrow = 2, byrow = TRUE
  )

  expect_error(mpm_elasticity(
    matA = matA,
    matF = matF,
    framework = "lambda"
  ),
  "only `matA` may be supplied")
  expect_error(mpm_elasticity(
    matA = matA,
    matU = matU,
    framework = "lambda"
  ),
  "only `matA` may be supplied")
  expect_error(mpm_elasticity(
    matA = matA,
    matC = matF,
    framework = "lambda"
  ),
  "only `matA` may be supplied")

  # matA must be a numeric matrix
  expect_error(mpm_elasticity(matA = list(1, 2), framework = "lambda"),
               "must be a numeric matrix")

  # matA must be square
  matA_ns <- matrix(1, nrow = 2, ncol = 3)
  expect_error(mpm_elasticity(matA = matA_ns, framework = "lambda"),
               "must be square")

  # matA must be nonnegative
  matA_neg <- matA
  matA_neg[1, 1] <- -0.01
  expect_error(mpm_elasticity(matA = matA_neg, framework = "lambda"),
               "must be nonnegative")

  # matA must not contain NA
  matA_na <- matA
  matA_na[1, 2] <- NA_real_
  expect_error(mpm_elasticity(matA = matA_na, framework = "lambda"), "contains NA")
})


test_that("mpm_elasticity: R0 framework input checks", {
  matF <- matrix(
    c(
      0.0, 1.2,
      0.0, 0.0
    ),
    nrow = 2, byrow = TRUE
  )

  matU <- matrix(
    c(
      0.2, 0.0,
      0.3, 0.4
    ),
    nrow = 2, byrow = TRUE
  )

  # matA must be NULL
  matA <- matF + matU
  expect_error(mpm_elasticity(
    matA = matA,
    matF = matF,
    matU = matU,
    framework = "R0"
  ),
  "`matA` must be NULL")

  # matF and matU required
  expect_error(
    mpm_elasticity(
      matF = NULL,
      matU = matU,
      framework = "R0"
    ),
    "`matF` and `matU` must be provided"
  )
  expect_error(
    mpm_elasticity(
      matF = matF,
      matU = NULL,
      framework = "R0"
    ),
    "`matF` and `matU` must be provided"
  )

  # must be numeric matrices
  expect_error(mpm_elasticity(
    matF = list(1, 2),
    matU = matU,
    framework = "R0"
  ),
  "must be a numeric matrix")

  # must be square
  matF_ns <- matrix(1, nrow = 2, ncol = 3)
  expect_error(mpm_elasticity(
    matF = matF_ns,
    matU = matU,
    framework = "R0"
  ),
  "must be square")

  # dims must match
  matU3 <- diag(3)
  expect_error(mpm_elasticity(
    matF = matF,
    matU = matU3,
    framework = "R0"
  ),
  "identical dimensionality")

  # matC dims must match if provided
  matC_bad <- diag(3)
  expect_error(
    mpm_elasticity(
      matF = matF,
      matU = matU,
      matC = matC_bad,
      framework = "R0"
    ),
    "identical dimensionality"
  )

  # nonnegativity
  matU_neg <- matU
  matU_neg[2, 2] <- -0.01
  expect_error(mpm_elasticity(
    matF = matF,
    matU = matU_neg,
    framework = "R0"
  ),
  "must be nonnegative")
})


test_that("mpm_elasticity: lambda framework returns expected structure and numeric matrix (mocked)",
          {
            matA <- matrix(
              c(
                0.2, 0.0,
                0.3, 0.4
              ),
              nrow = 2, byrow = TRUE
            )

            # Mock irreducibility checker (avoid dependence on graph structure in unit tests)
            local_mocked_bindings(
              .check_irreducible_hj = function(A, name = "matA", tol = 0)
                invisible(TRUE)
            )

            # Mock dominant_eigen to provide deterministic w, v, lambda
            w <- c(1, 2)
            v <- c(3, 4)
            lambda <- 5

            local_mocked_bindings(
              dominant_eigen = function(...)
                list(w = w, v = v, lambda = lambda)
            )

            out <- mpm_elasticity(matA = matA, framework = "lambda")

            expect_type(out, "list")
            expect_named(out, c("framework", "elasticity"))
            expect_equal(out$framework, "lambda")
            expect_true(is.matrix(out$elasticity))
            expect_equal(dim(out$elasticity), dim(matA))
            expect_true(all(is.finite(out$elasticity)))

            # Expected value from internal .elast: diag(v) %*% A %*% diag(w) / lambda
            expected <- diag(v) %*% matA %*% diag(w) / lambda
            expect_equal(out$elasticity, expected, tolerance = 1e-12)
          })


test_that("mpm_elasticity: R0 framework returns expected structure and numeric matrix (mocked)",
          {
            matF <- matrix(
              c(
                0.0, 1.2,
                0.0, 0.0
              ),
              nrow = 2, byrow = TRUE
            )

            matU <- matrix(
              c(
                0.2, 0.0,
                0.3, 0.4
              ),
              nrow = 2, byrow = TRUE
            )

            matR <- matF
            matA <- matR + matU

            # Mock irreducibility checker
            local_mocked_bindings(
              .check_irreducible_hj = function(A, name = "matA", tol = 0)
                invisible(TRUE)
            )

            # Mock spectral_radius and dominant_eigen (for Acohort)
            R0 <- 1.7
            w <- c(1, 2)
            v <- c(3, 4)

            local_mocked_bindings(
              spectral_radius = function(...)
                R0,
              dominant_eigen  = function(...)
                list(w = w, v = v, lambda = 999) # lambda unused in R0 branch
            )

            out <- mpm_elasticity(matF = matF,
                              matU = matU,
                              framework = "R0",
                              normalize=FALSE)

            expect_type(out, "list")
            expect_named(out, c("framework", "elasticity"))
            expect_equal(out$framework, "R0")
            expect_true(is.matrix(out$elasticity))
            expect_equal(dim(out$elasticity), dim(matA))
            expect_true(all(is.finite(out$elasticity)))

            Acohort <- matR + R0 * matU
            den <- as.numeric(t(v) %*% matR %*% w)
            expected <- diag(v) %*% Acohort %*% diag(w) / den

            expect_equal(out$elasticity, expected, tolerance = 1e-12)
          })


test_that("mpm_elasticity: R0 framework errors when (I - U) is singular", {
  # Make I - U singular: U = I
  matU <- diag(2)
  matF <- matrix(
    c(
      0, 1,
      0, 0
    ),
    nrow = 2, byrow = TRUE
  )

  # Mock irreducibility checker (we should fail after constructing matA but before spectral radius is needed)
  local_mocked_bindings(
    .check_irreducible_hj = function(A, name = "matA", tol = 0)
      invisible(TRUE),
    spectral_radius = function(...)
      1.0,
    dominant_eigen  = function(...)
      list(
        w = c(1, 1),
        v = c(1, 1),
        lambda = 1
      )
  )

  expect_error(mpm_elasticity(
    matF = matF,
    matU = matU,
    framework = "R0"
  ),
  "singular")
})


test_that("elasticity (lambda framework) matches numerical derivative", {
  ## small, irreducible, strictly positive matrix
  matA <- matrix(
    c(
      0.3, 0.1,
      0.4, 0.2
    ),
    nrow = 2, byrow = TRUE
  )

  ## compute analytic elasticity
  out <- mpm_elasticity(matA = matA, framework = "lambda")
  E_analytic <- out$elasticity

  ## baseline lambda
  lambda0 <- dominant_eigen(matA, ensure_positive = TRUE)$lambda

  ## numerical derivative settings
  eps <- 1e-6
  n <- nrow(matA)
  E_numeric <- matrix(NA_real_, n, n)

  ## finite-difference elasticity
  for (i in seq_len(n)) {
    for (j in seq_len(n)) {
      if (matA[i, j] == 0) {
        E_numeric[i, j] <- 0
      } else {
        matA_pert <- matA
        matA_pert[i, j] <- matA_pert[i, j] + eps

        lambda_pert <- dominant_eigen(matA_pert, ensure_positive = TRUE)$lambda

        d_lambda <- (lambda_pert - lambda0) / eps

        E_numeric[i, j] <- (matA[i, j] / lambda0) * d_lambda
      }
    }
  }

  ## compare analytic vs numerical elasticity
  expect_equal(E_analytic, E_numeric, tolerance = 1e-4)
})


test_that("elasticity (R0 framework) matches numerical derivative of R0 w.r.t. entries of matA",
          {
            # Construct matF (reproduction) and matU (transition) with disjoint nonzero supports.
            # This makes 'perturb entry of matA' unambiguous: it corresponds to exactly one component.
            matF <- matrix(
              c(
                0.0, 1.1, 0.4,
                0.0, 0.0, 0.0,
                0.0, 0.0, 0.0
              ),
              nrow = 3, byrow = TRUE
            )

            # NOTE: row 1, col 2 and col 3 are zero here to avoid overlap with matF
            matU <- matrix(
              c(
                0.15, 0.0, 0.0,
                0.30, 0.10, 0.25,
                0.05, 0.35, 0.10
              ),
              nrow = 3, byrow = TRUE
            )


            # matC optional; keep NULL here
            matC <- NULL

            # Analytic elasticity from your function
            out <- mpm_elasticity(matF = matF,
                              matU = matU,
                              framework = "R0",
                              normalize = FALSE)
            E_analytic <- out$elasticity

            # Define baseline A, R, and R0 (same construction used in your code)
            matR <- matF
            matA <- matR + matU

            n <- nrow(matA)
            I_minus_U <- diag(n) - matU

            # Baseline R0 from next-generation matrix
            K0 <- matR %*% solve(I_minus_U)
            R0_0 <- spectral_radius(K0)

            # Finite-difference settings
            eps <- 1e-6
            E_numeric <- matrix(0, n, n)

            # Helper to recompute R0 after perturbing a single entry in either matR or matU
            R0_from_parts <- function(R, U) {
              ImU <- diag(nrow(U)) - U
              K <- R %*% solve(ImU)
              spectral_radius(K)
            }

            for (i in seq_len(n)) {
              for (j in seq_len(n)) {
                # Elasticity at zero entries is defined as 0 because of the prefactor a_ij / R0.
                if (matA[i, j] == 0) {
                  E_numeric[i, j] <- 0
                  next
                }

                # Decide which component owns this A entry (disjoint supports assumed)
                in_R <- (matR[i, j] != 0)
                in_U <- (matU[i, j] != 0)

                # Sanity: with disjoint supports, exactly one should be TRUE for nonzero A entries
                expect_true(xor(in_R, in_U))

                # Central difference when possible (keeps better numerical accuracy)
                # For central diff on R entries, must keep R nonnegative; same for U entries.
                R_plus <- matR
                R_minus <- matR
                U_plus <- matU
                U_minus <- matU

                if (in_R) {
                  R_plus[i, j]  <- R_plus[i, j]  + eps
                  if (matR[i, j] > eps) {
                    R_minus[i, j] <- R_minus[i, j] - eps
                  } else {
                    R_minus <- NULL
                  }
                } else {
                  # in_U
                  U_plus[i, j]  <- U_plus[i, j]  + eps
                  if (matU[i, j] > eps) {
                    U_minus[i, j] <- U_minus[i, j] - eps
                  } else {
                    U_minus <- NULL
                  }
                }

                R0_plus <- R0_from_parts(R_plus, U_plus)

                if (is.null(R_minus) || is.null(U_minus)) {
                  # Forward difference fallback
                  dR0 <- (R0_plus - R0_0) / eps
                } else {
                  R0_minus <- R0_from_parts(R_minus, U_minus)
                  dR0 <- (R0_plus - R0_minus) / (2 * eps)
                }

                # Elasticity definition: e_ij = (a_ij / R0) * dR0/da_ij
                E_numeric[i, j] <- (matA[i, j] / R0_0) * dR0
              }
            }

            # Compare analytic vs numeric elasticities.
            # Tolerance is intentionally modest because eigenvalue-based R0 can be noisy under finite differences.
            expect_equal(E_analytic, E_numeric, tolerance = 1e-8)
          })
