async_true <- function() {
  promise_resolve(TRUE)
}

describe("Promise domains", {
  it("are reentered during handlers", {
    cd <- create_counting_domain(trackFinally = TRUE)
    p <- with_promise_domain(cd, {
      async_true() |>
        then(\(x) {
          expect_identical(cd$counts$onFulfilledCalled, 1L)
          expect_identical(cd$counts$onFulfilledActive, 1L)
          10 # sync result
        }) |>
        then(\(x) {
          expect_identical(x, 10)
          expect_identical(cd$counts$onFulfilledCalled, 2L)
          expect_identical(cd$counts$onFulfilledActive, 1L)
          promise_resolve(20) # async result
        })
    })

    expect_identical(cd$counts$onFulfilledBound, 2L)

    p <- p |>
      then(\(x) {
        expect_identical(cd$counts$onFulfilledCalled, 2L)
        expect_identical(cd$counts$onFulfilledActive, 0L)
      })

    expect_identical(cd$counts$onFulfilledBound, 2L)
    p |> wait_for_it()

    with_promise_domain(cd, {
      p <- async_true() |>
        finally(\() {
          expect_identical(cd$counts$onFinallyCalled, 1L)
          expect_identical(cd$counts$onFinallyActive, 1L)
        })
      expect_identical(cd$counts$onFinallyBound, 1L)

      expect_identical(cd$counts$onFulfilledBound, 2L)
      expect_identical(cd$counts$onRejectedBound, 0L)

      p |> wait_for_it()
    })

    expect_identical(cd$counts$onFulfilledBound, 2L)

    with_promise_domain(cd, {
      p <- async_true() |>
        then(\(x) {
          expect_identical(x, TRUE)
          expect_identical(cd$counts$onFulfilledCalled, 3L)
          ten <- 10
          # This tests if promise domain membership infects subscriptions made
          # in handlers.
          promise_resolve(invisible(ten)) |>
            then(\(value, .visible) {
              expect_identical(value, 10)
              expect_false(.visible)
              expect_true(!is.null(current_promise_domain()))
              expect_identical(cd$counts$onFulfilledCalled, 4L)
            })
        })
    })
    expect_true(is.null(current_promise_domain()))
    expect_identical(cd$counts$onFulfilledCalled, 2L)

    p |> wait_for_it()
  })

  it("pass finally binding to fulfill/reject by default", {
    cd1 <- create_counting_domain(trackFinally = FALSE)

    with_promise_domain(cd1, {
      p1 <- async_true() |>
        finally(\() {
          expect_identical(cd1$counts$onFulfilledActive, 1L)
          expect_identical(cd1$counts$onRejectedActive, 0L)
        })

      expect_identical(cd1$counts$onFulfilledBound, 1L)
      expect_identical(cd1$counts$onRejectedBound, 1L)
      p1 |> wait_for_it()
      expect_identical(cd1$counts$onFulfilledCalled, 1L)
      expect_identical(cd1$counts$onRejectedCalled, 0L)
    })

    cd2 <- create_counting_domain(trackFinally = FALSE)

    p2 <- with_promise_domain(cd2, {
      promise_reject("a problem") |>
        finally(\() {
          expect_identical(cd2$counts$onFulfilledActive, 0L)
          expect_identical(cd2$counts$onRejectedActive, 1L)
        })
    }) |>
      squelch_unhandled_promise_error()

    expect_identical(cd2$counts$onFulfilledBound, 1L)
    expect_identical(cd2$counts$onRejectedBound, 1L)
    p2 |> wait_for_it()
    expect_identical(cd2$counts$onFulfilledCalled, 0L)
    expect_identical(cd2$counts$onRejectedCalled, 1L)
  })

  it("doesn't intercept fulfill/reject on finally, if finally is explicitly intercepted", {
    cd1 <- create_counting_domain(trackFinally = TRUE)

    with_promise_domain(cd1, {
      p1 <- async_true() |>
        finally(\() {
          expect_identical(cd1$counts$onFinallyActive, 1L)
          expect_identical(cd1$counts$onFulfilledActive, 0L)
          expect_identical(cd1$counts$onRejectedActive, 0L)
        })

      expect_identical(cd1$counts$onFinallyBound, 1L)
      expect_identical(cd1$counts$onFulfilledBound, 0L)
      expect_identical(cd1$counts$onRejectedBound, 0L)
      p1 |> wait_for_it()
      expect_identical(cd1$counts$onFinallyCalled, 1L)
      expect_identical(cd1$counts$onFulfilledCalled, 0L)
      expect_identical(cd1$counts$onRejectedCalled, 0L)
    })

    cd2 <- create_counting_domain(trackFinally = TRUE)

    p2 <- with_promise_domain(cd2, {
      promise_reject(TRUE) |>
        finally(\() {
          expect_identical(cd2$counts$onFinallyActive, 1L)
          expect_identical(cd2$counts$onFulfilledActive, 0L)
          expect_identical(cd2$counts$onRejectedActive, 0L)
        })
    }) |>
      squelch_unhandled_promise_error()

    expect_identical(cd2$counts$onFinallyBound, 1L)
    expect_identical(cd2$counts$onFulfilledBound, 0L)
    expect_identical(cd2$counts$onRejectedBound, 0L)
    p2 |> wait_for_it()
    expect_identical(cd2$counts$onFinallyCalled, 1L)
    expect_identical(cd2$counts$onFulfilledCalled, 0L)
    expect_identical(cd2$counts$onRejectedCalled, 0L)
  })

  it("handles weird edge case relating to symbols", {
    # This test resulted from a bug in wrap_callback_reenter() where do.call()'s
    # default behavior of quote=FALSE would cause a resolved value that happens
    # to be a symbol, to be evaluated. This would only happen when a promise
    # domain was in effect, and the symbol was passed to an onFulfilled. Fixed
    # by using rlang::exec() instead of do.call().
    cd <- create_counting_domain()
    with_promise_domain(cd, {
      promise_resolve(as.symbol("foo")) |>
        then(\(x) {
          expect_identical(x, as.symbol("foo"))
        }) |>
        wait_for_it()
    })
  })

  it("executes wrap_callback_reenter handlers in the right lexical environment", {
    # No reason this shouldn't work, I haven't seen it fail, just making sure.
    cd <- create_counting_domain()
    x <- NULL
    with_promise_domain(cd, {
      promise_resolve(NULL) |>
        then(\() {
          x <<- 1L
        }) |>
        then(\() {
          x <<- x + 1L
        }) |>
        wait_for_it()
    })
    expect_identical(x, 2L)
  })

  it("doesn't grow the call stack", {
    # See https://github.com/rstudio/promises/issues/114
    # and also https://github.com/jcheng5/shinychat/issues/16

    recursive_promise <- function(n, .last_callstack_depth = NULL) {
      if (n == 0) {
        return(0)
      } else {
        promise_resolve(TRUE) |>
          then(\() {
            current_callstack_depth <- length(sys.calls())
            if (!is.null(.last_callstack_depth)) {
              expect_identical(current_callstack_depth, .last_callstack_depth)
            }

            recursive_promise(
              n - 1,
              .last_callstack_depth = current_callstack_depth
            )
          })
      }
    }

    cd <- create_counting_domain()
    with_promise_domain(cd, {
      recursive_promise(5) |>
        then(\() {
          # 5 (from inside recursive_promise) + 1 (for the current handler)
          expect_identical(cd$counts$onFulfilledCalled, 6L)
        }) |>
        wait_for_it()
    })

    cd2 <- create_counting_domain()
    p <- recursive_promise(5)
    with_promise_domain(cd2, {
      p |>
        then(\() {
          # This time, none of the ones inside recursive_promise count, since
          # they were bound outside of the influence of cd2 (even though they
          # are resolved within the influence of cd2, thanks to wait_for_it()).
          expect_identical(cd2$counts$onFulfilledCalled, 1L)
        }) |>
        wait_for_it()
    })
  })

  it("apply nested domains in reverse order (innermost domain closest to callback)", {
    # Test that when nesting promise domains, the latest domain to be added
    # (innermost) should be the one that wraps the onFulfilled/onRejected most closely.

    # Create a test environment to track domain activation order
    test_env <- new.env()
    test_env$x <- "initial"
    test_env$activations <- character(0)

    createVarPromiseDomain <- function(env, name, value) {
      force(env)
      force(name)
      force(value)

      new_promise_domain(
        wrapOnFulfilled = function(onFulfilled) {
          function(...) {
            orig <- env[[name]]
            env[[name]] <- value
            env$activations <- c(env$activations, paste0(name, "=", value))
            on.exit({
              env[[name]] <- orig
            })

            onFulfilled(...)
          }
        },
        wrapSync = function(expr) {
          orig <- env[[name]]
          env[[name]] <- value
          on.exit(env[[name]] <- orig)

          force(expr)
        }
      )
    }

    domain1 <- createVarPromiseDomain(test_env, "x", 1)
    domain2 <- createVarPromiseDomain(test_env, "x", 2)

    # Test the nested domain behavior
    result <- NULL

    with_promise_domain(domain1, {
      # Outer domain should set x=1
      expect_identical(test_env$x, 1)

      with_promise_domain(domain2, {
        # Inner domain should set x=2
        expect_identical(test_env$x, 2)

        # Create and resolve a promise
        promise_resolve(TRUE) |>
          then(function(value) {
            # The inner domain (domain2) should be closest to this callback,
            # so x should be 2 when this executes
            result <<- test_env$x
            test_env$x
          }) |>
          wait_for_it()
      })
    })

    # Verify that the innermost domain took precedence
    expect_identical(result, 2)

    # Verify the activation order: domain1 (outer) activates first,
    # then domain2 (inner) activates and takes precedence
    expect_length(test_env$activations, 2)
    expect_identical(test_env$activations[1], "x=1")
    expect_identical(test_env$activations[2], "x=2")
  })
})
