code:
/*!
 * Copyright (c) 2020 Aleksej Komarov
 * SPDX-License-Identifier: MIT
 */

/datum/tgui_window
	var/id
	var/client/client
	var/pooled
	var/pool_index
	var/is_browser = FALSE
	var/status = TGUI_WINDOW_CLOSED
	var/locked = FALSE
	var/datum/tgui/locked_by
	var/datum/subscriber_object
	var/subscriber_delegate
	var/fatally_errored = FALSE
	var/message_queue
	var/sent_assets = list()
	// Vars passed to initialize proc (and saved for later)
	var/inline_assets
	var/fancy

/**
 * public
 *
 * Create a new tgui window.
 *
 * required client /client
 * required id string A unique window identifier.
 */
/datum/tgui_window/New(client/client, id, pooled = FALSE)
	src.id = id
	src.client = client
	src.client.tgui_windows[id] = src
	src.pooled = pooled
	if(pooled)
		src.pool_index = TGUI_WINDOW_INDEX(id)

/**
 * public
 *
 * Initializes the window with a fresh page. Puts window into the "loading"
 * state. You can begin sending messages right after initializing. Messages
 * will be put into the queue until the window finishes loading.
 *
 * optional inline_assets list List of assets to inline into the html.
 * optional inline_html string Custom HTML to inject.
 * optional fancy bool If TRUE, will hide the window titlebar.
 */
/datum/tgui_window/proc/initialize(
		inline_assets = list(),
		inline_html = "",
		fancy = FALSE)
	log_tgui(client,
		context = "[id]/initialize",
		window = src)
	if(!client)
		return
	src.inline_assets = inline_assets
	src.fancy = fancy
	status = TGUI_WINDOW_LOADING
	fatally_errored = FALSE
	// Build window options
	var/options = "file=[id].html;can_minimize=0;auto_format=0;"
	// Remove titlebar and resize handles for a fancy window
	if(fancy)
		options += "titlebar=0;can_resize=0;"
	else
		options += "titlebar=1;can_resize=1;"
	// Generate page html
	var/html = SStgui.basehtml
	html = replacetextEx(html, "\[tgui:windowId]", id)
	// Inject inline assets
	var/inline_assets_str = ""
	for(var/datum/asset/asset in inline_assets)
		var/mappings = asset.get_url_mappings()
		for(var/name in mappings)
			var/url = mappings[name]
			// Not encoding since asset strings are considered safe
			if(copytext(name, -4) == ".css")
				inline_assets_str += "Byond.loadCss('[url]', true);\n"
			else if(copytext(name, -3) == ".js")
				inline_assets_str += "Byond.loadJs('[url]', true);\n"
		asset.send(client)
	if(length(inline_assets_str))
		inline_assets_str = "<script>\n" + inline_assets_str + "</script>\n"
	html = replacetextEx(html, "<!-- tgui:assets -->\n", inline_assets_str)
	// Inject custom HTML
	html = replacetextEx(html, "<!-- tgui:html -->\n", inline_html)
	// Open the window
	client << browse(html, "window=[id];[options]")
	// Detect whether the control is a browser
	is_browser = winexists(client, id) == "BROWSER"
	// Instruct the client to signal UI when the window is closed.
	if(!is_browser)
		winset(client, id, "on-close=\"uiclose [id]\"")

/**
 * public
 *
 * Checks if the window is ready to receive data.
 *
 * return bool
 */
/datum/tgui_window/proc/is_ready()
	return status == TGUI_WINDOW_READY

/**
 * public
 *
 * Checks if the window can be sanely suspended.
 *
 * return bool
 */
/datum/tgui_window/proc/can_be_suspended()
	return !fatally_errored \
		&& pooled \
		&& pool_index > 0 \
		&& pool_index <= TGUI_WINDOW_SOFT_LIMIT \
		&& status == TGUI_WINDOW_READY

/**
 * public
 *
 * Acquire the window lock. Pool will not be able to provide this window
 * to other UIs for the duration of the lock.
 *
 * Can be given an optional tgui datum, which will be automatically
 * subscribed to incoming messages via the on_message proc.
 *
 * optional ui /datum/tgui
 */
/datum/tgui_window/proc/acquire_lock(datum/tgui/ui)
	locked = TRUE
	locked_by = ui

/**
 * public
 *
 * Release the window lock.
 */
/datum/tgui_window/proc/release_lock()
	// Clean up assets sent by tgui datum which requested the lock
	if(locked)
		sent_assets = list()
	locked = FALSE
	locked_by = null

/**
 * public
 *
 * Subscribes the datum to consume window messages on a specified proc.
 *
 * Note, that this supports only one subscriber, because code for that
 * is simpler and therefore faster. If necessary, this can be rewritten
 * to support multiple subscribers.
 */
/datum/tgui_window/proc/subscribe(datum/object, delegate)
	subscriber_object = object
	subscriber_delegate = delegate

/**
 * public
 *
 * Unsubscribes the datum. Do not forget to call this when cleaning up.
 */
/datum/tgui_window/proc/unsubscribe(datum/object)
	subscriber_object = null
	subscriber_delegate = null

/**
 * public
 *
 * Close the UI.
 *
 * optional can_be_suspended bool
 */
/datum/tgui_window/proc/close(can_be_suspended = TRUE)
	if(!client)
		return
	if(can_be_suspended && can_be_suspended())
		log_tgui(client,
			context = "[id]/close (suspending)",
			window = src)
		status = TGUI_WINDOW_READY
		send_message("suspend")
		return
	log_tgui(client,
		context = "[id]/close",
		window = src)
	release_lock()
	status = TGUI_WINDOW_CLOSED
	message_queue = null
	// Do not close the window to give user some time
	// to read the error message.
	if(!fatally_errored)
		client << browse(null, "window=[id]")

/**
 * public
 *
 * Sends a message to tgui window.
 *
 * required type string Message type
 * required payload list Message payload
 * optional force bool Send regardless of the ready status.
 */
/datum/tgui_window/proc/send_message(type, payload, force)
	if(!client)
		return
	var/message = TGUI_CREATE_MESSAGE(type, payload)
	// Place into queue if window is still loading
	if(!force && status != TGUI_WINDOW_READY)
		if(!message_queue)
			message_queue = list()
		message_queue += list(message)
		return
	client << output(message, is_browser \
		? "[id]:update" \
		: "[id].browser:update")

/**
 * public
 *
 * Sends a raw payload to tgui window.
 *
 * required message string JSON+urlencoded blob to send.
 * optional force bool Send regardless of the ready status.
 */
/datum/tgui_window/proc/send_raw_message(message, force)
	if(!client)
		return
	// Place into queue if window is still loading
	if(!force && status != TGUI_WINDOW_READY)
		if(!message_queue)
			message_queue = list()
		message_queue += list(message)
		return
	client << output(message, is_browser \
		? "[id]:update" \
		: "[id].browser:update")

/**
 * public
 *
 * Makes an asset available to use in tgui.
 *
 * required asset datum/asset
 *
 * return bool - TRUE if any assets had to be sent to the client
 */
/datum/tgui_window/proc/send_asset(datum/asset/asset)
	if(!client || !asset)
		return
	sent_assets |= list(asset)
	. = asset.send(client)
	if(istype(asset, /datum/asset/spritesheet))
		var/datum/asset/spritesheet/spritesheet = asset
		send_message("asset/stylesheet", spritesheet.css_filename())
	send_raw_message(asset.get_serialized_url_mappings())

/**
 * private
 *
 * Sends queued messages if the queue wasn't empty.
 */
/datum/tgui_window/proc/flush_message_queue()
	if(!client || !message_queue)
		return
	for(var/message in message_queue)
		client << output(message, is_browser \
			? "[id]:update" \
			: "[id].browser:update")
	message_queue = null

/**
 * private
 *
 * Callback for handling incoming tgui messages.
 */
/datum/tgui_window/proc/on_message(type, payload, href_list)
	// Status can be READY if user has refreshed the window.
	if(type == "ready" && status == TGUI_WINDOW_READY)
		// Resend the assets
		for(var/asset in sent_assets)
			send_asset(asset)
	// Mark this window as fatally errored which prevents it from
	// being suspended.
	if(type == "log" && href_list["fatal"])
		fatally_errored = TRUE
	// Mark window as ready since we received this message from somewhere
	if(status != TGUI_WINDOW_READY)
		status = TGUI_WINDOW_READY
		flush_message_queue()
	// Pass message to UI that requested the lock
	if(locked && locked_by)
		var/prevent_default = locked_by.on_message(type, payload, href_list)
		if(prevent_default)
			return
	// Pass message to the subscriber
	else if(subscriber_object)
		var/prevent_default = call(
			subscriber_object,
			subscriber_delegate)(type, payload, href_list)
		if(prevent_default)
			return
	// If not locked, handle these message types
	switch(type)
		if("ping")
			send_message("pingReply", payload)
		if("suspend")
			close(can_be_suspended = TRUE)
		if("close")
			close(can_be_suspended = FALSE)
		if("openLink")
			client << link(href_list["url"])
		if("cacheReloaded")
			// Reinitialize
			initialize(inline_assets = inline_assets, fancy = fancy)
			// Resend the assets
			for(var/asset in sent_assets)
				send_asset(asset)


code:
/*!
 * Copyright (c) 2020 Aleksej Komarov
 * SPDX-License-Identifier: MIT
 */

/// Admin music volume, from 0 to 1.
/client/var/admin_music_volume = 1

/**
 * public
 *
 * Sends music data to the browser.
 *
 * Optional settings:
 * - pitch: the playback rate
 * - start: the start time of the sound
 * - end: when the musics stops playing
 *
 * required url string Must be an https URL.
 * optional extra_data list Optional settings.
 */
/datum/tgui_panel/proc/play_music(url, extra_data)
	if(!is_ready())
		return
	if(!findtext(url, GLOB.is_http_protocol))
		return
	var/list/payload = list()
	if(length(extra_data) > 0)
		for(var/key in extra_data)
			payload[key] = extra_data[key]
	payload["url"] = url
	window.send_message("audio/playMusic", payload)

/**
 * public
 *
 * Stops playing music through the browser.
 */
/datum/tgui_panel/proc/stop_music()
	if(!is_ready())
		return
	window.send_message("audio/stopMusic")

repo_name: AnalWerewolf/sundown

license: mit


code:
/*!
 * Copyright (c) 2020 Aleksej Komarov
 * SPDX-License-Identifier: MIT
 */

/client/var/datum/tgui_panel/tgui_panel

/**
 * tgui panel / chat troubleshooting verb
 */
/client/verb/fix_tgui_panel()
	set name = "Fix chat"
	set category = "OOC"
	var/action
	log_tgui(src, "Started fixing.", context = "verb/fix_tgui_panel")

	nuke_chat()

	// Failed to fix, using tgalert as fallback
	action = tgalert(src, "Did that work?", "", "Yes", "No, switch to old ui")
	if (action == "No, switch to old ui")
		winset(src, "output", "on-show=&is-disabled=0&is-visible=1")
		winset(src, "browseroutput", "is-disabled=1;is-visible=0")
		log_tgui(src, "Failed to fix.", context = "verb/fix_tgui_panel")

/client/proc/nuke_chat()
	// Catch all solution (kick the whole thing in the pants)
	winset(src, "output", "on-show=&is-disabled=0&is-visible=1")
	winset(src, "browseroutput", "is-disabled=1;is-visible=0")
	if(!tgui_panel || !istype(tgui_panel))
		log_tgui(src, "tgui_panel datum is missing",
			context = "verb/fix_tgui_panel")
		tgui_panel = new(src)
	tgui_panel.initialize(force = TRUE)
	// Force show the panel to see if there are any errors
	winset(src, "output", "is-disabled=1&is-visible=0")
	winset(src, "browseroutput", "is-disabled=0;is-visible=1")


code:
/*!
 * Copyright (c) 2020 Aleksej Komarov
 * SPDX-License-Identifier: MIT
 */

/**
 * Maximum number of connection records allowed to analyze.
 * Should match the value set in the browser.
 */
#define TGUI_TELEMETRY_MAX_CONNECTIONS 5

/**
 * Maximum time allocated for sending a telemetry packet.
 */
#define TGUI_TELEMETRY_RESPONSE_WINDOW 30 SECONDS

/// Time of telemetry request
/datum/tgui_panel/var/telemetry_requested_at
/// Time of telemetry analysis completion
/datum/tgui_panel/var/telemetry_analyzed_at
/// List of previous client connections
/datum/tgui_panel/var/list/telemetry_connections

/**
 * private
 *
 * Requests some telemetry from the client.
 */
/datum/tgui_panel/proc/request_telemetry()
	telemetry_requested_at = world.time
	telemetry_analyzed_at = null
	window.send_message("telemetry/request", list(
		"limits" = list(
			"connections" = TGUI_TELEMETRY_MAX_CONNECTIONS,
		),
	))

/**
 * private
 *
 * Analyzes a telemetry packet.
 *
 * Is currently only useful for detecting ban evasion attempts.
 */
/datum/tgui_panel/proc/analyze_telemetry(payload)
	if(world.time > telemetry_requested_at + TGUI_TELEMETRY_RESPONSE_WINDOW)
		message_admins("[key_name(client)] sent telemetry outside of the allocated time window.")
		return
	if(telemetry_analyzed_at)
		message_admins("[key_name(client)] sent telemetry more than once.")
		return
	telemetry_analyzed_at = world.time
	if(!payload)
		return
	telemetry_connections = payload["connections"]
	var/len = length(telemetry_connections)
	if(len == 0)
		return
	if(len > TGUI_TELEMETRY_MAX_CONNECTIONS)
		message_admins("[key_name(client)] was kicked for sending a huge telemetry payload")
		qdel(client)
		return
	var/list/found
	for(var/i in 1 to len)
		if(QDELETED(client))
			// He got cleaned up before we were done
			return
		var/list/row = telemetry_connections[i]
		// Check for a malformed history object
		if (!row || row.len < 3 || (!row["ckey"] || !row["address"] || !row["computer_id"]))
			return
		if (world.IsBanned(row["ckey"], row["address"], row["computer_id"], real_bans_only = TRUE))
			found = row
			break
		CHECK_TICK
	// This fucker has a history of playing on a banned account.
	if(found)
		var/msg = "[key_name(client)] has a banned account in connection history! (Matched: [found["ckey"]], [found["address"]], [found["computer_id"]])"
		message_admins(msg)
		log_admin_private(msg)
		log_suspicious_login(msg, access_log_mirror = FALSE)


code:
/*!
 * Copyright (c) 2020 Aleksej Komarov
 * SPDX-License-Identifier: MIT
 */

/**
 * tgui_panel datum
 * Hosts tgchat and other nice features.
 */
/datum/tgui_panel
	var/client/client
	var/datum/tgui_window/window
	var/broken = FALSE
	var/initialized_at

/datum/tgui_panel/New(client/client)
	src.client = client
	window = new(client, "browseroutput")
	window.subscribe(src, .proc/on_message)

/datum/tgui_panel/Del()
	window.unsubscribe(src)
	window.close()
	return ..()

/**
 * public
 *
 * TRUE if panel is initialized and ready to receive messages.
 */
/datum/tgui_panel/proc/is_ready()
	return !broken && window.is_ready()

/**
 * public
 *
 * Initializes tgui panel.
 */
/datum/tgui_panel/proc/initialize(force = FALSE)
	set waitfor = FALSE
	// Minimal sleep to defer initialization to after client constructor
	sleep(1)
	initialized_at = world.time
	// Perform a clean initialization
	window.initialize(inline_assets = list(
		get_asset_datum(/datum/asset/simple/tgui_panel),
	))
	window.send_asset(get_asset_datum(/datum/asset/simple/namespaced/fontawesome))
	window.send_asset(get_asset_datum(/datum/asset/simple/namespaced/tgfont))
	window.send_asset(get_asset_datum(/datum/asset/spritesheet/chat))
	// Other setup
	request_telemetry()
	addtimer(CALLBACK(src, .proc/on_initialize_timed_out), 5 SECONDS)

/**
 * private
 *
 * Called when initialization has timed out.
 */
/datum/tgui_panel/proc/on_initialize_timed_out()
	// Currently does nothing but sending a message to old chat.
	SEND_TEXT(client, "Failed to load fancy chat, click HERE to attempt to reload it.")

/**
 * private
 *
 * Callback for handling incoming tgui messages.
 */
/datum/tgui_panel/proc/on_message(type, payload)
	if(type == "ready")
		broken = FALSE
		window.send_message("update", list(
			"config" = list(
				"client" = list(
					"ckey" = client.ckey,
					"address" = client.address,
					"computer_id" = client.computer_id,
				),
				"window" = list(
					"fancy" = FALSE,
					"locked" = FALSE,
				),
			),
		))
		return TRUE
	if(type == "audio/setAdminMusicVolume")
		client.admin_music_volume = payload["volume"]
		return TRUE
	if(type == "telemetry")
		analyze_telemetry(payload)
		return TRUE

/**
 * public
 *
 * Sends a round restart notification.
 */
/datum/tgui_panel/proc/send_roundrestart()
	window.send_message("roundrestart")


code:
/*
Tooltips v1.1 - 22/10/15
Developed by Wire (#goonstation on irc.synirc.net)
- Added support for screen_loc pixel offsets. Should work. Maybe.
- Added init function to more efficiently send base vars

Configuration:
- Set control to the correct skin element (remember to actually place the skin element)
- Set file to the correct path for the .html file (remember to actually place the html file)
- Attach the datum to the user client on login, e.g.
	/client/New()
		src.tooltips = new /datum/tooltip(src)

Usage:
- Define mouse event procs on your (probably HUD) object and simply call the show and hide procs respectively:
	/atom/movable/screen/hud
		MouseEntered(location, control, params)
			usr.client.tooltip.show(params, title = src.name, content = src.desc)

		MouseExited()
			usr.client.tooltip.hide()

Customization:
- Theming can be done by passing the theme var to show() and using css in the html file to change the look
- For your convenience some pre-made themes are included

Notes:
- You may have noticed 90% of the work is done via javascript on the client. Gotta save those cycles man.
- This is entirely untested in any other codebase besides goonstation so I have no idea if it will port nicely. Good luck!
	- After testing and discussion (Wire, Remie, MrPerson, AnturK) ToolTips are ok and work for /tg/station13
*/

/datum/tooltip
	var/client/owner
	var/control = "mainwindow.tooltip"
	var/showing = 0
	var/queueHide = 0
	var/init = 0

/datum/tooltip/New(client/C)
	if (C)
		owner = C
		var/datum/asset/stuff = get_asset_datum(/datum/asset/simple/jquery)
		stuff.send(owner)
		owner << browse(file2text('code/modules/tooltip/tooltip.html'), "window=[control]")

	..()

/datum/tooltip/proc/show(atom/movable/thing, params = null, title = null, content = null, theme = "default", special = "none")
	if (!thing || !params || (!title && !content) || !owner || !isnum(world.icon_size))
		return FALSE
	if (!init)
		//Initialize some vars
		init = 1
		owner << output(list2params(list(world.icon_size, control)), "[control]:tooltip.init")

	showing = 1

	if (title && content)
		title = "
[title]
"
		content = "
[content]
"
	else if (title && !content)
		title = "
[title]
"
	else if (!title && content)
		content = "
[content]
"

	// Strip macros from item names
	title = replacetext(title, "\proper", "")
	title = replacetext(title, "\improper", "")

	//Make our dumb param object
	params = {"{ "cursor": "[params]", "screenLoc": "[thing.screen_loc]" }"}

	//Send stuff to the tooltip
	var/view_size = getviewsize(owner.view)
	owner << output(list2params(list(params, view_size[1] , view_size[2], "[title][content]", theme, special)), "[control]:tooltip.update")

	//If a hide() was hit while we were showing, run hide() again to avoid stuck tooltips
	showing = 0
	if (queueHide)
		hide()

	return TRUE

/datum/tooltip/proc/hide()
	if (queueHide)
		addtimer(CALLBACK(src, .proc/do_hide), 1)
	else
		do_hide()

	queueHide = showing ? TRUE : FALSE

	return TRUE

/datum/tooltip/proc/do_hide()
	winshow(owner, control, FALSE)

/* TG SPECIFIC CODE */

//Open a tooltip for user, at a location based on params
//Theme is a CSS class in tooltip.html, by default this wrapper chooses a CSS class based on the user's UI_style (Midnight, Plasmafire, Retro, etc)
//Includes sanity.checks
/proc/openToolTip(mob/user = null, atom/movable/tip_src = null, params = null,title = "",content = "",theme = "")
	if(istype(user))
		if(user.client && user.client.tooltips)
			var/ui_style = user.client?.prefs?.read_preference(/datum/preference/choiced/ui_style)
			if(!theme && ui_style)
				theme = lowertext(ui_style)
			if(!theme)
				theme = "default"
			user.client.tooltips.show(tip_src, params,title,content,theme)

//Arbitrarily close a user's tooltip
//Includes sanity checks.
/proc/closeToolTip(mob/user)
	if(istype(user))
		if(user.client && user.client.tooltips)
			user.client.tooltips.hide()


code:
<!DOCTYPE html>

	<title>Tooltip</title>
	<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />


code:
# Unit Tests

## What is unit testing?

Unit tests are automated code to verify that parts of the game work exactly as they should. For example, [a test to make sure that the amputation surgery actually amputates the limb](https://github.com/tgstation/tgstation/blob/e416283f162b86345a8623125ab866839b1ac40d/code/modules/unit_tests/surgeries.dm#L1-L13). These are ran every time a PR is made, and thus are very helpful for preventing bugs from cropping up in your code that would've otherwise gone unnoticed. For example, would you have thought to check [that beach boys would still work the same after editing pizza](https://github.com/tgstation/tgstation/pull/53641#issuecomment-691384934)? If you value your time, probably not.

On their most basic level, when `UNIT_TESTS` is defined, all subtypes of `/datum/unit_test` will have their `Run` proc executed. From here, if `Fail` is called at any point, then the tests will report as failed.

## How do I write one?
1. Find a relevant file.

All unit test related code is in `code/modules/unit_tests`. If you are adding a new test for a surgery, for example, then you'd open `surgeries.dm`. If a relevant file does not exist, simply create one in this folder, then `#include` it in `_unit_tests.dm`.

2. Create the unit test.

To make a new unit test, you simply need to define a `/datum/unit_test`.

For example, let's suppose that we are creating a test to make sure a proc `square` correctly raises inputs to the power of two. We'd start with first:

```
/datum/unit_test/square/Run()
```

This defines our new unit test, `/datum/unit_test/square`. Inside this function, we're then going to run through whatever we want to check. Tests provide a few assertion functions to make this easy. For now, we're going to use `TEST_ASSERT_EQUAL`.

```
/datum/unit_test/square/Run()
    TEST_ASSERT_EQUAL(square(3), 9, "square(3) did not return 9")
    TEST_ASSERT_EQUAL(square(4), 16, "square(4) did not return 16")
```

As you can hopefully tell, we're simply checking if the output of `square` matches the output we are expecting. If the test fails, it'll report the error message given as well as whatever the actual output was.

3. Run the unit test

Open `code/_compile_options.dm` and uncomment the following line.

```
//#define UNIT_TESTS			//If this is uncommented, we do a single run though of the game setup and tear down process with unit tests in between
```

Then, run tgstation.dmb in Dream Daemon. Don't bother trying to connect, you won't need to. You'll be able to see the outputs of all the tests. You'll get to see which tests failed and for what reason. If they all pass, you're set!

## How to think about tests

Unit tests exist to prevent bugs that would happen in a real game. Thus, they should attempt to emulate the game world wherever possible.  For example, the [quick swap sanity test](https://github.com/tgstation/tgstation/blob/e416283f162b86345a8623125ab866839b1ac40d/code/modules/unit_tests/quick_swap_sanity.dm) emulates a *real* scenario of the bug it fixed occurring by creating a character and giving it real items. The unrecommended alternative would be to create special test-only items. This isn't a hard rule, the [reagent method exposure tests](https://github.com/tgstation/tgstation/blob/e416283f162b86345a8623125ab866839b1ac40d/code/modules/unit_tests/reagent_mod_expose.dm) create a test-only reagent for example, but do keep it in mind.

Unit tests should also be just that--testing *units* of code. For example, instead of having one massive test for reagents, there are instead several smaller tests for testing exposure, metabolization, etc.

## The unit testing API

You can find more information about all of these from their respective doc comments, but for a brief overview:

`/datum/unit_test` - The base for all tests to be ran. Subtypes must override `Run()`. `New()` and `Destroy()` can be used for setup and teardown. To fail, use `Fail(reason)`.

`/datum/unit_test/proc/allocate(type, ...)` - Allocates an instance of the provided type with the given arguments. Is automatically destroyed when the test is over. Commonly seen in the form of `var/mob/living/carbon/human/human = allocate(/mob/living/carbon/human)`.

`TEST_ASSERT(assertion, reason)` - Stops the unit test and fails if the assertion is not met. For example: `TEST_ASSERT(powered(), "Machine is not powered")`.

`TEST_ASSERT_EQUAL(a, b, message)` - Same as `TEST_ASSERT`, but checks if `a == b`. If not, gives a helpful message showing what both `a` and `b` were. For example: `TEST_ASSERT_EQUAL(2 + 2, 4, "The universe is falling apart before our eyes!")`.

`TEST_ASSERT_NOTEQUAL(a, b, message)` - Same as `TEST_ASSERT_EQUAL`, but reversed.

`TEST_FOCUS(test_path)` - *Only* run the test provided within the parameters. Useful for reducing noise. For example, if we only want to run our example square test, we can add `TEST_FOCUS(/datum/unit_test/square)`. Should *never* be pushed in a pull request--you will be laughed at.

## Final Notes

- Writing tests before you attempt to fix the bug can actually speed up development a lot! It means you don't have to go in game and folllow the same exact steps manually every time. This process is known as "TDD" (test driven development). Write the test first, make sure it fails, *then* start work on the fix/feature, and you'll know you're done when your tests pass. If you do try this, do make sure to confirm in a non-testing environment just to double check.
- Make sure that your tests don't accidentally call RNG functions like `prob`. Since RNG is seeded during tests, you may not realize you have until someone else makes a PR and the tests fail!
- Do your best not to change the behavior of non-testing code during tests. While it may sometimes be necessary in the case of situations such as the above, it is still a slippery slope that can lead to the code you're testing being too different from the production environment to be useful.


code:
//include unit test files in this module in this ifdef
//Keep this sorted alphabetically

#if defined(UNIT_TESTS) || defined(SPACEMAN_DMM)

/// Asserts that a condition is true
/// If the condition is not true, fails the test
#define TEST_ASSERT(assertion, reason) if (!(assertion)) { return Fail("Assertion failed: [reason || "No reason"]") }

/// Asserts that the two parameters passed are equal, fails otherwise
/// Optionally allows an additional message in the case of a failure
#define TEST_ASSERT_EQUAL(a, b, message) do { \
	var/lhs = ##a; \
	var/rhs = ##b; \
	if (lhs != rhs) { \
		return Fail("Expected [isnull(lhs) ? "null" : lhs] to be equal to [isnull(rhs) ? "null" : rhs].[message ? " [message]" : ""]"); \
	} \
} while (FALSE)

/// Asserts that the two parameters passed are not equal, fails otherwise
/// Optionally allows an additional message in the case of a failure
#define TEST_ASSERT_NOTEQUAL(a, b, message) do { \
	var/lhs = ##a; \
	var/rhs = ##b; \
	if (lhs == rhs) { \
		return Fail("Expected [isnull(lhs) ? "null" : lhs] to not be equal to [isnull(rhs) ? "null" : rhs].[message ? " [message]" : ""]"); \
	} \
} while (FALSE)

/// *Only* run the test provided within the parentheses
/// This is useful for debugging when you want to reduce noise, but should never be pushed
/// Intended to be used in the manner of `TEST_FOCUS(/datum/unit_test/math)`
#define TEST_FOCUS(test_path) ##test_path { focus = TRUE; }

/// Constants indicating unit test completion status
#define UNIT_TEST_PASSED 0
#define UNIT_TEST_FAILED 1
#define UNIT_TEST_SKIPPED 2

#define TEST_DEFAULT 1
#define TEST_DEL_WORLD INFINITY

/// A trait source when adding traits through unit tests
#define TRAIT_SOURCE_UNIT_TESTS "unit_tests"

#include "anchored_mobs.dm"
#include "bespoke_id.dm"
#include "binary_insert.dm"
#include "bloody_footprints.dm"
#include "breath.dm"
#include "card_mismatch.dm"
#include "chain_pull_through_space.dm"
#include "chat_filter.dm"
#include "closets.dm"
#include "combat.dm"
#include "component_tests.dm"
#include "connect_loc.dm"
#include "confusion.dm"
#include "crayons.dm"
#include "create_and_destroy.dm"
#include "designs.dm"
#include "dynamic_ruleset_sanity.dm"
#include "egg_glands.dm"
#include "emoting.dm"
#include "food_edibility_check.dm"
#include "greyscale_config.dm"
#include "heretic_knowledge.dm"
#include "holidays.dm"
#include "hydroponics_harvest.dm"
#include "hydroponics_self_mutations.dm"
#include "keybinding_init.dm"
#include "machine_disassembly.dm"
#include "medical_wounds.dm"
#include "merge_type.dm"
#include "metabolizing.dm"
#include "ntnetwork_tests.dm"
#include "outfit_sanity.dm"
#include "pills.dm"
#include "plantgrowth_tests.dm"
#include "preferences.dm"
#include "projectiles.dm"
#include "quirks.dm"
#include "rcd.dm"
#include "reagent_id_typos.dm"
#include "reagent_mod_expose.dm"
#include "reagent_mod_procs.dm"
#include "reagent_recipe_collisions.dm"
#include "resist.dm"
#include "say.dm"
#include "security_officer_distribution.dm"
#include "serving_tray.dm"
#include "siunit.dm"
#include "spawn_humans.dm"
#include "spawn_mobs.dm"
#include "species_whitelists.dm"
#include "stomach.dm"
#include "strippable.dm"
#include "subsystem_init.dm"
#include "surgeries.dm"
#include "teleporters.dm"
#include "tgui_create_message.dm"
#include "timer_sanity.dm"
#include "unit_test.dm"
#include "wizard.dm"

#ifdef REFERENCE_TRACKING //Don't try and parse this file if ref tracking isn't turned on. IE: don't parse ref tracking please mr linter
#include "find_reference_sanity.dm"
#endif

#undef TEST_ASSERT
#undef TEST_ASSERT_EQUAL
#undef TEST_ASSERT_NOTEQUAL
#undef TEST_FOCUS
#endif


code:
/datum/unit_test/anchored_mobs/Run()
	var/list/L = list()
	for(var/i in typesof(/mob))
		var/mob/M = i
		if(initial(M.anchored))
			L += "[i]"
	if(!L.len)
		return //passed!
	Fail("The following mobs are defined as anchored. This is incompatible with the new move force/resist system and needs to be revised.: [L.Join(" ")]")


code:
/datum/unit_test/bespoke_id/Run()
	var/datum/element/base = /datum/element
	var/base_index = initial(base.id_arg_index)

	for(var/i in subtypesof(/datum/element))
		var/datum/element/faketype = i
		if((initial(faketype.element_flags) & ELEMENT_BESPOKE) && initial(faketype.id_arg_index) == base_index)
			Fail("A bespoke element was not configured with a proper id_arg_index: [faketype]")


code:
/// A test to ensure the sanity of BINARY_INSERT
/datum/unit_test/binary_insert/Run()
	var/list/datum/binary_insert_node/nodes = list()

	var/datum/binary_insert_node/node_a = new /datum/binary_insert_node(10)
	BINARY_INSERT(node_a, nodes, /datum/binary_insert_node, node_a, x, COMPARE_KEY)
	TEST_ASSERT_EQUAL(nodes.len, 1, "List should have one node")

	var/datum/binary_insert_node/node_b = new /datum/binary_insert_node(5)
	BINARY_INSERT(node_b, nodes, /datum/binary_insert_node, node_b, x, COMPARE_KEY)
	TEST_ASSERT_EQUAL(nodes.len, 2, "List should have two nodes")
	TEST_ASSERT_EQUAL(nodes[1].x, 5, "The first node should be the one with 5")
	TEST_ASSERT_EQUAL(nodes[2].x, 10, "The second node should be the one with 10")

	var/datum/binary_insert_node/node_c = new /datum/binary_insert_node(15)
	BINARY_INSERT(node_c, nodes, /datum/binary_insert_node, node_c, x, COMPARE_KEY)
	TEST_ASSERT_EQUAL(nodes.len, 3, "List should have three nodes")
	TEST_ASSERT_EQUAL(nodes[1].x, 5, "The first node should be the one with 5")
	TEST_ASSERT_EQUAL(nodes[2].x, 10, "The second node should be the one with 10")
	TEST_ASSERT_EQUAL(nodes[3].x, 15, "The third node should be the one with 15")

/datum/binary_insert_node
	var/x

/datum/binary_insert_node/New(_x)
	x = _x


code:
///Tests to make sure bloody footprits work as expected
///So no stacking, they actually apply, and shoe staining thrown in for free
/datum/unit_test/bloody_footprints

/datum/unit_test/bloody_footprints/Run()
	var/mob/living/carbon/human/blood_master = allocate(/mob/living/carbon/human)
	var/obj/item/clothing/shoes/holds_blood = allocate(/obj/item/clothing/shoes)

	blood_master.equip_to_slot_if_possible(holds_blood, ITEM_SLOT_FEET)

	var/turf/open/move_to = get_step(blood_master, get_dir(blood_master, run_loc_floor_top_right))
	//We need to not be "on" the same tile as the pool
	blood_master.forceMove(move_to)

	var/obj/effect/decal/cleanable/blood/pool = allocate(/obj/effect/decal/cleanable/blood)

	//Max out the pools blood, so each step will make things stained enough to matter
	pool.bloodiness = BLOOD_POOL_MAX

	pool.forceMove(run_loc_floor_bottom_left)
	blood_master.forceMove(run_loc_floor_bottom_left)

	var/datum/component/bloodysoles/soles = holds_blood.GetComponent(/datum/component/bloodysoles)
	var/blood_type = pool.blood_state

	TEST_ASSERT(soles.bloody_shoes[blood_type], "Shoes didn't become stained after stepping in a pool of [blood_type]")

	//The bloody soles component handles the order of stepping on blood/stepping on a bloody tile in a constranating way
	//Which means it needs to check and see if any time has passed between steps, so it can be sure the player is stepping onto a new tile (that should become bloody)
	//I on the other hand need to do all my movements in one tick, so we need to sidestep this check
	//Please kill me slowly
	soles.last_pickup -= 1

	//Move off the bloody tile, time to do some testing
	blood_master.forceMove(move_to)

	soles.last_pickup -= 1
	blood_master.forceMove(run_loc_floor_bottom_left)

	var/footprint_total = 0
	for(var/obj/effect/decal/cleanable/blood/footprints/print_set in move_to)
		if(print_set.blood_state == blood_type)
			footprint_total += 1

	TEST_ASSERT(footprint_total, "The floor didn't get covered in [blood_type] after being walked over")

	soles.last_pickup -= 1

	//Another set of movements to try and make some doubled up prints
	blood_master.forceMove(move_to)

	soles.last_pickup -= 1
	blood_master.forceMove(run_loc_floor_bottom_left)

	footprint_total = 0
	for(var/obj/effect/decal/cleanable/blood/footprints/print_set in move_to)
		if(print_set.blood_state == blood_type)
			footprint_total += 1

	TEST_ASSERT(footprint_total, "The floor somehow lost its footprints after being walked over")
	TEST_ASSERT_EQUAL(footprint_total, 1, "The floor had more than one set of footprints in it, something is fucked")


code:
/// Tests to make sure humans can breath in normal situations
/// Built to prevent regression on an issue surrounding QUANTIZE() and BREATH_VOLUME
/// See the comment on BREATH_VOLUME for more details
/datum/unit_test/breath_sanity

/datum/unit_test/breath_sanity/Run()
	var/mob/living/carbon/human/lab_rat = allocate(/mob/living/carbon/human)
	var/obj/item/clothing/mask/breath/tube = allocate(/obj/item/clothing/mask/breath)
	var/obj/item/tank/internals/emergency_oxygen/source = allocate(/obj/item/tank/internals/emergency_oxygen)

	lab_rat.equip_to_slot_if_possible(tube, ITEM_SLOT_MASK)
	lab_rat.equip_to_slot_if_possible(source, ITEM_SLOT_HANDS)
	source.toggle_internals(lab_rat)

	lab_rat.breathe()

	//SEPTIC EDIT BEGIN
	/*
	TEST_ASSERT(!lab_rat.has_alert("not_enough_oxy"), "Humans can't get a full breath from standard o2 tanks")
	lab_rat.clear_alert("not_enough_oxy")
	*/
	TEST_ASSERT(!lab_rat.has_alert("cant_breathe"), "Humans can't get a full breath from standard o2 tanks")
	lab_rat.clear_alert("cant_breathe")
	//SEPTIC EDIT END

	//Prep the mob
	lab_rat.forceMove(run_loc_floor_bottom_left)
	source.toggle_internals(lab_rat)
	TEST_ASSERT(!lab_rat.internal, "toggle_internals() failed to toggle internals")

	var/turf/open/to_fill = run_loc_floor_bottom_left
	//Prep the floor
	to_fill.initial_gas_mix = OPENTURF_DEFAULT_ATMOS
	to_fill.air = new
	to_fill.air.copy_from_turf(to_fill)

	lab_rat.breathe()

	//SEPTIC EDIT BEGIN
	/*
	TEST_ASSERT(!lab_rat.has_alert("not_enough_oxy"), "Humans can't get a full breath from the standard initial_gas_mix on a turf")
	*/
	TEST_ASSERT(!lab_rat.has_alert("cant_breathe"), "Humans can't get a full breath from the standard initial_gas_mix on a turf")
	//SEPTIC EDIT END


code:
/datum/unit_test/card_mismatch

/datum/unit_test/card_mismatch/Run()
	var/message = checkCardpacks(SStrading_card_game.card_packs)
	message += checkCardDatums()
	if(message)
		Fail(message)


code:
/datum/unit_test/chain_pull_through_space
	var/turf/open/space/space_tile
	var/claimed_tile
	var/mob/living/carbon/human/alice
	var/mob/living/carbon/human/bob
	var/mob/living/carbon/human/charlie

/datum/unit_test/chain_pull_through_space/New()
	..()

	// Create a space tile that goes to another z-level
	claimed_tile = run_loc_floor_bottom_left.type

	space_tile = new(locate(run_loc_floor_bottom_left.x, run_loc_floor_bottom_left.y, run_loc_floor_bottom_left.z))
	space_tile.destination_x = 100
	space_tile.destination_y = 100
	space_tile.destination_z = 5

	// Create our list of humans, all adjacent to one another
	alice = new(locate(run_loc_floor_bottom_left.x + 2, run_loc_floor_bottom_left.y, run_loc_floor_bottom_left.z))
	alice.name = "Alice"

	bob = new(locate(run_loc_floor_bottom_left.x + 3, run_loc_floor_bottom_left.y, run_loc_floor_bottom_left.z))
	bob.name = "Bob"

	charlie = new(locate(run_loc_floor_bottom_left.x + 4, run_loc_floor_bottom_left.y, run_loc_floor_bottom_left.z))
	charlie.name = "Charlie"

/datum/unit_test/chain_pull_through_space/Destroy()
	space_tile.ChangeTurf(claimed_tile)
	qdel(alice)
	qdel(bob)
	qdel(charlie)
	return ..()

/datum/unit_test/chain_pull_through_space/Run()
	// Alice pulls Bob, who pulls Charlie
	// Normally, when Alice moves forward, the rest follow
	alice.start_pulling(bob)
	bob.start_pulling(charlie)

	// Walk normally to the left, make sure we're still a chain
	alice.Move(locate(run_loc_floor_bottom_left.x + 1, run_loc_floor_bottom_left.y, run_loc_floor_bottom_left.z))
	if (bob.x != run_loc_floor_bottom_left.x + 2)
		return Fail("During normal move, Bob was not at the correct x ([bob.x])")
	if (charlie.x != run_loc_floor_bottom_left.x + 3)
		return Fail("During normal move, Charlie was not at the correct x ([charlie.x])")

	// We're going through the space turf now that should teleport us
	alice.Move(run_loc_floor_bottom_left)
	if (alice.z != space_tile.destination_z)
		return Fail("Alice did not teleport to the destination z-level. Current location: ([alice.x], [alice.y], [alice.z])")

	if (bob.z != space_tile.destination_z)
		return Fail("Bob did not teleport to the destination z-level. Current location: ([bob.x], [bob.y], [bob.z])")
	if (!bob.Adjacent(alice))
		return Fail("Bob is not adjacent to Alice. Bob is at [bob.x], Alice is at [alice.x]")

	if (charlie.z != space_tile.destination_z)
		return Fail("Charlie did not teleport to the destination z-level. Current location: ([charlie.x], [charlie.y], [charlie.z])")
	if (!charlie.Adjacent(bob))
		return Fail("Charlie is not adjacent to Bob. Charlie is at [charlie.x], Bob is at [bob.x]")


code:
#define BLOCKED_IC "This message is not allowed IC, please use a different weird test phrase."
#define BLOCKED_IC_OUTSIDE_PDA "Kirby dancing is strictly prohibited on this server."
#define BLOCKED_SHARED "This message is not allowed anywhere, please use a different weird test phrase."
#define SOFT_BLOCKED_IC "This term is commonly considered OOC, only use it with context."
#define SOFT_BLOCKED_IC_OUTSIDE_PDA "Stop saying debug phrases ICly."
#define SOFT_BLOCKED_SHARED "You risk being banned if this term is used improperly."

/// Tests the sanity of the chat filter, ensuring it properly blocks words and gives the reason
/datum/unit_test/chat_filter_sanity

/datum/unit_test/chat_filter_sanity/Run()
	// Update the chat filters to only have test phrases, just in case the toml is different
	config.shared_filter_reasons = list("blockedinshared" = BLOCKED_SHARED)
	config.ic_filter_reasons = list("blockedinic" = BLOCKED_IC)
	config.ic_outside_pda_filter_reasons = list("<(0_0<)" = BLOCKED_IC_OUTSIDE_PDA)
	config.soft_shared_filter_reasons = list("testsoftblocks" = SOFT_BLOCKED_SHARED)
	config.soft_ic_filter_reasons = list("testsofterblocks" = SOFT_BLOCKED_IC)
	config.soft_ic_outside_pda_filter_reasons = list("testsoftestblocks" = SOFT_BLOCKED_IC_OUTSIDE_PDA)
	config.update_chat_filter_regexes()

	test_filter(
		"this message is blockedinic, AND has a comma (which needs word bounds)",
		"blockedinic",
		BLOCKED_IC,
		BLOCKED_IC,
		null,
	)

	test_filter(
		"these words have filtered words in them: ablockedinic blockedinicbbbb aablockedinicbb",
		null,
		null,
		null,
		null,
	)

	test_filter(
		"<(0_0<) <(0_0)> (>0_0)> KIRBY DANCE!!!",
		"<(0_0<)",
		BLOCKED_IC_OUTSIDE_PDA,
		null,
		null,
	)

	test_filter(
		"This message is blockedinshared, meaning it's banned EVERYWHERE",
		"blockedinshared",
		BLOCKED_SHARED,
		BLOCKED_SHARED,
		BLOCKED_SHARED,
	)

/datum/unit_test/chat_filter_sanity/proc/test_filter(
	message,
	blocked_word,
	ic_filter_result,
	pda_filter_result,
	ooc_filter_result,
	soft_ic_filter_result,
	soft_pda_filter_result,
	soft_ooc_filter_result,
)
	var/ic_filter = is_ic_filtered(message)
	var/pda_filter = is_ic_filtered_for_pdas(message)
	var/ooc_filter = is_ooc_filtered(message)
	var/soft_ic_filter = is_soft_ic_filtered(message)
	var/soft_pda_filter = is_soft_ic_filtered_for_pdas(message)
	var/soft_ooc_filter = is_soft_ooc_filtered(message)

	test_filter_result("IC", message, ic_filter, ic_filter_result, blocked_word)
	test_filter_result("PDA", message, pda_filter, pda_filter_result, blocked_word)
	test_filter_result("OOC", message, ooc_filter, ooc_filter_result, blocked_word)
	test_filter_result("Soft_IC", message, soft_ic_filter, soft_ic_filter_result, blocked_word)
	test_filter_result("Soft_PDA", message, soft_pda_filter, soft_pda_filter_result, blocked_word)
	test_filter_result("Soft_OOC", message, soft_ooc_filter, soft_ooc_filter_result, blocked_word)

/datum/unit_test/chat_filter_sanity/proc/test_filter_result(
	filter_type,
	message,
	outcome,
	expected_reason,
	expected_blocked_word,
)
	if (isnull(outcome) && isnull(expected_reason))
		return

	if (isnull(outcome))
		Fail("[message] was not blocked on the [filter_type] filter when it was expected to")
		return

	if (isnull(expected_reason))
		Fail("[message] was blocked on the [filter_type] filter when it wasn't expected to: [json_encode(outcome)]")
		return

	if (outcome[CHAT_FILTER_INDEX_WORD] != expected_blocked_word)
		Fail("[message] was blocked on the [filter_type] filter, but for a different word: \"[outcome[CHAT_FILTER_INDEX_WORD]]\" (instead of [expected_blocked_word])")
		return

	if (outcome[CHAT_FILTER_INDEX_REASON] != expected_reason)
		Fail("[message] was blocked on the [filter_type] filter, but for a different reason: \"[outcome[CHAT_FILTER_INDEX_REASON]]\" (instead of [expected_reason])")

#undef BLOCKED_IC
#undef BLOCKED_IC_OUTSIDE_PDA
#undef BLOCKED_SHARED
#undef SOFT_BLOCKED_IC
#undef SOFT_BLOCKED_IC_OUTSIDE_PDA
#undef SOFT_BLOCKED_SHARED


code:
/// Checks that the length of the initial contents of a closet doesn't exceed its storage capacity
/datum/unit_test/closets

/datum/unit_test/closets/Run()
	var/list/all_closets = subtypesof(/obj/structure/closet)
	//Supply pods. They are sent, crashed, opened and never closed again. They also cause exceptions in nullspace.
	all_closets -= typesof(/obj/structure/closet/supplypod)

	for(var/closet_type in all_closets)
		var/obj/structure/closet/closet = allocate(closet_type)
		var/contents_len = length(closet.contents)
		TEST_ASSERT(contents_len <= closet.storage_capacity, "Initial Contents of [closet.type] ([contents_len]) exceed its storage capacity ([closet.storage_capacity]).")


code:
/datum/unit_test/harm_punch/Run()
	var/mob/living/carbon/human/puncher = allocate(/mob/living/carbon/human)
	var/mob/living/carbon/human/victim = allocate(/mob/living/carbon/human)

	// Avoid all randomness in tests
	ADD_TRAIT(puncher, TRAIT_PERFECT_ATTACKER, INNATE_TRAIT)

	//puncher.set_combat_mode(TRUE) //SEPTIC EDIT REMOVAL
	//SEPTIC EDIT BEGIN
	SET_HARM_INTENT(puncher)
	//SEPTIC EDIT END
	victim.attack_hand(puncher, list(RIGHT_CLICK = FALSE))

	TEST_ASSERT(victim.getBruteLoss() > 0, "Victim took no brute damage after being punched")

/datum/unit_test/harm_melee/Run()
	var/mob/living/carbon/human/tider = allocate(/mob/living/carbon/human)
	var/mob/living/carbon/human/victim = allocate(/mob/living/carbon/human)
	var/obj/item/storage/toolbox/toolbox = allocate(/obj/item/storage/toolbox)

	tider.put_in_active_hand(toolbox, forced = TRUE)
	//tider.set_combat_mode(TRUE) //SEPTIC EDIT REMOVAL
	//SEPTIC EDIT BEGIN
	SET_HARM_INTENT(tider)
	//SEPTIC EDIT END
	victim.attackby(toolbox, tider)

	TEST_ASSERT(victim.getBruteLoss() > 0, "Victim took no brute damage after being hit by a toolbox")

/datum/unit_test/harm_different_damage/Run()
	var/mob/living/carbon/human/attacker = allocate(/mob/living/carbon/human)
	var/mob/living/carbon/human/victim = allocate(/mob/living/carbon/human)
	var/obj/item/weldingtool/welding_tool = allocate(/obj/item/weldingtool)

	attacker.put_in_active_hand(welding_tool, forced = TRUE)
	//attacker.set_combat_mode(TRUE) //SEPTIC EDIT REMOVAL
	//SEPTIC EDIT BEGIN
	SET_HARM_INTENT(attacker)
	//SEPTIC EDIT END

	welding_tool.attack_self(attacker) // Turn it on
	victim.attackby(welding_tool, attacker)

	TEST_ASSERT_EQUAL(victim.getBruteLoss(), 0, "Victim took brute damage from a lit welding tool")
	TEST_ASSERT(victim.getFireLoss() > 0, "Victim took no burn damage after being hit by a lit welding tool")

/datum/unit_test/attack_chain
	var/attack_hit
	var/post_attack_hit
	var/pre_attack_hit

/datum/unit_test/attack_chain/proc/attack_hit()
	SIGNAL_HANDLER
	attack_hit = TRUE

/datum/unit_test/attack_chain/proc/post_attack_hit()
	SIGNAL_HANDLER
	post_attack_hit = TRUE

/datum/unit_test/attack_chain/proc/pre_attack_hit()
	SIGNAL_HANDLER
	pre_attack_hit = TRUE

/datum/unit_test/attack_chain/Run()
	var/mob/living/carbon/human/attacker = allocate(/mob/living/carbon/human)
	var/mob/living/carbon/human/victim = allocate(/mob/living/carbon/human)
	var/obj/item/storage/toolbox/toolbox = allocate(/obj/item/storage/toolbox)

	RegisterSignal(toolbox, COMSIG_ITEM_PRE_ATTACK, .proc/pre_attack_hit)
	RegisterSignal(toolbox, COMSIG_ITEM_ATTACK, .proc/attack_hit)
	RegisterSignal(toolbox, COMSIG_ITEM_AFTERATTACK, .proc/post_attack_hit)

	attacker.put_in_active_hand(toolbox, forced = TRUE)
	//attacker.set_combat_mode(TRUE) //SEPTIC EDIT REMOVAL
	//SEPTIC EDIT BEGIN
	SET_HARM_INTENT(attacker)
	//SEPTIC EDIT END
	toolbox.melee_attack_chain(attacker, victim)

	TEST_ASSERT(pre_attack_hit, "Pre-attack signal was not fired")
	TEST_ASSERT(attack_hit, "Attack signal was not fired")
	TEST_ASSERT(post_attack_hit, "Post-attack signal was not fired")

/datum/unit_test/disarm/Run()
	var/mob/living/carbon/human/attacker = allocate(/mob/living/carbon/human)
	var/mob/living/carbon/human/victim = allocate(/mob/living/carbon/human)
	var/obj/item/storage/toolbox/toolbox = allocate(/obj/item/storage/toolbox)

	victim.put_in_active_hand(toolbox, forced = TRUE)

	var/obj/structure/barricade/dense_object = allocate(/obj/structure/barricade)

	// Attacker --> Victim --> Empty space --> Wall
	attacker.forceMove(run_loc_floor_bottom_left)
	victim.forceMove(locate(run_loc_floor_bottom_left.x + 1, run_loc_floor_bottom_left.y, run_loc_floor_bottom_left.z))
	dense_object.forceMove(locate(run_loc_floor_bottom_left.x + 3, run_loc_floor_bottom_left.y, run_loc_floor_bottom_left.z))

	// First disarm, world should now look like:
	// Attacker --> Empty space --> Victim --> Wall
	victim.attack_hand(attacker, list(RIGHT_CLICK = TRUE))

	TEST_ASSERT_EQUAL(victim.loc.x, run_loc_floor_bottom_left.x + 2, "Victim wasn't moved back after being pushed")
	TEST_ASSERT(!victim.has_status_effect(STATUS_EFFECT_KNOCKDOWN), "Victim was knocked down despite not being against a wall")
	TEST_ASSERT_EQUAL(victim.get_active_held_item(), toolbox, "Victim dropped toolbox despite not being against a wall")

	attacker.forceMove(get_step(attacker, EAST))

	// Second disarm, victim was against wall and should be down
	victim.attack_hand(attacker, list(RIGHT_CLICK = TRUE))

	TEST_ASSERT_EQUAL(victim.loc.x, run_loc_floor_bottom_left.x + 2, "Victim was moved after being pushed against a wall")
	TEST_ASSERT(victim.has_status_effect(STATUS_EFFECT_KNOCKDOWN), "Victim was not knocked down after being pushed against a wall")
	TEST_ASSERT_EQUAL(victim.get_active_held_item(), null, "Victim didn't drop toolbox after being pushed against a wall")


code:
/datum/unit_test/component_duping/Run()
	var/list/bad_dms = list()
	var/list/bad_dts = list()
	for(var/t in typesof(/datum/component))
		var/datum/component/comp = t
		if(!isnum(initial(comp.dupe_mode)))
			bad_dms += t
		var/dupe_type = initial(comp.dupe_type)
		if(dupe_type && !ispath(dupe_type))
			bad_dts += t
	if(length(bad_dms) || length(bad_dts))
		Fail("Components with invalid dupe modes: ([bad_dms.Join(",")]) ||| Components with invalid dupe types: ([bad_dts.Join(",")])")


code:
// Checks that the confusion symptom correctly gives, and removes, confusion
/datum/unit_test/confusion_symptom/Run()
	var/mob/living/carbon/human/H = allocate(/mob/living/carbon/human)
	var/datum/disease/advance/confusion/disease = allocate(/datum/disease/advance/confusion)
	var/datum/symptom/confusion/confusion = disease.symptoms[1]
	disease.processing = TRUE
	disease.update_stage(5)
	disease.infect(H, make_copy = FALSE)
	confusion.Activate(disease)
	TEST_ASSERT(H.get_confusion() > 0, "Human is not confused after getting symptom.")
	disease.cure()
	TEST_ASSERT_EQUAL(H.get_confusion(), 0, "Human is still confused after curing confusion.")

/datum/disease/advance/confusion/New()
	symptoms += new /datum/symptom/confusion
	Refresh()


code:
#define COMSIG_MOCK_SIGNAL "mock_signal"

/// Test that the connect_loc element handles basic movement cases
/datum/unit_test/connect_loc_basic

/datum/unit_test/connect_loc_basic/Run()
	var/obj/item/watches_mock_calls/watcher = allocate(/obj/item/watches_mock_calls)

	var/turf/current_turf = get_turf(watcher)

	SEND_SIGNAL(current_turf, COMSIG_MOCK_SIGNAL)
	TEST_ASSERT_EQUAL(watcher.times_called, 1, "After firing mock signal, connect_loc didn't send it")

	watcher.forceMove(run_loc_floor_top_right)

	SEND_SIGNAL(current_turf, COMSIG_MOCK_SIGNAL)
	TEST_ASSERT_EQUAL(watcher.times_called, 1, "Mock signal was fired on old turf, but connect_loc still picked it up")

	current_turf = get_turf(watcher)
	SEND_SIGNAL(current_turf, COMSIG_MOCK_SIGNAL)
	TEST_ASSERT_EQUAL(watcher.times_called, 2, "Mock signal was fired after turf move, but it wasn't picked up")

/// Test that the connect_loc element handles turf changes
/datum/unit_test/connect_loc_change_turf
	var/old_turf_type

/datum/unit_test/connect_loc_change_turf/Run()
	var/obj/item/watches_mock_calls/watcher = allocate(/obj/item/watches_mock_calls, run_loc_floor_bottom_left)

	var/turf/current_turf = get_turf(watcher)
	old_turf_type = current_turf.type

	SEND_SIGNAL(current_turf, COMSIG_MOCK_SIGNAL)
	TEST_ASSERT_EQUAL(watcher.times_called, 1, "After firing mock signal, connect_loc didn't send it")

	current_turf.ChangeTurf(/turf/closed/wall)

	SEND_SIGNAL(current_turf, COMSIG_MOCK_SIGNAL)
	TEST_ASSERT_EQUAL(watcher.times_called, 2, "After changing turf, connect_loc didn't reconnect it")

	current_turf.ChangeTurf(/turf/open/floor/carpet)
	SEND_SIGNAL(current_turf, COMSIG_MOCK_SIGNAL)
	TEST_ASSERT_EQUAL(watcher.times_called, 3, "After changing turf a second time, connect_loc didn't reconnect it")

/datum/unit_test/connect_loc_change_turf/Destroy()
	run_loc_floor_bottom_left.ChangeTurf(old_turf_type)
	return ..()

/// Tests that multiple objects can have connect_loc on the same turf without runtimes.
/datum/unit_test/connect_loc_multiple_on_turf

/datum/unit_test/connect_loc_multiple_on_turf/Run()
	var/obj/item/watches_mock_calls/watcher_one = allocate(/obj/item/watches_mock_calls, run_loc_floor_bottom_left)
	qdel(watcher_one)

	var/obj/item/watches_mock_calls/watcher_two = allocate(/obj/item/watches_mock_calls, run_loc_floor_bottom_left)
	qdel(watcher_two)

/obj/item/watches_mock_calls
	var/times_called

/obj/item/watches_mock_calls/Initialize(mapload)
	. = ..()

	var/static/list/connections = list(
		COMSIG_MOCK_SIGNAL = .proc/on_receive_mock_signal,
	)

	AddElement(/datum/element/connect_loc, connections)

/obj/item/watches_mock_calls/proc/on_receive_mock_signal(datum/source)
	SIGNAL_HANDLER
	times_called += 1

#undef COMSIG_MOCK_SIGNAL


code:
/// Makes sure that crayons have their crayon_color in their initial name (to differentiate them in the crafting menu).
/datum/unit_test/crayon_naming

/datum/unit_test/crayon_naming/Run()
	for(var/obj/item/toy/crayon/crayon_path as anything in typesof(/obj/item/toy/crayon))
		if(ispath(crayon_path, /obj/item/toy/crayon/spraycan))
			continue
		var/obj/item/toy/crayon/real_crayon = new crayon_path
		if(!findtext(initial(real_crayon.name),real_crayon.crayon_color))
			Fail("[real_crayon] does not have its crayon_color ([real_crayon.crayon_color]) in its initial name ([initial(real_crayon.name)]).")
		qdel(real_crayon)


code:
///Delete one of every type, sleep a while, then check to see if anything has gone fucky
/datum/unit_test/create_and_destroy
	//You absolutely must run last
	priority = TEST_DEL_WORLD

/datum/unit_test/create_and_destroy/Run()
	//We'll spawn everything here
	var/turf/spawn_at = run_loc_floor_bottom_left
	var/list/ignore = list(
		//Never meant to be created, errors out the ass for mobcode reasons
		/mob/living/carbon,
		//Nother template type, doesn't like being created with no seed
		/obj/item/food/grown,
		//And another
		/obj/item/slimecross/recurring,
		//This should be obvious
		/obj/machinery/doomsday_device,
		//Yet more templates
		/obj/machinery/restaurant_portal,
		//Template type
		/obj/effect/mob_spawn,
		//Template type
		/obj/structure/holosign/robot_seat,
		//Say it with me now, type template
		/obj/effect/mapping_helpers/component_injector,
		//template type
		/obj/effect/mapping_helpers/trait_injector,
		//Singleton
		/mob/dview,
		//Template,
		/obj/effect/mapping_helpers/custom_icon,
	)
	//This turf existing is an error in and of itself
	ignore += typesof(/turf/baseturf_skipover)
	ignore += typesof(/turf/baseturf_bottom)
	//This demands a borg, so we'll let if off easy
	ignore += typesof(/obj/item/modular_computer/tablet/integrated)
	//This one demands a computer, ditto
	ignore += typesof(/obj/item/modular_computer/processor)
	//Needs special input, let's be nice
	ignore += typesof(/obj/effect/abstract/proximity_checker)
	//Very finiky, blacklisting to make things easier
	ignore += typesof(/obj/item/poster/wanted)
	//We can't pass a mind into this
	ignore += typesof(/obj/item/phylactery)
	//This expects a seed, we can't pass it
	ignore += typesof(/obj/item/food/grown)
	//Nothing to hallucinate if there's nothing to hallicinate
	ignore += typesof(/obj/effect/hallucination)
	//These want fried food to take on the shape of, we can't pass that in
	ignore += typesof(/obj/item/food/deepfryholder)
	//Can't pass in a thing to glow
	ignore += typesof(/obj/effect/abstract/eye_lighting)
	//We don't have a pod
	ignore += typesof(/obj/effect/pod_landingzone_effect)
	ignore += typesof(/obj/effect/pod_landingzone)
	//We don't have a disease to pass in
	ignore += typesof(/obj/effect/mapping_helpers/component_injector/infective)
	//It's a trapdoor to nowhere
	ignore += typesof(/obj/effect/mapping_helpers/trapdoor_placer)
	//There's no shapeshift to hold
	ignore += typesof(/obj/shapeshift_holder)
	//No tauma to pass in
	ignore += typesof(/mob/camera/imaginary_friend)
	//No pod to gondola
	ignore += typesof(/mob/living/simple_animal/pet/gondola/gondolapod)
	//No heart to give
	ignore += typesof(/obj/structure/ethereal_crystal)
	//No linked console
	ignore += typesof(/mob/camera/ai_eye/remote/base_construction)
	//See above
	ignore += typesof(/mob/camera/ai_eye/remote/shuttle_docker)
	//Hangs a ref post invoke async, which we don't support. Could put a qdeleted check but it feels hacky
	ignore += typesof(/obj/effect/anomaly/grav/high)
	//See above
	ignore += typesof(/obj/effect/timestop)
	//Invoke async in init, skippppp
	ignore += typesof(/mob/living/silicon/robot/model)
	//This lad also sleeps
	ignore += typesof(/obj/item/hilbertshotel)
	//this boi spawns turf changing stuff, and it stacks and causes pain. Let's just not
	ignore += typesof(/obj/effect/sliding_puzzle)
	//Stacks baseturfs, can't be tested here
	ignore += typesof(/obj/effect/temp_visual/lava_warning)
	//Stacks baseturfs, can't be tested here
	ignore += typesof(/obj/effect/landmark/ctf)
	//Our system doesn't support it without warning spam from unregister calls on things that never registered
	ignore += typesof(/obj/docking_port)
	//Asks for a shuttle that may not exist, let's leave it alone
	ignore += typesof(/obj/item/pinpointer/shuttle)
	//This spawns beams as a part of init, which can sleep past an async proc. This hangs a ref, and fucks us. It's only a problem here because the beam sleeps with CHECK_TICK
	ignore += typesof(/obj/structure/alien/resin/flower_bud)
	//Needs a linked mecha
	ignore += typesof(/obj/effect/skyfall_landingzone)
	//Leads to errors as a consequence of the logic behind moving back to a tile that's moving you somewhere else
	ignore += typesof(/obj/effect/mapping_helpers/component_injector/areabound)
	//Expects a mob to holderize, we have nothing to give
	ignore += typesof(/obj/item/clothing/head/mob_holder)

	var/list/cached_contents = spawn_at.contents.Copy()
	var/baseturf_count = length(spawn_at.baseturfs)

	for(var/type_path in typesof(/atom/movable, /turf) - ignore) //No areas please
		if(ispath(type_path, /turf))
			spawn_at.ChangeTurf(type_path, /turf/baseturf_skipover)
			//We change it back to prevent pain, please don't ask
			spawn_at.ChangeTurf(/turf/open/floor/wood, /turf/baseturf_skipover)
			if(baseturf_count != length(spawn_at.baseturfs))
				Fail("[type_path] changed the amount of baseturfs we have [baseturf_count] -> [length(spawn_at.baseturfs)]")
				baseturf_count = length(spawn_at.baseturfs)
		else
			var/atom/creation = new type_path(spawn_at)
			if(QDELETED(creation))
				continue
			//Go all in
			qdel(creation, force = TRUE)
			//This will hold a ref to the last thing we process unless we set it to null
			//Yes byond is fucking sinful
			creation = null

		//There's a lot of stuff that either spawns stuff in on create, or removes stuff on destroy. Let's cut it all out so things are easier to deal with
		var/list/to_del = spawn_at.contents - cached_contents
		if(length(to_del))
			for(var/atom/to_kill in to_del)
				qdel(to_kill)

	//Hell code, we're bound to have ended the round somehow so let's stop if from ending while we work
	SSticker.delay_end = TRUE
	//Prevent the garbage subsystem from harddeling anything, if only to save time
	SSgarbage.collection_timeout[GC_QUEUE_HARDDELETE] = 10000 HOURS
	//Clear it, just in case
	cached_contents.Cut()

	//Now that we've qdel'd everything, let's sleep until the gc has processed all the shit we care about
	var/time_needed = SSgarbage.collection_timeout[GC_QUEUE_CHECK]
	var/start_time = world.time
	var/garbage_queue_processed = FALSE

	sleep(time_needed)
	while(!garbage_queue_processed)
		var/list/queue_to_check = SSgarbage.queues[GC_QUEUE_CHECK]
		//How the hell did you manage to empty this? Good job!
		if(!length(queue_to_check))
			garbage_queue_processed = TRUE
			break

		var/list/oldest_packet = queue_to_check[1]
		//Pull out the time we deld at
		var/qdeld_at = oldest_packet[1]
		//If we've found a packet that got del'd later then we finished, then all our shit has been processed
		if(qdeld_at > start_time)
			garbage_queue_processed = TRUE
			break

		if(world.time > start_time + time_needed + 8 MINUTES)
			Fail("Something has gone horribly wrong, the garbage queue has been processing for well over 10 minutes. What the hell did you do")
			break

		//Immediately fire the gc right after
		SSgarbage.next_fire = 1
		//Unless you've seriously fucked up, queue processing shouldn't take "that" long. Let her run for a bit, see if anything's changed
		sleep(20 SECONDS)

	//Alright, time to see if anything messed up
	var/list/cache_for_sonic_speed = SSgarbage.items
	for(var/path in cache_for_sonic_speed)
		var/datum/qdel_item/item = cache_for_sonic_speed[path]
		if(item.failures)
			Fail("[item.name] hard deleted [item.failures] times out of a total del count of [item.qdels]")
		if(item.no_respect_force)
			Fail("[item.name] failed to respect force deletion [item.no_respect_force] times out of a total del count of [item.qdels]")
		if(item.no_hint)
			Fail("[item.name] failed to return a qdel hint [item.no_hint] times out of a total del count of [item.qdels]")

	cache_for_sonic_speed = SSatoms.BadInitializeCalls
	for(var/path in cache_for_sonic_speed)
		var/fails = cache_for_sonic_speed[path]
		if(fails & BAD_INIT_NO_HINT)
			Fail("[path] didn't return an Initialize hint")
		if(fails & BAD_INIT_QDEL_BEFORE)
			Fail("[path] qdel'd in New()")
		if(fails & BAD_INIT_SLEPT)
			Fail("[path] slept during Initialize()")

	SSticker.delay_end = FALSE
	//This shouldn't be needed, but let's be polite
	SSgarbage.collection_timeout[GC_QUEUE_HARDDELETE] = 10 SECONDS


code:
/datum/unit_test/designs

/datum/unit_test/designs/Run()
//Can't use allocate because of bug with certain datums
	var/datum/design/default_design = new /datum/design()
	/* SEPTIC EDIT REMOVAL
	var/datum/design/surgery/default_design_surgery = new /datum/design/surgery()
	*/
	for(var/path in subtypesof(/datum/design))
		/* SEPTIC EDIT REMOVAL
		if (ispath(path, /datum/design/surgery)) //We are checking surgery design separatly later since they work differently
			continue
		*/
		var/datum/design/current_design = new path //Create an instance of each design
		if (current_design.id == DESIGN_ID_IGNORE) //Don't check designs with ignore ID
			continue
		if (isnull(current_design.name) || current_design.name == default_design.name) //Designs with ID must have non default/null Name
			Fail("Design [current_design.type] has default or null name var but has an ID")
		if ((!isnull(current_design.materials) && LAZYLEN(current_design.materials)) || (!isnull(current_design.reagents_list) && LAZYLEN(current_design.reagents_list))) //Design requires materials
			if ((isnull(current_design.build_path) || current_design.build_path == default_design.build_path) && (isnull(current_design.make_reagents) || current_design.make_reagents == default_design.make_reagents)) //Check if design gives any output
				Fail("Design [current_design.type] requires materials but does not have have any build_path or make_reagents set")
		else if (!isnull(current_design.build_path) || !isnull(current_design.build_path)) // //Design requires no materials but creates stuff
			Fail("Design [current_design.type] requires NO materials but has build_path or make_reagents set")

	/* SEPTIC EDIT REMOVAL
	for(var/path in subtypesof(/datum/design/surgery))
		var/datum/design/surgery/current_design = new path //Create an instance of each design
		if (isnull(current_design.id) || current_design.id == default_design_surgery.id) //Check if ID was not set
			Fail("Surgery Design [current_design.type] has no ID set")
		if (isnull(current_design.id) || current_design.name == default_design_surgery.name) //Check if name was not set
			Fail("Surgery Design [current_design.type] has default or null name var")
		if (isnull(current_design.desc) || current_design.desc == default_design_surgery.desc) //Check if desc was not set
			Fail("Surgery Design [current_design.type] has default or null desc var")
		if (isnull(current_design.surgery) || current_design.surgery == default_design_surgery.surgery) //Check if surgery was not set
			Fail("Surgery Design [current_design.type] has default or null surgery var")
	*/


code:
/// Verifies that roundstart dynamic rulesets are setup properly without external configuration.
/datum/unit_test/dynamic_roundstart_ruleset_sanity

/datum/unit_test/dynamic_roundstart_ruleset_sanity/Run()
	for (var/_ruleset in subtypesof(/datum/dynamic_ruleset/roundstart))
		var/datum/dynamic_ruleset/roundstart/ruleset = _ruleset

		var/has_scaling_cost = initial(ruleset.scaling_cost)
		var/is_lone = initial(ruleset.flags) & (LONE_RULESET | HIGH_IMPACT_RULESET)

		if (has_scaling_cost && is_lone)
			Fail("[ruleset] has a scaling_cost, but is also a lone/highlander ruleset.")
		else if (!has_scaling_cost && !is_lone)
			Fail("[ruleset] has no scaling cost, but is also not a lone/highlander ruleset.")

/// Verifies that dynamic rulesets have unique antag_flag.
/datum/unit_test/dynamic_unique_antag_flags

/datum/unit_test/dynamic_unique_antag_flags/Run()
	var/list/known_antag_flags = list()

	for (var/datum/dynamic_ruleset/ruleset as anything in subtypesof(/datum/dynamic_ruleset))
		if (isnull(initial(ruleset.antag_datum)))
			continue

		var/antag_flag = initial(ruleset.antag_flag)

		if (isnull(antag_flag))
			Fail("[ruleset] has a null antag_flag!")
			continue

		if (antag_flag in known_antag_flags)
			Fail("[ruleset] has a non-unique antag_flag [antag_flag] (used by [known_antag_flags[antag_flag]])!")
			continue

		known_antag_flags[antag_flag] = ruleset


code:
/// Verifies that all glands for an egg are valid
/datum/unit_test/egg_glands

/datum/unit_test/egg_glands/Run()
	var/obj/item/food/egg/egg = allocate(/obj/item/food/egg)

	for (var/datum/reagent/reagent_type as anything in subtypesof(/datum/reagent))
		if (!(initial(reagent_type.chemical_flags) & REAGENT_CAN_BE_SYNTHESIZED))
			continue

		try
			mix_color_from_reagents(egg.reagents.reagent_list + list(new reagent_type))
		catch (var/exception/exception)
			Fail("[reagent_type] fails mixing\n[exception]")


code:
/datum/unit_test/emoting
	var/emotes_used = 0

/datum/unit_test/emoting/Run()
	var/mob/living/carbon/human/human = allocate(/mob/living/carbon/human)
	RegisterSignal(human, COMSIG_MOB_EMOTE, .proc/on_emote_used)

	human.say("*shrug")
	TEST_ASSERT_EQUAL(emotes_used, 1, "Human did not shrug")

	human.say("*beep")
	TEST_ASSERT_EQUAL(emotes_used, 1, "Human beeped, when that should be restricted to silicons")

	human.setOxyLoss(140)

	TEST_ASSERT(human.stat != CONSCIOUS, "Human is somehow conscious after receiving suffocation damage")

	human.say("*shrug")
	TEST_ASSERT_EQUAL(emotes_used, 1, "Human shrugged while unconscious")

	human.say("*deathgasp")
	TEST_ASSERT_EQUAL(emotes_used, 2, "Human could not deathgasp while unconscious")

/datum/unit_test/emoting/proc/on_emote_used()
	SIGNAL_HANDLER
	emotes_used += 1


code:
///Used to test the completeness of the reference finder proc.
/datum/unit_test/find_reference_sanity

/atom/movable/ref_holder
	var/atom/movable/ref_test/test
	var/list/test_list = list()
	var/list/test_assoc_list = list()

/atom/movable/ref_holder/Destroy()
	test = null
	test_list.Cut()
	test_assoc_list.Cut()
	return ..()

/atom/movable/ref_test
	var/atom/movable/ref_test/self_ref

/atom/movable/ref_test/Destroy(force)
	self_ref = null
	return ..()

/datum/unit_test/find_reference_sanity/Run()
	var/atom/movable/ref_test/victim = allocate(/atom/movable/ref_test)
	var/atom/movable/ref_holder/testbed = allocate(/atom/movable/ref_holder)
	SSgarbage.should_save_refs = TRUE

	//Sanity check
	victim.DoSearchVar(testbed, "Sanity Check", search_time = 1) //We increment search time to get around an optimization
	TEST_ASSERT(!victim.found_refs.len, "The ref-tracking tool found a ref where none existed")
	SSgarbage.should_save_refs = FALSE

/datum/unit_test/find_reference_baseline/Run()
	var/atom/movable/ref_test/victim = allocate(/atom/movable/ref_test)
	var/atom/movable/ref_holder/testbed = allocate(/atom/movable/ref_holder)
	SSgarbage.should_save_refs = TRUE

	//Set up for the first round of tests
	testbed.test = victim
	testbed.test_list += victim
	testbed.test_assoc_list["baseline"] = victim

	victim.DoSearchVar(testbed, "First Run", search_time = 2)

	TEST_ASSERT(victim.found_refs["test"], "The ref-tracking tool failed to find a regular value")
	TEST_ASSERT(victim.found_refs[testbed.test_list], "The ref-tracking tool failed to find a list entry")
	TEST_ASSERT(victim.found_refs[testbed.test_assoc_list], "The ref-tracking tool failed to find an assoc list value")
	SSgarbage.should_save_refs = FALSE

/datum/unit_test/find_reference_exotic/Run()
	var/atom/movable/ref_test/victim = allocate(/atom/movable/ref_test)
	var/atom/movable/ref_holder/testbed = allocate(/atom/movable/ref_holder)
	SSgarbage.should_save_refs = TRUE

	//Second round, bit harder this time
	testbed.overlays += victim
	testbed.vis_contents += victim
	testbed.test_assoc_list[victim] = TRUE

	victim.DoSearchVar(testbed, "Second Run", search_time = 3)

	//This is another sanity check
	TEST_ASSERT(!victim.found_refs[testbed.overlays], "The ref-tracking tool found an overlays entry? That shouldn't be possible")
	TEST_ASSERT(victim.found_refs[testbed.vis_contents], "The ref-tracking tool failed to find a vis_contents entry")
	TEST_ASSERT(victim.found_refs[testbed.test_assoc_list], "The ref-tracking tool failed to find an assoc list key")
	SSgarbage.should_save_refs = FALSE

/datum/unit_test/find_reference_esoteric/Run()
	var/atom/movable/ref_test/victim = allocate(/atom/movable/ref_test)
	var/atom/movable/ref_holder/testbed = allocate(/atom/movable/ref_holder)
	SSgarbage.should_save_refs = TRUE

	//Let's get a bit esoteric
	victim.self_ref = victim
	var/list/to_find = list(victim)
	testbed.test_list += list(to_find)
	var/list/to_find_assoc = list(victim)
	testbed.test_assoc_list["Nesting"] = to_find_assoc

	victim.DoSearchVar(victim, "Third Run Self", search_time = 4)
	victim.DoSearchVar(testbed, "Third Run Testbed", search_time = 4)
	TEST_ASSERT(victim.found_refs["self_ref"], "The ref-tracking tool failed to find a self reference")
	TEST_ASSERT(victim.found_refs[to_find], "The ref-tracking tool failed to find a nested list entry")
	TEST_ASSERT(victim.found_refs[to_find_assoc], "The ref-tracking tool failed to find a nested assoc list entry")
	SSgarbage.should_save_refs = FALSE

/datum/unit_test/find_reference_null_key_entry/Run()
	var/atom/movable/ref_test/victim = allocate(/atom/movable/ref_test)
	var/atom/movable/ref_holder/testbed = allocate(/atom/movable/ref_holder)
	SSgarbage.should_save_refs = TRUE

	//Calm before the storm
	testbed.test_assoc_list = list(null = victim)

	victim.DoSearchVar(testbed, "Fourth Run", search_time = 5)
	TEST_ASSERT(testbed.test_assoc_list, "The ref-tracking tool failed to find a null key'd assoc list entry")

/datum/unit_test/find_reference_assoc_investigation/Run()
	var/atom/movable/ref_test/victim = allocate(/atom/movable/ref_test)
	var/atom/movable/ref_holder/testbed = allocate(/atom/movable/ref_holder)
	SSgarbage.should_save_refs = TRUE

	//Let's do some more complex assoc list investigation
	var/list/to_find_in_key = list(victim)
	testbed.test_assoc_list[to_find_in_key] = list("memes")
	var/list/to_find_null_assoc_nested = list(victim)
	testbed.test_assoc_list[null] = to_find_null_assoc_nested

	victim.DoSearchVar(testbed, "Fifth Run", search_time = 6)
	TEST_ASSERT(victim.found_refs[to_find_in_key], "The ref-tracking tool failed to find a nested assoc list key")
	TEST_ASSERT(victim.found_refs[to_find_null_assoc_nested], "The ref-tracking tool failed to find a null key'd nested assoc list entry")
	SSgarbage.should_save_refs = FALSE


code:
/// Makes sure that spawned food has reagents and the edible component (or else it can't be eaten).
/datum/unit_test/food_edibility_check

/datum/unit_test/food_edibility_check/Run()
	var/list/not_food = list(
		/obj/item/food/grown,
		/obj/item/food/grown/mushroom,
		/obj/item/food/deepfryholder,
		/obj/item/food/clothing,
		/obj/item/food/meat/slab/human/mutant,
		/obj/item/food/grown/shell)

	var/list/food_paths = subtypesof(/obj/item/food) - not_food

	for(var/food_path in food_paths)
		var/obj/item/food/spawned_food = allocate(food_path)

		if(!spawned_food.reagents)
			Fail("[food_path] does not have any reagents, making it inedible!")

		if(!IS_EDIBLE(spawned_food))
			Fail("[food_path] does not have the edible component, making it inedible!")

		qdel(spawned_food)


code:
/// Makes sure items using GAGS have all the icon states needed to work
/datum/unit_test/greyscale_item_icon_states

/datum/unit_test/greyscale_item_icon_states/Run()
	for(var/obj/item/item_path as anything in subtypesof(/obj/item))
		var/held_icon_state = initial(item_path.inhand_icon_state) || initial(item_path.icon_state)

		var/datum/greyscale_config/lefthand = SSgreyscale.configurations["[initial(item_path.greyscale_config_inhand_left)]"]
		if(lefthand && !lefthand.icon_states[held_icon_state])
			Fail("[lefthand.DebugName()] is missing a sprite for the held lefthand for [item_path]. Expected icon state: '[held_icon_state]'")

		var/datum/greyscale_config/righthand = SSgreyscale.configurations["[initial(item_path.greyscale_config_inhand_right)]"]
		if(righthand && !righthand.icon_states[held_icon_state])
			Fail("[righthand.DebugName()] is missing a sprite for the held righthand for [item_path]. Expected icon state: '[held_icon_state]'")

		var/datum/greyscale_config/worn = SSgreyscale.configurations["[initial(item_path.greyscale_config_worn)]"]
		var/worn_icon_state = initial(item_path.worn_icon_state) || initial(item_path.icon_state)
		if(worn && !worn.icon_states[worn_icon_state])
			Fail("[worn.DebugName()] is missing a sprite for the worn overlay for [item_path]. Expected icon state: '[worn_icon_state]'")

		var/datum/greyscale_config/belt = SSgreyscale.configurations["[initial(item_path.greyscale_config_belt)]"]
		var/belt_icon_state = initial(item_path.belt_icon_state) || initial(item_path.icon_state)
		if(belt && !belt.icon_states[belt_icon_state])
			Fail("[belt.DebugName()] is missing a sprite for the belt overlay for [item_path]. Expected icon state: '[belt_icon_state]'")

/// Makes sure objects using greyscale configs have, if any, the correct number of colors
/datum/unit_test/greyscale_color_count

/datum/unit_test/greyscale_color_count/Run()
	for(var/atom/thing as anything in subtypesof(/atom))
		var/datum/greyscale_config/config = SSgreyscale.configurations["[initial(thing.greyscale_config)]"]
		if(!config)
			continue
		var/list/colors = splittext(initial(thing.greyscale_colors), "#")
		if(!length(colors))
			continue
		var/number_of_colors = length(colors) - 1
		if(config.expected_colors != number_of_colors)
			Fail("[thing] has the wrong amount of colors configured for [config.DebugName()]. Expected [config.expected_colors] but only found [number_of_colors].")


code:
/// This test checks all heretic knowledge nodes - excluding the ones which are unreachable on purpose - and ensures players can reach them in game.
/// If it finds a node that is unreachable, it throws an error.
/datum/unit_test/heretic_knowledge/Run()
	///List of all knowledge excluding the unreachable base types.
	var/list/blacklist = list(/datum/eldritch_knowledge/spell,/datum/eldritch_knowledge/curse,/datum/eldritch_knowledge/final,/datum/eldritch_knowledge/summon)
	var/list/all_possible_knowledge = subtypesof(/datum/eldritch_knowledge) - blacklist

	var/list/list_to_check = GLOB.heretic_start_knowledge.Copy()
	var/i = 0
	while(i < length(list_to_check))
		var/datum/eldritch_knowledge/eldritch_knowledge = allocate(list_to_check[++i])
		for(var/next_knowledge in eldritch_knowledge.next_knowledge)
			if(next_knowledge in list_to_check)
				continue
			list_to_check += next_knowledge

	if(length(all_possible_knowledge) != length(all_possible_knowledge & list_to_check))
		var/list/unreachables = all_possible_knowledge - list_to_check
		for(var/X in unreachables)
			var/datum/eldritch_knowledge/eldritch_knowledge = X
			Fail("[initial(eldritch_knowledge.name)] is unreachable by players! Add it to the blacklist in /code/modules/unit_tests/heretic_knowledge.dm if it is purposeful!")


code:
// test Jewish holiday
/datum/unit_test/hanukkah_2123/Run()
	var/datum/holiday/hebrew/hanukkah/hanukkah = new
	TEST_ASSERT(hanukkah.shouldCelebrate(14, DECEMBER, 2123, TUESDAY), "December 14, 2123 was not Hanukkah.")

// test Islamic holiday
/datum/unit_test/ramadan_2165/Run()
	var/datum/holiday/islamic/ramadan/ramadan = new
	TEST_ASSERT(ramadan.shouldCelebrate(6, NOVEMBER, 2165, WEDNESDAY), "November 6, 2165 was not Ramadan.")

// nth day of week
/datum/unit_test/thanksgiving_2020/Run()
	var/datum/holiday/nth_week/thanksgiving/thanksgiving = new
	TEST_ASSERT(thanksgiving.shouldCelebrate(26, NOVEMBER, 2020, THURSDAY), "November 26, 2020 was not Thanksgiving.")

// another nth day of week
/datum/unit_test/mother_3683/Run()
	var/datum/holiday/nth_week/mother/mother = new
	TEST_ASSERT(mother.shouldCelebrate(9, MAY, 3683, 2, SUNDAY), "May 9, 3683 was not Mother's Day.")

// plain old simple holiday
/datum/unit_test/hello_2020/Run()
	var/datum/holiday/hello/hello = new
	TEST_ASSERT(hello.shouldCelebrate(21, NOVEMBER, 2020, SATURDAY), "November 21, 2020 was not Hello day.")

// holiday which goes across months
/datum/unit_test/new_year_1983/Run()
	var/datum/holiday/new_year/new_year = new
	TEST_ASSERT(new_year.shouldCelebrate(2, JANUARY, 1983, SUNDAY), "January 2, 1983 was not New Year.")

/datum/unit_test/moth_week_2020/Run()
	var/datum/holiday/nth_week/moth/moth = new
	TEST_ASSERT(moth.shouldCelebrate(18, JULY, 2020, SATURDAY), "July 18, 2020 was not Moth Week.")
	TEST_ASSERT(moth.shouldCelebrate(20, JULY, 2020, MONDAY), "July 20, 2020 was not Moth Week.")
	TEST_ASSERT(moth.shouldCelebrate(24, JULY, 2020, FRIDAY), "July 24, 2020 was not Moth Week.")
	TEST_ASSERT(moth.shouldCelebrate(26, JULY, 2020, SUNDAY), "July 26, 2020 was not Moth Week.")


code:
/**
 * A test to make sure harvesting plants in hydroponics results in the correct number of plants with the correct chemicals inside of it.
 *
 * We plant a seed into a tray and harvest it with a human.
 * This seed is set to have the maximum potency and yield with no instability to prevent mutations.
 * Then we check how many products we got from the harvest. For most plants, this should be 10 products, as we have a yield of 10.
 * Alternatively, if the plant has a trait that halves the products on harvest, it should result in 5 products.
 *
 * After we harvest our seed, we check for the plant's nutriments and vitamins.
 * Most plants have nutriments, so most plants should result in a number of nutriments.
 * Some plants have vitamins and some don't, so we then check the number of vitamins.
 * Additionally, the plant may have traits that double the amount of chemicals it can hold. We check the max volume in that case and adjust accordingly.
 * Plants may have additional chemicals genes that we don't check.
 * Plants may have traits that effect the final product's contents that we don't check.
 * Chemicals may react inside of the plant on harvest, which we don't check.
 *
 * After we check the harvest and the chemicals in the harvest, we go ahead and clean up the harvested products and remove the seed if it has perennial growth.
 *
 * This test checks both /obj/item/food/grown items and /obj/item/grown items since, despite both being used in hydroponics,
 * they aren't the same type so everything that works with one isn't guaranteed to work with the other.
 */
/datum/unit_test/hydroponics_harvest/Run()
	var/obj/machinery/hydroponics/hydroponics_tray = allocate(/obj/machinery/hydroponics)
	var/obj/item/seeds/planted_food_seed = allocate(/obj/item/seeds/apple) //grown food
	var/obj/item/seeds/planted_not_food_seed = allocate(/obj/item/seeds/sunflower) //grown inedible
	var/obj/item/seeds/planted_densified_seed = allocate(/obj/item/seeds/redbeet) //grown + densified chemicals

	var/mob/living/carbon/human/human = allocate(/mob/living/carbon/human)

	hydroponics_tray.forceMove(run_loc_floor_bottom_left)
	human.forceMove(locate((run_loc_floor_bottom_left.x + 1), run_loc_floor_bottom_left.y, run_loc_floor_bottom_left.z))

	// Apples should harvest 10 apples with 10u nutrients and 4u vitamins.
	test_seed(hydroponics_tray, planted_food_seed, human)
	// Sunflowers should harvest 10 sunflowers with 4u nutriment and 0u vitamins. It should also have 8u corn oil.
	test_seed(hydroponics_tray, planted_not_food_seed, human)
	// Redbeets should harvest 5 beets (10 / 2) with 10u nutriments (5 x 2) and 10u vitamins (5 x 2) thanks to densified chemicals.
	test_seed(hydroponics_tray, planted_densified_seed, human)

/datum/unit_test/hydroponics_harvest/proc/plant_and_update_seed(obj/machinery/hydroponics/tray, obj/item/seeds/seed)
	seed.set_yield(10) // Sets the seed yield to 10. This gets clamped to 5 if the plant has traits to half the yield.
	seed.set_potency(100) // Sets the seed potency to 100.
	seed.set_instability(0) // Sets the seed instability to 0, to prevent mutations.

	tray.myseed = seed
	seed.forceMove(tray)
	tray.name = tray.myseed ? "[initial(tray.name)] ([tray.myseed.plantname])" : initial(tray.name)

	tray.plant_health = seed.endurance
	tray.age = 20
	tray.harvest = TRUE

/datum/unit_test/hydroponics_harvest/proc/test_seed(obj/machinery/hydroponics/tray, obj/item/seeds/seed, mob/living/carbon/user)
	tray.reagents.add_reagent(/datum/reagent/plantnutriment/eznutriment, 20)
	plant_and_update_seed(tray, seed)
	var/saved_name = tray.name // Name gets cleared when some plants are harvested.

	if(!tray.myseed)
		Fail("Hydroponics harvest from [saved_name] had no seed set properly to test.")

	if(tray.myseed != seed)
		Fail("Hydroponics harvest from [saved_name] had [tray.myseed] planted when it was testing [seed].")

	var/double_chemicals = seed.get_gene(/datum/plant_gene/trait/maxchem)
	var/expected_yield = seed.getYield()
	var/max_volume = 100 //For 99% of plants, max volume is 100.

	if(double_chemicals)
		max_volume *= 2

	tray.attack_hand(user)
	var/list/obj/item/all_harvested_items = list()
	for(var/obj/item/harvested_food in user.drop_location())
		all_harvested_items += harvested_food

	if(!all_harvested_items.len)
		Fail("Hydroponics harvest from [saved_name] resulted in 0 harvest.")

	TEST_ASSERT_EQUAL(all_harvested_items.len, expected_yield, "Hydroponics harvest from [saved_name] only harvested [all_harvested_items.len] items instead of [expected_yield] items.")
	TEST_ASSERT(all_harvested_items[1].reagents, "Hydroponics harvest from [saved_name] had no reagent container.")
	TEST_ASSERT_EQUAL(all_harvested_items[1].reagents.maximum_volume, max_volume, "Hydroponics harvest from [saved_name] [double_chemicals ? "did not have its reagent capacity doubled to [max_volume] properly." : "did not have its reagents capped at [max_volume] properly."]")

	var/expected_nutriments = seed.reagents_add[/datum/reagent/consumable/nutriment]
	var/expected_vitamins = seed.reagents_add[/datum/reagent/consumable/nutriment/vitamin]

	var/found_nutriments = all_harvested_items[1].reagents.get_reagent_amount(/datum/reagent/consumable/nutriment)
	var/found_vitamins = all_harvested_items[1].reagents.get_reagent_amount(/datum/reagent/consumable/nutriment/vitamin)
	QDEL_LIST(all_harvested_items) //We got everything we needed from our harvest, we can clean it up.

	TEST_ASSERT_EQUAL(found_nutriments, expected_nutriments * max_volume, "Hydroponics harvest from [saved_name] has a [expected_nutriments] nutriment gene (expecting [expected_nutriments * max_volume]) but only had [found_nutriments] units of nutriment inside.")
	TEST_ASSERT_EQUAL(found_vitamins, expected_vitamins * max_volume, "Hydroponics harvest from [saved_name] has a [expected_vitamins] vitamin gene (expecting [expected_nutriments * max_volume]) but only had [found_vitamins] units of vitamins inside.")

	if(tray.myseed)
		tray.harvest = FALSE
		tray.age = 0
		tray.plant_health = 0

		QDEL_NULL(tray.myseed)
		tray.name = tray.myseed ? "[initial(tray.name)] ([tray.myseed.plantname])" : initial(tray.name)


code:
/// Unit test to ensure plants can't self-mutate into themselves.
/datum/unit_test/hydroponics_self_mutation

/datum/unit_test/hydroponics_self_mutation/Run()
	var/list/all_seeds = subtypesof(/obj/item/seeds)

	for(var/seed in all_seeds)
		var/obj/item/seeds/instantiated_seed = new seed()
		for(var/path in instantiated_seed.mutatelist)
			TEST_ASSERT(!istype(instantiated_seed, path), "[instantiated_seed] - [instantiated_seed.type] is able to mutate into itself! Its mutatelist is not set correctly.")
		qdel(instantiated_seed)


code:
/datum/unit_test/keybinding_init/Run()
	for(var/i in subtypesof(/datum/keybinding))
		var/datum/keybinding/KB = i
		if(initial(KB.keybind_signal) || !initial(KB.name))
			continue
		Fail("[KB.name] does not have a keybind signal defined.")


code:
/// Ensures that when disassembling a machine, all the parts are given back
/datum/unit_test/machine_disassembly/Run()
	var/obj/machinery/freezer = allocate(/obj/machinery/atmospherics/components/binary/thermomachine/freezer)

	var/turf/freezer_location = freezer.loc
	freezer.deconstruct()

	// Check that the components are created
	TEST_ASSERT(locate(/obj/item/stock_parts/micro_laser) in freezer_location, "Couldn't find micro-laser when disassembling freezer")

	// Check that the circuit board itself is created
	TEST_ASSERT(locate(/obj/item/circuitboard/machine/thermomachine) in freezer_location, "Couldn't find the circuit board when disassembling freezer")


code:
/// This test is used to make sure a flesh-and-bone base human can suffer all the types of wounds, and that suffering more severe wounds removes and replaces the lesser wound. Also tests that [/mob/living/carbon/proc/fully_heal] removes all wounds
/* SEPTIC EDIT REMOVAL
/datum/unit_test/test_human_base/Run()
	var/mob/living/carbon/human/victim = allocate(/mob/living/carbon/human)

	/// the limbs have no wound resistance like the chest and head do, so let's go with the r_arm
	var/obj/item/bodypart/tested_part = victim.get_bodypart(BODY_ZONE_R_ARM)
	/// In order of the wound types we're trying to inflict, what sharpness do we need to deal them?
	var/list/sharps = list(NONE, SHARP_EDGED, SHARP_POINTY, NONE)
	/// Since burn wounds need burn damage, duh
	var/list/dam_types = list(BRUTE, BRUTE, BRUTE, BURN)

	var/i = 1
	var/list/iter_test_wound_list

	for(iter_test_wound_list in list(list(/datum/wound/blunt/moderate, /datum/wound/blunt/severe, /datum/wound/blunt/critical),\
										list(/datum/wound/slash/moderate, /datum/wound/slash/severe, /datum/wound/slash/critical),\
										list(/datum/wound/pierce/moderate, /datum/wound/pierce/severe, /datum/wound/pierce/critical),\
										list(/datum/wound/burn/moderate, /datum/wound/burn/severe, /datum/wound/burn/critical)))

		TEST_ASSERT_EQUAL(length(victim.all_wounds), 0, "Patient is somehow wounded before test")
		var/datum/wound/iter_test_wound
		var/threshold_penalty = 0

		for(iter_test_wound in iter_test_wound_list)
			var/threshold = initial(iter_test_wound.threshold_minimum) - threshold_penalty // just enough to guarantee the next tier of wound, given the existing wound threshold penalty
			if(dam_types[i] == BRUTE)
				tested_part.receive_damage(WOUND_MINIMUM_DAMAGE, 0, wound_bonus = threshold, sharpness=sharps[i])
			else if(dam_types[i] == BURN)
				tested_part.receive_damage(0, WOUND_MINIMUM_DAMAGE, wound_bonus = threshold, sharpness=sharps[i])

			TEST_ASSERT(length(victim.all_wounds), "Patient has no wounds when one wound is expected. Severity: [initial(iter_test_wound.severity)]")
			TEST_ASSERT_EQUAL(length(victim.all_wounds), 1, "Patient has more than one wound when only one is expected. Severity: [initial(iter_test_wound.severity)]")
			var/datum/wound/actual_wound = victim.all_wounds[1]
			TEST_ASSERT_EQUAL(actual_wound.type, iter_test_wound, "Patient has wound of incorrect severity. Expected: [initial(iter_test_wound.name)] Got: [actual_wound]")
			threshold_penalty = actual_wound.threshold_penalty
		i++
		victim.fully_heal(TRUE) // should clear all wounds between types

/// This test is used for making sure species with bones but no flesh (skeletons, plasmamen) can only suffer BONE_WOUNDS, and nothing tagged with FLESH_WOUND (it's possible to require both)
/datum/unit_test/test_human_bone/Run()
	var/mob/living/carbon/human/victim = allocate(/mob/living/carbon/human)

	/// the limbs have no wound resistance like the chest and head do, so let's go with the r_arm
	var/obj/item/bodypart/tested_part = victim.get_bodypart(BODY_ZONE_R_ARM)
	/// In order of the wound types we're trying to inflict, what sharpness do we need to deal them?
	var/list/sharps = list(NONE, SHARP_EDGED, SHARP_POINTY, NONE)
	/// Since burn wounds need burn damage, duh
	var/list/dam_types = list(BRUTE, BRUTE, BRUTE, BURN)

	var/i = 1
	var/list/iter_test_wound_list
	victim.dna.species.species_traits &= HAS_FLESH // take away the base human's flesh (ouchie!) ((not actually ouchie, this just affects their wounds and dismemberment handling))

	for(iter_test_wound_list in list(list(/datum/wound/blunt/moderate, /datum/wound/blunt/severe, /datum/wound/blunt/critical),\
										list(/datum/wound/slash/moderate, /datum/wound/slash/severe, /datum/wound/slash/critical),\
										list(/datum/wound/pierce/moderate, /datum/wound/pierce/severe, /datum/wound/pierce/critical),\
										list(/datum/wound/burn/moderate, /datum/wound/burn/severe, /datum/wound/burn/critical)))

		TEST_ASSERT_EQUAL(length(victim.all_wounds), 0, "Patient is somehow wounded before test")
		var/datum/wound/iter_test_wound
		var/threshold_penalty = 0

		for(iter_test_wound in iter_test_wound_list)
			var/threshold = initial(iter_test_wound.threshold_minimum) - threshold_penalty // just enough to guarantee the next tier of wound, given the existing wound threshold penalty
			if(dam_types[i] == BRUTE)
				tested_part.receive_damage(WOUND_MINIMUM_DAMAGE, 0, wound_bonus = threshold, sharpness=sharps[i])
			else if(dam_types[i] == BURN)
				tested_part.receive_damage(0, WOUND_MINIMUM_DAMAGE, wound_bonus = threshold, sharpness=sharps[i])

			// so if we just tried to deal a flesh wound, make sure we didn't actually suffer it. We may have suffered a bone wound instead, but we just want to make sure we don't have a flesh wound
			if(initial(iter_test_wound.wound_flags) & FLESH_WOUND)
				if(!length(victim.all_wounds)) // not having a wound is good news
					continue
				else // we have to check that it's actually a bone wound and not the intended wound type
					TEST_ASSERT_EQUAL(length(victim.all_wounds), 1, "Patient has more than one wound when only one is expected. Severity: [initial(iter_test_wound.severity)]")
					var/datum/wound/actual_wound = victim.all_wounds[1]
					TEST_ASSERT((actual_wound.wound_flags & ~FLESH_WOUND), "Patient has flesh wound despite no HAS_FLESH flag, expected either no wound or bone wound. Offending wound: [actual_wound]")
					threshold_penalty = actual_wound.threshold_penalty
			else // otherwise if it's a bone wound, check that we have it per usual
				TEST_ASSERT(length(victim.all_wounds), "Patient has no wounds when one wound is expected. Severity: [initial(iter_test_wound.severity)]")
				TEST_ASSERT_EQUAL(length(victim.all_wounds), 1, "Patient has more than one wound when only one is expected. Severity: [initial(iter_test_wound.severity)]")
				var/datum/wound/actual_wound = victim.all_wounds[1]
				TEST_ASSERT_EQUAL(actual_wound.type, iter_test_wound, "Patient has wound of incorrect severity. Expected: [initial(iter_test_wound.name)] Got: [actual_wound]")
				threshold_penalty = actual_wound.threshold_penalty
		i++
		victim.fully_heal(TRUE) // should clear all wounds between types
*/


code:
/datum/unit_test/merge_type/Run()
	var/list/blacklist = list(/obj/item/stack/sheet,
							  /obj/item/stack/sheet/mineral,
							  /obj/item/stack/ore,
							  /obj/item/stack/spacecash,
							  /obj/item/stack/license_plates,
							  /obj/item/stack/tile/mineral,
							  /obj/item/stack/tile)

	var/list/paths = subtypesof(/obj/item/stack) - blacklist

	for(var/stackpath in paths)
		var/obj/item/stack/stack = stackpath
		if(!initial(stack.merge_type))
			Fail("([stack]) lacks set merge_type variable!")


code:
/datum/unit_test/metabolization/Run()
	// Pause natural mob life so it can be handled entirely by the test
	SSmobs.pause()

	var/mob/living/carbon/human/human = allocate(/mob/living/carbon/human)

	for (var/reagent_type in subtypesof(/datum/reagent))
		if(reagent_type in GLOB.fake_reagent_blacklist)
			continue
		test_reagent(human, reagent_type)

/datum/unit_test/metabolization/proc/test_reagent(mob/living/carbon/C, reagent_type)
	C.reagents.add_reagent(reagent_type, 10)
	C.reagents.metabolize(C, SSMOBS_DT, 0, can_overdose = TRUE)
	C.reagents.clear_reagents()

/datum/unit_test/metabolization/Destroy()
	SSmobs.ignite()
	return ..()

/datum/unit_test/on_mob_end_metabolize/Run()
	SSmobs.pause()

	var/mob/living/carbon/human/user = allocate(/mob/living/carbon/human)
	var/obj/item/reagent_containers/pill/pill = allocate(/obj/item/reagent_containers/pill)
	var/datum/reagent/drug/methamphetamine/meth = /datum/reagent/drug/methamphetamine

	// Give them enough meth to be consumed in 2 metabolizations
	pill.reagents.add_reagent(meth, 1.9 * initial(meth.metabolization_rate) * SSMOBS_DT)
	pill.attack(user, user)

	user.Life(SSMOBS_DT)

	TEST_ASSERT(user.reagents.has_reagent(meth), "User does not have meth in their system after consuming it")
	TEST_ASSERT(user.has_movespeed_modifier(/datum/movespeed_modifier/reagent/methamphetamine), "User consumed meth, but did not gain movespeed modifier")

	user.Life(SSMOBS_DT)

	TEST_ASSERT(!user.reagents.has_reagent(meth), "User still has meth in their system when it should've finished metabolizing")
	TEST_ASSERT(!user.has_movespeed_modifier(/datum/movespeed_modifier/reagent/methamphetamine), "User still has movespeed modifier despite not containing any more meth")

/datum/unit_test/on_mob_end_metabolize/Destroy()
	SSmobs.ignite()
	return ..()

/datum/unit_test/addictions/Run()
	SSmobs.pause()

	var/mob/living/carbon/human/pill_user = allocate(/mob/living/carbon/human)
	var/mob/living/carbon/human/syringe_user = allocate(/mob/living/carbon/human)
	var/mob/living/carbon/human/pill_syringe_user = allocate(/mob/living/carbon/human)

	var/datum/mind/pill_mind = new /datum/mind("Mothcocks")
	pill_mind.active = TRUE
	pill_mind.transfer_to(pill_user)

	var/datum/mind/syringe_mind = new /datum/mind("Mothcocks")
	syringe_mind.active = TRUE
	syringe_mind.transfer_to(syringe_user)

	var/datum/mind/pill_syringe_mind = new /datum/mind("Mothcocks")
	pill_syringe_mind.active = TRUE
	pill_syringe_mind.transfer_to(pill_syringe_user)

	var/obj/item/reagent_containers/pill/pill = allocate(/obj/item/reagent_containers/pill)
	var/obj/item/reagent_containers/pill/pill_two = allocate(/obj/item/reagent_containers/pill)

	var/obj/item/reagent_containers/syringe/syringe = allocate(/obj/item/reagent_containers/syringe)

	var/datum/reagent/drug/methamphetamine/meth = allocate(/datum/reagent/drug/methamphetamine)

	var/addiction_type_to_check

	for(var/key in meth.addiction_types)
		addiction_type_to_check = key //idk how to do this otherwise

	// Let's start with stomach metabolism
	pill.reagents.add_reagent(meth.type, 5)
	pill.attack(pill_user, pill_user)

	// Set the metabolism efficiency to 1.0 so it transfers all reagents to the body in one go.
	var/obj/item/organ/stomach/pill_belly = pill_user.getorganslot(ORGAN_SLOT_STOMACH)
	pill_belly.metabolism_efficiency = 1

	pill_user.Life()

	TEST_ASSERT(pill_user.mind.addiction_points[addiction_type_to_check], "User did not gain addiction points after metabolizing meth")

	// Then injected metabolism
	syringe.volume = 5
	syringe.amount_per_transfer_from_this = 5
	syringe.reagents.add_reagent(meth.type, 5)
	syringe.melee_attack_chain(syringe_user, syringe_user)

	syringe_user.Life()

	TEST_ASSERT(syringe_user.mind.addiction_points[addiction_type_to_check], "User did not gain addiction points after metabolizing meth")

	// One half syringe
	syringe.reagents.remove_all()
	syringe.volume = 5
	syringe.amount_per_transfer_from_this = 5
	syringe.reagents.add_reagent(meth.type, (5 * 0.5) + 1)

	// One half pill
	pill_two.reagents.add_reagent(meth.type, (5 * 0.5) + 1)
	pill_two.attack(pill_syringe_user, pill_syringe_user)
	syringe.melee_attack_chain(pill_syringe_user, pill_syringe_user)

	// Set the metabolism efficiency to 1.0 so it transfers all reagents to the body in one go.
	pill_belly = pill_syringe_user.getorganslot(ORGAN_SLOT_STOMACH)
	pill_belly.metabolism_efficiency = 1

	pill_syringe_user.Life()

	TEST_ASSERT(pill_syringe_user.mind.addiction_points[addiction_type_to_check], "User did not gain addiction points after metabolizing meth")

/datum/unit_test/addictions/Destroy()
	SSmobs.ignite()
	return ..()


code:
/datum/unit_test/ntnetwork
	var/list/valid_network_names = list("SS13.ATMOS.SCRUBBERS.SM", "DEEPSPACE.HYDRO.PLANT", "SINDIE.STINKS.BUTT")

	var/list/invalid_network_names = list(".SS13.BOB", "SS13.OHMAN.", "SS13.HAS A SPACE" )

	var/list/valid_network_trees = list(list("SS13","ATMOS","SCRUBBERS","SM"),list("DEEPSPACE","HYDRO","PLANT"), list("SINDIE","STINKS","BUTT"))

	var/list/network_roots = list(
				__STATION_NETWORK_ROOT,
				__CENTCOM_NETWORK_ROOT,
				__SYNDICATE_NETWORK_ROOT,
				__LIMBO_NETWORK_ROOT)

	var/list/random_words_for_testing = list(
				__NETWORK_TOOLS, __NETWORK_REMOTES, __NETWORK_AIRLOCKS,
				__NETWORK_DOORS, __NETWORK_ATMOS, __NETWORK_SCUBBERS,
				__NETWORK_AIRALARMS, __NETWORK_CONTROL, __NETWORK_STORAGE,
				__NETWORK_CARGO, __NETWORK_BOTS, __NETWORK_COMPUTER,
				__NETWORK_CARDS)

	var/number_of_names_to_test = 50
	var/length_of_test_network = 5

/datum/unit_test/proc/mangle_word(word)
	var/len = length(word)
	var/space_pos = round(len/2)
	word = copytext(word,1,space_pos) + " " + copytext(word,space_pos,len)
	return word

/datum/unit_test/ntnetwork/Run()
	// First check if name checks work
	for(var/name in valid_network_names)
		TEST_ASSERT(verify_network_name(name), "Network name ([name]) marked as invalid but supposed to valid")

	for(var/name in invalid_network_names)
		TEST_ASSERT(!verify_network_name(name), "Network name ([name]) marked as valid but supposed to be invalid")

	// Next check if we can pack and unpack network names
	for(var/i in 1 to valid_network_names.len)
		var/name = valid_network_names[i]
		var/list/name_list = SSnetworks.network_string_to_list(name)
		TEST_ASSERT(compare_list(name_list,  valid_network_trees[i]), "Network name ([name]) did not unpack into a proper list")
	for(var/i in 1 to valid_network_trees.len)
		var/list/name_list = valid_network_trees[i]
		var/name = SSnetworks.network_list_to_string(name_list)
		TEST_ASSERT_EQUAL(name, valid_network_names[i], "Network name ([name]) did not pack into a proper string")

	// Ok, we know we can verify network names now, and that we can pack and unpack.  Lets try making some random good names
	var/list/generated_network_names = list()
	var/test_string
	for(var/i in 1 to number_of_names_to_test)
		var/list/builder = list()
		builder += pick(network_roots)
		for(var/j in 1 to length_of_test_network)
			builder += pick(random_words_for_testing)
		test_string = SSnetworks.network_list_to_string(builder)
		var/name_fix = simple_network_name_fix(test_string)
		TEST_ASSERT_EQUAL(name_fix, test_string, "Network name ([test_string]) was not fixed correctly to ([name_fix])")
		generated_network_names += name_fix // save for future
	// test badly generated names
	for(var/i in 1 to number_of_names_to_test)
		var/list/builder = list()
		builder += mangle_word(pick(network_roots))
		for(var/j in 1 to length_of_test_network)
			builder += mangle_word(pick(random_words_for_testing))
		test_string = builder.Join(".")
		var/name_fix = simple_network_name_fix(test_string)
		TEST_ASSERT(verify_network_name(name_fix), "Network name ([test_string]) was not fixed correctly ([name_fix]) with bad name")