local F=game:GetService("\082\101\112\108\105\099\097\116\101\100\083\116\111\114\097\103\101")local K=require(F.DevPackages.JestGlobals)local d=require(F.Packages.Lyra.MockDataStoreService)local J=require(F.Packages.Lyra.MockMemoryStoreService)local N=require(script.Parent.Parent.Universe)local P=require(F.Packages.Lyra.PlayerStore)local M=require(F.Packages.Lyra.Tables)local j=K.expect local W=K.describe local w=K.it local B=K.beforeEach local G=K.afterEach local Z=K.jest local x={Template={coins=0,inventory={}}}W("\080\108\097\121\101\114\083\116\111\114\101 \098\097\115\105\099 \102\117\110\099\116\105\111\110\097\108\105\116\121",function()B(function()Z.useFakeTimers()end)G(function()Z.clearAllTimers()Z.useRealTimers()end)w("\104\097\110\100\108\101\115 \112\108\097\121\101\114 \106\111\105\110 \097\110\100 \108\101\097\118\101 \099\111\114\114\101\099\116\108\121",function()local F=d.new()local K=J.new()local W local w=function(d)local J=d.Players local N=d.game local j=P.createPlayerStore({name="\080\108\097\121\101\114\068\097\116\097";template=M.copyDeep(x.Template);dataStoreService=F,memoryStoreService=K,schema=function(F)return typeof(F.coins)=="\110\117\109\098\101\114"and typeof(F.inventory)=="\116\097\098\108\101","\073\110\118\097\108\105\100 \100\097\116\097 \102\111\114\109\097\116"end})J.PlayerAdded:Connect(function(F)j:loadAsync(F)W:_traceEvent("\080\108\097\121\101\114\068\097\116\097\076\111\097\100\101\100",{UserId=F.UserId})j:updateAsync(F,function(F)F.coins+=100 return true end)W:_traceEvent("\067\111\105\110\115\065\100\100\101\100",{UserId=F.UserId})end)J.PlayerRemoving:Connect(function(F)j:unloadAsync(F)W:_traceEvent("\080\108\097\121\101\114\068\097\116\097\085\110\108\111\097\100\101\100",{UserId=F.UserId})end)N:BindToClose(function()j:closeAsync()end)end W=N.new({seed=1;places={[1]={Script=w;MaxPlayers=10}}})local B=W:StartServer(1)W:JoinPlayerToServer(123,B)Z.advanceTimersByTime(1000)local G=W:GetTrace()local S=false local f=false for F,K in G do if K.type=="\080\108\097\121\101\114\068\097\116\097\076\111\097\100\101\100"and K.details.UserId==123 then S=true end if K.type=="\067\111\105\110\115\065\100\100\101\100"and K.details.UserId==123 then f=true end end;(j(S)).toBe(true);(j(f)).toBe(true)W:RemovePlayerFromServer(123,B)Z.advanceTimersByTime(1000)local O=false G=W:GetTrace()for F,K in G do if K.type=="\080\108\097\121\101\114\068\097\116\097\085\110\108\111\097\100\101\100"and K.details.UserId==123 then O=true end end;(j(O)).toBe(true)W:StopServer(B)Z.advanceTimersByTime(1000)local Q=false G=W:GetTrace()for F,K in G do if K.type=="\083\101\114\118\101\114\083\116\111\112\112\101\100"and K.details.JobId==B then Q=true end end;(j(Q)).toBe(true)end)w("\104\097\110\100\108\101\115 \115\101\114\118\101\114 \099\114\097\115\104 \097\110\100 \100\097\116\097 \114\101\099\111\118\101\114\121",function()local F=d.new()local K=J.new()local W local w local B=function(d)local J=d.Players local N=d.game local j=P.createPlayerStore({name="\080\108\097\121\101\114\068\097\116\097";template=M.copyDeep(x.Template),dataStoreService=F;memoryStoreService=K;schema=function(F)return typeof(F.coins)=="\110\117\109\098\101\114"and typeof(F.inventory)=="\116\097\098\108\101","\073\110\118\097\108\105\100 \100\097\116\097 \102\111\114\109\097\116"end})J.PlayerAdded:Connect(function(F)j:loadAsync(F)W:_traceEvent("\080\108\097\121\101\114\068\097\116\097\076\111\097\100\101\100",{UserId=F.UserId})j:update(F,function(F)F.coins+=100 return true end)j:saveAsync(F)local K=j:getAsync(F)w=M.copyDeep(K)W:_traceEvent("\068\097\116\097\083\097\118\101\100",{UserId=F.UserId;data=K})end)J.PlayerRemoving:Connect(function(F)j:unloadAsync(F)end)N:BindToClose(function()j:closeAsync()end)end W=N.new({seed=1;places={[1]={Script=B;MaxPlayers=10}}})local G=W:StartServer(1)W:JoinPlayerToServer(123,G)Z.advanceTimersByTime(1000)W:CrashServer(G)Z.advanceTimersByTime(1000)local S=W:StartServer(1)W:JoinPlayerToServer(123,S)Z.advanceTimersByTime(1000)local f=W:GetTrace()local O=false for F,K in f do if K.type=="\068\097\116\097\083\097\118\101\100"and K.details.UserId==123 then(j(M.equalsDeep(K.details.data,w))).toBe(true)O=true end end;(j(O)).toBe(true)end)w("\112\108\097\121\101\114 \105\100\108\105\110\103 \105\110 \115\101\114\118\101\114",function()local F=d.new()local K=J.new()J.setErrorRate(K,"\085\112\100\097\116\101\065\115\121\110\099",.1)J.setRandomLatency(K,.1,.5)local W local w=3600 local B=function(d)local J=d.Players local N=d.game local j=P.createPlayerStore({name="\080\108\097\121\101\114\068\097\116\097";template=M.copyDeep(x.Template);dataStoreService=F;memoryStoreService=K,schema=function(F)return typeof(F.coins)=="\110\117\109\098\101\114"and typeof(F.inventory)=="\116\097\098\108\101","\073\110\118\097\108\105\100 \100\097\116\097 \102\111\114\109\097\116"end})J.PlayerAdded:Connect(function(F)j:loadAsync(F)W:_traceEvent("\080\108\097\121\101\114\068\097\116\097\076\111\097\100\101\100",{UserId=F.UserId})j:updateAsync(F,function(F)F.coins+=100 return true end)W:_traceEvent("\067\111\105\110\115\065\100\100\101\100",{UserId=F.UserId})end)J.PlayerRemoving:Connect(function(F)j:unloadAsync(F)W:_traceEvent("\080\108\097\121\101\114\068\097\116\097\085\110\108\111\097\100\101\100",{UserId=F.UserId})end)N:BindToClose(function()j:closeAsync()end)end W=N.new({seed=1;places={[1]={Script=B;MaxPlayers=10}}})local G=W:StartServer(1)W:JoinPlayerToServer(123,G)Z.advanceTimersByTime(5000)local S=W:GetTrace()local f=false local O=false for F,K in S do if K.type=="\080\108\097\121\101\114\068\097\116\097\076\111\097\100\101\100"and K.details.UserId==123 then f=true end if K.type=="\067\111\105\110\115\065\100\100\101\100"and K.details.UserId==123 then O=true end end;(j(f)).toBe(true);(j(O)).toBe(true)Z.advanceTimersByTime(w*1000)W:RemovePlayerFromServer(123,G)Z.advanceTimersByTime(1000)local Q=false S=W:GetTrace()for F,K in S do if K.type=="\080\108\097\121\101\114\068\097\116\097\085\110\108\111\097\100\101\100"and K.details.UserId==123 then Q=true end end;(j(Q)).toBe(true)W:StopServer(G)Z.advanceTimersByTime(1000)local n=false S=W:GetTrace()for F,K in S do if K.type=="\083\101\114\118\101\114\083\116\111\112\112\101\100"and K.details.JobId==G then n=true end end;(j(n)).toBe(true)end)end)
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local JestGlobals = require(ReplicatedStorage.DevPackages.JestGlobals)
local MockDataStoreService = require(ReplicatedStorage.Packages.Lyra.MockDataStoreService)
local MockMemoryStoreService = require(ReplicatedStorage.Packages.Lyra.MockMemoryStoreService)
local Universe = require(script.Parent.Parent.Universe)
local PlayerStore = require(ReplicatedStorage.Packages.Lyra.PlayerStore)
local Tables = require(ReplicatedStorage.Packages.Lyra.Tables)

local expect = JestGlobals.expect
local describe = JestGlobals.describe
local it = JestGlobals.it
local beforeEach = JestGlobals.beforeEach
local afterEach = JestGlobals.afterEach
local jest = JestGlobals.jest

local TestConfig = {
	Template = {
		coins = 0,
		inventory = {},
	},
}

describe("PlayerStore basic functionality", function()
	beforeEach(function()
		jest.useFakeTimers()
	end)

	afterEach(function()
		jest.clearAllTimers()
		jest.useRealTimers()
	end)

	it("handles player join and leave correctly", function()
		local mockData = MockDataStoreService.new()
		local mockMemory = MockMemoryStoreService.new()
		local universe

		local serverScript = function(env)
			local Players = env.Players
			local game = env.game

			local store = PlayerStore.createPlayerStore({
				name = "PlayerData",
				template = Tables.copyDeep(TestConfig.Template),
				dataStoreService = mockData,
				memoryStoreService = mockMemory,
				schema = function(data)
					return typeof(data.coins) == "number" and typeof(data.inventory) == "table", "Invalid data format"
				end,
			})

			Players.PlayerAdded:Connect(function(player)
				store:loadAsync(player)
				universe:_traceEvent("PlayerDataLoaded", { UserId = player.UserId })

				store:updateAsync(player, function(data)
					data.coins += 100
					return true
				end)

				universe:_traceEvent("CoinsAdded", { UserId = player.UserId })
			end)

			Players.PlayerRemoving:Connect(function(player)
				store:unloadAsync(player)
				universe:_traceEvent("PlayerDataUnloaded", { UserId = player.UserId })
			end)

			game:BindToClose(function()
				store:closeAsync()
			end)
		end

		universe = Universe.new({
			seed = 1,
			places = {
				[1] = { Script = serverScript, MaxPlayers = 10 },
			},
		})

		local jobId = universe:StartServer(1)
		universe:JoinPlayerToServer(123, jobId)
		jest.advanceTimersByTime(1000)

		local trace = universe:GetTrace()
		local foundLoad = false
		local foundCoins = false
		for _, event in trace do
			if event.type == "PlayerDataLoaded" and event.details.UserId == 123 then
				foundLoad = true
			end
			if event.type == "CoinsAdded" and event.details.UserId == 123 then
				foundCoins = true
			end
		end
		expect(foundLoad).toBe(true)
		expect(foundCoins).toBe(true)

		universe:RemovePlayerFromServer(123, jobId)
		jest.advanceTimersByTime(1000)

		local foundUnload = false
		trace = universe:GetTrace()
		for _, event in trace do
			if event.type == "PlayerDataUnloaded" and event.details.UserId == 123 then
				foundUnload = true
			end
		end
		expect(foundUnload).toBe(true)

		universe:StopServer(jobId)
		jest.advanceTimersByTime(1000)

		local serverStopped = false
		trace = universe:GetTrace()
		for _, event in trace do
			if event.type == "ServerStopped" and event.details.JobId == jobId then
				serverStopped = true
			end
		end
		expect(serverStopped).toBe(true)
	end)

	it("handles server crash and data recovery", function()
		local mockData = MockDataStoreService.new()
		local mockMemory = MockMemoryStoreService.new()
		local universe
		local savedData

		local serverScript = function(env)
			local Players = env.Players
			local game = env.game

			local store = PlayerStore.createPlayerStore({
				name = "PlayerData",
				template = Tables.copyDeep(TestConfig.Template),
				dataStoreService = mockData,
				memoryStoreService = mockMemory,
				schema = function(data)
					return typeof(data.coins) == "number" and typeof(data.inventory) == "table", "Invalid data format"
				end,
			})

			Players.PlayerAdded:Connect(function(player)
				store:loadAsync(player)
				universe:_traceEvent("PlayerDataLoaded", { UserId = player.UserId })

				store:update(player, function(data)
					data.coins += 100
					return true
				end)

				store:saveAsync(player)
				local data = store:getAsync(player)
				savedData = Tables.copyDeep(data)
				universe:_traceEvent("DataSaved", { UserId = player.UserId, data = data })
			end)

			Players.PlayerRemoving:Connect(function(player)
				store:unloadAsync(player)
			end)

			game:BindToClose(function()
				store:closeAsync()
			end)
		end

		universe = Universe.new({
			seed = 1,
			places = {
				[1] = { Script = serverScript, MaxPlayers = 10 },
			},
		})

		local jobId = universe:StartServer(1)
		universe:JoinPlayerToServer(123, jobId)
		jest.advanceTimersByTime(1000)

		universe:CrashServer(jobId)
		jest.advanceTimersByTime(1000)

		local newJobId = universe:StartServer(1)
		universe:JoinPlayerToServer(123, newJobId)
		jest.advanceTimersByTime(1000)

		local trace = universe:GetTrace()
		local foundRecover = false
		for _, event in trace do
			if event.type == "DataSaved" and event.details.UserId == 123 then
				expect(Tables.equalsDeep(event.details.data, savedData)).toBe(true)
				foundRecover = true
			end
		end
		expect(foundRecover).toBe(true)
	end)

	it("player idling in server", function()
		local mockData = MockDataStoreService.new()
		local mockMemory = MockMemoryStoreService.new()
		MockMemoryStoreService.setErrorRate(mockMemory, "UpdateAsync", 0.1)
		MockMemoryStoreService.setRandomLatency(mockMemory, 0.1, 0.5)
		local universe
		local idleDurationSeconds = 60 * 60

		local serverScript = function(env)
			local Players = env.Players
			local game = env.game

			local store = PlayerStore.createPlayerStore({
				name = "PlayerData",
				template = Tables.copyDeep(TestConfig.Template),
				dataStoreService = mockData,
				memoryStoreService = mockMemory,
				schema = function(data)
					return typeof(data.coins) == "number" and typeof(data.inventory) == "table", "Invalid data format"
				end,
			})

			Players.PlayerAdded:Connect(function(player)
				store:loadAsync(player)
				universe:_traceEvent("PlayerDataLoaded", { UserId = player.UserId })

				store:updateAsync(player, function(data)
					data.coins += 100
					return true
				end)

				universe:_traceEvent("CoinsAdded", { UserId = player.UserId })
			end)

			Players.PlayerRemoving:Connect(function(player)
				store:unloadAsync(player)
				universe:_traceEvent("PlayerDataUnloaded", { UserId = player.UserId })
			end)

			game:BindToClose(function()
				store:closeAsync()
			end)
		end

		universe = Universe.new({
			seed = 1,
			places = {
				[1] = { Script = serverScript, MaxPlayers = 10 },
			},
		})

		local jobId = universe:StartServer(1)
		universe:JoinPlayerToServer(123, jobId)
		jest.advanceTimersByTime(5000)

		local trace = universe:GetTrace()
		local foundLoad = false
		local foundCoins = false
		for _, event in trace do
			if event.type == "PlayerDataLoaded" and event.details.UserId == 123 then
				foundLoad = true
			end
			if event.type == "CoinsAdded" and event.details.UserId == 123 then
				foundCoins = true
			end
		end
		expect(foundLoad).toBe(true)
		expect(foundCoins).toBe(true)

		jest.advanceTimersByTime(idleDurationSeconds * 1000)

		universe:RemovePlayerFromServer(123, jobId)
		jest.advanceTimersByTime(1000)

		local foundUnload = false
		trace = universe:GetTrace()
		for _, event in trace do
			if event.type == "PlayerDataUnloaded" and event.details.UserId == 123 then
				foundUnload = true
			end
		end
		expect(foundUnload).toBe(true)

		universe:StopServer(jobId)
		jest.advanceTimersByTime(1000)

		local serverStopped = false
		trace = universe:GetTrace()
		for _, event in trace do
			if event.type == "ServerStopped" and event.details.JobId == jobId then
				serverStopped = true
			end
		end
		expect(serverStopped).toBe(true)
	end)
end)
