context('basic')

is_module_loaded = function (mod) {
    box:::is_mod_loaded(attr(mod, 'info'))
}

test_teardown(clear_mods())

module_source_path = function (mod) {
    attr(mod, 'info')$source_path
}

test_that('module can be imported', {
    box::use(mod/a)
    expect_true(is_module_loaded(a))
    expect_in('double', ls(a))
})

test_that('import works in global namespace', {
    in_globalenv({
        box::use(mod/a)
        expect_true(box:::is_mod_loaded(attr(a, 'info')))
        # `expect_in` isn’t found here.
        expect_true('double' %in% ls(a))
    })
})

test_that('module is uniquely identified by path', {
    box::use(mod/a)
    box::use(ba = mod/b/a)
    expect_true(is_module_loaded(a))
    expect_true(is_module_loaded(ba))
    expect_not_identical(module_source_path(a), module_source_path(ba))
    expect_in('double', ls(a))
    expect_not_in('double', ls(ba))
})

test_that('can use imported function', {
    box::use(mod/a)
    expect_equal(a$double(42), 42 * 2)
})

test_that('modules export all objects', {
    box::use(mod/a)
    expect_gt(length(lsf.str(a)), 0L)
    expect_gt(length(ls(a)), length(lsf.str(a)))
    a_namespace = environment(a$double)
    expect_equal(a$get_counter(), 1L)
})

test_that('module can modify its variables', {
    box::use(mod/a)
    counter = a$get_counter()
    a$inc()
    expect_equal(a$get_counter(), counter + 1L)
})

test_that('hidden objects are not exported', {
    box::use(mod/a)
    ns = environment(a$get_counter)
    expect_true(exists('modname', envir = a))
    expect_true(exists('make_counter', envir = ns))
    expect_false(exists('make_counter', envir = a))
    expect_true(exists('private_modname', envir = ns))
    expect_false(exists('private_modname', envir = a))
})

test_that('module bindings are locked', {
    box::use(mod/a)

    ns = attr(a, 'namespace')

    expect_true(environmentIsLocked(a))
    expect_true(environmentIsLocked(ns))
    expect_true(bindingIsLocked('get_counter', a))
    expect_true(bindingIsLocked('modname', a))
    expect_true(bindingIsLocked('get_counter', ns))
    expect_true(bindingIsLocked('modname', ns))

    err = try({a$counter = 2L}, silent = TRUE)
    expect_s3_class(err, 'try-error')
})

test_that('modules don’t need exports', {
    expect_equal(ls(), character(0L))
    expect_error(box::use(mod/no_exports), NA)
    expect_error(capture.output(box::use(mod/no_names)), NA)
    expect_setequal(ls(), c('no_exports', 'no_names'))
})

test_that('modules can be empty', {
    expect_error(box::use(mod/empty), NA)
    expect_error(box::use(mod/export_empty), NA)
})

test_that('global scope is not leaking into modules', {
    in_globalenv({
        x = 1L
        expect_error(box::use(mod/issue151), 'object .* not found')
    })
})

test_that('package exports do not leak into modules', {
    expect_true('package:stats' %in% search())
    box::use(mod/a)
    ns_a = attr(a, 'namespace')
    # First, ensure this is run in the right environment:
    expect_equal(get0('modname', envir = ns_a, inherits = FALSE), 'a')
    expect_true(get0('T', envir = ns_a))
    expect_null(get0('t.test', envir = ns_a))
    expect_not_null(get0('t.test', envir = .GlobalEnv))
})

test_that('partial name causes error', {
    box::use(mod/a)
    expect_error(a$double, NA)
    expect_error(a$doubl, "name 'doubl' not found in 'a'")

    # Check whether error call is correct:
    error = tryCatch(a$doubl, error = identity)
    expect_s3_class(error, 'simpleError')
    expect_identical(error$call, quote(a$doubl))
})

test_that('trailing comma is accepted', {
    expect_error(box::use(mod/a, ), NA)
    expect_error(box::use(mod/a, mod/b, ), NA)
    expect_error(box::use(mod/a[modname, double, ]), NA)
})

test_that('nested module can use parent', {
    box::use(mod/b/b)
    expect_true(exists('z', b))
    expect_equal(b$z, 1)
})

test_that('using legacy functions raises warning', {
    on.exit({
        box::unload(library)
        box::unload(require)
        box::unload(source)
    })

    expect_warning(box::use(mod/legacy/library), '.+library.+ inside a module')
    expect_warning(box::use(mod/legacy/require), '.+require.+ inside a module')

    expect_false(exists('source_test', envir = .GlobalEnv))
    on.exit(rm(source_test, envir = .GlobalEnv))
    expect_warning(box::use(mod/legacy/source), '.+source.+ inside a module')
    expect_true(exists('source_test', envir = .GlobalEnv))
})

test_that('legacy function warning can be silenced', {
    old_opts = options(box.warn.legacy = FALSE)
    box:::set_import_env_parent()
    on.exit({
        options(old_opts)
        box:::set_import_env_parent()
    })

    expect_warning(box::use(mod/legacy/library), NA)
    expect_warning(box::use(mod/legacy/require), NA)

    expect_false(exists('source_test', envir = .GlobalEnv))
    on.exit(rm(source_test, envir = .GlobalEnv), add = TRUE)
    expect_warning(box::use(mod/legacy/source), NA)
    expect_true(exists('source_test', envir = .GlobalEnv))
})

test_that('r/core can be imported', {
    # All the test case logic is inside the `core_test` module.
    box::use(mod/core_test)

    # Ensure tests were actually run:
    expect_gt(core_test$tests_run, 0L)
})

test_that('modules can be imported and exported by different local names', {
    expect_error(box::use(mod/issue211), NA)
})

test_that('legacy modules export all names', {
    is_legacy = function (mod) {
        box:::namespace_info(attr(mod, 'namespace'), 'legacy', default = FALSE)
    }

    box::use(
        mod/a,
        mod/legacy,
        mod/no_exports
    )

    expect_false(is_legacy(a))
    expect_true(is_legacy(legacy))
    expect_false(is_legacy(no_exports))

    expect_setequal(ls(legacy), c('a', 'b'))
    expect_setequal(ls(no_exports), character())
})

test_that('modules can specify explicit exports', {
    box::use(ex = mod/explicit_exports)
    expect_setequal(ls(ex), c('a', 'double', 'modname'))
    expect_identical(ex$double, attr(ex, 'namespace')$double)
    expect_equal(ex$modname, ex$a$modname)
})

test_that('`box::export()` checks its arguments', {
    # Create a test module namespace since `box::export()` can only be called inside a module.
    test_spec = box:::mod_spec(list(mod = 'test', name = 'test', explicit = FALSE))
    test_info = box:::mod_info(test_spec, '.')
    test_ns = box:::make_namespace(test_info)
    test_ns$a = 1

    expect_error(local(box::export(a), envir = test_ns), NA)
    expect_error(
        local(box::export(a = a), envir = test_ns),
        'unexpected named argument'
    )
})

test_that('legcay modules do not call hooks', {
    # Ensure module is first unloaded:
    box::use(mod/legacy)
    box::unload(legacy)

    expect_messages(
        box::use(mod/legacy),
        has = 'legacy module loaded',
        has_not = '\\.on_load is not called'
    )
})
