local Q={ProfileTemplate1={Counter=0,Array={},Dictionary={}};ProfileTemplate2={Value=0}}local f={TEST_MOCK=false}local R=game:GetService("\083\101\114\118\101\114\083\099\114\105\112\116\083\101\114\118\105\099\101")local j do local Q=true task.spawn(function()j=require(R.ProfileService)Q=false end)if Q==true then error("\091\080\114\111\102\105\108\101\084\101\115\116\093\058 \080\114\111\102\105\108\101\083\101\114\118\105\099\101 \077\111\100\117\108\101\083\099\114\105\112\116 \115\104\111\117\108\100 \110\111\116 \121\105\101\108\100 \119\104\101\110 \114\101\113\117\105\114\101\100\033")end end local D=game:GetService("\072\116\116\112\083\101\114\118\105\099\101")local U=game:GetService("\068\097\116\097\083\116\111\114\101\083\101\114\118\105\099\101")local k="\084\101\115\116\095"..tostring(D:GenerateGUID())local O="\083\099\111\112\101\095"..tostring(D:GenerateGUID())local e="\084\101\115\116\095"..tostring(D:GenerateGUID())local N="\083\099\111\112\101\095"..tostring(D:GenerateGUID())local S=j.GetProfileStore({Name=k,Scope=O},Q.ProfileTemplate1)local L=j.GetProfileStore({Name=e;Scope=N},Q.ProfileTemplate2)local l=j._mock_data_store local function Z(Q)local f={}for Q,R in pairs(Q)do if type(R)=="\116\097\098\108\101"then f[Q]=Z(R)else f[Q]=R end end return f end local function s(Q)if type(Q)=="\115\116\114\105\110\103"then return"\""..(Q.."\"")else return tostring(Q)end end local function u(Q)local f="\123"local R=0 for Q,j in pairs(Q)do R=R+1 if type(j)=="\115\116\114\105\110\103"then f=f..((R>1 and"\044 "or"")..("\091"..(s(Q)..("\093 \061 \""..(j.."\"")))))elseif type(j)=="\110\117\109\098\101\114"then f=f..((R>1 and"\044 "or"")..("\091"..(s(Q)..("\093 \061 "..j))))elseif type(j)=="\116\097\098\108\101"then f=f..((R>1 and"\044 "or"")..("\091"..(s(Q)..("\093 \061 "..u(j)))))elseif type(j)=="\117\115\101\114\100\097\116\097"then if typeof(j)=="\073\110\115\116\097\110\099\101"then f=f..((R>1 and"\044 "or"")..("\091"..(s(Q)..("\093 \061 \073\110\115\116\097\110\099\101\058"..tostring(j)))))else f=f..((R>1 and"\044 "or"")..("\091"..(s(Q)..("\093 \061 \117\115\101\114\100\097\116\097\058"..typeof(j)))))end else f=f..((R>1 and"\044 "or"")..("\091"..(s(Q)..("\093 \061 "..tostring(j)))))end end f=f.."\125"return f end local function F(Q,f)print(Q..("\058 "..(f==true and"\080\065\083\083"or"\070\065\073\076\069\068")))end local function T(Q,f,R,j)local D=Q[f]if D==nil then D={}Q[f]=D end local U=j(D[R])if U==nil then return nil else local Q=math.floor(os.time()*1000)local f=D[R]if f==nil then f={Data=nil;CreatedTime=Q,UpdatedTime=Q,VersionId=0,UserIds={};MetaData={}}D[R]=f end f.UpdatedTime=Q f.VersionId+=1 f.Data=Z(U)return Z(f.Data)end end local function Y(Q,R,D,k)if j._use_mock_data_store==true or f.TEST_MOCK==true then T(l,Q..("\000"..(R or"")),D,k)else local f=U:GetDataStore(Q,R)f:UpdateAsync(D,k)end end local W if f.TEST_MOCK==true then l=j._user_mock_data_store W=S S=S.Mock L=L.Mock print("\091\077\079\067\075\093")end task.spawn(function()print("\082\085\078\078\073\078\071 \080\082\079\070\073\076\069 \084\069\083\084")local Q=j.IsLive()if Q==true then print("\082\117\110\110\105\110\103 \118\101\114\115\105\111\110\105\110\103 \116\101\115\116\046\046\046 \040\067\097\110 \116\097\107\101 \097 \102\101\119 \109\105\110\117\116\101\115\041")local Q=S:LoadProfileAsync("\086\101\114\115\105\111\110\105\110\103\084\101\115\116")Q.Data.Gold=10 Q:Release()local f=S:ViewProfileAsync("\086\101\114\115\105\111\110\105\110\103\084\101\115\116")for Q=1,3,1 do f.Data.Gold+=10 f:OverwriteAsync()end local R=S:ProfileVersionQuery("\086\101\114\115\105\111\110\105\110\103\084\101\115\116",Enum.SortDirection.Descending,nil,DateTime.fromUnixTimestamp(os.time()+10000))local j={}local D=0 for Q=1,6,1 do D+=1 task.spawn(function()local f=R:NextAsync()if f~=nil then if Q==6 then error("\054 \101\110\116\114\105\101\115 \102\111\117\110\100 \119\104\101\110 \116\104\101\114\101 \115\104\111\117\108\100 \098\101 \111\110\108\121 \053 \040\109\105\103\104\116 \098\101 \097\110 \111\114\100\101\114 \111\102 \113\117\101\114\121 \114\101\116\117\114\110\115 \112\114\111\098\108\101\109\041")end table.insert(j,f)end D-=1 end)end while D>0 do task.wait()end local U={0;10;20;30;40}for Q,f in ipairs(j)do local R=table.find(U,f.Data.Gold or 0)if R~=nil then table.remove(U,R)end end for Q=1,10,1 do task.wait()end if#R._query_queue>0 then error("\086\101\114\115\105\111\110 \113\117\101\114\121 \113\117\101\117\101 \108\101\097\107 \100\101\116\101\099\116\101\100")end F("\080\114\111\102\105\108\101\083\101\114\118\105\099\101 \118\101\114\115\105\111\110\105\110\103 \116\101\115\116",#U==0)else print("\086\101\114\115\105\111\110\105\110\103 \116\101\115\116 \110\111\116 \115\117\112\112\111\114\116\101\100 \105\110 \109\111\099\107 \109\111\100\101")end do print("\082\117\110\110\105\110\103 \112\097\121\108\111\097\100 \116\101\115\116\046\046\046 \040\067\097\110 \116\097\107\101 \097 \102\101\119 \109\105\110\117\116\101\115\041")local Q=S:LoadProfileAsync("\080\097\121\108\111\097\100\084\101\115\116")Q.Data={Coins=68}Q:AddUserId(2312310)Q:SetMetaTag("\086\101\114\115\105\111\110",1)Q.RobloxMetaData={Playtime=123456}S:GlobalUpdateProfileAsync("\080\097\121\108\111\097\100\084\101\115\116",function(Q)Q:AddActiveUpdate({GiftType="\083\112\101\099\105\097\108\071\105\102\116"})end)local f=true Q.KeyInfoUpdated:Connect(function()f=false end)while f==true do wait()end if#Q.GlobalUpdates:GetActiveUpdates()==0 then error("\071\108\111\098\097\108 \117\112\100\097\116\101 \119\097\115 \110\111\116 \114\101\099\101\105\118\101\100")end Q.Coins=1000000 local R=S:ViewProfileAsync("\080\097\121\108\111\097\100\084\101\115\116")R.Data.Coins+=1 R:AddUserId(50)local j=(R.GlobalUpdates:GetActiveUpdates())[1]j=j and j[2]j=j and j.GiftType if j~="\083\112\101\099\105\097\108\071\105\102\116"then error("\071\108\111\098\097\108 \117\112\100\097\116\101 \119\097\115 \110\111\116 \114\101\099\101\105\118\101\100 \105\110 \116\104\101 \118\105\101\119\101\100 \112\114\111\102\105\108\101")end R:ClearGlobalUpdates()if#R.GlobalUpdates:GetActiveUpdates()~=0 then error("\071\108\111\098\097\108 \117\112\100\097\116\101\115 \099\111\117\108\100 \110\111\116 \098\101 \099\108\101\097\114\101\100")end R:OverwriteAsync()local D=os.clock()f=true Q:ListenToHopReady(function()f=false end)while f==true do if os.clock()-D>240 then error("\083\101\115\115\105\111\110 \115\116\101\097\108 \098\121 \112\097\121\108\111\097\100 \116\105\109\101\111\117\116")end wait()end if Q:IsActive()==true then error("\070\097\117\108\116\121 \058\073\115\065\099\116\105\118\101\040\041")end Q=S:LoadProfileAsync("\080\097\121\108\111\097\100\084\101\115\116")local U=Q.KeyInfo local k=U:GetMetadata()local O=U:GetUserIds()local e=Q.Data.Coins==69 and(k.Playtime==123456 and(table.find(O,2312310)~=nil and(table.find(O,50)~=nil and#Q.GlobalUpdates:GetActiveUpdates()==0)))F("\080\114\111\102\105\108\101\083\101\114\118\105\099\101 \112\097\121\108\111\097\100 \116\101\115\116",e)Q:Release()end do local f=S:LoadProfileAsync("\080\114\111\102\105\108\101\075\101\121\073\110\102\111")f.RobloxMetaData={Color={.955,0;0};Dedication="\086\101\116\101\114\097\110"}f:AddUserId(2312310)f:AddUserId(50)f:AddUserId(420)f:RemoveUserId(50)f:Release()local R=true f:ListenToHopReady(function()R=false end)while R==true do wait()end local j=f.KeyInfo local D=j:GetMetadata()local U=j:GetUserIds()local k=type(D.Color)=="\116\097\098\108\101"and(D.Color[1]==.955 and(D.Color[2]==0 and D.Color[3]==0))local O=k==true and(D.Dedication=="\086\101\116\101\114\097\110"and(table.find(U,2312310)~=nil and(table.find(U,420)~=nil and table.find(U,50)==nil)))if Q==true and(f._is_user_mock~=true and type(j)=="\116\097\098\108\101")then error("\077\079\067\075 \075\069\089 \073\078\070\079 \076\069\065\075")end F("\080\114\111\102\105\108\101\083\101\114\118\105\099\101 \107\101\121 \105\110\102\111 \040\082\111\098\108\111\120 \077\101\116\097\100\097\116\097\041 \116\101\115\116",O)end if f.TEST_MOCK==true then local Q=pcall(function()local Q=S:LoadProfileAsync("\080\114\111\102\105\108\101\077\111\099\107","\070\111\114\099\101\076\111\097\100")wait(2)local f=W:LoadProfileAsync("\080\114\111\102\105\108\101\077\111\099\107","\070\111\114\099\101\076\111\097\100")end)F("\080\114\111\102\105\108\101\083\101\114\118\105\099\101 \109\111\099\107 \116\101\115\116",Q==true)end for Q=1,2,1 do local f=S:LoadProfileAsync("\080\114\111\102\105\108\101\049","\070\111\114\099\101\076\111\097\100")f.Data.Counter=f.Data.Counter+1 f:Release()end local R=S:ViewProfileAsync("\080\114\111\102\105\108\101\049")F("\080\114\111\102\105\108\101\083\101\114\118\105\099\101 \116\101\115\116 \035\049 - \112\097\114\116\049",R.Data.Counter==2)S:WipeProfileAsync("\080\114\111\102\105\108\101\049")R=S:ViewProfileAsync("\080\114\111\102\105\108\101\049")F("\080\114\111\102\105\108\101\083\101\114\118\105\099\101 \116\101\115\116 \035\049 - \112\097\114\116\050",R==nil)local D=S:LoadProfileAsync("\080\114\111\102\105\108\101\050","\070\111\114\099\101\076\111\097\100")D.GlobalUpdates:ListenToNewActiveUpdate(function(Q,f)if f.UpdateTag=="\072\101\108\108\111\033"then F("\080\114\111\102\105\108\101\083\101\114\118\105\099\101 \116\101\115\116 \035\050 - \112\097\114\116 \049",#D.GlobalUpdates:GetActiveUpdates()==1)D.GlobalUpdates:LockActiveUpdate(Q)F("\080\114\111\102\105\108\101\083\101\114\118\105\099\101 \116\101\115\116 \035\050 - \112\097\114\116 \050",#D.GlobalUpdates:GetActiveUpdates()==0)D:Save()end end)local U=false D.GlobalUpdates:ListenToNewLockedUpdate(function(Q,f)if f.UpdateTag=="\072\101\108\108\111\033"then F("\080\114\111\102\105\108\101\083\101\114\118\105\099\101 \116\101\115\116 \035\050 - \112\097\114\116 \051",#D.GlobalUpdates:GetActiveUpdates()==0)F("\080\114\111\102\105\108\101\083\101\114\118\105\099\101 \116\101\115\116 \035\050 - \112\097\114\116 \052",#D.GlobalUpdates:GetLockedUpdates()==1)U=true D.GlobalUpdates:ClearLockedUpdate(Q)F("\080\114\111\102\105\108\101\083\101\114\118\105\099\101 \116\101\115\116 \035\050 - \112\097\114\116 \053",#D.GlobalUpdates:GetLockedUpdates()==0)D:Save()end end)S:GlobalUpdateProfileAsync("\080\114\111\102\105\108\101\050",function(Q)Q:AddActiveUpdate({UpdateTag="\072\101\108\108\111\033"})end)D:Save()while U==false do wait()end while#D.GlobalUpdates._updates_latest[2]~=0 do wait()end F("\080\114\111\102\105\108\101\083\101\114\118\105\099\101 \116\101\115\116 \035\050 - \112\097\114\116 \054",true)S:GlobalUpdateProfileAsync("\080\114\111\102\105\108\101\051",function(Q)Q:AddActiveUpdate({UpdateTag="\084\101\115\116\049"})end)S:GlobalUpdateProfileAsync("\080\114\111\102\105\108\101\051",function(Q)Q:AddActiveUpdate({UpdateTag="\084\101\115\116\050"})Q:AddActiveUpdate({UpdateTag="\084\101\115\116\051"})Q:AddActiveUpdate({UpdateTag="\084\101\115\116\052"})for f,R in pairs(Q:GetActiveUpdates())do if R[2].UpdateTag=="\084\101\115\116\049"then Q:ClearActiveUpdate(R[1])elseif R[2].UpdateTag=="\084\101\115\116\052"then Q:ChangeActiveUpdate(R[1],{UpdateTag="\084\101\115\116\049\048\048\048\048\048\048"})end end end)local e=S:ViewProfileAsync("\080\114\111\102\105\108\101\051")local N=e.GlobalUpdates:GetActiveUpdates()local L=false pcall(function()L=N[1][2].UpdateTag=="\084\101\115\116\050"and(N[2][2].UpdateTag=="\084\101\115\116\051"and N[3][2].UpdateTag=="\084\101\115\116\049\048\048\048\048\048\048")end)F("\080\114\111\102\105\108\101\083\101\114\118\105\099\101 \116\101\115\116 \035\051 - \112\097\114\116 \049",L)e=S:LoadProfileAsync("\080\114\111\102\105\108\101\051","\070\111\114\099\101\076\111\097\100")N=e.GlobalUpdates:GetActiveUpdates()L=false pcall(function()L=N[1][2].UpdateTag=="\084\101\115\116\050"and(N[2][2].UpdateTag=="\084\101\115\116\051"and N[3][2].UpdateTag=="\084\101\115\116\049\048\048\048\048\048\048")end)F("\080\114\111\102\105\108\101\083\101\114\118\105\099\101 \116\101\115\116 \035\051 - \112\097\114\116 \050",L)local l=S:LoadProfileAsync("\080\114\111\102\105\108\101\052","\070\111\114\099\101\076\111\097\100")l:SetMetaTag("\076\111\097\100\101\100\070\105\114\115\116\084\105\109\101",true)l.MetaData.MetaTags.PurchaseIds={}table.insert(l.MetaData.MetaTags.PurchaseIds,"\097")F("\080\114\111\102\105\108\101\083\101\114\118\105\099\101 \116\101\115\116 \035\052 - \112\097\114\116 \049",l.MetaData.MetaTags.LoadedFirstTime==true)l:Save()while l.MetaData.MetaTagsLatest.LoadedFirstTime==nil do wait()end L=false pcall(function()L=l.MetaData.MetaTagsLatest.PurchaseIds[1]=="\097"end)F("\080\114\111\102\105\108\101\083\101\114\118\105\099\101 \116\101\115\116 \035\052 - \112\097\114\116 \050",l.MetaData.MetaTagsLatest.LoadedFirstTime==true and L)l:SetMetaTag("\076\111\097\100\101\100\070\105\114\115\116\084\105\109\101",false)F("\080\114\111\102\105\108\101\083\101\114\118\105\099\101 \116\101\115\116 \035\052 - \112\097\114\116 \051",l.MetaData.MetaTagsLatest.LoadedFirstTime==true and l.MetaData.MetaTags.LoadedFirstTime==false)local Z=nil local s=nil local u=pcall(function()Z=S:LoadProfileAsync("\080\114\111\102\105\108\101\053","\070\111\114\099\101\076\111\097\100")wait(2)s=S:LoadProfileAsync("\080\114\111\102\105\108\101\053","\070\111\114\099\101\076\111\097\100")end)F("\080\114\111\102\105\108\101\083\101\114\118\105\099\101 \116\101\115\116 \035\053 - \112\097\114\116 \049",u==false)if Z~=nil then Z:Release()end if s~=nil then s:Release()end Z=nil s=nil local T=pcall(function()Z=S:LoadProfileAsync("\080\114\111\102\105\108\101\053","\070\111\114\099\101\076\111\097\100")Z:Release()s=S:LoadProfileAsync("\080\114\111\102\105\108\101\053","\070\111\114\099\101\076\111\097\100")end)F("\080\114\111\102\105\108\101\083\101\114\118\105\099\101 \116\101\115\116 \035\053 - \112\097\114\116 \050",T==true)local A local h local q coroutine.resume(coroutine.create(function()A=S:LoadProfileAsync("\080\114\111\102\105\108\101\054","\070\111\114\099\101\076\111\097\100")if A==nil then A=0 end end))coroutine.resume(coroutine.create(function()h=S:LoadProfileAsync("\080\114\111\102\105\108\101\054","\070\111\114\099\101\076\111\097\100")if h==nil then h=0 end end))coroutine.resume(coroutine.create(function()q=S:LoadProfileAsync("\080\114\111\102\105\108\101\054","\070\111\114\099\101\076\111\097\100")if q==nil then q=0 end end))while A==nil or h==nil or q==nil do wait()end F("\080\114\111\102\105\108\101\083\101\114\118\105\099\101 \116\101\115\116 \035\054",A==0 and(h==0 and type(q)=="\116\097\098\108\101"))print("\084\101\115\116 \035\055 \098\101\103\105\110\046\046\046 \040\084\104\105\115 \099\097\110 \116\097\107\101 \097 \098\105\116\041")local d=false j.CorruptionSignal:Connect(function(Q,f)if Q==k and f=="\080\114\111\102\105\108\101\055"then d=true end end)Y(k,O,"\080\114\111\102\105\108\101\055",function()return{"\084\104\105\115\065\105\110\116\082\105\103\104\116"}end)local i=S:LoadProfileAsync("\080\114\111\102\105\108\101\055","\070\111\114\099\101\076\111\097\100")F("\080\114\111\102\105\108\101\083\101\114\118\105\099\101 \116\101\115\116 \035\055",d)print("\084\101\115\116 \035\056 \098\101\103\105\110\046\046\046 \040\065\083\089\078\067 - \072\065\083 \084\079 \080\065\083\083 \073\078 \065\082\079\085\078\068 \065 \077\073\078\085\084\069\041")local o=S:LoadProfileAsync("\080\114\111\102\105\108\101\056","\070\111\114\099\101\076\111\097\100")o:ListenToRelease(function()F("\080\114\111\102\105\108\101\083\101\114\118\105\099\101 \116\101\115\116 \035\056",true)end)Y(k,O,"\080\114\111\102\105\108\101\056",function()return{Data={},MetaData={ProfileCreateTime=0;SessionLoadCount=0;ActiveSession={123;123},ForceLoadSession=nil,MetaTags={}};GlobalUpdates={0;{}}}end)print("\084\101\115\116 \035\057 \098\101\103\105\110\046\046\046 \040\065\083\089\078\067 - \072\065\083 \084\079 \080\065\083\083 \073\078 \065\082\079\085\078\068 \065 \077\073\078\085\084\069\041")local x=S:LoadProfileAsync("\080\114\111\102\105\108\101\057","\070\111\114\099\101\076\111\097\100")x:ListenToRelease(function()F("\080\114\111\102\105\108\101\083\101\114\118\105\099\101 \116\101\115\116 \035\057",true)end)Y(k,O,"\080\114\111\102\105\108\101\057",function()return{Data={};MetaData={ProfileCreateTime=0,SessionLoadCount=0,ActiveSession=x.MetaData.ActiveSession,ForceLoadSession={123,123};MetaTags={}};GlobalUpdates={0,{}}}end)print("\084\101\115\116 \035\049\048 \098\101\103\105\110\046\046\046 \040\084\104\105\115 \119\105\108\108 \116\097\107\101 \111\118\101\114 \097 \109\105\110\117\116\101\041")local H=false Y(k,O,"\080\114\111\102\105\108\101\049\048",function()return{Data={},MetaData={ProfileCreateTime=0;SessionLoadCount=0,ActiveSession={123;123};ForceLoadSession=nil,MetaTags={}};GlobalUpdates={0,{}}}end)local M=S:LoadProfileAsync("\080\114\111\102\105\108\101\049\048",function(Q,f)if Q==123 and f==123 then H=true return"\070\111\114\099\101\076\111\097\100"else return"\067\097\110\099\101\108"end end)F("\080\114\111\102\105\108\101\083\101\114\118\105\099\101 \116\101\115\116 \035\049\048",M~=nil and H==true)local g=S:LoadProfileAsync("\080\114\111\102\105\108\101\049\049","\070\111\114\099\101\076\111\097\100")g.Data={Array=false}g:Reconcile()F("\080\114\111\102\105\108\101\083\101\114\118\105\099\101 \116\101\115\116 \035\049\049",g.Data.Counter==0 and(g.Data.Array==false and type(g.Data.Dictionary)=="\116\097\098\108\101"))end)
-- local Madwork = _G.Madwork
--[[
{Madwork}

-[ProfileTest]---------------------------------------
	(STANDALONE VERSION)
	A brief testing of ProfileService
	
	NOTICE: Assumes that "ProfileService" ModuleScript is inside ServerScriptService
--]]

local SETTINGS = {
	
	ProfileTemplate1 = {
		Counter = 0,
		Array = {},
		Dictionary = {},
	},
	
	ProfileTemplate2 = {
		Value = 0,
	}
	
}

----- Service Table -----

local ProfileTest = {
	TEST_MOCK = false, -- When set to true, tests ProfileStore.Mock functionality
}

----- Loaded Services & Modules -----

local ServerScriptService = game:GetService("ServerScriptService")

local ProfileService
do
	local did_yield = true
	task.spawn(function()
		ProfileService = require(ServerScriptService.ProfileService)
		did_yield = false
	end)
	if did_yield == true then
		error("[ProfileTest]: ProfileService ModuleScript should not yield when required!")
	end
end

local HttpService = game:GetService("HttpService")
local DataStoreService = game:GetService("DataStoreService")

----- Private Variables -----

local RandomProfileStoreKey1 = "Test_" .. tostring(HttpService:GenerateGUID())
local RandomProfileStoreScope1 = "Scope_" .. tostring(HttpService:GenerateGUID())

local RandomProfileStoreKey2 = "Test_" .. tostring(HttpService:GenerateGUID())
local RandomProfileStoreScope2 = "Scope_" .. tostring(HttpService:GenerateGUID())

local GameProfileStore1 = ProfileService.GetProfileStore(
	{
		Name = RandomProfileStoreKey1,
		Scope = RandomProfileStoreScope1,
	},
	SETTINGS.ProfileTemplate1
)

local GameProfileStore2 = ProfileService.GetProfileStore(
	{
		Name = RandomProfileStoreKey2,
		Scope = RandomProfileStoreScope2,
	},
	SETTINGS.ProfileTemplate2
)

local MockDataStore = ProfileService._mock_data_store -- For studio testing

----- Utils -----

local function DeepCopyTable(t)
	local copy = {}
	for key, value in pairs(t) do
		if type(value) == "table" then
			copy[key] = DeepCopyTable(value)
		else
			copy[key] = value
		end
	end
	return copy
end

local function KeyToString(key)
	if type(key) == "string" then
		return "\"" .. key .. "\""
	else
		return tostring(key)
	end
end

local function TableToString(t)
	local output = "{"
	local entries = 0
	for key, value in pairs(t) do
		entries = entries + 1
		if type(value) == "string" then
			output = output .. (entries > 1 and ", " or "") .. "[" .. KeyToString(key) .. "] = \"" .. value .. "\""
		elseif type(value) == "number" then
			output = output .. (entries > 1 and ", " or "") .. "[" .. KeyToString(key) .. "] = " .. value
		elseif type(value) == "table" then
			output = output .. (entries > 1 and ", " or "") .. "[" .. KeyToString(key) .. "] = " .. TableToString(value)
		elseif type(value) == "userdata" then
			if typeof(value) == "Instance" then
				output = output .. (entries > 1 and ", " or "") .. "[" .. KeyToString(key) .. "] = Instance:" .. tostring(value)
			else
				output = output .. (entries > 1 and ", " or "") .. "[" .. KeyToString(key) .. "] = userdata:" .. typeof(value)
			end
		else
			output = output .. (entries > 1 and ", " or "") .. "[" .. KeyToString(key) .. "] = " .. tostring(value)
		end
	end
	output = output .. "}"
	return output
end

local function TestPass(test_txt, mode)
	print(test_txt .. ": " .. ((mode == true) and "PASS" or "FAILED"))
end

----- Private functions -----

local function MockUpdateAsync(mock_data_store, store_lookup, key, transform_function)
	local profile_store = mock_data_store[store_lookup]
	if profile_store == nil then
		profile_store = {}
		mock_data_store[store_lookup] = profile_store
	end
	local transform = transform_function(profile_store[key])
	if transform == nil then
		return nil
	else
		local epoch_time = math.floor(os.time() * 1000)
		local mock_entry = profile_store[key]
		if mock_entry == nil then
			mock_entry = {
				Data = nil,
				CreatedTime = epoch_time,
				UpdatedTime = epoch_time,
				VersionId = 0,
				UserIds = {},
				MetaData = {},
			}
			profile_store[key] = mock_entry
		end
		mock_entry.UpdatedTime = epoch_time
		mock_entry.VersionId += 1
		mock_entry.Data = DeepCopyTable(transform)
		return DeepCopyTable(mock_entry.Data)
	end
end

local function TestUpdateAsync(store_name, store_scope, key, transform_function)
	if ProfileService._use_mock_data_store == true or ProfileTest.TEST_MOCK == true then
		MockUpdateAsync(
			MockDataStore,
			store_name .. "\0" .. (store_scope or ""),
			key,
			transform_function
		)
	else
		local data_store = DataStoreService:GetDataStore(store_name, store_scope)
		data_store:UpdateAsync(key, transform_function)
	end
end

----- Initialize -----

local GameProfileStore1_live
if ProfileTest.TEST_MOCK == true then
	MockDataStore = ProfileService._user_mock_data_store
	GameProfileStore1_live = GameProfileStore1
	GameProfileStore1 = GameProfileStore1.Mock
	GameProfileStore2 = GameProfileStore2.Mock
	print("[MOCK]")
end

task.spawn(function()
	
	print("RUNNING PROFILE TEST")
	
	-- NOTICE: Will finish much faster in studio than in Roblox servers - DataStore limits the frequency
	-- of UpdateAsync calls and will throw nasty warnings, but it should all work the same... Just much
	-- slower.
	
	--[[
	What to test:
		1) (Load - Release)(x5) - view
		2) Load - Global Update - Listen to new active update - lock - save - clear - release - load - release
		3) Global update (add, add, clear) on empty - view - load - release
		4) Load - set meta tag - wait for auto-save - check meta tag latest - release
		5) Test errors
		6) Test load yoinks
	--]]

	local live_mode = ProfileService.IsLive()

	-- Versioning test:

	if live_mode == true then

		print("Running versioning test... (Can take a few minutes)")

		-- Creating several versions for the same key:

		local profile = GameProfileStore1:LoadProfileAsync("VersioningTest")
		profile.Data.Gold = 10
		profile:Release()

		local payload = GameProfileStore1:ViewProfileAsync("VersioningTest")
		for i = 1, 3 do
			payload.Data.Gold += 10
			payload:OverwriteAsync()
		end

		-- There should be 5 versions for this key in total:

		local query = GameProfileStore1:ProfileVersionQuery(
			"VersioningTest",
			Enum.SortDirection.Descending,
			nil,
			DateTime.fromUnixTimestamp(os.time() + 10000) -- Future time
		  )
		local query_results = {}
		local pending_tasks = 0

		for i = 1, 6 do
			pending_tasks += 1
			task.spawn(function()
			  local result = query:NextAsync()
			  if result ~= nil then
				if i == 6 then
					error("6 entries found when there should be only 5 (might be an order of query returns problem)")
				end
				table.insert(query_results, result)
			  end
			  pending_tasks -= 1
			end)
		end

		while pending_tasks > 0 do
			task.wait()
		end

		local expecting_values = {0, 10, 20, 30, 40}

		for _, entry in ipairs(query_results) do
			local index = table.find(expecting_values, entry.Data.Gold or 0)
			if index ~= nil then
				table.remove(expecting_values, index)
			end
		end

		for i = 1, 10 do
			task.wait() -- Wait a few frames for the query queue to resolve
		end

		if #query._query_queue > 0 then
			error("Version query queue leak detected")
		end

		TestPass("ProfileService versioning test", #expecting_values == 0)

	else
		print("Versioning test not supported in mock mode")
	end

	-- Test profile payloads:

	do

		print("Running payload test... (Can take a few minutes)")

		-- Create profile with some data and an active global update:
		
		local profile = GameProfileStore1:LoadProfileAsync("PayloadTest")
		profile.Data = {
			Coins = 68,
		}
		profile:AddUserId(2312310)
		profile:SetMetaTag("Version", 1)
		profile.RobloxMetaData = {Playtime = 123456}
		
		GameProfileStore1:GlobalUpdateProfileAsync("PayloadTest",
			function(global_updates)
				global_updates:AddActiveUpdate({GiftType = "SpecialGift"})
			end
		)

		-- We need the profile to be active, and ensure all above data is saved in the DataStore:

		local wait_for_profile = true
		profile.KeyInfoUpdated:Connect(function()
			wait_for_profile = false
		end)
		while wait_for_profile == true do wait() end

		if #profile.GlobalUpdates:GetActiveUpdates() == 0 then
			error("Global update was not received")
		end

		-- Create a profile payload:

		profile.Coins = 1000000 -- The player JUST received tons of cash!!! Payloads can lose up to
			-- several minutes of in-game progress when overwriting. You should use :LoadProfileAsync()
			-- If you want to protect in-game progress.

		local payload = GameProfileStore1:ViewProfileAsync("PayloadTest")
		payload.Data.Coins += 1
		payload:AddUserId(50)

		local active_update_check = payload.GlobalUpdates:GetActiveUpdates()[1]
		active_update_check = active_update_check and active_update_check[2]
		active_update_check = active_update_check and active_update_check.GiftType

		if active_update_check ~= "SpecialGift" then
			error("Global update was not received in the viewed profile")
		end

		payload:ClearGlobalUpdates()

		if #payload.GlobalUpdates:GetActiveUpdates() ~= 0 then
			error("Global updates could not be cleared")
		end

		payload:OverwriteAsync()

		-- The profile should lose its session lock:

		local start_time = os.clock()
		wait_for_profile = true
		profile:ListenToHopReady(function()
			wait_for_profile = false
		end)
		while wait_for_profile == true do
			if os.clock() - start_time > 240 then
				error("Session steal by payload timeout")
			end
			wait()
		end

		if profile:IsActive() == true then
			error("Faulty :IsActive()")
		end

		-- Load and check new data:

		profile = GameProfileStore1:LoadProfileAsync("PayloadTest")

		local key_info = profile.KeyInfo
		local metadata = key_info:GetMetadata()
		local user_ids = key_info:GetUserIds()

		local is_passed = profile.Data.Coins == 69
			and metadata.Playtime == 123456 and table.find(user_ids, 2312310) ~= nil
			and table.find(user_ids, 50) ~= nil and #profile.GlobalUpdates:GetActiveUpdates() == 0

		TestPass("ProfileService payload test", is_passed)

		profile:Release()

	end

	-- Test KeyInfo:

	do

		local profile = GameProfileStore1:LoadProfileAsync("ProfileKeyInfo")
		profile.RobloxMetaData = {Color = {0.955, 0, 0}, Dedication = "Veteran"}
		profile:AddUserId(2312310)
		profile:AddUserId(50)
		profile:AddUserId(420)
		profile:RemoveUserId(50)

		profile:Release()

		local wait_for_profile = true
		profile:ListenToHopReady(function()
			wait_for_profile = false
		end)

		while wait_for_profile == true do wait() end

		local key_info = profile.KeyInfo
		local metadata = key_info:GetMetadata()
		local user_ids = key_info:GetUserIds()
		
		local is_color_good = type(metadata.Color) == "table"
			and metadata.Color[1] == 0.955
			and metadata.Color[2] == 0
			and metadata.Color[3] == 0

		local is_passed = is_color_good == true
			and metadata.Dedication == "Veteran" and table.find(user_ids, 2312310) ~= nil
			and table.find(user_ids, 420) ~= nil and table.find(user_ids, 50) == nil

		if live_mode == true and profile._is_user_mock ~= true and type(key_info) == "table" then
			error("MOCK KEY INFO LEAK")
		end

		TestPass("ProfileService key info (Roblox Metadata) test", is_passed)

	end
	
	-- Test MOCK: --
	if ProfileTest.TEST_MOCK == true then
		local success_mock = pcall(function()
			local profile_live = GameProfileStore1:LoadProfileAsync("ProfileMock", "ForceLoad")
			wait(2)
			local profile_mock = GameProfileStore1_live:LoadProfileAsync("ProfileMock", "ForceLoad")
		end)
		TestPass("ProfileService mock test", success_mock == true)
	end
	
	-- Test #1: --
	for i = 1, 2 do
		local profile1 = GameProfileStore1:LoadProfileAsync("Profile1", "ForceLoad")
		profile1.Data.Counter = profile1.Data.Counter + 1
		profile1:Release()
	end
	local profile1 = GameProfileStore1:ViewProfileAsync("Profile1")
	TestPass("ProfileService test #1 - part1", profile1.Data.Counter == 2)
	GameProfileStore1:WipeProfileAsync("Profile1")
	profile1 = GameProfileStore1:ViewProfileAsync("Profile1")
	TestPass("ProfileService test #1 - part2", profile1 == nil)
	
	-- Test #2: --
	local profile2 = GameProfileStore1:LoadProfileAsync("Profile2", "ForceLoad")
	
	profile2.GlobalUpdates:ListenToNewActiveUpdate(function(update_id, update_data)
		if update_data.UpdateTag == "Hello!" then
			TestPass("ProfileService test #2 - part 1", #(profile2.GlobalUpdates:GetActiveUpdates()) == 1)
			profile2.GlobalUpdates:LockActiveUpdate(update_id)
			TestPass("ProfileService test #2 - part 2", #(profile2.GlobalUpdates:GetActiveUpdates()) == 0)
			profile2:Save()
		end
	end)
	
	local test2_checkpoint2 = false
	
	profile2.GlobalUpdates:ListenToNewLockedUpdate(function(update_id, update_data)
		if update_data.UpdateTag == "Hello!" then
			TestPass("ProfileService test #2 - part 3", #(profile2.GlobalUpdates:GetActiveUpdates()) == 0)
			TestPass("ProfileService test #2 - part 4", #(profile2.GlobalUpdates:GetLockedUpdates()) == 1)
			test2_checkpoint2 = true
			profile2.GlobalUpdates:ClearLockedUpdate(update_id)
			TestPass("ProfileService test #2 - part 5", #(profile2.GlobalUpdates:GetLockedUpdates()) == 0)
			profile2:Save()
		end
	end)
	
	GameProfileStore1:GlobalUpdateProfileAsync("Profile2", function(global_updates)
		global_updates:AddActiveUpdate({UpdateTag = "Hello!"})
	end)
	profile2:Save() -- Fetch global update
	
	--[[
	while true do
		print(TableToString(profile2.GlobalUpdates._updates_latest))
		wait(1)
	end
	--]]
	
	while test2_checkpoint2 == false do wait() end
	while #profile2.GlobalUpdates._updates_latest[2] ~= 0 do wait() end
	TestPass("ProfileService test #2 - part 6", true)
	
	-- Test #3: --
	GameProfileStore1:GlobalUpdateProfileAsync("Profile3", function(global_updates)
		global_updates:AddActiveUpdate({UpdateTag = "Test1"})
	end)
	GameProfileStore1:GlobalUpdateProfileAsync("Profile3", function(global_updates)
		global_updates:AddActiveUpdate({UpdateTag = "Test2"})
		global_updates:AddActiveUpdate({UpdateTag = "Test3"})
		global_updates:AddActiveUpdate({UpdateTag = "Test4"})
		for _, update_data in pairs(global_updates:GetActiveUpdates()) do
			if update_data[2].UpdateTag == "Test1" then
				global_updates:ClearActiveUpdate(update_data[1])
			elseif update_data[2].UpdateTag == "Test4" then
				global_updates:ChangeActiveUpdate(update_data[1], {UpdateTag = "Test1000000"})
			end
		end
	end)
	local profile3 = GameProfileStore1:ViewProfileAsync("Profile3")
	local profile_3_updates = profile3.GlobalUpdates:GetActiveUpdates()
	
	local updates_good = false
	pcall(function()
		updates_good = (profile_3_updates[1][2].UpdateTag == "Test2") and
			(profile_3_updates[2][2].UpdateTag == "Test3") and
			(profile_3_updates[3][2].UpdateTag == "Test1000000")
	end)
	TestPass("ProfileService test #3 - part 1", updates_good)
	
	profile3 = GameProfileStore1:LoadProfileAsync("Profile3", "ForceLoad")
	profile_3_updates = profile3.GlobalUpdates:GetActiveUpdates()
	
	updates_good = false
	pcall(function()
		updates_good = (profile_3_updates[1][2].UpdateTag == "Test2") and
			(profile_3_updates[2][2].UpdateTag == "Test3") and
			(profile_3_updates[3][2].UpdateTag == "Test1000000")
	end)
	TestPass("ProfileService test #3 - part 2", updates_good)
	
	-- Test #4: --
	local profile4 = GameProfileStore1:LoadProfileAsync("Profile4", "ForceLoad")
	profile4:SetMetaTag("LoadedFirstTime", true)
	profile4.MetaData.MetaTags["PurchaseIds"] = {}
	table.insert(profile4.MetaData.MetaTags["PurchaseIds"], "a")
	
	TestPass("ProfileService test #4 - part 1", profile4.MetaData.MetaTags.LoadedFirstTime == true)
	profile4:Save()
	
	while profile4.MetaData.MetaTagsLatest.LoadedFirstTime == nil do wait() end
	updates_good = false
	pcall(function()
		updates_good = profile4.MetaData.MetaTagsLatest.PurchaseIds[1] == "a"
	end)
	TestPass("ProfileService test #4 - part 2", profile4.MetaData.MetaTagsLatest.LoadedFirstTime == true and updates_good)
	profile4:SetMetaTag("LoadedFirstTime", false)
	TestPass("ProfileService test #4 - part 3", profile4.MetaData.MetaTagsLatest.LoadedFirstTime == true and profile4.MetaData.MetaTags.LoadedFirstTime == false)
	
	-- Test #5: --
	local load1 = nil
	local load2 = nil
	
	local success5_1 = pcall(function()
		load1 = GameProfileStore1:LoadProfileAsync("Profile5", "ForceLoad")
		wait(2)
		load2 = GameProfileStore1:LoadProfileAsync("Profile5", "ForceLoad")
	end)
	TestPass("ProfileService test #5 - part 1", success5_1 == false)
	
	if load1 ~= nil then load1:Release() end if load2 ~= nil then load2:Release() end
	load1 = nil
	load2 = nil
	local success5_2 = pcall(function()
		load1 = GameProfileStore1:LoadProfileAsync("Profile5", "ForceLoad")
		load1:Release()
		load2 = GameProfileStore1:LoadProfileAsync("Profile5", "ForceLoad")
	end)
	TestPass("ProfileService test #5 - part 2", success5_2 == true)
	
	-- Test #6: --
	local profile6_1
	local profile6_2
	local profile6_3
	
	coroutine.resume(coroutine.create(function()
		profile6_1 = GameProfileStore1:LoadProfileAsync("Profile6", "ForceLoad")
		if profile6_1 == nil then
			profile6_1 = 0
		end
	end))
	coroutine.resume(coroutine.create(function()
		profile6_2 = GameProfileStore1:LoadProfileAsync("Profile6", "ForceLoad")
		if profile6_2 == nil then
			profile6_2 = 0
		end
	end))
	coroutine.resume(coroutine.create(function()
		profile6_3 = GameProfileStore1:LoadProfileAsync("Profile6", "ForceLoad")
		if profile6_3 == nil then
			profile6_3 = 0
		end
	end))
	
	while profile6_1 == nil or profile6_2 == nil or profile6_3 == nil do
		wait()
	end
	
	TestPass("ProfileService test #6", profile6_1 == 0 and profile6_2 == 0 and type(profile6_3) == "table")
	
	-- Test #7: --
	print("Test #7 begin... (This can take a bit)")
	local corruption_signal_received = false
	ProfileService.CorruptionSignal:Connect(function(profile_store_name, profile_key)
		if profile_store_name == RandomProfileStoreKey1 and profile_key == "Profile7" then
			corruption_signal_received = true
		end
	end)
	TestUpdateAsync(RandomProfileStoreKey1, RandomProfileStoreScope1, "Profile7", function() -- Injecting a faulty profile table
		return {"ThisAintRight"}
	end)
	local profile7 = GameProfileStore1:LoadProfileAsync("Profile7", "ForceLoad")
	TestPass("ProfileService test #7", corruption_signal_received)
	
	-- Test #8: --
	print("Test #8 begin... (ASYNC - HAS TO PASS IN AROUND A MINUTE)")
	local profile8 = GameProfileStore1:LoadProfileAsync("Profile8", "ForceLoad")
	profile8:ListenToRelease(function()
		TestPass("ProfileService test #8", true)
	end)
	TestUpdateAsync(RandomProfileStoreKey1, RandomProfileStoreScope1, "Profile8", function() -- Injecting profile table of an unreleased session
		return {
			Data = {},
			MetaData = {
				ProfileCreateTime = 0,
				SessionLoadCount = 0,
				ActiveSession = {123, 123},
				ForceLoadSession = nil,
				MetaTags = {},
			},
			GlobalUpdates = {0, {}},
		}
	end)
	
	-- Test #9: --
	print("Test #9 begin... (ASYNC - HAS TO PASS IN AROUND A MINUTE)")
	local profile9 = GameProfileStore1:LoadProfileAsync("Profile9", "ForceLoad")
	profile9:ListenToRelease(function()
		TestPass("ProfileService test #9", true)
	end)
	TestUpdateAsync(RandomProfileStoreKey1, RandomProfileStoreScope1, "Profile9", function() -- Injecting profile table with force load request
		return {
			Data = {},
			MetaData = {
				ProfileCreateTime = 0,
				SessionLoadCount = 0,
				ActiveSession = profile9.MetaData.ActiveSession,
				ForceLoadSession = {123, 123},
				MetaTags = {},
			},
			GlobalUpdates = {0, {}},
		}
	end)
	
	-- Test #10: --
	print("Test #10 begin... (This will take over a minute)")
	local not_released_handler_test = false
	TestUpdateAsync(RandomProfileStoreKey1, RandomProfileStoreScope1, "Profile10", function() -- Injecting profile table of an unreleased session
		return {
			Data = {},
			MetaData = {
				ProfileCreateTime = 0,
				SessionLoadCount = 0,
				ActiveSession = {123, 123},
				ForceLoadSession = nil,
				MetaTags = {},
			},
			GlobalUpdates = {0, {}},
		}
	end)
	local profile10 = GameProfileStore1:LoadProfileAsync("Profile10", function(place_id, game_job_id)
		if place_id == 123 and game_job_id == 123 then
			not_released_handler_test = true
			return "ForceLoad"
		else
			return "Cancel"
		end
	end)
	TestPass("ProfileService test #10", profile10 ~= nil and not_released_handler_test == true)
	
	-- Test #11: --
	local profile11 = GameProfileStore1:LoadProfileAsync("Profile11", "ForceLoad")
	profile11.Data = {Array = false}
	profile11:Reconcile()
	TestPass("ProfileService test #11", profile11.Data.Counter == 0 and profile11.Data.Array == false and type(profile11.Data.Dictionary) == "table")
	
end)
