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(F.Packages.Lyra.Store)local P=K.expect local M=K.describe local j=K.it local W=K.beforeEach local w=K.afterEach local B=K.jest M("\116\120",function()local F local K local M local function G()F=N.createStore({name="\116\120 \116\101\115\116\115",template={coins=0};dataStoreService=K;memoryStoreService=M;schema=function(F)return typeof(F.coins)=="\110\117\109\098\101\114","\073\110\118\097\108\105\100 \100\097\116\097 \102\111\114\109\097\116"end})end local function Z()local J=d.snapshot(K)F:close()for F=1,5,1 do B.advanceTimersByTime(1000)end d.restore(K,J)end W(function()B.useFakeTimers()K=d.new()M=J.new()G()end)w(function()B.clearAllTimers()B.useRealTimers()end)j("\099\114\097\115\104 \098\101\102\111\114\101 \099\111\109\109\105\116 \115\104\111\117\108\100 \114\101\115\117\108\116 \105\110 \097\098\111\114\116",function()F:load("\107\101\121\049")F:load("\107\101\121\050")B.advanceTimersByTime(1000)local J=false local N,M=d.mockStoreMethod(K,"\083\101\116\065\115\121\110\099",B)N.mockImplementation(function(F,K,...)if K=="\107\101\121\049"then local d=table.pack(M(F,K,...))J=true return table.unpack(d)end return M(F,K,...)end)F:tx({"\107\101\121\049","\107\101\121\050"},function(F)F.key1.coins+=10 F.key2.coins+=10 return true end)local j=0 while J==false do j+=1 if j>1000 then break end B.advanceTimersByTime(1)end Z()G()F:load("\107\101\121\049")B.advanceTimersByTime(1000)local W=F:get("\107\101\121\049")B.advanceTimersByTime(1000);(P(J)).toBe(true)return(P(W)).resolves.toEqual({coins=0})end)j("\099\114\097\115\104 \097\102\116\101\114 \099\111\109\109\105\116 \115\104\111\117\108\100 \115\097\118\101 \099\104\097\110\103\101\100",function()F:load("\107\101\121\049")F:load("\107\101\121\050")B.advanceTimersByTime(1000)local J=false local N,M=d.mockStoreMethod(K,"\082\101\109\111\118\101\065\115\121\110\099",B)N.mockImplementation(function(F,K,...)local d=table.pack(M(F,K,...))J=true return table.unpack(d)end)F:tx({"\107\101\121\049","\107\101\121\050"},function(F)F.key1.coins+=10 F.key2.coins+=10 return true end)local j=0 while J==false do j+=1 if j>1000 then break end B.advanceTimersByTime(1)end Z()G()local W=F:load("\107\101\121\049")local w=F:load("\107\101\121\050")B.advanceTimersByTime(1000)W:expect()w:expect()local x=F:get("\107\101\121\049")local S=F:get("\107\101\121\050")B.advanceTimersByTime(1000)return((P(x)).resolves.toEqual({coins=10})):andThen(function()return(P(S)).resolves.toEqual({coins=10})end)end)j("\115\104\111\117\108\100 \114\101\106\101\099\116 \105\102 \097\110\121 \107\101\121 \105\115 \110\111\116 \108\111\097\100\101\100",function()F:load("\107\101\121\049")B.advanceTimersByTime(1000)local K=F:tx({"\107\101\121\049";"\107\101\121\050"},function(F)F.key1.coins+=10 F.key2.coins+=10 return true end)return(P(K)).rejects.toEqual("\075\101\121 \110\111\116 \108\111\097\100\101\100\058 \107\101\121\050")end)j("\115\104\111\117\108\100 \114\101\106\101\099\116 \105\102 \115\116\111\114\101 \105\115 \099\108\111\115\101\100",function()F:load("\107\101\121\049")F:load("\107\101\121\050")B.advanceTimersByTime(1000)F:close()B.advanceTimersByTime(1000)local K=F:tx({"\107\101\121\049","\107\101\121\050"},function(F)F.key1.coins+=10 F.key2.coins+=10 return true end)return(P(K)).rejects.toEqual("\083\116\111\114\101 \105\115 \099\108\111\115\101\100")end)j("\115\104\111\117\108\100 \114\101\106\101\099\116 \105\102 \115\099\104\101\109\097 \118\097\108\105\100\097\116\105\111\110 \102\097\105\108\115",function()F:load("\107\101\121\049")F:load("\107\101\121\050")B.advanceTimersByTime(1000)local K=F:tx({"\107\101\121\049","\107\101\121\050"},function(F)F.key1.coins="\105\110\118\097\108\105\100"F.key2.coins+=10 return true end)B.advanceTimersByTime(1000)return(P(K)).rejects.toMatch("\115\099\104\101\109\097 \118\097\108\105\100\097\116\105\111\110 \102\097\105\108\101\100")end)j("\115\104\111\117\108\100 \114\101\106\101\099\116 \105\102 \107\101\121\115 \097\114\101 \109\111\100\105\102\105\101\100 \105\110 \116\114\097\110\115\102\111\114\109",function()F:load("\107\101\121\049")F:load("\107\101\121\050")B.advanceTimersByTime(1000)local K=F:tx({"\107\101\121\049";"\107\101\121\050"},function(F)F.key3={coins=10}return true end)B.advanceTimersByTime(1000)return(P(K)).rejects.toMatch("\075\101\121\115 \099\104\097\110\103\101\100 \105\110 \116\114\097\110\115\097\099\116\105\111\110")end)j("\115\104\111\117\108\100 \104\097\110\100\108\101 \115\105\110\103\108\101 \107\101\121 \099\104\097\110\103\101\115 \097\115 \110\111\114\109\097\108 \117\112\100\097\116\101",function()F:load("\107\101\121\049")F:load("\107\101\121\050")B.advanceTimersByTime(1000)local J=false local N,M=d.mockStoreMethod(K,"\083\101\116\065\115\121\110\099",B)N.mockImplementation(function(F,K,...)J=true return M(F,K,...)end)local j=F:tx({"\107\101\121\049";"\107\101\121\050"},function(F)F.key1.coins+=10 return true end)B.advanceTimersByTime(1000)return(j:andThen(function()(P(J)).toBe(false)return F:get("\107\101\121\049")end)):andThen(function(F)return(P(F)).toEqual({coins=10})end)end)j("\115\104\111\117\108\100 \101\097\114\108\121 \101\120\105\116 \119\105\116\104 \110\111 \099\104\097\110\103\101\115",function()F:load("\107\101\121\049")F:load("\107\101\121\050")B.advanceTimersByTime(1000)local J=false local N,M=d.mockStoreMethod(K,"\083\101\116\065\115\121\110\099",B)N.mockImplementation(function(F,K,...)J=true return M(F,K,...)end)local j=F:tx({"\107\101\121\049";"\107\101\121\050"},function()return true end)B.advanceTimersByTime(1000)return((P(j)).resolves.toBeUndefined()):andThen(function()(P(J)).toBe(false)end)end)j("\115\104\111\117\108\100 \112\114\101\118\101\110\116 \099\111\110\099\117\114\114\101\110\116 \117\112\100\097\116\101\115 \100\117\114\105\110\103 \116\114\097\110\115\097\099\116\105\111\110",function()F:load("\107\101\121\049")F:load("\107\101\121\050")B.advanceTimersByTime(1000)local J=F:tx({"\107\101\121\049";"\107\101\121\050"},function(F)F.key1.coins+=10 F.key2.coins+=10 return true end)local N=false local M=false local j,W=d.mockStoreMethod(K,"\082\101\109\111\118\101\065\115\121\110\099",B)j.mockImplementation(function(F,K,...)local d=table.pack(W(F,K,...));(P(N)).toBe(false)M=true return table.unpack(d)end)F:update("\107\101\121\049",function(F)N=true F.coins+=5 return true end)B.advanceTimersByTime(1000)return(((P(J)).resolves.toBeUndefined()):andThen(function()(P(M)).toBe(true)return F:get("\107\101\121\049")end)):andThen(function(F)return(P(F)).toEqual({coins=15})end)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 Store = require(ReplicatedStorage.Packages.Lyra.Store)

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

describe("tx", function()
	local store
	local mockData
	local mockMemory

	local function resetStore()
		store = Store.createStore({
			name = "tx tests",
			template = { coins = 0 },
			dataStoreService = mockData,
			memoryStoreService = mockMemory,
			schema = function(data)
				return typeof(data.coins) == "number", "Invalid data format"
			end,
		})
	end

	local function crash()
		local snap = MockDataStoreService.snapshot(mockData)
		store:close()
		for _ = 1, 5 do
			jest.advanceTimersByTime(1000)
		end
		MockDataStoreService.restore(mockData, snap)
	end

	beforeEach(function()
		jest.useFakeTimers()

		mockData = MockDataStoreService.new()
		mockMemory = MockMemoryStoreService.new()
		resetStore()
	end)

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

	it("crash before commit should result in abort", function()
		store:load("key1")
		store:load("key2")

		jest.advanceTimersByTime(1000)

		local key2Set = false
		local setMock, realSet = MockDataStoreService.mockStoreMethod(mockData, "SetAsync", jest)
		setMock.mockImplementation(function(self, key, ...)
			if key == "key1" then
				local ret = table.pack(realSet(self, key, ...))
				key2Set = true
				return table.unpack(ret)
			end
			return realSet(self, key, ...)
		end)

		store:tx({ "key1", "key2" }, function(state)
			state.key1.coins += 10
			state.key2.coins += 10
			return true
		end)

		local i = 0
		while key2Set == false do
			i += 1
			if i > 1000 then
				break
			end
			jest.advanceTimersByTime(1)
		end

		crash()
		resetStore()

		store:load("key1")
		jest.advanceTimersByTime(1000)

		local key1Data = store:get("key1")
		jest.advanceTimersByTime(1000)

		expect(key2Set).toBe(true)
		return expect(key1Data).resolves.toEqual({ coins = 0 })
	end)

	it("crash after commit should save changed", function()
		store:load("key1")
		store:load("key2")

		jest.advanceTimersByTime(1000)

		local txCommitted = false
		local removeMock, realRemove = MockDataStoreService.mockStoreMethod(mockData, "RemoveAsync", jest)
		removeMock.mockImplementation(function(self, key, ...)
			local ret = table.pack(realRemove(self, key, ...))
			txCommitted = true
			return table.unpack(ret)
		end)

		store:tx({ "key1", "key2" }, function(state)
			state.key1.coins += 10
			state.key2.coins += 10
			return true
		end)

		local i = 0
		while txCommitted == false do
			i += 1
			if i > 1000 then
				break
			end
			jest.advanceTimersByTime(1)
		end

		crash()
		resetStore()

		local load1 = store:load("key1")
		local load2 = store:load("key2")
		jest.advanceTimersByTime(1000)

		load1:expect()
		load2:expect()

		local key1Data = store:get("key1")
		local key2Data = store:get("key2")
		jest.advanceTimersByTime(1000)

		return expect(key1Data).resolves.toEqual({ coins = 10 }):andThen(function()
			return expect(key2Data).resolves.toEqual({ coins = 10 })
		end)
	end)

	it("should reject if any key is not loaded", function()
		store:load("key1")
		jest.advanceTimersByTime(1000)

		local txPromise = store:tx({ "key1", "key2" }, function(state)
			state.key1.coins += 10
			state.key2.coins += 10
			return true
		end)

		return expect(txPromise).rejects.toEqual("Key not loaded: key2")
	end)

	it("should reject if store is closed", function()
		store:load("key1")
		store:load("key2")
		jest.advanceTimersByTime(1000)

		store:close()
		jest.advanceTimersByTime(1000)

		local txPromise = store:tx({ "key1", "key2" }, function(state)
			state.key1.coins += 10
			state.key2.coins += 10
			return true
		end)

		return expect(txPromise).rejects.toEqual("Store is closed")
	end)

	it("should reject if schema validation fails", function()
		store:load("key1")
		store:load("key2")
		jest.advanceTimersByTime(1000)

		local txPromise = store:tx({ "key1", "key2" }, function(state)
			state.key1.coins = "invalid"
			state.key2.coins += 10
			return true
		end)
		jest.advanceTimersByTime(1000)

		return expect(txPromise).rejects.toMatch("schema validation failed")
	end)

	it("should reject if keys are modified in transform", function()
		store:load("key1")
		store:load("key2")
		jest.advanceTimersByTime(1000)

		local txPromise = store:tx({ "key1", "key2" }, function(state)
			state.key3 = { coins = 10 }
			return true
		end)
		jest.advanceTimersByTime(1000)

		return expect(txPromise).rejects.toMatch("Keys changed in transaction")
	end)

	it("should handle single key changes as normal update", function()
		store:load("key1")
		store:load("key2")
		jest.advanceTimersByTime(1000)

		local keySet = false
		local setMock, realSet = MockDataStoreService.mockStoreMethod(mockData, "SetAsync", jest)
		setMock.mockImplementation(function(self, key, ...)
			keySet = true
			return realSet(self, key, ...)
		end)

		local txPromise = store:tx({ "key1", "key2" }, function(state)
			state.key1.coins += 10
			return true
		end)

		jest.advanceTimersByTime(1000)

		return txPromise
			:andThen(function()
				expect(keySet).toBe(false)
				return store:get("key1")
			end)
			:andThen(function(key1Data)
				return expect(key1Data).toEqual({ coins = 10 })
			end)
	end)

	it("should early exit with no changes", function()
		store:load("key1")
		store:load("key2")
		jest.advanceTimersByTime(1000)

		local keySet = false
		local setMock, realSet = MockDataStoreService.mockStoreMethod(mockData, "SetAsync", jest)
		setMock.mockImplementation(function(self, key, ...)
			keySet = true
			return realSet(self, key, ...)
		end)

		local txPromise = store:tx({ "key1", "key2" }, function()
			return true
		end)

		jest.advanceTimersByTime(1000)

		return expect(txPromise).resolves.toBeUndefined():andThen(function()
			expect(keySet).toBe(false)
		end)
	end)

	it("should prevent concurrent updates during transaction", function()
		store:load("key1")
		store:load("key2")
		jest.advanceTimersByTime(1000)

		local txPromise = store:tx({ "key1", "key2" }, function(state)
			state.key1.coins += 10
			state.key2.coins += 10
			return true
		end)

		local updateStarted = false

		local txCommitted = false
		local removeMock, realRemove = MockDataStoreService.mockStoreMethod(mockData, "RemoveAsync", jest)
		removeMock.mockImplementation(function(self, key, ...)
			local ret = table.pack(realRemove(self, key, ...))
			expect(updateStarted).toBe(false)
			txCommitted = true
			return table.unpack(ret)
		end)

		store:update("key1", function(data)
			updateStarted = true
			data.coins += 5
			return true
		end)

		jest.advanceTimersByTime(1000)

		return expect(txPromise).resolves
			.toBeUndefined()
			:andThen(function()
				expect(txCommitted).toBe(true)
				return store:get("key1")
			end)
			:andThen(function(key1Data)
				return expect(key1Data).toEqual({ coins = 15 })
			end)
	end)
end)
