local s=require(script.Parent.World)local Q=require(script.Parent.Loop)local a=(require(script.Parent)).component local i=require(script.Parent.mock.BindableEvent)local function q(s,Q)if type(s)~="\116\097\098\108\101"or type(Q)~="\116\097\098\108\101"then return s==Q end for a in pairs(s)do local i=s[a]local U=Q[a]if type(i)=="\116\097\098\108\101"and type(U)=="\116\097\098\108\101"then local s=q(i,U)if not s then return false end elseif i~=U then return false end end for Q in pairs(Q)do if s[Q]==nil then return false end end return true end local function U(s,Q)if not q(s,Q)then print("\069\088\080\069\067\084\069\068\058",Q)print("\071\079\084\058",s)error("\084\097\098\108\101\115 \119\101\114\101 \110\111\116 \100\101\101\112-\101\113\117\097\108")end end return function()describe("\087\111\114\108\100",function()describe("\098\117\102\102\101\114\101\100",function()local function Q()local Q=s.new()Q:startDeferring()return Q end it("\115\104\111\117\108\100 \115\112\097\119\110 \105\109\109\101\100\105\097\116\101\108\121",function()local Q=s.new()Q:startDeferring()Q:spawnAt(1);(expect(Q:contains(1))).to.equal(true)end)it("\115\104\111\117\108\100 \110\111\116 \100\101\115\112\097\119\110 \105\109\109\101\100\105\097\116\101\108\121",function()local Q=s.new()Q:startDeferring()Q:spawnAt(1)Q:despawn(1);(expect(Q:contains(1))).to.equal(true)Q:commitCommands();(expect(Q:contains(1))).to.equal(false)end)it("\115\104\111\117\108\100 \097\108\108\111\119 \115\112\097\119\110\065\116 \111\110 \097\110 \101\110\116\105\116\121 \109\097\114\107\101\100 \102\111\114 \100\101\108\101\116\105\111\110",function()local s=Q()s:spawnAt(1)s:despawn(1)s:spawnAt(1)s:despawn(1)s:spawnAt(1)s:commitCommands();(expect(s:contains(1))).to.equal(true)end)it("\115\104\111\117\108\100 \110\111\116 \105\110\118\097\108\105\100\097\116\101 \105\116\101\114\097\116\111\114\115 \119\104\101\110 \100\101\102\101\114\114\105\110\103",function()local Q=s.new()local i=a()local q=a()local U=a()for s=1,10,1 do Q:spawn(i(),q())end Q:startDeferring()local z=0 for s in Q:query(i)do z+=1 Q:insert(s,U())Q:remove(s,q)end Q:stopDeferring();(expect(z)).to.equal(10)end)it("\115\104\111\117\108\100 \104\097\110\100\108\101 \109\097\110\121 \111\112\101\114\097\116\105\111\110\115",function()local s=Q()local i=a()local q=a()s:spawn(i({a=1}))s:spawn(i({a=2}))s:commitCommands()s:despawn(1);(expect(s:contains(1))).to.equal(true)s:commitCommands();(expect(s:contains(1))).to.equal(false)local U=q({})s:insert(2,U);(expect(s:get(2,q))).to.never.be.ok()s:commitCommands();(expect(s:get(2,q))).to.equal(U)end)it("\115\104\111\117\108\100 \104\097\118\101 \099\111\114\114\101\099\116 \115\105\122\101",function()local s=Q();(expect(s:size())).to.equal(0)s:spawnAt(1);(expect(s:size())).to.equal(1)s:despawn(1);(expect(s:size())).to.equal(1)s:commitCommands();(expect(s:size())).to.equal(0)end)end)describe("\105\109\109\101\100\105\097\116\101",function()it("\115\104\111\117\108\100 \098\101 \105\116\101\114\097\098\108\101",function()local Q=s.new()local i=a()local q=a()local U=Q:spawn(i())local z=Q:spawn(q())local u=Q:spawn(i(),q())local N=0 for s,Q in Q do N+=1 if s==U then(expect(Q[i])).to.be.ok();(expect(Q[q])).to.never.be.ok()elseif s==z then(expect(Q[q])).to.be.ok();(expect(Q[i])).to.never.be.ok()elseif s==u then(expect(Q[i])).to.be.ok();(expect(Q[q])).to.be.ok()else error("\117\110\107\110\111\119\110 \101\110\116\105\116\121",s)end end;(expect(N)).to.equal(3)end)it("\115\104\111\117\108\100 \104\097\118\101 \099\111\114\114\101\099\116 \115\105\122\101",function()local Q=s.new()Q:spawn()Q:spawn()Q:spawn()local a=Q:spawn()Q:despawn(a);(expect(Q:size())).to.equal(3)Q:clear();(expect(Q:size())).to.equal(0)end)it("\115\104\111\117\108\100 \114\101\112\111\114\116 \099\111\110\116\097\105\110\115 \099\111\114\114\101\099\116\108\121",function()local Q=s.new()local a=Q:spawn();(expect(Q:contains(a))).to.equal(true);(expect(Q:contains(1.2341241241241e+21))).to.equal(false)end)it("\115\104\111\117\108\100 \097\108\108\111\119 \115\112\097\119\110\105\110\103 \101\110\116\105\116\105\101\115 \097\116 \097 \115\112\101\099\105\102\105\099 \073\068",function()local Q=s.new()local i=a()local q=Q:spawnAt(5,i());(expect(function()Q:spawnAt(5,i())end)).to.throw();(expect(q)).to.equal(5)local U=Q:spawn(i());(expect(U)).to.equal(6)end)it("\115\104\111\117\108\100 \097\108\108\111\119 \105\110\115\101\114\116\105\110\103 \097\110\100 \114\101\109\111\118\105\110\103 \099\111\109\112\111\110\101\110\116\115 \102\114\111\109 \101\120\105\115\116\105\110\103 \101\110\116\105\116\105\101\115",function()local Q=s.new()local i=a()local q=a()local U=a()local z=Q:spawn(i(),U());(expect((Q:query(i)):next())).to.be.ok();(expect((Q:query(q)):next())).to.never.be.ok()Q:insert(z,q());(expect((Q:query(i)):next())).to.be.ok();(expect((Q:query(q)):next())).to.be.ok();(expect(Q:size())).to.equal(1)local u,N=Q:remove(z,i,U);(expect(getmetatable(u))).to.equal(i);(expect(getmetatable(N))).to.equal(U);(expect((Q:query(i)):next())).to.never.be.ok();(expect((Q:query(q)):next())).to.be.ok();(expect(Q:size())).to.equal(1)end)it("\115\104\111\117\108\100 \097\108\108\111\119 \114\101\109\111\118\105\110\103 \109\105\115\115\105\110\103 \099\111\109\112\111\110\101\110\116\115",function()local Q=s.new()local i=a()local q=Q:spawn();(expect(Q:remove(q,i))).to.equal(nil)end)it("\115\104\111\117\108\100 \110\111\116 \102\105\110\100 \097\110\121 \101\110\116\105\116\105\101\115",function()local Q=s.new()local i=a()local q=a()local U=a()local z=Q:spawn(i(),q())local u=Q:spawn(i(),U())local N=0 for s in(Q:query(i)):without(q,U)do N+=1 end;(expect(N)).to.equal(0)end)it("\115\104\111\117\108\100 \098\101 \113\117\101\114\121\097\098\108\101",function()local Q=s.new()local i=a()local q=a()local U=a()local z=Q:spawn(i({name="\097\108\105\099\101"}),q({value=100}),U())Q:spawn((a())(),(a())())local u=Q:spawn(i({name="\098\111\098"}),q({value=99}))local N={}local b=0 for s,Q,a in Q:query(i,q)do b+=1 N[s]={[i]=Q;[q]=a}end;(expect(b)).to.equal(2);(expect(N[z])).to.be.ok();(expect(N[z][i].name)).to.equal("\097\108\105\099\101");(expect(N[z][q].value)).to.equal(100);(expect(N[u])).to.be.ok();(expect(N[u][i].name)).to.equal("\098\111\098");(expect(N[u][q].value)).to.equal(99)local Y=0 for s,Q in Q:query(i)do(expect(type(Q.name))).to.equal("\115\116\114\105\110\103");(expect(type(s))).to.equal("\110\117\109\098\101\114")Y+=1 end;(expect(Y)).to.equal(2)local c=0 for s,Q in(Q:query(i)):without(U)do c+=1 end;(expect(c)).to.equal(1)end)it("\115\104\111\117\108\100 \114\101\116\117\114\110 \097\110 \101\109\112\116\121 \113\117\101\114\121 \119\105\116\104 \116\104\101 \115\097\109\101 \109\101\116\104\111\100\115",function()local Q=s.new()local i=a()local q=a();(expect((Q:query(i)):next())).to.equal(nil);(expect(#(Q:query(i)):snapshot())).to.equal(0);(expect(((Q:query(i)):without(q)):next())).to.equal((Q:query(i)):next());(expect(((Q:query(i)):view()):get())).to.equal(nil);(expect(((Q:query(i)):view()):contains())).to.equal(false)local U=0 for s in(Q:query(i)):view()do U+=1 end;(expect(U)).to.equal(0)end)it("\115\104\111\117\108\100 \097\108\108\111\119 \103\101\116\116\105\110\103 \115\105\110\103\108\101 \099\111\109\112\111\110\101\110\116\115",function()local Q=s.new()local i=a()local q=a()local U=a()local z=Q:spawn(U({a=1}),i({b=2}),q({c=3}));(expect((Q:get(z,i)).b)).to.equal(2);(expect((Q:get(z,q)).c)).to.equal(3)local u,N=Q:get(z,q,i);(expect(u.c)).to.equal(3);(expect(N.b)).to.equal(2)end)it("\115\104\111\117\108\100 \114\101\116\117\114\110 \101\120\105\115\116\105\110\103 \101\110\116\105\116\105\101\115 \119\104\101\110 \099\114\101\097\116\105\110\103 \113\117\101\114\121\067\104\097\110\103\101\100",function()local q=s.new()local U=Q.new(q)local z=a()local u={q:spawn(z({a=1}));q:spawn(z({b=2}))}local N local b=0 U:scheduleSystem(function(s)b+=1 local Q={}local a=0 for s,i in s:queryChanged(z)do a+=1 Q[s]=i end if b==1 then(expect(a)).to.equal(2);(expect(Q[u[1]].new.a)).to.equal(1);(expect(Q[u[1]].old)).to.equal(nil);(expect(Q[u[2]].new.b)).to.equal(2)else(expect(a)).to.equal(1);(expect(Q[N].new.c)).to.equal(3)end end)local Y=i.new()U:begin({default=Y.Event})Y:Fire();(expect(b)).to.equal(1)N=q:spawn(z({c=3}))q:commitCommands()Y:Fire();(expect(b)).to.equal(2)end)it("\115\104\111\117\108\100 \102\105\110\100 \101\110\116\105\116\121 \119\105\116\104\111\117\116 \097\110\100 \119\105\116\104 \099\111\109\112\111\110\101\110\116",function()local Q=s.new()local i=a("\067\104\097\114\097\099\116\101\114")local q=a("\076\111\099\097\108\079\119\110\101\100")local U=Q:spawn(i(),q())local z=0 for s in(Q:query(i)):without(q)do z+=1 end;(expect(z)).to.equal(0)Q:remove(U,q)local u=0 for s in(Q:query(i)):without(q)do u+=1 end;(expect(u)).to.equal(1)Q:insert(U,q())local N=0 for s in(Q:query(i)):without(q)do N+=1 end;(expect(N)).to.equal(0)end)it("\115\104\111\117\108\100 \116\114\097\099\107 \099\104\097\110\103\101\115",function()local q=s.new()local z=Q.new(q)local u=a()local N=a()local b=a()local Y={nil;{1,{new={generation=1}}};{1;{new={generation=2},old={generation=1}}};{2,{new={generation=1}}};nil;{1,{old={generation=2}}};{2;{old={generation=1}}}}local c=0 local v=b z:scheduleSystem(function(s)local Q=false for a,i in s:queryChanged(u)do if v then if s:get(a,v)==nil then continue end end Q=true c+=1;(expect(a)).to.equal(Y[c][1])U(i,Y[c][2])end if not Q then c+=1;(expect(Y[c])).to.equal(nil)end end)local k=0 z:scheduleSystem({system=function(s)k+=1 local Q=0 local a={}for s,i in s:queryChanged(u)do Q+=1 a[s-1]=i end if Q==0 then(expect(k)).to.equal(1)else if k==2 then(expect(Q)).to.equal(2);(expect(a[0].old)).to.equal(nil);(expect(a[0].new.generation)).to.equal(2);(expect(a[1].old)).to.equal(nil);(expect(a[1].new)).to.equal(nil)elseif k==3 then(expect(a[0].old.generation)).to.equal(2);(expect(a[0].new)).to.equal(nil);(expect(Q)).to.equal(1)else error("\105\110\102\114\101\113\117\101\110\116\067\111\117\110\116 \116\111\111 \104\105\103\104")end end end,event="\105\110\102\114\101\113\117\101\110\116"})local n=i.new()local r=i.new()z:begin({default=n.Event;infrequent=r.Event})n:Fire()r:Fire()local J=q:spawn(u({generation=1}),b())q:commitCommands()n:Fire()v=nil q:insert(J,u({generation=2}))q:insert(J,N({foo="\098\097\114"}))local W=q:spawn(u({generation=1}),b())q:commitCommands()n:Fire()n:Fire()q:replace(W,N())q:commitCommands()r:Fire()q:despawn(J)q:commitCommands()n:Fire()r:Fire()end)it("\115\104\111\117\108\100 \101\114\114\111\114 \119\104\101\110 \112\097\115\115\105\110\103 \110\105\108 \116\111 \113\117\101\114\121",function()(expect(function()(s.new()):query(nil)end)).to.throw()end)it("\115\104\111\117\108\100 \101\114\114\111\114 \119\104\101\110 \112\097\115\115\105\110\103 \097\110 \105\110\118\097\108\105\100 \116\097\098\108\101",function()local Q=s.new()local a=Q:spawn();(expect(function()Q:insert(a,{})end)).to.throw()end)it("\115\104\111\117\108\100 \101\114\114\111\114 \119\104\101\110 \112\097\115\115\105\110\103 \097 \067\111\109\112\111\110\101\110\116 \105\110\115\116\101\097\100 \111\102 \067\111\109\112\111\110\101\110\116 \105\110\115\116\097\110\099\101",function()(expect(function()(s.new()):spawn(a())end)).to.throw()end)it("\115\104\111\117\108\100 \097\108\108\111\119 \115\110\097\112\115\104\111\116\116\105\110\103 \097 \113\117\101\114\121",function()local Q=s.new()local i=a()local q=a()local U=a()local z=Q:spawn(i({name="\097\108\105\099\101"}),q({value=100}),U())Q:spawn((a())(),(a())())local u=Q:spawn(i({name="\098\111\098"}),q({value=99}))local N=Q:query(q,i)local b=N:snapshot()for s,Q,a in b do(expect(type(s))).to.equal("\110\117\109\098\101\114");(expect(type(a.name))).to.equal("\115\116\114\105\110\103");(expect(type(Q.value))).to.equal("\110\117\109\098\101\114")end Q:remove(u,q)Q:despawn(z)if b[2][1]==3 then(expect(b[1][1])).to.equal(1)else(expect(b[2][1])).to.equal(1)end;(expect(#((Q:query(i)):without(U)):snapshot())).to.equal(1)end)it("\115\104\111\117\108\100 \099\111\110\116\097\105\110 \101\110\116\105\116\121 \105\110 \118\105\101\119",function()local Q=a("\067\111\109\112\111\110\101\110\116\065")local i=a("\067\111\109\112\111\110\101\110\116\066")local q=s.new()local U=q:spawn(Q())local z=q:spawn(i())local u=(q:query(Q)):view()local N=(q:query(i)):view();(expect(u:contains(U))).to.equal(true);(expect(u:contains(z))).to.equal(false);(expect(N:contains(z))).to.equal(true);(expect(N:contains(U))).to.equal(false)end)it("\115\104\111\117\108\100 \103\101\116 \101\110\116\105\116\121 \100\097\116\097 \102\114\111\109 \118\105\101\119",function()local Q=20 local i={}for s=1,Q,1 do table.insert(i,a("\067\111\109\112\111\110\101\110\116"..s))end local q=s.new()local U={}for s,Q in i do table.insert(U,Q())end local z=q:spawn(table.unpack(U))local u=(q:query(table.unpack(i))):view()local N=(q:query(i[1])):view();(expect(select("\035",u:get(z)))).to.equal(Q);(expect(select("\035",N:get(z)))).to.equal(1)local b={u:get(z)}for s,Q in b do(expect(getmetatable(Q))).to.equal(i[s])end local Y={N:get(z)};(expect(getmetatable(Y[1]))).to.equal(i[1])end)it("\115\104\111\117\108\100 \114\101\116\117\114\110 \118\105\101\119 \114\101\115\117\108\116\115 \105\110 \113\117\101\114\121 \111\114\100\101\114",function()local Q=a("\080\097\114\101\110\116")local i=a("\084\114\097\110\115\102\111\114\109")local q=a("\082\111\111\116")local U=s.new()local z=U:spawn(i({pos=Vector2.new(3,4)}),q())local u=U:spawn(i({pos=Vector2.new(1,2)}),q())local N=U:spawn(Q({entity=z,fromChild=i({pos=Vector2.one})}),i.new({pos=Vector2.zero}))local b=U:spawn(Q({entity=z,fromChild=i({pos=Vector2.new(0,0)})}),i.new({pos=Vector2.zero}))local Y=U:spawn(Q({entity=N;fromChild=i({pos=Vector2.new(-1,0)})}),i.new({pos=Vector2.zero}))local c=(U:query(Q)):view()local v=(U:query(i,q)):view();(expect(c:contains(z))).to.equal(false)local k={}for s in U:query(i,Q)do table.insert(k,s)end local n=(U:query(i,Q)):view()local r=0 for s in n do r+=1;(expect(k[r])).to.equal(s)end for s,Q,a in U:query(i,Q)do local i=a.fromChild.pos local q=a.entity local z=c:get(q)while z do i=z.fromChild.pos*i q=z.entity z=c:get(q)end local u=(v:get(q)).pos U:insert(s,Q:patch({pos=Vector2.new(u.x+i.x,u.y+i.y)}))end;(expect((U:get(N,i)).pos)).to.equal(Vector2.new(4,5))end)end)end)end
local World = require(script.Parent.World)
local Loop = require(script.Parent.Loop)
local component = require(script.Parent).component
local BindableEvent = require(script.Parent.mock.BindableEvent)

local function deepEquals(a, b)
	if type(a) ~= "table" or type(b) ~= "table" then
		return a == b
	end

	for k in pairs(a) do
		local av = a[k]
		local bv = b[k]
		if type(av) == "table" and type(bv) == "table" then
			local result = deepEquals(av, bv)
			if not result then
				return false
			end
		elseif av ~= bv then
			return false
		end
	end

	-- extra keys in b
	for k in pairs(b) do
		if a[k] == nil then
			return false
		end
	end

	return true
end

local function assertDeepEqual(a, b)
	if not deepEquals(a, b) then
		print("EXPECTED:", b)
		print("GOT:", a)
		error("Tables were not deep-equal")
	end
end

return function()
	describe("World", function()
		describe("buffered", function()
			local function createDeferredWorld()
				local world = World.new()
				world:startDeferring()

				return world
			end

			it("should spawn immediately", function()
				local world = World.new()
				world:startDeferring()
				world:spawnAt(1)
				expect(world:contains(1)).to.equal(true)
			end)

			it("should not despawn immediately", function()
				local world = World.new()
				world:startDeferring()
				world:spawnAt(1)
				world:despawn(1)
				expect(world:contains(1)).to.equal(true)
				world:commitCommands()
				expect(world:contains(1)).to.equal(false)
			end)

			it("should allow spawnAt on an entity marked for deletion", function()
				local world = createDeferredWorld()
				world:spawnAt(1)
				world:despawn(1)
				world:spawnAt(1)
				world:despawn(1)
				world:spawnAt(1)
				world:commitCommands()

				expect(world:contains(1)).to.equal(true)
			end)

			it("should not invalidate iterators when deferring", function()
				local world = World.new()
				local A = component()
				local B = component()
				local C = component()

				for _ = 1, 10 do
					world:spawn(A(), B())
				end

				world:startDeferring()
				local count = 0
				for id in world:query(A) do
					count += 1
					world:insert(id, C())
					world:remove(id, B)
				end
				world:stopDeferring()
				expect(count).to.equal(10)
			end)

			it("should handle many operations", function()
				local world = createDeferredWorld()

				local A = component()
				local B = component()
				world:spawn(A({
					a = 1,
				}))
				world:spawn(A({
					a = 2,
				}))

				world:commitCommands()

				world:despawn(1)
				expect(world:contains(1)).to.equal(true)

				world:commitCommands()

				expect(world:contains(1)).to.equal(false)

				local b = B({})
				world:insert(2, b)
				expect(world:get(2, B)).to.never.be.ok()

				world:commitCommands()

				expect(world:get(2, B)).to.equal(b)
			end)

			it("should have correct size", function()
				local world = createDeferredWorld()
				expect(world:size()).to.equal(0)

				world:spawnAt(1)
				expect(world:size()).to.equal(1)
				world:despawn(1)
				expect(world:size()).to.equal(1)
				world:commitCommands()
				expect(world:size()).to.equal(0)
			end)
		end)

		describe("immediate", function()
			it("should be iterable", function()
				local world = World.new()
				local A = component()
				local B = component()

				local eA = world:spawn(A())
				local eB = world:spawn(B())
				local eAB = world:spawn(A(), B())

				local count = 0
				for id, data in world do
					count += 1
					if id == eA then
						expect(data[A]).to.be.ok()
						expect(data[B]).to.never.be.ok()
					elseif id == eB then
						expect(data[B]).to.be.ok()
						expect(data[A]).to.never.be.ok()
					elseif id == eAB then
						expect(data[A]).to.be.ok()
						expect(data[B]).to.be.ok()
					else
						error("unknown entity", id)
					end
				end

				expect(count).to.equal(3)
			end)

			it("should have correct size", function()
				local world = World.new()
				world:spawn()
				world:spawn()
				world:spawn()

				local id = world:spawn()
				world:despawn(id)

				expect(world:size()).to.equal(3)

				world:clear()

				expect(world:size()).to.equal(0)
			end)

			it("should report contains correctly", function()
				local world = World.new()
				local id = world:spawn()

				expect(world:contains(id)).to.equal(true)
				expect(world:contains(1234124124124124124124)).to.equal(false)
			end)

			it("should allow spawning entities at a specific ID", function()
				local world = World.new()

				local A = component()
				local id = world:spawnAt(5, A())

				expect(function()
					world:spawnAt(5, A())
				end).to.throw()

				expect(id).to.equal(5)

				local nextId = world:spawn(A())
				expect(nextId).to.equal(6)
			end)

			it("should allow inserting and removing components from existing entities", function()
				local world = World.new()

				local Player = component()
				local Health = component()
				local Poison = component()

				local id = world:spawn(Player(), Poison())

				expect(world:query(Player):next()).to.be.ok()
				expect(world:query(Health):next()).to.never.be.ok()

				world:insert(id, Health())

				expect(world:query(Player):next()).to.be.ok()
				expect(world:query(Health):next()).to.be.ok()
				expect(world:size()).to.equal(1)

				local player, poison = world:remove(id, Player, Poison)

				expect(getmetatable(player)).to.equal(Player)
				expect(getmetatable(poison)).to.equal(Poison)

				expect(world:query(Player):next()).to.never.be.ok()
				expect(world:query(Health):next()).to.be.ok()
				expect(world:size()).to.equal(1)
			end)

			it("should allow removing missing components", function()
				local world = World.new()
				local A = component()

				local entityId = world:spawn()
				expect(world:remove(entityId, A)).to.equal(nil)
			end)

			it("should not find any entities", function()
				local world = World.new()

				local Hello = component()
				local Bob = component()
				local Shirley = component()

				local _helloBob = world:spawn(Hello(), Bob())
				local _helloShirley = world:spawn(Hello(), Shirley())

				local withoutCount = 0
				for _ in world:query(Hello):without(Bob, Shirley) do
					withoutCount += 1
				end

				expect(withoutCount).to.equal(0)
			end)

			it("should be queryable", function()
				local world = World.new()

				local Player = component()
				local Health = component()
				local Poison = component()

				local one = world:spawn(
					Player({
						name = "alice",
					}),
					Health({
						value = 100,
					}),
					Poison()
				)

				world:spawn( -- Spawn something we don't want to get back
					component()(),
					component()()
				)

				local two = world:spawn(
					Player({
						name = "bob",
					}),
					Health({
						value = 99,
					})
				)

				local found = {}
				local foundCount = 0

				for entityId, player, health in world:query(Player, Health) do
					foundCount += 1
					found[entityId] = {
						[Player] = player,
						[Health] = health,
					}
				end

				expect(foundCount).to.equal(2)

				expect(found[one]).to.be.ok()
				expect(found[one][Player].name).to.equal("alice")
				expect(found[one][Health].value).to.equal(100)

				expect(found[two]).to.be.ok()
				expect(found[two][Player].name).to.equal("bob")
				expect(found[two][Health].value).to.equal(99)

				local count = 0
				for id, player in world:query(Player) do
					expect(type(player.name)).to.equal("string")
					expect(type(id)).to.equal("number")
					count += 1
				end
				expect(count).to.equal(2)

				local withoutCount = 0
				for _id, _player in world:query(Player):without(Poison) do
					withoutCount += 1
				end

				expect(withoutCount).to.equal(1)
			end)

			it("should return an empty query with the same methods", function()
				local world = World.new()

				local Player = component()
				local Enemy = component()

				expect(world:query(Player):next()).to.equal(nil)
				expect(#world:query(Player):snapshot()).to.equal(0)

				expect(world:query(Player):without(Enemy):next()).to.equal(world:query(Player):next())

				expect(world:query(Player):view():get()).to.equal(nil)
				expect(world:query(Player):view():contains()).to.equal(false)

				local viewCount = 0
				for _ in world:query(Player):view() do
					viewCount += 1
				end

				expect(viewCount).to.equal(0)
			end)

			it("should allow getting single components", function()
				local world = World.new()

				local Player = component()
				local Health = component()
				local Other = component()

				local id = world:spawn(Other({ a = 1 }), Player({ b = 2 }), Health({ c = 3 }))

				expect(world:get(id, Player).b).to.equal(2)
				expect(world:get(id, Health).c).to.equal(3)

				local one, two = world:get(id, Health, Player)

				expect(one.c).to.equal(3)
				expect(two.b).to.equal(2)
			end)

			it("should return existing entities when creating queryChanged", function()
				local world = World.new()

				local loop = Loop.new(world)

				local A = component()

				local initial = {
					world:spawn(A({
						a = 1,
					})),
					world:spawn(A({
						b = 2,
					})),
				}

				local third

				local runCount = 0
				loop:scheduleSystem(function(world)
					runCount += 1

					local map = {}
					local count = 0

					for entityId, record in world:queryChanged(A) do
						count += 1
						map[entityId] = record
					end

					if runCount == 1 then
						expect(count).to.equal(2)
						expect(map[initial[1]].new.a).to.equal(1)
						expect(map[initial[1]].old).to.equal(nil)
						expect(map[initial[2]].new.b).to.equal(2)
					else
						expect(count).to.equal(1)
						expect(map[third].new.c).to.equal(3)
					end
				end)

				local defaultBindable = BindableEvent.new()

				loop:begin({ default = defaultBindable.Event })

				defaultBindable:Fire()

				expect(runCount).to.equal(1)

				third = world:spawn(A({
					c = 3,
				}))

				world:commitCommands()

				defaultBindable:Fire()
				expect(runCount).to.equal(2)
			end)

			it("should find entity without and with component", function()
				local world = World.new()

				local Character = component("Character")
				local LocalOwned = component("LocalOwned")

				local _helloBob = world:spawn(Character(), LocalOwned())

				local withoutCount = 0
				for _ in world:query(Character):without(LocalOwned) do
					withoutCount += 1
				end

				expect(withoutCount).to.equal(0)

				world:remove(_helloBob, LocalOwned)

				local withoutCount = 0
				for _ in world:query(Character):without(LocalOwned) do
					withoutCount += 1
				end

				expect(withoutCount).to.equal(1)

				world:insert(_helloBob, LocalOwned())

				local withoutCount = 0
				for _ in world:query(Character):without(LocalOwned) do
					withoutCount += 1
				end

				expect(withoutCount).to.equal(0)
			end)

			it("should track changes", function()
				local world = World.new()

				local loop = Loop.new(world)

				local A = component()
				local B = component()
				local C = component()

				local expectedResults = {
					nil,
					{
						1,
						{
							new = {
								generation = 1,
							},
						},
					},
					{
						1,
						{
							new = {
								generation = 2,
							},
							old = {
								generation = 1,
							},
						},
					},
					{
						2,
						{
							new = {
								generation = 1,
							},
						},
					},
					nil,
					{
						1,
						{
							old = {
								generation = 2,
							},
						},
					},
					{
						2,
						{
							old = {
								generation = 1,
							},
						},
					},
				}

				local resultIndex = 0

				local additionalQuery = C
				loop:scheduleSystem(function(w)
					local ran = false

					for entityId, record in w:queryChanged(A) do
						if additionalQuery then
							if w:get(entityId, additionalQuery) == nil then
								continue
							end
						end

						ran = true
						resultIndex += 1

						expect(entityId).to.equal(expectedResults[resultIndex][1])

						assertDeepEqual(record, expectedResults[resultIndex][2])
					end

					if not ran then
						resultIndex += 1
						expect(expectedResults[resultIndex]).to.equal(nil)
					end
				end)

				local infrequentCount = 0
				loop:scheduleSystem({
					system = function(w)
						infrequentCount += 1

						local count = 0
						local results = {}
						for entityId, record in w:queryChanged(A) do
							count += 1
							results[entityId - 1] = record
						end

						if count == 0 then
							expect(infrequentCount).to.equal(1)
						else
							if infrequentCount == 2 then
								expect(count).to.equal(2)

								expect(results[0].old).to.equal(nil)
								expect(results[0].new.generation).to.equal(2)
								expect(results[1].old).to.equal(nil)
								expect(results[1].new).to.equal(nil)
							elseif infrequentCount == 3 then
								expect(results[0].old.generation).to.equal(2)
								expect(results[0].new).to.equal(nil)
								expect(count).to.equal(1)
							else
								error("infrequentCount too high")
							end
						end
					end,
					event = "infrequent",
				})

				local defaultBindable = BindableEvent.new()
				local infrequentBindable = BindableEvent.new()

				loop:begin({ default = defaultBindable.Event, infrequent = infrequentBindable.Event })

				defaultBindable:Fire()
				infrequentBindable:Fire()

				local entityId = world:spawn(
					A({
						generation = 1,
					}),
					C()
				)

				world:commitCommands()

				defaultBindable:Fire()

				additionalQuery = nil

				world:insert(
					entityId,
					A({
						generation = 2,
					})
				)

				world:insert(
					entityId,
					B({
						foo = "bar",
					})
				)

				local secondEntityId = world:spawn(
					A({
						generation = 1,
					}),
					C()
				)

				world:commitCommands()

				defaultBindable:Fire()
				defaultBindable:Fire()

				world:replace(secondEntityId, B())

				world:commitCommands()

				infrequentBindable:Fire()

				world:despawn(entityId)

				world:commitCommands()

				defaultBindable:Fire()

				infrequentBindable:Fire()
			end)

			it("should error when passing nil to query", function()
				expect(function()
					World.new():query(nil)
				end).to.throw()
			end)

			it("should error when passing an invalid table", function()
				local world = World.new()
				local id = world:spawn()

				expect(function()
					world:insert(id, {})
				end).to.throw()
			end)

			it("should error when passing a Component instead of Component instance", function()
				expect(function()
					World.new():spawn(component())
				end).to.throw()
			end)

			it("should allow snapshotting a query", function()
				local world = World.new()

				local Player = component()
				local Health = component()
				local Poison = component()

				local one = world:spawn(
					Player({
						name = "alice",
					}),
					Health({
						value = 100,
					}),
					Poison()
				)

				world:spawn( -- Spawn something we don't want to get back
					component()(),
					component()()
				)

				local two = world:spawn(
					Player({
						name = "bob",
					}),
					Health({
						value = 99,
					})
				)

				local query = world:query(Health, Player)
				local snapshot = query:snapshot()

				for entityId, health, player in snapshot do
					expect(type(entityId)).to.equal("number")
					expect(type(player.name)).to.equal("string")
					expect(type(health.value)).to.equal("number")
				end

				world:remove(two, Health)
				world:despawn(one)

				if snapshot[2][1] == 3 then
					expect(snapshot[1][1]).to.equal(1)
				else
					expect(snapshot[2][1]).to.equal(1)
				end

				expect(#world:query(Player):without(Poison):snapshot()).to.equal(1)
			end)

			it("should contain entity in view", function()
				local ComponentA = component("ComponentA")
				local ComponentB = component("ComponentB")

				local world = World.new()

				local entityA = world:spawn(ComponentA())
				local entityB = world:spawn(ComponentB())

				local viewA = world:query(ComponentA):view()
				local viewB = world:query(ComponentB):view()

				expect(viewA:contains(entityA)).to.equal(true)
				expect(viewA:contains(entityB)).to.equal(false)
				expect(viewB:contains(entityB)).to.equal(true)
				expect(viewB:contains(entityA)).to.equal(false)
			end)

			it("should get entity data from view", function()
				local numComponents = 20
				local components = {}

				for i = 1, numComponents do
					table.insert(components, component("Component" .. i))
				end

				local world = World.new()

				local componentInstances = {}

				for _, componentFn in components do
					table.insert(componentInstances, componentFn())
				end

				local entityA = world:spawn(table.unpack(componentInstances))

				local viewA = world:query(table.unpack(components)):view()
				local viewB = world:query(components[1]):view()

				expect(select("#", viewA:get(entityA))).to.equal(numComponents)
				expect(select("#", viewB:get(entityA))).to.equal(1)

				local viewAEntityAData = { viewA:get(entityA) }

				for index, componentData in viewAEntityAData do
					expect(getmetatable(componentData)).to.equal(components[index])
				end

				local viewBEntityAData = { viewB:get(entityA) }

				expect(getmetatable(viewBEntityAData[1])).to.equal(components[1])
			end)

			it("should return view results in query order", function()
				local Parent = component("Parent")
				local Transform = component("Transform")
				local Root = component("Root")

				local world = World.new()

				local root = world:spawn(Transform({ pos = Vector2.new(3, 4) }), Root())
				local _otherRoot = world:spawn(Transform({ pos = Vector2.new(1, 2) }), Root())

				local child = world:spawn(
					Parent({
						entity = root,
						fromChild = Transform({ pos = Vector2.one }),
					}),
					Transform.new({ pos = Vector2.zero })
				)

				local _otherChild = world:spawn(
					Parent({
						entity = root,
						fromChild = Transform({ pos = Vector2.new(0, 0) }),
					}),
					Transform.new({ pos = Vector2.zero })
				)

				local _grandChild = world:spawn(
					Parent({
						entity = child,
						fromChild = Transform({ pos = Vector2.new(-1, 0) }),
					}),
					Transform.new({ pos = Vector2.zero })
				)

				local parents = world:query(Parent):view()
				local roots = world:query(Transform, Root):view()

				expect(parents:contains(root)).to.equal(false)

				local orderOfIteration = {}

				for id in world:query(Transform, Parent) do
					table.insert(orderOfIteration, id)
				end

				local view = world:query(Transform, Parent):view()
				local i = 0
				for id in view do
					i += 1
					expect(orderOfIteration[i]).to.equal(id)
				end

				for id, absolute, parent in world:query(Transform, Parent) do
					local relative = parent.fromChild.pos
					local ancestor = parent.entity
					local current = parents:get(ancestor)
					while current do
						relative = current.fromChild.pos * relative
						ancestor = current.entity
						current = parents:get(ancestor)
					end

					local pos = roots:get(ancestor).pos

					world:insert(id, absolute:patch({ pos = Vector2.new(pos.x + relative.x, pos.y + relative.y) }))
				end

				expect(world:get(child, Transform).pos).to.equal(Vector2.new(4, 5))
			end)
		end)
	end)
end
