import { expect } from "@esm-bundle/chai"; import sinon from "sinon"; // Import the singleton instance AND the class for constants import { gameStateManager, GameStateManager, } from "../../src/core/GameStateManager.js"; describe("Core: GameStateManager (Singleton)", () => { let mockPersistence; let mockGameLoop; beforeEach(() => { // 1. Reset Singleton State gameStateManager.reset(); // 2. Mock Persistence mockPersistence = { init: sinon.stub().resolves(), saveRun: sinon.stub().resolves(), loadRun: sinon.stub().resolves(null), loadRoster: sinon.stub().resolves(null), saveRoster: sinon.stub().resolves(), loadCampaign: sinon.stub().resolves(null), saveCampaign: sinon.stub().resolves(), loadMarketState: sinon.stub().resolves(null), saveMarketState: sinon.stub().resolves(), }; // Inject Mock (replacing the real Persistence instance) gameStateManager.persistence = mockPersistence; // 3. Mock GameLoop mockGameLoop = { init: sinon.spy(), startLevel: sinon.spy(), stop: sinon.spy(), }; // 4. Mock MissionManager gameStateManager.missionManager = { setupActiveMission: sinon.stub(), getActiveMission: sinon.stub().returns({ id: "MISSION_TUTORIAL_01", config: { title: "Test Mission" }, biome: { generator_config: { seed_type: "RANDOM", seed: 12345, }, }, objectives: [], }), playIntro: sinon.stub().resolves(), load: sinon.stub(), save: sinon.stub().returns({ completedMissions: [] }), completedMissions: new Set(), }; }); it("CoA 1: Should initialize and transition to MAIN_MENU", async () => { const eventSpy = sinon.spy(); window.addEventListener("gamestate-changed", eventSpy); await gameStateManager.init(); expect(mockPersistence.init.calledOnce).to.be.true; expect(gameStateManager.currentState).to.equal( GameStateManager.STATES.MAIN_MENU ); expect(eventSpy.called).to.be.true; }); it("CoA 2: startNewGame should transition to TEAM_BUILDER", async () => { await gameStateManager.init(); gameStateManager.startNewGame(); expect(gameStateManager.currentState).to.equal( GameStateManager.STATES.TEAM_BUILDER ); }); it("CoA 3: handleEmbark should initialize run, save, and start engine", async () => { // Mock RosterManager.recruitUnit to return async unit with generated name const mockRecruitedUnit = { id: "UNIT_123", name: "Valerius", // Generated character name className: "Vanguard", // Class name classId: "CLASS_VANGUARD", }; gameStateManager.rosterManager.recruitUnit = sinon.stub().resolves(mockRecruitedUnit); gameStateManager.setGameLoop(mockGameLoop); await gameStateManager.init(); const mockSquad = [{ id: "u1", isNew: false }]; // Existing unit, not new // Mock startLevel to resolve immediately mockGameLoop.startLevel = sinon.stub().resolves(); // Await the full async chain await gameStateManager.handleEmbark({ detail: { squad: mockSquad, mode: "SELECT" } }); expect(gameStateManager.currentState).to.equal( GameStateManager.STATES.DEPLOYMENT ); expect(mockPersistence.saveRun.calledWith(gameStateManager.activeRunData)) .to.be.true; expect(mockGameLoop.startLevel.calledWith(gameStateManager.activeRunData)) .to.be.true; }); it("CoA 3b: handleEmbark should dispatch run-data-updated event", async () => { // Mock RosterManager.recruitUnit const mockRecruitedUnit = { id: "UNIT_123", name: "Valerius", className: "Vanguard", classId: "CLASS_VANGUARD", }; gameStateManager.rosterManager.recruitUnit = sinon.stub().resolves(mockRecruitedUnit); gameStateManager.setGameLoop(mockGameLoop); await gameStateManager.init(); const mockSquad = [{ id: "u1", isNew: true, name: "Vanguard", classId: "CLASS_VANGUARD" }]; mockGameLoop.startLevel = sinon.stub().resolves(); let eventDispatched = false; let eventData = null; window.addEventListener("run-data-updated", (e) => { eventDispatched = true; eventData = e.detail.runData; }); await gameStateManager.handleEmbark({ detail: { squad: mockSquad, mode: "DRAFT" } }); expect(eventDispatched).to.be.true; expect(eventData).to.exist; expect(eventData.squad).to.exist; expect(eventData.squad[0].name).to.equal("Valerius"); expect(eventData.squad[0].className).to.equal("Vanguard"); }); it("CoA 4: continueGame should load save and resume engine", async () => { gameStateManager.setGameLoop(mockGameLoop); const savedData = { seed: 999, depth: 5, squad: [] }; mockPersistence.loadRun.resolves(savedData); await gameStateManager.init(); await gameStateManager.continueGame(); expect(mockPersistence.loadRun.called).to.be.true; expect(gameStateManager.activeRunData).to.deep.equal(savedData); expect(mockGameLoop.startLevel.calledWith(savedData)).to.be.true; }); it("CoA 5: init should load campaign data if available", async () => { const savedCampaignData = { completedMissions: ["MISSION_TUTORIAL_01"], }; mockPersistence.loadCampaign.resolves(savedCampaignData); await gameStateManager.init(); expect(mockPersistence.loadCampaign.called).to.be.true; expect(gameStateManager.missionManager.load.calledWith(savedCampaignData)).to.be.true; }); it("CoA 6: init should handle missing campaign data gracefully", async () => { mockPersistence.loadCampaign.resolves(null); await gameStateManager.init(); expect(mockPersistence.loadCampaign.called).to.be.true; expect(gameStateManager.missionManager.load.called).to.be.false; }); it("CoA 7: campaign-data-changed event should trigger save", async () => { await gameStateManager.init(); // Clear any previous calls mockPersistence.saveCampaign.resetHistory(); // Dispatch campaign-data-changed event window.dispatchEvent(new CustomEvent("campaign-data-changed", { detail: { missionCompleted: "MISSION_TUTORIAL_01" } })); // Wait for async save (event listener is synchronous but save is async) await new Promise((resolve) => setTimeout(resolve, 50)); expect(mockPersistence.saveCampaign.called).to.be.true; const savedData = mockPersistence.saveCampaign.firstCall.args[0]; expect(savedData).to.exist; expect(savedData.completedMissions).to.be.an("array"); }); });