package market.guess.ui.desktop; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.math.BigDecimal; import java.nio.file.Path; import java.util.concurrent.atomic.AtomicBoolean; import market.guess.api.CatalogContext; import market.guess.model.event.CommissionTiming; import market.guess.model.event.CreateEventRequest; import market.guess.model.event.MechanismType; import market.guess.ui.desktop.components.AppTab; import market.guess.ui.desktop.components.ModalDialog; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class AppStateTest { private AppState state; private CatalogContext catalog; private ServiceEngine engine; @BeforeEach void setUp() { engine = new ServiceEngine(); var path = Path.of("test-data/ex-2/small.xml"); var res = engine.getCatalogContext().loadEvents(path); assertTrue(res.isSuccess(), "Loading small.xml should succeed"); catalog = engine.getCatalogContext(); state = new AppState(engine); state.setLoadedFile(path.toString()); state.refreshData(); } @Test void reactivePropertiesNotifyListeners() { AtomicBoolean skinChanged = new AtomicBoolean(false); state.skinProperty().addListener((obs, oldVal, newVal) -> skinChanged.set(true)); state.setSkin(Skin.CATPPUCCIN); assertTrue(skinChanged.get()); assertEquals(Skin.CATPPUCCIN, state.getSkin()); AtomicBoolean tabChanged = new AtomicBoolean(false); state.activeTabProperty().addListener((obs, oldVal, newVal) -> tabChanged.set(true)); state.setActiveTab(AppTab.USERS); assertTrue(tabChanged.get()); assertEquals(AppTab.USERS, state.getActiveTab()); AtomicBoolean dialogChanged = new AtomicBoolean(false); state.dialogProperty().addListener((obs, oldVal, newVal) -> dialogChanged.set(true)); state.setDialog(ModalDialog.CREATE_EVENT); assertTrue(dialogChanged.get()); assertEquals(ModalDialog.CREATE_EVENT, state.getDialog()); } @Test void selectedEventUpdatesWhenIdChanges() { assertNotNull(state.getSelectedEvent()); // Add a new event var req = new CreateEventRequest( "New Test Event", "Test Description", MechanismType.LMSR, 5, CommissionTiming.ON_PURCHASE, "Avrum", 150, null, java.util.List.of("YES", "NO"), true); var res = catalog.createEvent(req); assertTrue(res.isSuccess(), "Create event should succeed: " + res.getMessage()); state.refreshData(); String newId = res.getData().summary().key(); state.setSelectedEventId(newId); assertEquals(newId, state.getSelectedEvent().id); assertEquals("New Test Event", state.getSelectedEvent().name); } @Test void actingUserUpdatesWhenNameChanges() { assertNotNull(state.getActingUser()); assertEquals("Avrum", state.getActingUserName()); state.setActingUserName("Menash"); assertNotNull(state.getActingUser()); assertEquals("Menash", state.getActingUser().name); } @Test void createEventWithFullParameters() { var req = new CreateEventRequest( "Summit 2026", "Will the summit take place?", MechanismType.ORDER_BOOK, 10, CommissionTiming.ON_CLOSE, "Avrum", null, new BigDecimal("2.50"), java.util.List.of("YES", "NO"), false); var res = catalog.createEvent(req); assertTrue(res.isSuccess(), "Create event should succeed: " + res.getMessage()); var detail = res.getData(); assertEquals("Summit 2026", detail.summary().name()); assertEquals(MechanismType.ORDER_BOOK, detail.summary().mechanism()); assertEquals(10, detail.summary().commissionPercent()); assertEquals(CommissionTiming.ON_CLOSE, detail.summary().commissionTiming()); } @Test void testServiceEngineAndXmlLoad() { assertEquals(2, state.getEvents().size()); assertEquals(3, state.getUsers().size()); assertNotNull(state.getActingUser()); } @Test void testLmsrTradingAndUpdate() { var openRes = catalog.openEvent("1"); assertTrue(openRes.isSuccess(), "Opening event 1 should succeed: " + openRes.getMessage()); state.refreshData(); var evBefore = state.event("1"); assertNotNull(evBefore); System.out.println( "Before trade: yesPrice=" + evBefore.yesPrice + ", noPrice=" + evBefore.noPrice); var orderRes = engine.getMarketContext().placeOrder("Avrum", "1", "YES", "BUY", evBefore.yesPrice, 20); assertTrue(orderRes.isSuccess(), "Place order should succeed: " + orderRes.getMessage()); state.refreshData(); var evAfter = state.event("1"); assertEquals(new BigDecimal("0.55"), evAfter.yesPrice); assertEquals(new BigDecimal("0.45"), evAfter.noPrice); assertEquals(new BigDecimal("0.55"), state.getSelectedEvent().yesPrice); System.out.println("=== LMSR EVENT AFTER TRADE ==="); System.out.println("evAfter.trades.size() = " + evAfter.trades.size()); for (var t : evAfter.trades) { System.out.println( " Trade: n=" + t.n() + ", user=" + t.user() + ", option=" + t.option() + ", yes=" + t.yesOption() + ", shares=" + t.shares() + ", paid=" + t.paid()); } System.out.println("evAfter.participants.size() = " + evAfter.participants.size()); for (var p : evAfter.participants) { System.out.println( " Participant: user=" + p.user() + ", tag=" + p.tag() + ", yes=" + p.yes() + ", no=" + p.no() + ", val=" + p.value() + ", fees=" + p.fees()); } var avrum = state.user("Avrum"); System.out.println("Avrum events size: " + avrum.events.size()); for (var ue : avrum.events) { System.out.println( " userEvent: name=" + ue.eventName() + ", role=" + ue.role() + ", yes=" + ue.yes() + ", no=" + ue.no()); } System.out.println("Avrum balanceHistory size: " + avrum.balanceHistory.size()); for (var cp : avrum.balanceHistory) { System.out.println(" BalancePoint: x=" + cp.x() + ", y=" + cp.y()); } assertTrue(avrum.balanceHistory.size() >= 2); assertTrue(avrum.balanceHistory.get(0).x() > 1_000_000_000); // Avrum sells 10 YES shares in LMSR var sellRes = engine.getMarketContext().placeOrder("Avrum", "1", "YES", "SELL", null, 10); assertTrue( sellRes.isSuccess(), "Place LMSR sell order should succeed: " + sellRes.getMessage()); state.refreshData(); var evAfterSell = state.event("1"); var avrumParticipant = evAfterSell.participants.stream() .filter(p -> p.user().equals("Avrum")) .findFirst() .orElseThrow(); assertEquals(10, avrumParticipant.yes(), "Avrum should hold 10 YES shares after selling 10"); assertEquals(10, evAfterSell.yesShares, "Event circulating YES shares should be 10"); assertEquals(2, evAfterSell.trades.size(), "Event should have 2 trades recorded"); var avrumAfterSell = state.user("Avrum"); var avrumEv = avrumAfterSell.events.stream() .filter(e -> e.eventId().equals("1")) .findFirst() .orElseThrow(); assertEquals(10, avrumEv.yes(), "Avrum user event row should show 10 shares"); } @Test void testOrderBookTradingAndParticipants() { var openRes = catalog.openEvent("2"); assertTrue(openRes.isSuccess()); state.refreshData(); var ev2 = state.event("2"); System.out.println("=== ORDER BOOK EVENT BEFORE TRADE ==="); System.out.println("ev2.participants.size() = " + ev2.participants.size()); for (var p : ev2.participants) { System.out.println( " Participant: user=" + p.user() + ", tag=" + p.tag() + ", yes=" + p.yes() + ", no=" + p.no() + ", val=" + p.value() + ", fees=" + p.fees()); } // Avrum (MM) quotes his minted YES shares; opening the event doesn't do it for him. var quote = engine .getMarketContext() .placeOrder("Avrum", "2", "YES", "SELL", new BigDecimal("0.50"), 100); assertTrue(quote.isSuccess(), "Avrum quote should succeed: " + quote.getMessage()); // Menash buys 20 YES @ 0.50 on event 2 var res = engine .getMarketContext() .placeOrder("Menash", "2", "YES", "BUY", new BigDecimal("0.50"), 20); assertTrue(res.isSuccess(), "Menash place order should succeed: " + res.getMessage()); state.refreshData(); var ev2After = state.event("2"); System.out.println("=== ORDER BOOK EVENT AFTER TRADE ==="); System.out.println("ev2After.trades.size() = " + ev2After.trades.size()); for (var t : ev2After.trades) { System.out.println( " Trade: n=" + t.n() + ", user=" + t.user() + ", option=" + t.option() + ", yes=" + t.yesOption() + ", shares=" + t.shares() + ", paid=" + t.paid()); } System.out.println("ev2After.participants.size() = " + ev2After.participants.size()); for (var p : ev2After.participants) { System.out.println( " Participant: user=" + p.user() + ", tag=" + p.tag() + ", yes=" + p.yes() + ", no=" + p.no() + ", val=" + p.value() + ", fees=" + p.fees()); } var menash = state.user("Menash"); System.out.println("Menash events size: " + menash.events.size()); for (var ue : menash.events) { System.out.println( " Menash userEvent: name=" + ue.eventName() + ", role=" + ue.role() + ", yes=" + ue.yes() + ", no=" + ue.no()); } // Menash SELLS 10 YES @ 0.40 (better price than Avrum's 0.50) var sellRes = engine .getMarketContext() .placeOrder("Menash", "2", "YES", "SELL", new BigDecimal("0.40"), 10); assertTrue(sellRes.isSuccess(), "Menash sell order should succeed: " + sellRes.getMessage()); state.refreshData(); // Tikva BUYS 10 YES @ 0.50 (crosses Menash's 0.40 ask!) var tikvaBuy = engine .getMarketContext() .placeOrder("Tikva", "2", "YES", "BUY", new BigDecimal("0.50"), 10); assertTrue(tikvaBuy.isSuccess()); state.refreshData(); var ev2AfterSell = state.event("2"); System.out.println("=== ORDER BOOK EVENT AFTER SELL TRADE ==="); System.out.println("ev2AfterSell.trades.size() = " + ev2AfterSell.trades.size()); for (var t : ev2AfterSell.trades) { System.out.println( " Trade: n=" + t.n() + ", user=" + t.user() + ", option=" + t.option() + ", yes=" + t.yesOption() + ", shares=" + t.shares() + ", paid=" + t.paid()); } var domainEv2 = engine.getCatalogContext().getEvent("2"); System.out.println("Domain event 2 trades count: " + domainEv2.getData()); long mShares0 = engine.getCatalogContext().getEvent("2").getData().participants().stream() .filter(p -> p.user().equals("Menash")) .findFirst() .get() .yes(); System.out.println("Menash YES shares in ParticipantDTO: " + mShares0); System.out.println("ev2AfterSell.participants.size() = " + ev2AfterSell.participants.size()); for (var p : ev2AfterSell.participants) { System.out.println( " Participant: user=" + p.user() + ", tag=" + p.tag() + ", yes=" + p.yes() + ", no=" + p.no() + ", val=" + p.value() + ", fees=" + p.fees()); } menash = state.user("Menash"); for (var ue : menash.events) { System.out.println( " Menash userEvent after sell: name=" + ue.eventName() + ", role=" + ue.role() + ", yes=" + ue.yes() + ", no=" + ue.no()); } var menashP = ev2AfterSell.participants.stream() .filter(p -> p.user().equals("Menash")) .findFirst() .orElseThrow(); assertEquals(10, menashP.yes(), "Menash should have 10 YES shares after selling 10 of 20"); var tikvaP = ev2AfterSell.participants.stream() .filter(p -> p.user().equals("Tikva")) .findFirst() .orElseThrow(); assertEquals(10, tikvaP.yes(), "Tikva should have 10 YES shares after buying 10"); var avrumP = ev2AfterSell.participants.stream() .filter(p -> p.user().equals("Avrum")) .findFirst() .orElseThrow(); assertEquals(80, avrumP.yes(), "Avrum (MM) should have 80 YES shares"); assertEquals(2, ev2AfterSell.trades.size(), "Order book event should have 2 trades"); } @Test void testEventsTabControllerProgressBar() throws Exception { try { javafx.application.Platform.startup(() -> {}); } catch (IllegalStateException ignored) { } var loader = new javafx.fxml.FXMLLoader(getClass().getResource("controllers/events_tab.fxml")); javafx.scene.Parent root = loader.load(); market.guess.ui.desktop.controllers.EventsTabController ctrl = loader.getController(); ctrl.init(state); ctrl.refresh(); var field = market.guess.ui.desktop.controllers.EventsTabController.class.getDeclaredField( "lmsrYesProgressBar"); field.setAccessible(true); javafx.scene.control.ProgressBar pb = (javafx.scene.control.ProgressBar) field.get(ctrl); assertEquals(0.5, pb.getProgress(), 0.001); // Switch to event 2 (Order Book) state.setSelectedEventId("2"); // Switch back to event 1 (LMSR) state.setSelectedEventId("1"); assertEquals(0.5, pb.getProgress(), 0.001); // Open event 1 catalog.openEvent("1"); state.refreshData(); assertEquals(0.5, pb.getProgress(), 0.001); // Trade 20 YES on event 1 var res = engine .getMarketContext() .placeOrder("Avrum", "1", "YES", "BUY", state.event("1").yesPrice, 20); assertTrue(res.isSuccess()); state.refreshData(); assertEquals(0.55, pb.getProgress(), 0.001); root.getStyleClass().add("gm-root"); javafx.scene.Scene scene = new javafx.scene.Scene(root); scene.getStylesheets().add(getClass().getResource("theme.css").toExternalForm()); root.applyCss(); root.layout(); javafx.scene.Node track = pb.lookup(".track"); javafx.scene.Node bar = pb.lookup(".bar"); assertNotNull(track); assertNotNull(bar); if (bar instanceof javafx.scene.layout.Region rBar) { assertEquals(0.0, rBar.getPadding().getTop(), 0.001); assertEquals(0.0, rBar.getPadding().getBottom(), 0.001); assertNotNull(rBar.getBackground()); assertEquals(1, rBar.getBackground().getFills().size()); assertEquals(0.0, rBar.getBackground().getFills().get(0).getInsets().getTop(), 0.001); } if (track instanceof javafx.scene.layout.Region rTrack) { assertEquals(0.0, rTrack.getPadding().getTop(), 0.001); assertEquals(0.0, rTrack.getPadding().getBottom(), 0.001); assertNotNull(rTrack.getBackground()); } } @Test void testEventDataAndUserDataEquals() { var ev1 = state.event("1"); assertNotNull(ev1); var ev1Copy = new market.guess.ui.desktop.model.EventData( ev1.id, ev1.num, ev1.name, ev1.desc, ev1.type, ev1.status, ev1.mm, ev1.feePercent, ev1.feeMode, ev1.contract.doubleValue(), ev1.liquidityB, ev1.baseValueD); assertEquals(ev1, ev1Copy); assertEquals(ev1.hashCode(), ev1Copy.hashCode()); var u1 = state.user("Avrum"); assertNotNull(u1); var u1Copy = new market.guess.ui.desktop.model.UserData( "Avrum", u1.role, u1.balance.doubleValue(), u1.blocked, u1.isMm); assertEquals(u1, u1Copy); assertEquals(u1.hashCode(), u1Copy.hashCode()); } }