diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..319ff73 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.checkstyle.configuration": "/google_checks.xml" +} \ No newline at end of file diff --git a/checkstyle.xml b/checkstyle.xml new file mode 100644 index 0000000..9f86503 --- /dev/null +++ b/checkstyle.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index a74b899..1b52053 100644 --- a/pom.xml +++ b/pom.xml @@ -70,6 +70,27 @@ maven-javadoc-plugin 3.12.0 + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.3.1 + + checkstyle.xml + UTF-8 + true + false + false + + + + com.puppycrawl.tools + checkstyle + 10.12.5 + + + \ No newline at end of file diff --git a/src/main/java/Controller/StockFileHandler.java b/src/main/java/Controller/StockFileHandler.java index b64f9c0..bf808a7 100644 --- a/src/main/java/Controller/StockFileHandler.java +++ b/src/main/java/Controller/StockFileHandler.java @@ -1,37 +1,58 @@ package Controller; -import java.nio.file.*; + +import Model.Stock; import java.io.IOException; import java.math.BigDecimal; +import java.nio.file.*; import java.util.ArrayList; import java.util.List; -import Model.Stock; +/** + * Handles loading and saving Stock data from/to CSV files. + * The CSV format is: ticker,company_name,price + * Lines starting with '#' are treated as comments and ignored. + */ public class StockFileHandler { - // lesing - public List loadStocksFromFile(String filename) throws IOException { - return Files.readAllLines(Paths.get(filename)).stream() - .map(String::trim) - .filter(line -> !line.isEmpty() && !line.startsWith("#")) - .map(line -> line.split(",")) - .filter(parts -> parts.length == 3) - .map(parts -> new Stock(parts[0], parts[1], new BigDecimal(parts[2]))) - .toList(); - } + /** + * Loads stocks from a CSV file. + * Expected format: ticker,company,price + * Empty lines and lines starting with '#' are ignored. + * + * @param filename the path to the CSV file + * @return a list of Stock objects loaded from the file + * @throws IOException if the file cannot be read + */ + public List loadStocksFromFile(String filename) throws IOException { + return Files.readAllLines(Paths.get(filename)).stream() + .map(String::trim) + .filter(line -> !line.isEmpty() && !line.startsWith("#")) + .map(line -> line.split(",")) + .filter(parts -> parts.length == 3) + .map(parts -> new Stock(parts[0], parts[1], new BigDecimal(parts[2]))) + .toList(); + } - // lagring - public void saveStocksToFile(String filename, List stocks) throws IOException { - List lines = new ArrayList<>(); - lines.add("# Ticker,Name,Price"); + /** + * Saves stocks to a CSV file. + * The file includes a header comment line followed by one stock per line. + * + * @param filename the path to the CSV file to write + * @param stocks the list of stocks to save + * @throws IOException if the file cannot be written + */ + public void saveStocksToFile(String filename, List stocks) throws IOException { + List lines = new ArrayList<>(); + lines.add("# Ticker,Name,Price"); - lines.addAll(stocks.stream() - .map(stock -> String.format("%s,%s,%s", - stock.getSymbol(), - stock.getCompany(), - stock.getSalesPrice().toString())) - .toList()); + lines.addAll(stocks.stream() + .map(stock -> String.format("%s,%s,%s", + stock.getSymbol(), + stock.getCompany(), + stock.getSalesPrice().toString())) + .toList()); - Files.write(Paths.get(filename), lines); - } + Files.write(Paths.get(filename), lines); + } } \ No newline at end of file diff --git a/src/main/java/Model/Exchange.java b/src/main/java/Model/Exchange.java index 6db979c..7feb4f2 100644 --- a/src/main/java/Model/Exchange.java +++ b/src/main/java/Model/Exchange.java @@ -1,4 +1,5 @@ package Model; + import java.math.BigDecimal; import java.util.ArrayList; import java.util.HashMap; @@ -6,216 +7,323 @@ import java.util.Map; import java.util.Random; +/** + * Represents a stock exchange where players can buy and sell stocks. + * The Exchange maintains a collection of stocks, advances through weeks, + * and executes trades. It implements the Observer pattern to notify listeners + * (typically the UI) whenever its state changes. + */ public class Exchange { - private final String name; - private int week; - private final Map stockMap; - private final Random random; - - // Registered observers - private final List observers = new ArrayList<>(); - - public Exchange(String name, List stocks) { - if (name == null || name.isBlank()) { - throw new IllegalArgumentException("Exchange name cannot be null or blank"); - } - if (stocks == null) { - throw new IllegalArgumentException("Stock list cannot be null"); - } - - this.name = name; - this.week = 1; - this.stockMap = new HashMap<>(); - this.random = new Random(); - - for (Stock stock : stocks) { - if (stock == null) { - throw new IllegalArgumentException("Stock list must not contain null entries"); - } - stockMap.put(stock.getSymbol(), stock); - } + private final String name; + private int week; + private final Map stockMap; + private final Random random; + + // Registered observers + private final List observers = new ArrayList<>(); + + /** + * Constructs a new Exchange with the given name and initial stocks. + * + * @param name the exchange name. Cannot be null or blank + * @param stocks the initial list of stocks. Cannot be null or contain null entries + * @throws IllegalArgumentException if any parameter is invalid + */ + public Exchange(String name, List stocks) { + if (name == null || name.isBlank()) { + throw new IllegalArgumentException("Exchange name cannot be null or blank"); } - - // ---- Observer ---- - - public void addObserver(ExchangeObserver observer) { - if (observer == null) { - throw new IllegalArgumentException("Observer cannot be null"); - } - if (!observers.contains(observer)) { - observers.add(observer); - } + if (stocks == null) { + throw new IllegalArgumentException("Stock list cannot be null"); } - - public void removeObserver(ExchangeObserver observer) { - observers.remove(observer); + + this.name = name; + this.week = 1; + this.stockMap = new HashMap<>(); + this.random = new Random(); + + for (Stock stock : stocks) { + if (stock == null) { + throw new IllegalArgumentException("Stock list must not contain null entries"); + } + stockMap.put(stock.getSymbol(), stock); } - - private void notifyObservers() { - for (ExchangeObserver observer : observers) { - observer.onExchangeUpdated(this); - } + } + + // ---- Observer ---- + + /** + * Registers an observer to receive notifications of state changes. + * + * @param observer the observer to register. Cannot be null + * @throws IllegalArgumentException if observer is null + */ + public void addObserver(ExchangeObserver observer) { + if (observer == null) { + throw new IllegalArgumentException("Observer cannot be null"); } - - public String getName() { - return name; + if (!observers.contains(observer)) { + observers.add(observer); } - - public int getWeek() { - return week; + } + + /** + * Unregisters an observer from receiving notifications. + * + * @param observer the observer to remove + */ + public void removeObserver(ExchangeObserver observer) { + observers.remove(observer); + } + + /** + * Notifies all registered observers of a state change. + */ + private void notifyObservers() { + for (ExchangeObserver observer : observers) { + observer.onExchangeUpdated(this); } - - public boolean hasStock(String symbol) { - if (symbol == null || symbol.isBlank()) { - throw new IllegalArgumentException("Symbol cannot be null or blank"); - } - return stockMap.containsKey(symbol); + } + + /** + * Returns the exchange name. + * + * @return the name + */ + public String getName() { + return name; + } + + /** + * Returns the current week number. + * + * @return the week + */ + public int getWeek() { + return week; + } + + /** + * Checks if a stock with the given symbol is available on this exchange. + * + * @param symbol the stock symbol. Cannot be null or blank + * @return true if the stock exists; false otherwise + * @throws IllegalArgumentException if symbol is invalid + */ + public boolean hasStock(String symbol) { + if (symbol == null || symbol.isBlank()) { + throw new IllegalArgumentException("Symbol cannot be null or blank"); } - - public Stock getStock(String symbol) { - if (symbol == null || symbol.isBlank()) { - throw new IllegalArgumentException("Symbol cannot be null or blank"); - } - return stockMap.get(symbol); + return stockMap.containsKey(symbol); + } + + /** + * Returns the stock with the given symbol. + * + * @param symbol the stock symbol. Cannot be null or blank + * @return the Stock, or null if not found + * @throws IllegalArgumentException if symbol is invalid + */ + public Stock getStock(String symbol) { + if (symbol == null || symbol.isBlank()) { + throw new IllegalArgumentException("Symbol cannot be null or blank"); + } + return stockMap.get(symbol); + } + + /** + * Searches for stocks matching the search term by symbol or company name. + * + * @param searchTerm the search term. Cannot be null + * @return a list of matching stocks (case-insensitive) + * @throws IllegalArgumentException if searchTerm is null + */ + public List findStocks(String searchTerm) { + if (searchTerm == null) { + throw new IllegalArgumentException("Search term cannot be null"); } - - public List findStocks(String searchTerm) { - if (searchTerm == null) { - throw new IllegalArgumentException("Search term cannot be null"); - } - List result = new ArrayList<>(); - String lowerSearch = searchTerm.toLowerCase(); - - for (Stock stock : stockMap.values()) { - if (stock.getSymbol().toLowerCase().contains(lowerSearch) - || stock.getCompany().toLowerCase().contains(lowerSearch)) { - result.add(stock); - } - } - - return result; + List result = new ArrayList<>(); + String lowerSearch = searchTerm.toLowerCase(); + + for (Stock stock : stockMap.values()) { + if (stock.getSymbol().toLowerCase().contains(lowerSearch) + || stock.getCompany().toLowerCase().contains(lowerSearch)) { + result.add(stock); + } } - public Transaction buy(String symbol, BigDecimal quantity, Player player) { - if (symbol == null || symbol.isBlank()) { - throw new IllegalArgumentException("Symbol cannot be null or blank"); - } - if (quantity == null) { - throw new IllegalArgumentException("Quantity cannot be null"); - } - if (quantity.compareTo(BigDecimal.ZERO) <= 0) { - throw new IllegalArgumentException("Quantity must be greater than zero"); - } - if (player == null) { - throw new IllegalArgumentException("Player cannot be null"); - } + return result; + } + + /** + * Executes a buy transaction for the specified stock and quantity. + * + * @param symbol the stock symbol. Cannot be null or blank + * @param quantity the number of shares to buy. Must be greater than zero + * @param player the player executing the purchase. Cannot be null + * @return the completed Purchase transaction + * @throws IllegalArgumentException if any parameter is invalid or stock not found + * @throws IllegalStateException if player has insufficient funds + */ + public Transaction buy(String symbol, BigDecimal quantity, Player player) { + if (symbol == null || symbol.isBlank()) { + throw new IllegalArgumentException("Symbol cannot be null or blank"); + } + if (quantity == null) { + throw new IllegalArgumentException("Quantity cannot be null"); + } + if (quantity.compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalArgumentException("Quantity must be greater than zero"); + } + if (player == null) { + throw new IllegalArgumentException("Player cannot be null"); + } - Stock stock = getStock(symbol); - if (stock == null) { - throw new IllegalArgumentException("No stock found with symbol: " + symbol); - } - - // lager en ny "andel" basert på nåværende salgspris - Share shareToBuy = new Share(stock, quantity, stock.getSalesPrice()); - Purchase purchase = new Purchase(shareToBuy, this.week); - purchase.commit(player); - - notifyObservers(); - - return purchase; + Stock stock = getStock(symbol); + if (stock == null) { + throw new IllegalArgumentException("No stock found with symbol: " + symbol); } - public Transaction sell(Share share, Player player) { - - return sell(share, share.getQuantity(), player); + // Creates a new share based on current selling price + Share shareToBuy = new Share(stock, quantity, stock.getSalesPrice()); + Purchase purchase = new Purchase(shareToBuy, this.week); + purchase.commit(player); + + notifyObservers(); + + return purchase; + } + + /** + * Executes a full sale transaction for all shares in the given holding. + * + * @param share the share to sell completely. Cannot be null + * @param player the player executing the sale. Cannot be null + * @return the completed Sale transaction + * @throws IllegalArgumentException if any parameter is invalid + * @throws IllegalStateException if player doesn't own the share + */ + public Transaction sell(Share share, Player player) { + return sell(share, share.getQuantity(), player); + } + + /** + * Executes a partial or complete sale transaction. + * If the sell quantity is less than the share quantity, creates a partial sale + * and leaves the remainder in the portfolio. + * + * @param originalShare the share to sell from. Cannot be null + * @param sellQuantity the quantity to sell. Cannot exceed the original quantity + * @param player the player executing the sale. Cannot be null + * @return the completed Sale transaction + * @throws IllegalArgumentException if any parameter is invalid + * @throws IllegalStateException if sell quantity exceeds holding + */ + public Transaction sell(Share originalShare, BigDecimal sellQuantity, Player player) { + if (originalShare == null || sellQuantity == null) { + throw new IllegalArgumentException("Share and quantity cannot be null"); + } + if (player == null) { + throw new IllegalArgumentException("Player cannot be null"); } - public Transaction sell(Share originalShare, BigDecimal sellQuantity, Player player) { - if (originalShare == null || sellQuantity == null) { - throw new IllegalArgumentException("Share and quantity cannot be null"); - } - if (player == null) { - throw new IllegalArgumentException("Player cannot be null"); - } - - // Kan ikke selge mer enn man eier - if (sellQuantity.compareTo(originalShare.getQuantity()) > 0) { - throw new IllegalArgumentException("Cannot sell more shares than owned"); - } - + // Cannot sell more shares than you have + if (sellQuantity.compareTo(originalShare.getQuantity()) > 0) { + throw new IllegalArgumentException("Cannot sell more shares than owned"); + } - Share shareToSell; - if (sellQuantity.compareTo(originalShare.getQuantity()) < 0) { + Share shareToSell; - /** - * Delsalg: - * Original mengde - mengde som selges = gjenværende mengde - */ + if (sellQuantity.compareTo(originalShare.getQuantity()) < 0) { - player.getPortfolio().removeShare(originalShare); - BigDecimal remainderShare = originalShare.getQuantity().subtract(sellQuantity); + // Partial sale: + // Original quantity - quantity sold = remaining quantity - player.getPortfolio().addShare(new Share(originalShare.getStock(), remainderShare, originalShare.getPurchasePrice())); + player.getPortfolio().removeShare(originalShare); - /** - * Legger delmengden midlertidig til i portfolio slik at Sale.commit() finner den - */ + BigDecimal remainderShare = originalShare.getQuantity().subtract(sellQuantity); - shareToSell = new Share(originalShare.getStock(), sellQuantity, originalShare.getPurchasePrice()); + player.getPortfolio().addShare(new Share(originalShare + .getStock(), remainderShare, originalShare.getPurchasePrice())); - player.getPortfolio().addShare(shareToSell); + + // Temporarily adds the subset to the portfolio so that Sale.commit() can find it - } + shareToSell = new Share(originalShare + .getStock(), sellQuantity, originalShare.getPurchasePrice()); - else { - // Fullstendig salg: hele andelen selges som normalt - shareToSell = originalShare; - } + player.getPortfolio().addShare(shareToSell); - // Salgstransaksjon - Transaction sale = TransactionFactory.createSale(shareToSell, this.week); + } - sale.commit(player); - notifyObservers(); - return sale; + else { + // Complete sale: the entire share is sold as normal + shareToSell = originalShare; } - public void advance() { - - week++; + // Salestransaction + Transaction sale = TransactionFactory.createSale(shareToSell, this.week); - for (Stock stock : stockMap.values()) { // henter stock-objektene + sale.commit(player); + notifyObservers(); + return sale; + } - BigDecimal currentPrice = stock.getSalesPrice(); // henter siste pris fra Stock + /** + * Advances the game to the next week and updates all stock prices. + * Each stock's price is randomly adjusted by -10% to +10% per week. + * Notifies all observers of the change. + */ + public void advance() { + + week++; - double changePercent = (random.nextDouble() - 0.5) * 0.1; + for (Stock stock : stockMap.values()) { // Retrieves the stock-objects - BigDecimal change = currentPrice.multiply(BigDecimal.valueOf(changePercent)); + BigDecimal currentPrice = stock.getSalesPrice(); // Retrieves last price from Stock - BigDecimal newPrice = currentPrice.add(change); + double changePercent = (random.nextDouble() - 0.5) * 0.1; - if (newPrice.compareTo(BigDecimal.ZERO) > 0) { // unngå negativ pris - stock.addNewSalesPrice(newPrice); - } - } + BigDecimal change = currentPrice.multiply(BigDecimal.valueOf(changePercent)); - notifyObservers(); - } + BigDecimal newPrice = currentPrice.add(change); - public List getGainers(int limit) { // viser "vinnerne" - return stockMap.values().stream() - .sorted((s1, s2) -> s2.getLatestPriceChange().compareTo(s1.getLatestPriceChange())) - .limit(limit) - .toList(); + if (newPrice.compareTo(BigDecimal.ZERO) > 0) { // avoid negative price + stock.addNewSalesPrice(newPrice); + } } - public List getLosers(int limit) { // viser "taperne" - return stockMap.values().stream() - .sorted((s1, s2) -> s1.getLatestPriceChange().compareTo(s2.getLatestPriceChange())) - .limit(limit) - .toList(); - } + notifyObservers(); + } + + /** + * Returns the top-performing stocks (gainers) for the current week. + * Gainers are stocks with the highest positive price change. + * + * @param limit the maximum number of gainers to return + * @return a list of the top gainers, sorted by price change (highest first) + */ + public List getGainers(int limit) { // shows the "winners" + return stockMap.values().stream() + .sorted((s1, s2) -> s2.getLatestPriceChange().compareTo(s1.getLatestPriceChange())) + .limit(limit) + .toList(); + } + + /** + * Returns the lowest-performing stocks (losers) for the current week. + * Losers are stocks with the highest negative price change. + * + * @param limit the maximum number of losers to return + * @return a list of the top losers, sorted by price change (lowest first) + */ + public List getLosers(int limit) { // shows the "losers" + return stockMap.values().stream() + .sorted((s1, s2) -> s1.getLatestPriceChange().compareTo(s2.getLatestPriceChange())) + .limit(limit) + .toList(); + } } diff --git a/src/main/java/Model/ExchangeObserver.java b/src/main/java/Model/ExchangeObserver.java index ad3f6a5..7cef281 100644 --- a/src/main/java/Model/ExchangeObserver.java +++ b/src/main/java/Model/ExchangeObserver.java @@ -1,16 +1,16 @@ package Model; /** - * Observer interface for the Exchange subject. - * Implement this interface to receive notifications whenever the exchange - * state changes (week advances, a trade is committed). + * Observer interface for receiving notifications from the Exchange. + * Implementations are notified whenever the Exchange state changes, + * allowing the View and other components to stay synchronized. */ public interface ExchangeObserver { - /** - * Called by the Exchange after its state has changed. - * - * @param exchange the Exchange that triggered the notification - */ - void onExchangeUpdated(Exchange exchange); + /** + * Called by the Exchange when its state changes. + * + * @param exchange the Exchange that triggered the notification + */ + void onExchangeUpdated(Exchange exchange); } diff --git a/src/main/java/Model/Player.java b/src/main/java/Model/Player.java index ef078a5..dd9d81e 100644 --- a/src/main/java/Model/Player.java +++ b/src/main/java/Model/Player.java @@ -1,96 +1,144 @@ package Model; + import java.math.BigDecimal; +/** + * Represents a player in the stock trading game. + * A Player manages their personal portfolio of shares, cash balance, and transaction + * history. Players can buy and sell stocks, and their performance is tracked through + * status levels based on trading activity and net worth growth. + */ public class Player { - private String name; - private BigDecimal startingMoney; - private BigDecimal money; - private Portfolio portfolio; - private TransactionArchive transactionArchive; - - public Player(String name, BigDecimal startingMoney) { - if (name == null || name.isBlank()) { - throw new IllegalArgumentException("Player name cannot be null or blank"); - } - if (startingMoney == null) { - throw new IllegalArgumentException("Starting money cannot be null"); - } - if (startingMoney.compareTo(BigDecimal.ZERO) < 0) { - throw new IllegalArgumentException("Starting money cannot be negative"); - } + private String name; + private BigDecimal startingMoney; + private BigDecimal money; + private Portfolio portfolio; + private TransactionArchive transactionArchive; - this.name = name; - this.startingMoney = startingMoney; - this.money = startingMoney; - this.portfolio = new Portfolio(); - this.transactionArchive = new TransactionArchive(); + /** + * Constructs a new Player with the given name and starting capital. + * + * @param name the player's name. Cannot be null or blank + * @param startingMoney the initial cash balance. Cannot be null or negative + * @throws IllegalArgumentException if any parameter is invalid + */ + public Player(String name, BigDecimal startingMoney) { + if (name == null || name.isBlank()) { + throw new IllegalArgumentException("Player name cannot be null or blank"); } - - public String getName() { - return this.name; + if (startingMoney == null) { + throw new IllegalArgumentException("Starting money cannot be null"); } - - public BigDecimal getMoney() { - return this.money; + if (startingMoney.compareTo(BigDecimal.ZERO) < 0) { + throw new IllegalArgumentException("Starting money cannot be negative"); } - public void addMoney(BigDecimal amount) { - if (amount == null) { - throw new IllegalArgumentException("Amount cannot be null"); - } - if (amount.compareTo(BigDecimal.ZERO) < 0) { - throw new IllegalArgumentException("Amount to add cannot be negative"); - } - this.money = this.money.add(amount); - } + this.name = name; + this.startingMoney = startingMoney; + this.money = startingMoney; + this.portfolio = new Portfolio(); + this.transactionArchive = new TransactionArchive(); + } - public void withdrawMoney(BigDecimal amount) { - if (amount == null) { - throw new IllegalArgumentException("Amount cannot be null"); - } - if (amount.compareTo(BigDecimal.ZERO) < 0) { - throw new IllegalArgumentException("Amount to withdraw cannot be negative"); - } - this.money = this.money.subtract(amount); - } + /** + * Returns the player's name. + * + * @return the player's name + */ + public String getName() { + return this.name; + } - public Portfolio getPortfolio() { - return this.portfolio; + /** + * Returns the player's current cash balance. + * + * @return the current amount of money available + */ + public BigDecimal getMoney() { + return this.money; + } + + /** + * Adds the specified amount to the player's cash balance. + * + * @param amount the amount to add. Cannot be null or negative + * @throws IllegalArgumentException if amount is invalid + */ + public void addMoney(BigDecimal amount) { + if (amount == null) { + throw new IllegalArgumentException("Amount cannot be null"); + } + if (amount.compareTo(BigDecimal.ZERO) < 0) { + throw new IllegalArgumentException("Amount to add cannot be negative"); } + this.money = this.money.add(amount); + } - public TransactionArchive getTransactionArchive() { - return this.transactionArchive; + /** + * Removes the specified amount from the player's cash balance. + * + * @param amount the amount to withdraw. Cannot be null or negative + * @throws IllegalArgumentException if amount is invalid + */ + public void withdrawMoney(BigDecimal amount) { + if (amount == null) { + throw new IllegalArgumentException("Amount cannot be null"); } + if (amount.compareTo(BigDecimal.ZERO) < 0) { + throw new IllegalArgumentException("Amount to withdraw cannot be negative"); + } + this.money = this.money.subtract(amount); + } - /** - * Returns the player's current status based on trading activity and net worth performance. - * - * Status levels: - * - NOVICE: Starting level, no requirements - * - INVESTOR: At least 10 weeks of trading AND net worth increased by at least 20% - * - SPECULATOR: At least 20 weeks of trading AND net worth at least doubled - * - * @return the player's current PlayerStatus - */ - public PlayerStatus getStatus() { - // Calculate total net worth: current cash + portfolio value - BigDecimal totalNetWorth = this.money.add(this.portfolio.getNetWorth()); - - // Get number of weeks with trading activity - int weeksActive = this.transactionArchive.countDistinctWeeks(); + /** + * Returns the player's portfolio. + * + * @return the portfolio containing held shares + */ + public Portfolio getPortfolio() { + return this.portfolio; + } + + /** + * Returns the player's transaction archive. + * + * @return the archive of all transactions + */ + public TransactionArchive getTransactionArchive() { + return this.transactionArchive; + } + + + /** + * Returns the player's current status based on trading activity and net worth performance. + * Status levels: + * - NOVICE: Starting level, no requirements + * - INVESTOR: At least 10 weeks of trading AND net worth increased by at least 20% + * - SPECULATOR: At least 20 weeks of trading AND net worth at least doubled + * + * @return the player's current PlayerStatus + */ + public PlayerStatus getStatus() { + // Calculate total net worth: current cash + portfolio value + BigDecimal totalNetWorth = this.money.add(this.portfolio.getNetWorth()); - // Check for SPECULATOR status: 20+ weeks AND net worth doubled - if (weeksActive >= 20 && totalNetWorth.compareTo(this.startingMoney.multiply(new BigDecimal("2"))) >= 0) { - return PlayerStatus.SPECULATOR; - } + // Get number of weeks with trading activity + int weeksActive = this.transactionArchive.countDistinctWeeks(); - // Check for INVESTOR status: 10+ weeks AND net worth increased by 20% - if (weeksActive >= 10 && totalNetWorth.compareTo(this.startingMoney.multiply(new BigDecimal("1.2"))) >= 0) { - return PlayerStatus.INVESTOR; - } + // Check for SPECULATOR status: 20+ weeks AND net worth doubled + if (weeksActive >= 20 && totalNetWorth.compareTo(this + .startingMoney.multiply(new BigDecimal("2"))) >= 0) { + return PlayerStatus.SPECULATOR; + } - // Default: NOVICE - return PlayerStatus.NOVICE; + // Check for INVESTOR status: 10+ weeks AND net worth increased by 20% + if (weeksActive >= 10 && totalNetWorth.compareTo(this + .startingMoney.multiply(new BigDecimal("1.2"))) >= 0) { + return PlayerStatus.INVESTOR; } + + // Default: NOVICE + return PlayerStatus.NOVICE; + } } diff --git a/src/main/java/Model/PlayerStatus.java b/src/main/java/Model/PlayerStatus.java index cf14ea8..312e38f 100644 --- a/src/main/java/Model/PlayerStatus.java +++ b/src/main/java/Model/PlayerStatus.java @@ -1,25 +1,36 @@ package Model; /** - * Enum representing the player's status level based on trading activity and performance. - * - * Status levels: - * - NOVICE: Starting level, no requirements - * - INVESTOR: Traded for at least 10 weeks AND increased net worth by at least 20% - * - SPECULATOR: Traded for at least 20 weeks AND doubled the net worth (100% increase) + * Enum representing the player's trading status level. + * Status levels are determined by trading activity (weeks with transactions) + * and net worth performance compared to starting capital. + * Levels: + * - NOVICE: Initial status (default) + * - INVESTOR: 10+ weeks of trading AND net worth increased by 20% + * - SPECULATOR: 20+ weeks of trading AND net worth doubled */ public enum PlayerStatus { NOVICE("Novice"), INVESTOR("Investor"), SPECULATOR("Speculator"); - private final String displayName; + private final String displayName; - PlayerStatus(String displayName) { - this.displayName = displayName; - } + /** + * Constructs a PlayerStatus with the given display name. + * + * @param displayName the name for UI display + */ + PlayerStatus(String displayName) { + this.displayName = displayName; + } - public String getDisplayName() { - return displayName; - } + /** + * Returns the display name for this status. + * + * @return the display name + */ + public String getDisplayName() { + return displayName; + } } diff --git a/src/main/java/Model/Portfolio.java b/src/main/java/Model/Portfolio.java index ba050e7..7c9e38d 100644 --- a/src/main/java/Model/Portfolio.java +++ b/src/main/java/Model/Portfolio.java @@ -1,54 +1,104 @@ package Model; + import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; +/** + * Represents a portfolio of shares held by a player. + * A Portfolio manages a collection of Share objects and provides methods to add, + * remove, and query holdings. It can calculate the total net worth based on current + * market prices. + */ public class Portfolio { - private final List shares; + private final List shares; - public Portfolio() { - this.shares = new ArrayList<>(); - } + /** + * Constructs a new empty Portfolio. + */ + public Portfolio() { + this.shares = new ArrayList<>(); + } - public boolean addShare(Share share) { - if (share == null) { - throw new IllegalArgumentException("Share cannot be null"); - } - return shares.add(share); + /** + * Adds a share to the portfolio. + * + * @param share the share to add. Cannot be null + * @return true if the share was added successfully + * @throws IllegalArgumentException if share is null + */ + public boolean addShare(Share share) { + if (share == null) { + throw new IllegalArgumentException("Share cannot be null"); } + return shares.add(share); + } - public boolean removeShare(Share share) { - if (share == null) { - throw new IllegalArgumentException("Share cannot be null"); - } - return shares.remove(share); + /** + * Removes a share from the portfolio. + * + * @param share the share to remove. Cannot be null + * @return true if the share was removed; false if it was not in the portfolio + * @throws IllegalArgumentException if share is null + */ + public boolean removeShare(Share share) { + if (share == null) { + throw new IllegalArgumentException("Share cannot be null"); } + return shares.remove(share); + } - public List getShares() { - return new ArrayList<>(shares); - } + /** + * Returns a defensive copy of all shares in the portfolio. + * + * @return a copy of the shares list + */ + public List getShares() { + return new ArrayList<>(shares); + } - public List getShares(String symbol) { - if (symbol == null || symbol.isBlank()) { - throw new IllegalArgumentException("Symbol cannot be null or blank"); - } - return shares.stream() - .filter(share -> share.getStock().getSymbol().equals(symbol)) - .toList(); + /** + * Returns all shares in the portfolio for a specific stock symbol. + * + * @param symbol the stock symbol to filter by. Cannot be null or blank + * @return a list of shares matching the symbol + * @throws IllegalArgumentException if symbol is null or blank + */ + public List getShares(String symbol) { + if (symbol == null || symbol.isBlank()) { + throw new IllegalArgumentException("Symbol cannot be null or blank"); } + return shares.stream() + .filter(share -> share.getStock().getSymbol().equals(symbol)) + .toList(); + } - public boolean contains(Share share) { - if (share == null) { - throw new IllegalArgumentException("Share cannot be null"); - } - return shares.contains(share); + /** + * Checks if the portfolio contains a specific share. + * + * @param share the share to check for. Cannot be null + * @return true if the share is in the portfolio; false otherwise + * @throws IllegalArgumentException if share is null + */ + public boolean contains(Share share) { + if (share == null) { + throw new IllegalArgumentException("Share cannot be null"); } + return shares.contains(share); + } - public BigDecimal getNetWorth() { - return shares.stream() - .map(share -> new SaleCalculator(share).calculateTotal()) - .reduce(BigDecimal.ZERO, BigDecimal::add); - } + /** + * Calculates the total net worth of the portfolio based on current market prices. + * The net worth is the sum of all shares valued at their current sales prices, + * minus any transaction costs (commissions and taxes). + * + * @return the total net worth of all holdings + */ + public BigDecimal getNetWorth() { + return shares.stream() + .map(share -> new SaleCalculator(share).calculateTotal()) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } } diff --git a/src/main/java/Model/Purchase.java b/src/main/java/Model/Purchase.java index 33c41e5..b81f655 100644 --- a/src/main/java/Model/Purchase.java +++ b/src/main/java/Model/Purchase.java @@ -1,32 +1,53 @@ package Model; + import java.math.BigDecimal; +/** + * Represents a stock purchase transaction. + * A Purchase transaction represents a player buying shares. When committed, + * it deducts money from the player's account, adds the shares to their portfolio, + * and records the transaction in the archive. + */ public class Purchase extends Transaction { - public Purchase(Share share, int week) { - super(share, week, new PurchaseCalculator(share)); - } + /** + * Constructs a new Purchase transaction. + * + * @param share the share to purchase. Cannot be null + * @param week the week number when the purchase occurs. Must be at least 1 + */ + public Purchase(Share share, int week) { + super(share, week, new PurchaseCalculator(share)); + } - @Override - public void commit(Player player) { - if (player == null) { - throw new IllegalArgumentException("Player cannot be null"); - } - if (isCommitted()) { - throw new IllegalStateException("Purchase has already been committed"); - } + /** + * Commits this purchase transaction, deducting funds and adding shares to the portfolio. + * + * @param player the player executing the purchase. Cannot be null + * @throws IllegalArgumentException if player is null + * @throws IllegalStateException if purchase has already been committed or if + * the player has insufficient funds + */ + @Override + public void commit(Player player) { + if (player == null) { + throw new IllegalArgumentException("Player cannot be null"); + } + if (isCommitted()) { + throw new IllegalStateException("Purchase has already been committed"); + } - BigDecimal price = this.getCalculator().calculateTotal(); + BigDecimal price = this.getCalculator().calculateTotal(); - if (player.getMoney().compareTo(price) < 0) { - throw new IllegalStateException( - "Insufficient funds: required " + price + ", available " + player.getMoney() - ); - } + if (player.getMoney().compareTo(price) < 0) { + throw new IllegalStateException( + "Insufficient funds: required " + price + ", available " + player.getMoney() + ); + } - player.withdrawMoney(price); - player.getPortfolio().addShare(this.getShare()); - player.getTransactionArchive().add(this); + player.withdrawMoney(price); + player.getPortfolio().addShare(this.getShare()); + player.getTransactionArchive().add(this); - this.committed = true; - } + this.committed = true; + } } diff --git a/src/main/java/Model/PurchaseCalculator.java b/src/main/java/Model/PurchaseCalculator.java index 6a29413..e2a7482 100644 --- a/src/main/java/Model/PurchaseCalculator.java +++ b/src/main/java/Model/PurchaseCalculator.java @@ -1,30 +1,64 @@ package Model; + import java.math.BigDecimal; +/** + * Calculates costs and totals for purchase transactions. + * A PurchaseCalculator computes the gross cost (quantity × price), applies + * a commission fee, and produces the total transaction cost. + */ public class PurchaseCalculator implements TransactionCalculator { - private BigDecimal purchasePrice; - private BigDecimal quantity; - - public PurchaseCalculator(Share share) { - this.purchasePrice = share.getPurchasePrice(); - this.quantity = share.getQuantity(); - } + private final BigDecimal purchasePrice; + private final BigDecimal quantity; + + /** + * Constructs a PurchaseCalculator for the given share. + * + * @param share the share being purchased. Cannot be null + */ + public PurchaseCalculator(Share share) { + this.purchasePrice = share.getPurchasePrice(); + this.quantity = share.getQuantity(); + } - public BigDecimal calculateGross() { - return this.purchasePrice.multiply(this.quantity); - } + /** + * Calculates the gross cost without fees (quantity × purchase price). + * + * @return the gross cost + */ + @Override + public BigDecimal calculateGross() { + return this.purchasePrice.multiply(this.quantity); + } - public BigDecimal calculateCommission() { - BigDecimal rate = new BigDecimal("0.005"); - return calculateGross().multiply(rate); - } + /** + * Calculates the purchase commission (0.5% of gross). + * + * @return the commission fee + */ + @Override + public BigDecimal calculateCommission() { + BigDecimal rate = new BigDecimal("0.005"); + return calculateGross().multiply(rate); + } - public BigDecimal calculateTax() { - BigDecimal tax = new BigDecimal("0"); - return tax; - } + /** + * Calculates the tax on purchase (always 0 for purchases). + * + * @return zero + */ + @Override + public BigDecimal calculateTax() { + return BigDecimal.ZERO; + } - public BigDecimal calculateTotal() { - return calculateGross().add(calculateCommission()).add(calculateTax()); - } + /** + * Calculates the total purchase cost (gross + commission + tax). + * + * @return the total transaction cost + */ + @Override + public BigDecimal calculateTotal() { + return calculateGross().add(calculateCommission()).add(calculateTax()); + } } diff --git a/src/main/java/Model/Sale.java b/src/main/java/Model/Sale.java index 7653df4..8b2e05e 100644 --- a/src/main/java/Model/Sale.java +++ b/src/main/java/Model/Sale.java @@ -1,31 +1,52 @@ package Model; + import java.math.BigDecimal; +/** + * Represents a stock sale transaction. + * A Sale transaction represents a player selling shares from their portfolio. + * When committed, it adds money to the player's account, removes the shares from + * their portfolio, and records the transaction in the archive. + */ public class Sale extends Transaction { - public Sale(Share share, int week) { - super(share, week, new SaleCalculator(share)); - } + /** + * Constructs a new Sale transaction. + * + * @param share the share to sell. Cannot be null + * @param week the week number when the sale occurs. Must be at least 1 + */ + public Sale(Share share, int week) { + super(share, week, new SaleCalculator(share)); + } - @Override - public void commit(Player player) { - if (player == null) { - throw new IllegalArgumentException("Player cannot be null"); - } - if (isCommitted()) { - throw new IllegalStateException("Sale has already been committed"); - } - if (!player.getPortfolio().contains(this.getShare())) { - throw new IllegalStateException( - "Share not found in player's portfolio: " + this.getShare().getStock().getSymbol() - ); - } + /** + * Commits this sale transaction, crediting funds and removing shares from the portfolio. + * + * @param player the player executing the sale. Cannot be null + * @throws IllegalArgumentException if player is null + * @throws IllegalStateException if sale has already been committed or if the + * player does not own the share being sold + */ + @Override + public void commit(Player player) { + if (player == null) { + throw new IllegalArgumentException("Player cannot be null"); + } + if (isCommitted()) { + throw new IllegalStateException("Sale has already been committed"); + } + if (!player.getPortfolio().contains(this.getShare())) { + throw new IllegalStateException( + "Share not found in player's portfolio: " + this.getShare().getStock().getSymbol() + ); + } - BigDecimal price = getCalculator().calculateTotal(); + BigDecimal price = getCalculator().calculateTotal(); - player.addMoney(price); - player.getPortfolio().removeShare(this.getShare()); - player.getTransactionArchive().add(this); + player.addMoney(price); + player.getPortfolio().removeShare(this.getShare()); + player.getTransactionArchive().add(this); - this.committed = true; - } + this.committed = true; + } } diff --git a/src/main/java/Model/SaleCalculator.java b/src/main/java/Model/SaleCalculator.java index 27fd394..271225a 100644 --- a/src/main/java/Model/SaleCalculator.java +++ b/src/main/java/Model/SaleCalculator.java @@ -1,36 +1,72 @@ package Model; + import java.math.BigDecimal; -public class SaleCalculator implements TransactionCalculator{ - private BigDecimal purchasePrice; - private BigDecimal salesPrice; - private BigDecimal quantity; - - public SaleCalculator(Share share) { - this.purchasePrice = share.getPurchasePrice(); - this.salesPrice = share.getStock().getSalesPrice(); - this.quantity = share.getQuantity(); - } - - public BigDecimal calculateGross() { - return this.salesPrice.multiply(this.quantity); - } - - public BigDecimal calculateCommission() { - BigDecimal rate = new BigDecimal("0.01"); - return calculateGross().multiply(rate); - } - - public BigDecimal calculateTax() { - BigDecimal sellingCost = this.purchasePrice.multiply(this.quantity); - BigDecimal profit = calculateGross().subtract(calculateCommission()).subtract(sellingCost); - BigDecimal rate = new BigDecimal("0.3"); - return profit.multiply(rate); - } - - public BigDecimal calculateTotal() { - return calculateGross().subtract(calculateCommission()).subtract(calculateTax()); - } +/** + * Calculates costs, taxes, and proceeds for sale transactions. + * A SaleCalculator computes the gross revenue (quantity × sales price), applies + * a commission fee, calculates capital gains tax, and produces the net proceeds. + */ +public class SaleCalculator implements TransactionCalculator { + private final BigDecimal purchasePrice; + private final BigDecimal salesPrice; + private final BigDecimal quantity; + + /** + * Constructs a SaleCalculator for the given share. + * + * @param share the share being sold. Cannot be null + */ + public SaleCalculator(Share share) { + this.purchasePrice = share.getPurchasePrice(); + this.salesPrice = share.getStock().getSalesPrice(); + this.quantity = share.getQuantity(); + } + + /** + * Calculates the gross revenue (quantity × sales price). + * + * @return the gross revenue + */ + @Override + public BigDecimal calculateGross() { + return this.salesPrice.multiply(this.quantity); + } + + /** + * Calculates the sale commission (1% of gross). + * + * @return the commission fee + */ + @Override + public BigDecimal calculateCommission() { + BigDecimal rate = new BigDecimal("0.01"); + return calculateGross().multiply(rate); + } + + /** + * Calculates the capital gains tax (30% of profit). + * Profit is calculated as: revenue - commission - original purchase cost. + * + * @return the capital gains tax + */ + @Override + public BigDecimal calculateTax() { + BigDecimal sellingCost = this.purchasePrice.multiply(this.quantity); + BigDecimal profit = calculateGross().subtract(calculateCommission()).subtract(sellingCost); + BigDecimal rate = new BigDecimal("0.3"); + return profit.multiply(rate); + } + + /** + * Calculates the net proceeds (gross - commission - tax). + * + * @return the total amount credited to the player + */ + @Override + public BigDecimal calculateTotal() { + return calculateGross().subtract(calculateCommission()).subtract(calculateTax()); + } diff --git a/src/main/java/Model/Share.java b/src/main/java/Model/Share.java index 92d6101..05a4c08 100644 --- a/src/main/java/Model/Share.java +++ b/src/main/java/Model/Share.java @@ -1,44 +1,73 @@ package Model; + import java.math.BigDecimal; +/** + * Represents a share holding of a stock. + * A Share encapsulates a quantity of a stock purchased at a specific price. + * It provides information about the stock and the purchase terms. + */ public class Share { - private final Stock stock; - private final BigDecimal quantity; - private final BigDecimal purchasePrice; - - public Share(Stock stock, BigDecimal quantity, BigDecimal purchasePrice) { - if (stock == null) { - throw new IllegalArgumentException("Stock cannot be null"); - } - if (quantity == null) { - throw new IllegalArgumentException("Quantity cannot be null"); - } - if (quantity.compareTo(BigDecimal.ZERO) <= 0) { - throw new IllegalArgumentException("Quantity must be greater than zero"); - } - if (purchasePrice == null) { - throw new IllegalArgumentException("Purchase price cannot be null"); - } - if (purchasePrice.compareTo(BigDecimal.ZERO) < 0) { - throw new IllegalArgumentException("Purchase price cannot be negative"); - } - - this.stock = stock; - this.quantity = quantity; - this.purchasePrice = purchasePrice; - } + private final Stock stock; + private final BigDecimal quantity; + private final BigDecimal purchasePrice; - public Stock getStock() { - return stock; + /** + * Constructs a new Share with the specified stock, quantity, and purchase price. + * + * @param stock the Stock being held. Cannot be null + * @param quantity the number of shares held. Must be greater than zero + * @param purchasePrice the price per share at purchase. Cannot be null or negative + * @throws IllegalArgumentException if any parameter is invalid + */ + public Share(Stock stock, BigDecimal quantity, BigDecimal purchasePrice) { + if (stock == null) { + throw new IllegalArgumentException("Stock cannot be null"); + } + if (quantity == null) { + throw new IllegalArgumentException("Quantity cannot be null"); + } + if (quantity.compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalArgumentException("Quantity must be greater than zero"); } - - public BigDecimal getQuantity() { - return quantity; + if (purchasePrice == null) { + throw new IllegalArgumentException("Purchase price cannot be null"); } - - public BigDecimal getPurchasePrice() { - return purchasePrice; + if (purchasePrice.compareTo(BigDecimal.ZERO) < 0) { + throw new IllegalArgumentException("Purchase price cannot be negative"); } + + this.stock = stock; + this.quantity = quantity; + this.purchasePrice = purchasePrice; + } + + /** + * Returns the Stock associated with this share. + * + * @return the stock being held + */ + public Stock getStock() { + return stock; + } + + /** + * Returns the quantity of shares held. + * + * @return the number of shares + */ + public BigDecimal getQuantity() { + return quantity; + } + + /** + * Returns the price per share at the time of purchase. + * + * @return the purchase price + */ + public BigDecimal getPurchasePrice() { + return purchasePrice; + } } diff --git a/src/main/java/Model/Stock.java b/src/main/java/Model/Stock.java index 52c1fd2..3c6866e 100644 --- a/src/main/java/Model/Stock.java +++ b/src/main/java/Model/Stock.java @@ -1,77 +1,118 @@ package Model; + import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; +/** + * Represents a stock traded on the exchange. + * A Stock tracks the company's trading symbol, company name, and historical price + * data. Prices are maintained in chronological order, with the latest price being + * the current sales price. + */ public class Stock { - private final String symbol; - private final String company; - private final List prices; - - public Stock(String symbol, String company, BigDecimal salesPrice) { - if (symbol == null || symbol.isBlank()) { - throw new IllegalArgumentException("Symbol cannot be null or blank"); - } - if (company == null || company.isBlank()) { - throw new IllegalArgumentException("Company name cannot be null or blank"); - } - if (salesPrice == null) { - throw new IllegalArgumentException("Initial sales price cannot be null"); - } - if (salesPrice.compareTo(BigDecimal.ZERO) < 0) { - throw new IllegalArgumentException("Initial sales price cannot be negative"); - } + private final String symbol; + private final String company; + private final List prices; - this.symbol = symbol; - this.company = company; - this.prices = new ArrayList<>(); - this.prices.add(salesPrice); + /** + * Constructs a new Stock with the given symbol, company name, and initial sales price. + * + * @param symbol the unique ticker symbol (e.g., "AAPL"). Cannot be null or blank + * @param company the company name. Cannot be null or blank + * @param salesPrice the initial sales price. Cannot be null or negative + * @throws IllegalArgumentException if any parameter is invalid + */ + public Stock(String symbol, String company, BigDecimal salesPrice) { + if (symbol == null || symbol.isBlank()) { + throw new IllegalArgumentException("Symbol cannot be null or blank"); } - - public String getSymbol() { - return symbol; + if (company == null || company.isBlank()) { + throw new IllegalArgumentException("Company name cannot be null or blank"); } - - public String getCompany() { - return company; + if (salesPrice == null) { + throw new IllegalArgumentException("Initial sales price cannot be null"); } - - public BigDecimal getSalesPrice() { - return prices.get(prices.size() - 1); - } - - public void addNewSalesPrice(BigDecimal price) { - if (price == null) { - throw new IllegalArgumentException("Price cannot be null"); - } - if (price.compareTo(BigDecimal.ZERO) < 0) { - throw new IllegalArgumentException("Price cannot be negative"); - } - prices.add(price); + if (salesPrice.compareTo(BigDecimal.ZERO) < 0) { + throw new IllegalArgumentException("Initial sales price cannot be negative"); } - public List getHistoricalPrices() { - return new ArrayList<>(prices); // returnerer en kopi for å beskytte selve listen - } + this.symbol = symbol; + this.company = company; + this.prices = new ArrayList<>(); + this.prices.add(salesPrice); + } - public BigDecimal getHighestPrice() { - return prices.stream() - .reduce(prices.get(0), BigDecimal::max); - } + /** + * Returns the ticker symbol of this stock. + * + * @return the stock symbol + */ + public String getSymbol() { + return symbol; + } + + /** + * Returns the company name associated with this stock. + * + * @return the company name + */ + public String getCompany() { + return company; + } - public BigDecimal getLowestPrice() { - return prices.stream() - .reduce(prices.get(0), BigDecimal::min); + /** + * Returns the current (most recent) sales price of this stock. + * + * @return the current sales price + */ + public BigDecimal getSalesPrice() { + return prices.get(prices.size() - 1); + } + + /** + * Records a new sales price for this stock. + * + * @param price the new sales price. Cannot be null or negative + * @throws IllegalArgumentException if price is invalid + */ + public void addNewSalesPrice(BigDecimal price) { + if (price == null) { + throw new IllegalArgumentException("Price cannot be null"); + } + if (price.compareTo(BigDecimal.ZERO) < 0) { + throw new IllegalArgumentException("Price cannot be negative"); } + prices.add(price); + } - public BigDecimal getLatestPriceChange() { - if (prices.size() < 2) { - return BigDecimal.ZERO; - } + public List getHistoricalPrices() { + return new ArrayList<>(prices); // returns a copy to protect the list itself + } - BigDecimal latest = prices.get(prices.size() - 1); - BigDecimal previous = prices.get(prices.size() - 2); - return latest.subtract(previous); + public BigDecimal getHighestPrice() { + return prices.stream() + .reduce(prices.get(0), BigDecimal::max); + } + + public BigDecimal getLowestPrice() { + return prices.stream() + .reduce(prices.get(0), BigDecimal::min); + } + + /** + * Method that gets the latest price change. + * + * @return returns the price change + */ + public BigDecimal getLatestPriceChange() { + if (prices.size() < 2) { + return BigDecimal.ZERO; } + + BigDecimal latest = prices.get(prices.size() - 1); + BigDecimal previous = prices.get(prices.size() - 2); + return latest.subtract(previous); + } } diff --git a/src/main/java/Model/Transaction.java b/src/main/java/Model/Transaction.java index de68854..a8eb4f1 100644 --- a/src/main/java/Model/Transaction.java +++ b/src/main/java/Model/Transaction.java @@ -1,42 +1,84 @@ package Model; + +/** + * Abstract base class for all transactions (purchases and sales). + * A Transaction represents a trading action taken by a player on a specific week. + * Transactions use the Strategy pattern via TransactionCalculator to compute costs + * and can be committed to apply their effects to the player's portfolio. + */ public abstract class Transaction { - private Share share; - private int week; - private TransactionCalculator calculator; - protected boolean committed; - - protected Transaction(Share share, int week, TransactionCalculator calculator) { - if (share == null) { - throw new IllegalArgumentException("Share cannot be null"); - } - if (week < 1) { - throw new IllegalArgumentException("Week must be at least 1"); - } - if (calculator == null) { - throw new IllegalArgumentException("Calculator cannot be null"); - } - - this.share = share; - this.week = week; - this.calculator = calculator; - } + private Share share; + private int week; + private TransactionCalculator calculator; + protected boolean committed; - public Share getShare() { - return this.share; + /** + * Constructs a Transaction for the specified share in the given week. + * + * @param share the share being traded. Cannot be null + * @param week the week number when the transaction occurs. Must be at least 1 + * @param calculator the calculator for transaction costs. Cannot be null + * @throws IllegalArgumentException if any parameter is invalid + */ + protected Transaction(Share share, int week, TransactionCalculator calculator) { + if (share == null) { + throw new IllegalArgumentException("Share cannot be null"); } - - public int getWeek() { - return this.week; + if (week < 1) { + throw new IllegalArgumentException("Week must be at least 1"); } - - public TransactionCalculator getCalculator() { - return this.calculator; + if (calculator == null) { + throw new IllegalArgumentException("Calculator cannot be null"); } - public boolean isCommitted() { - return this.committed; - } + this.share = share; + this.week = week; + this.calculator = calculator; + } + + /** + * Returns the share being traded in this transaction. + * + * @return the share + */ + public Share getShare() { + return this.share; + } + + /** + * Returns the week number when this transaction occurs. + * + * @return the week number + */ + public int getWeek() { + return this.week; + } + + /** + * Returns the calculator used to compute transaction costs. + * + * @return the transaction calculator + */ + public TransactionCalculator getCalculator() { + return this.calculator; + } + + /** + * Returns whether this transaction has been committed. + * + * @return true if committed; false otherwise + */ + public boolean isCommitted() { + return this.committed; + } - public abstract void commit(Player player); + /** + * Commits this transaction, applying its effects to the player's portfolio. + * + * @param player the player executing the transaction. Cannot be null + * @throws IllegalArgumentException if player is null + * @throws IllegalStateException if the transaction has already been committed + */ + public abstract void commit(Player player); } diff --git a/src/main/java/Model/TransactionArchive.java b/src/main/java/Model/TransactionArchive.java index 149e2be..4af5bff 100644 --- a/src/main/java/Model/TransactionArchive.java +++ b/src/main/java/Model/TransactionArchive.java @@ -1,45 +1,100 @@ package Model; + import java.util.ArrayList; -import java.util.stream.Collectors; import java.util.List; +import java.util.stream.Collectors; +/** + * Maintains a chronological archive of all transactions executed by a player. + * The TransactionArchive allows retrieval of transactions by week and type + * (purchase or sale), and provides statistics about trading activity. + */ public class TransactionArchive { - private List transactions; + private final List transactions; - public TransactionArchive() { - this.transactions = new ArrayList<>(); - } + /** + * Constructs a new empty TransactionArchive. + */ + public TransactionArchive() { + this.transactions = new ArrayList<>(); + } - public boolean add(Transaction transaction) { - if (transaction == null) { - throw new IllegalArgumentException("Should not be null"); // Eller NullPointerExeption? - } + /** + * Adds a transaction to the archive. + * + * @param transaction the transaction to add. Cannot be null + * @return true if the transaction was added successfully + * @throws IllegalArgumentException if transaction is null + */ + public boolean add(Transaction transaction) { + if (transaction == null) { + throw new IllegalArgumentException("Should not be null"); + } - return transactions.add(transaction); + return transactions.add(transaction); + } - } + /** + * Checks if the archive is empty. + * + * @return true if no transactions have been recorded + */ + public boolean isEmpty() { + return transactions.isEmpty(); + } - public boolean isEmpty() { - return transactions.isEmpty(); - } + /** + * Returns all transactions that occurred in a specific week. + * + * @param week the week number + * @return a list of transactions in that week + */ + public List getTransactions(int week) { + return transactions.stream() + .filter(transaction -> transaction.getWeek() == week).collect(Collectors.toList()); + } - public List getTransactions(int week) { - return transactions.stream().filter(transaction -> transaction.getWeek() == week).collect(Collectors.toList()); - } - - public List getPurchase(int week) { - return transactions.stream().filter(transaction -> transaction.getWeek() == week).filter(purchase -> purchase instanceof Purchase).map(transaction -> (Purchase) transaction).collect(Collectors.toList()); - } + /** + * Returns all purchases that occurred in a specific week. + * + * @param week the week number + * @return a list of purchases in that week + */ + public List getPurchase(int week) { + return transactions.stream() + .filter(transaction -> transaction.getWeek() == week) + .filter(purchase -> purchase instanceof Purchase) + .map(transaction -> (Purchase) transaction).collect(Collectors.toList()); + } - public List getSale(int week) { - return transactions.stream().filter(transaction -> transaction.getWeek() == week).filter(sale -> sale instanceof Sale).map(transaction -> (Sale) transaction).collect(Collectors.toList()); - } + /** + * Returns all sales that occurred in a specific week. + * + * @param week the week number + * @return a list of sales in that week + */ + public List getSale(int week) { + return transactions.stream() + .filter(transaction -> transaction.getWeek() == week) + .filter(sale -> sale instanceof Sale) + .map(transaction -> (Sale) transaction).collect(Collectors.toList()); + } - public List getAllTransactions() { - return new ArrayList<>(transactions); - } + /** + * Returns a defensive copy of all transactions in the archive. + * + * @return a copy of the transaction list + */ + public List getAllTransactions() { + return new ArrayList<>(transactions); + } - public int countDistinctWeeks() { - return (int) transactions.stream().map(Transaction::getWeek).distinct().count(); - } + /** + * Counts the number of distinct weeks with trading activity. + * + * @return the number of unique weeks represented in the archive + */ + public int countDistinctWeeks() { + return (int) transactions.stream().map(Transaction::getWeek).distinct().count(); + } } diff --git a/src/main/java/Model/TransactionCalculator.java b/src/main/java/Model/TransactionCalculator.java index 560992d..2691e02 100644 --- a/src/main/java/Model/TransactionCalculator.java +++ b/src/main/java/Model/TransactionCalculator.java @@ -1,11 +1,40 @@ package Model; + import java.math.BigDecimal; +/** + * Strategy interface for calculating transaction costs and proceeds. + * Implementations compute fees and totals for different transaction types + * (purchases and sales), following the Strategy design pattern. + */ public interface TransactionCalculator { - // Methods in interface is automatically public abstract. - BigDecimal calculateGross(); - BigDecimal calculateCommission(); - BigDecimal calculateTax(); - BigDecimal calculateTotal(); + /** + * Calculates the base cost/revenue without fees. + * + * @return the gross amount + */ + BigDecimal calculateGross(); + + /** + * Calculates the commission fee on the transaction. + * + * @return the commission amount + */ + BigDecimal calculateCommission(); + + /** + * Calculates any applicable taxes on the transaction. + * + * @return the tax amount + */ + BigDecimal calculateTax(); + + /** + * Calculates the final total after all fees and taxes. + * + * @return the total transaction amount + */ + BigDecimal calculateTotal(); + } diff --git a/src/main/java/Model/TransactionFactory.java b/src/main/java/Model/TransactionFactory.java index a3b8c9a..1f799d1 100644 --- a/src/main/java/Model/TransactionFactory.java +++ b/src/main/java/Model/TransactionFactory.java @@ -1,22 +1,33 @@ package Model; /** - * Factory for creating transaction objects + * Factory for creating transaction objects. + * Uses the Factory pattern to encapsulate transaction creation logic, + * allowing clients to create Purchase and Sale transactions without + * knowing implementation details. */ public class TransactionFactory { - /** - * Create purchase transaction - */ - public static Transaction createPurchase(Share share, int week) { - return new Purchase(share, week); - } + /** + * Creates a new Purchase transaction. + * + * @param share the share to purchase. Cannot be null + * @param week the week when the purchase occurs. Must be at least 1 + * @return a new Purchase transaction + */ + public static Transaction createPurchase(Share share, int week) { + return new Purchase(share, week); + } - /** - * Create sale transaction - */ - public static Transaction createSale(Share share, int week) { - return new Sale(share, week); - } + /** + * Creates a new Sale transaction. + * + * @param share the share to sell. Cannot be null + * @param week the week when the sale occurs. Must be at least 1 + * @return a new Sale transaction + */ + public static Transaction createSale(Share share, int week) { + return new Sale(share, week); + } } diff --git a/src/main/java/View/GameSetupScene.java b/src/main/java/View/GameSetupScene.java index 0ed8ded..44052ca 100644 --- a/src/main/java/View/GameSetupScene.java +++ b/src/main/java/View/GameSetupScene.java @@ -1,6 +1,12 @@ package View; -import javafx.geometry.Insets; +import Controller.StockFileHandler; +import Model.Exchange; +import Model.Stock; +import java.io.File; +import java.math.BigDecimal; +import java.util.List; +import java.util.function.Consumer; import javafx.geometry.Pos; import javafx.scene.Scene; import javafx.scene.control.*; @@ -8,220 +14,259 @@ import javafx.stage.FileChooser; import javafx.stage.Stage; -import Controller.StockFileHandler; -import Model.Exchange; -import Model.Stock; -import java.io.File; -import java.math.BigDecimal; -import java.util.List; -import java.util.function.Consumer; +/** + * Displays the game setup scene where players configure their game parameters. + * Allows players to enter their name, starting capital, and select a stock data file + * before starting the main game. Notifies listeners when the game starts. + */ public class GameSetupScene { - private Scene scene; - private Consumer onGameStart; - private File selectedFile; - private Label fileLabel; - - public GameSetupScene(Consumer onGameStart) { - this.onGameStart = onGameStart; - this.scene = createScene(); + private final Scene scene; + private final Consumer onGameStart; + private File selectedFile; + private Label fileLabel; + + /** + * Constructs a GameSetupScene with a callback for when the game starts. + * + * @param onGameStart callback invoked when the player starts the game. Cannot be null + */ + public GameSetupScene(Consumer onGameStart) { + this.onGameStart = onGameStart; + this.scene = createScene(); + } + + /** + * Creates the UI layout for the setup scene. + * + * @return the constructed Scene + */ + private Scene createScene() { + + // Card (centered cream box) + VBox card = new VBox(20); + card.getStyleClass().add("card"); + card.setMaxWidth(560); + card.setMinWidth(400); + + // Title + Label titleLabel = new Label("Stock Trading Game"); + titleLabel.getStyleClass().add("title-label"); + + // Spacer below title + Region titleSpacer = new Region(); + titleSpacer.setMinHeight(6); + + // Player name row + HBox nameBox = new HBox(16); + nameBox.setAlignment(Pos.CENTER_LEFT); + nameBox.setMaxWidth(Double.MAX_VALUE); + + Label nameLabel = new Label("Player:"); + nameLabel.setMinWidth(130); + nameLabel.getStyleClass().add("form-label"); + + TextField nameField = new TextField(); + nameField.setPromptText("Enter your name"); + HBox.setHgrow(nameField, Priority.ALWAYS); + + nameBox.getChildren().addAll(nameLabel, nameField); + + // Starting capital row + HBox capitalBox = new HBox(16); + capitalBox.setAlignment(Pos.CENTER_LEFT); + capitalBox.setMaxWidth(Double.MAX_VALUE); + + Label capitalLabel = new Label("Start Capital:"); + capitalLabel.setMinWidth(130); + capitalLabel.getStyleClass().add("form-label"); + + TextField capitalField = new TextField(); + capitalField.setPromptText("e.g. 10000"); + HBox.setHgrow(capitalField, Priority.ALWAYS); + + capitalBox.getChildren().addAll(capitalLabel, capitalField); + + // File selection row + HBox fileBox = new HBox(16); + fileBox.setAlignment(Pos.CENTER_LEFT); + fileBox.setMaxWidth(Double.MAX_VALUE); + + Label fileSelectLabel = new Label("Stock Data File:"); + fileSelectLabel.setMinWidth(130); + fileSelectLabel.getStyleClass().add("form-label"); + + fileLabel = new Label("No file selected"); + fileLabel.getStyleClass().add("file-label"); + HBox.setHgrow(fileLabel, Priority.ALWAYS); + + Button browseButton = new Button("Browse"); + browseButton.getStyleClass().add("browse-button"); + browseButton.setOnAction(e -> selectFile()); + + fileBox.getChildren().addAll(fileSelectLabel, fileLabel, browseButton); + + // Spacer before buttons + Region buttonSpacer = new Region(); + buttonSpacer.setMinHeight(10); + + // Button row + HBox buttonBox = new HBox(12); + buttonBox.setAlignment(Pos.CENTER_LEFT); + buttonBox.setMaxWidth(Double.MAX_VALUE); + + Button startButton = new Button("Start Game"); + startButton.getStyleClass().add("start-button"); + startButton.setMaxWidth(Double.MAX_VALUE); + startButton.setOnAction(e -> handleStart(nameField, capitalField)); + HBox.setHgrow(startButton, Priority.ALWAYS); + + Button exitButton = new Button("Exit"); + exitButton.getStyleClass().add("exit-button"); + exitButton.setMaxWidth(Double.MAX_VALUE); + exitButton.setOnAction(e -> System.exit(0)); + HBox.setHgrow(exitButton, Priority.ALWAYS); + + buttonBox.getChildren().addAll(startButton, exitButton); + + card.getChildren().addAll( + titleLabel, + titleSpacer, + nameBox, + capitalBox, + fileBox, + buttonSpacer, + buttonBox + ); + + // Centers the root + VBox root = new VBox(card); + root.setAlignment(Pos.CENTER); + root.setFillWidth(false); + + Scene scene = new Scene(root, 800, 550); + scene.getStylesheets().add( + getClass().getResource("/Style/Global.css").toExternalForm() + ); + + return scene; + } + + /** + * Validates inputs and starts the game when the Start button is clicked. + * + * @param nameField the text field containing the player name + * @param capitalField the text field containing the starting capital + */ + private void handleStart(TextField nameField, TextField capitalField) { + String name = nameField.getText().trim(); + String capitalStr = capitalField.getText().trim(); + + if (name.isEmpty()) { + showAlert("Validation Error", "Please enter a player name"); + return; } - private Scene createScene() { - - // Card (centered cream box) - VBox card = new VBox(20); - card.getStyleClass().add("card"); - card.setMaxWidth(560); - card.setMinWidth(400); - - // Title - Label titleLabel = new Label("Stock Trading Game"); - titleLabel.getStyleClass().add("title-label"); - - // Spacer below title - Region titleSpacer = new Region(); - titleSpacer.setMinHeight(6); - - // Player name row - HBox nameBox = new HBox(16); - nameBox.setAlignment(Pos.CENTER_LEFT); - nameBox.setMaxWidth(Double.MAX_VALUE); - - Label nameLabel = new Label("Player:"); - nameLabel.setMinWidth(130); - nameLabel.getStyleClass().add("form-label"); - - TextField nameField = new TextField(); - nameField.setPromptText("Enter your name"); - HBox.setHgrow(nameField, Priority.ALWAYS); - - nameBox.getChildren().addAll(nameLabel, nameField); - - // Starting capital row - HBox capitalBox = new HBox(16); - capitalBox.setAlignment(Pos.CENTER_LEFT); - capitalBox.setMaxWidth(Double.MAX_VALUE); - - Label capitalLabel = new Label("Start Capital:"); - capitalLabel.setMinWidth(130); - capitalLabel.getStyleClass().add("form-label"); - - TextField capitalField = new TextField(); - capitalField.setPromptText("e.g. 10000"); - HBox.setHgrow(capitalField, Priority.ALWAYS); - - capitalBox.getChildren().addAll(capitalLabel, capitalField); - - // File selection row - HBox fileBox = new HBox(16); - fileBox.setAlignment(Pos.CENTER_LEFT); - fileBox.setMaxWidth(Double.MAX_VALUE); - - Label fileSelectLabel = new Label("Stock Data File:"); - fileSelectLabel.setMinWidth(130); - fileSelectLabel.getStyleClass().add("form-label"); - - fileLabel = new Label("No file selected"); - fileLabel.getStyleClass().add("file-label"); - HBox.setHgrow(fileLabel, Priority.ALWAYS); - - Button browseButton = new Button("Browse"); - browseButton.getStyleClass().add("browse-button"); - browseButton.setOnAction(e -> selectFile()); - - fileBox.getChildren().addAll(fileSelectLabel, fileLabel, browseButton); - - // Spacer before buttons - Region buttonSpacer = new Region(); - buttonSpacer.setMinHeight(10); - - // Button row — buttons fill full card width equally - HBox buttonBox = new HBox(12); - buttonBox.setAlignment(Pos.CENTER_LEFT); - buttonBox.setMaxWidth(Double.MAX_VALUE); - - Button startButton = new Button("Start Game"); - startButton.getStyleClass().add("start-button"); - startButton.setMaxWidth(Double.MAX_VALUE); - startButton.setOnAction(e -> handleStart(nameField, capitalField)); - HBox.setHgrow(startButton, Priority.ALWAYS); - - Button exitButton = new Button("Exit"); - exitButton.getStyleClass().add("exit-button"); - exitButton.setMaxWidth(Double.MAX_VALUE); - exitButton.setOnAction(e -> System.exit(0)); - HBox.setHgrow(exitButton, Priority.ALWAYS); - - buttonBox.getChildren().addAll(startButton, exitButton); - - card.getChildren().addAll( - titleLabel, - titleSpacer, - nameBox, - capitalBox, - fileBox, - buttonSpacer, - buttonBox - ); - - // Center the card without stretching it vertically - VBox root = new VBox(card); - root.setAlignment(Pos.CENTER); - root.setFillWidth(false); - - Scene scene = new Scene(root, 800, 550); - scene.getStylesheets().add( - getClass().getResource("/Style/Global.css").toExternalForm() - ); - - return scene; + if (capitalStr.isEmpty()) { + showAlert("Validation Error", "Please enter starting capital"); + return; } - private void handleStart(TextField nameField, TextField capitalField) { - String name = nameField.getText().trim(); - String capitalStr = capitalField.getText().trim(); - - if (name.isEmpty()) { - showAlert("Validation Error", "Please enter a player name"); - return; - } - - if (capitalStr.isEmpty()) { - showAlert("Validation Error", "Please enter starting capital"); - return; - } - - if (selectedFile == null) { - showAlert("Validation Error", "Please select a stock data file"); - return; - } - - try { - BigDecimal capital = new BigDecimal(capitalStr); - - if (capital.compareTo(BigDecimal.ZERO) <= 0) { - showAlert("Validation Error", "Starting capital must be greater than 0"); - return; - } - - StockFileHandler fileHandler = new StockFileHandler(); - List stocks = fileHandler.loadStocksFromFile(selectedFile.getAbsolutePath()); - - if (stocks.isEmpty()) { - showAlert("Error", "No stocks found in the selected file"); - return; - } - - Exchange exchange = new Exchange("Main Exchange", stocks); - onGameStart.accept(new StartGameData(name, capital, exchange)); - - } catch (NumberFormatException ex) { - showAlert("Validation Error", "Starting capital must be a valid number"); - } catch (Exception ex) { - showAlert("Error", "Failed to load file: " + ex.getMessage()); - } + if (selectedFile == null) { + showAlert("Validation Error", "Please select a stock data file"); + return; } - private void selectFile() { - FileChooser fileChooser = new FileChooser(); - fileChooser.setTitle("Select Stock Data File"); - fileChooser.getExtensionFilters().addAll( - new FileChooser.ExtensionFilter("CSV Files", "*.csv"), - new FileChooser.ExtensionFilter("All Files", "*.*") - ); + try { + BigDecimal capital = new BigDecimal(capitalStr); - File file = fileChooser.showOpenDialog(new Stage()); + if (capital.compareTo(BigDecimal.ZERO) <= 0) { + showAlert("Validation Error", "Starting capital must be greater than 0"); + return; + } - if (file != null) { - selectedFile = file; - fileLabel.setText(file.getName()); - } - } + StockFileHandler fileHandler = new StockFileHandler(); + List stocks = fileHandler.loadStocksFromFile(selectedFile.getAbsolutePath()); - private void showAlert(String title, String message) { - Alert alert = new Alert(Alert.AlertType.WARNING); - alert.setTitle(title); - alert.setHeaderText(null); - alert.setContentText(message); - alert.showAndWait(); - } + if (stocks.isEmpty()) { + showAlert("Error", "No stocks found in the selected file"); + return; + } - public Scene getScene() { - return scene; - } + Exchange exchange = new Exchange("Main Exchange", stocks); + onGameStart.accept(new StartGameData(name, capital, exchange)); - public static class StartGameData { - public final String playerName; - public final BigDecimal startingCapital; - public final Exchange exchange; - - public StartGameData(String playerName, BigDecimal startingCapital, Exchange exchange) { - this.playerName = playerName; - this.startingCapital = startingCapital; - this.exchange = exchange; - } + } catch (NumberFormatException ex) { + showAlert("Validation Error", "Starting capital must be a valid number"); + } catch (Exception ex) { + showAlert("Error", "Failed to load file: " + ex.getMessage()); + } + } + + /** + * Opens a file chooser dialog to select a stock data file. + */ + private void selectFile() { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Select Stock Data File"); + fileChooser.getExtensionFilters().addAll( + new FileChooser.ExtensionFilter("CSV Files", "*.csv"), + new FileChooser.ExtensionFilter("All Files", "*.*") + ); + + File file = fileChooser.showOpenDialog(new Stage()); + + if (file != null) { + selectedFile = file; + fileLabel.setText(file.getName()); + } + } + + /** + * Displays an alert dialog with the given title and message. + * + * @param title the dialog title + * @param message the alert message + */ + private void showAlert(String title, String message) { + Alert alert = new Alert(Alert.AlertType.WARNING); + alert.setTitle(title); + alert.setHeaderText(null); + alert.setContentText(message); + alert.showAndWait(); + } + + /** + * Returns the Scene for display in the application window. + * + * @return the setup scene + */ + public Scene getScene() { + return scene; + } + + /** + * Data class encapsulating game setup parameters. + * Contains the exchange configured with stocks, player name, and starting capital. + */ + public static class StartGameData { + public final String playerName; + public final BigDecimal startingCapital; + public final Exchange exchange; + + /** + * StartGameData method that includes playername, startingcapital and exchange. + * + * @param playerName the player's name + * @param startingCapital startingcapital of the player + * @param exchange exchange class + */ + public StartGameData(String playerName, BigDecimal startingCapital, Exchange exchange) { + this.playerName = playerName; + this.startingCapital = startingCapital; + this.exchange = exchange; } + } } diff --git a/src/main/java/View/Launcher.java b/src/main/java/View/Launcher.java index 0dd5db2..fb7bc49 100644 --- a/src/main/java/View/Launcher.java +++ b/src/main/java/View/Launcher.java @@ -1,10 +1,17 @@ package View; /** - * Launcher class for the Stock Trading Game + * Entry point for launching the Stock Trading Game application. + * This launcher delegates to the main StockTradingGameApp class to start the JavaFX + * application. It provides a conventional main method entry point. */ public class Launcher { - public static void main(String[] args) { - StockTradingGameApp.main(args); - } + /** + * Main method to launch the application. + * + * @param args command-line arguments (not used) + */ + public static void main(String[] args) { + StockTradingGameApp.main(args); + } } diff --git a/src/main/java/View/MainGameScene.java b/src/main/java/View/MainGameScene.java index 749d2da..6f4cd27 100644 --- a/src/main/java/View/MainGameScene.java +++ b/src/main/java/View/MainGameScene.java @@ -1,656 +1,693 @@ package View; -import javafx.geometry.Insets; +import Model.*; +import java.math.BigDecimal; +import java.util.List; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; import javafx.geometry.Pos; import javafx.scene.Scene; import javafx.scene.control.*; import javafx.scene.layout.*; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import Model.*; -import java.math.BigDecimal; -import java.util.List; /** - * Main game UI. Implements ExchangeObserver so the view automatically refreshes - * whenever the Exchange notifies it of a state change (week advance or trade). + * The main game user interface scene. + * Displays the game board with stocks, portfolio, trading interface, and transaction + * history. Implements ExchangeObserver to automatically refresh UI when the game state changes. */ public class MainGameScene implements ExchangeObserver { - private Scene scene; - private Exchange exchange; - private Player player; - private Runnable onExit; - private Label statusLabel; - - // References to UI components that need refreshing - private TableView portfolioTable; - private ListView holdingsList; - private TableView historyTable; - private ComboBox weekFilterCombo; - - public MainGameScene(Exchange exchange, Player player, Runnable onExit) { - this.exchange = exchange; - this.player = player; - this.onExit = onExit; - - // Register this view as an observer of the exchange - this.exchange.addObserver(this); - - this.scene = createScene(); - } - - /** - * Called automatically by Exchange whenever its state changes. - * Refreshes all UI elements to reflect the latest data. - */ - @Override - public void onExchangeUpdated(Exchange exchange) { - updateStatus(); - refreshAllUI(); - } - - private Scene createScene() { - VBox root = new VBox(0); - - // TOP STATUS BAR - HBox topBar = new HBox(12); - topBar.getStyleClass().add("top-bar"); - topBar.setAlignment(Pos.CENTER_LEFT); - - statusLabel = new Label(); - statusLabel.getStyleClass().add("status-label"); - updateStatus(); - - Separator sep = new Separator(javafx.geometry.Orientation.VERTICAL); - sep.setPrefHeight(20); - - Button nextWeekBtn = new Button("Next week"); - nextWeekBtn.getStyleClass().addAll("action-button"); - nextWeekBtn.setOnAction(e -> exchange.advance()); // observer handles the UI update - - Button exitBtn = new Button("Exit"); - exitBtn.getStyleClass().add("exit-button"); - exitBtn.setOnAction(e -> { - if (confirm("Exit Game?", "Final Net Worth: $" + formatMoney(getNetWorth()))) { - exchange.removeObserver(this); // clean up before closing - onExit.run(); - } - }); - - topBar.getChildren().addAll(statusLabel, sep, nextWeekBtn, exitBtn); - - // MAIN CONTENT TABS - TabPane tabs = new TabPane(); - tabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE); - tabs.getTabs().addAll( - new Tab("Stocks", createStocksPanel()), - new Tab("Portfolio", createPortfolioPanel()), - new Tab("Trade", createTradePanel()), - new Tab("History", createHistoryPanel()) - ); - - VBox.setVgrow(tabs, Priority.ALWAYS); - root.getChildren().addAll(topBar, tabs); - - Scene scene = new Scene(root, 1000, 700); - scene.getStylesheets().add( - getClass().getResource("/Style/Global.css").toExternalForm() - ); - return scene; - } - - private VBox createStocksPanel() { - VBox panel = new VBox(10); - panel.getStyleClass().add("content-area"); - - HBox searchBox = new HBox(8); - searchBox.setAlignment(Pos.CENTER_LEFT); - - TextField search = new TextField(); - search.setPromptText("Search symbol or company..."); - HBox.setHgrow(search, Priority.ALWAYS); - - Button searchBtn = new Button("Search"); - searchBtn.getStyleClass().add("action-button"); - - ComboBox filter = new ComboBox<>(); - filter.setItems(FXCollections.observableArrayList("All", "Gainers", "Losers")); - filter.setValue("All"); - - TableView table = new TableView<>(); - addStockColumns(table); - - Runnable loadStocks = () -> { - List stocks; - String filterVal = filter.getValue(); - String searchVal = search.getText().trim(); - - if ("Gainers".equals(filterVal)) { - stocks = exchange.getGainers(20); - } else if ("Losers".equals(filterVal)) { - stocks = exchange.getLosers(20); - } else { - stocks = exchange.findStocks(searchVal); - } - - ObservableList data = FXCollections.observableArrayList(); - for (Stock s : stocks) { - data.add(new StockRow(s)); - } - table.setItems(data); + private final Scene scene; + private final Exchange exchange; + private final Player player; + private final Runnable onExit; + private Label statusLabel; + + // References to UI components that need refreshing + private TableView portfolioTable; + private ListView holdingsList; + private TableView historyTable; + private ComboBox weekFilterCombo; + + /** + * Constructs the main game scene. + * + * @param exchange the Exchange managing the game state. Cannot be null + * @param player the Player being displayed. Cannot be null + * @param onExit callback to run when exiting the game. Cannot be null + */ + public MainGameScene(Exchange exchange, Player player, Runnable onExit) { + this.exchange = exchange; + this.player = player; + this.onExit = onExit; + + // Register this view as an observer of the exchange + this.exchange.addObserver(this); + + this.scene = createScene(); + } + + /** + * Callback invoked when the Exchange notifies observers of state changes. + * + * @param exchange the Exchange that changed + */ + @Override + public void onExchangeUpdated(Exchange exchange) { + updateStatus(); + refreshAllUI(); + } + + /** + * Creates the main game scene layout. + * + * @return the constructed Scene + */ + private Scene createScene() { + VBox root = new VBox(0); + + // TOP STATUS BAR + HBox topBar = new HBox(12); + topBar.getStyleClass().add("top-bar"); + topBar.setAlignment(Pos.CENTER_LEFT); + + statusLabel = new Label(); + statusLabel.getStyleClass().add("status-label"); + updateStatus(); + + Separator sep = new Separator(javafx.geometry.Orientation.VERTICAL); + sep.setPrefHeight(20); + + Button nextWeekBtn = new Button("Next week"); + nextWeekBtn.getStyleClass().addAll("action-button"); + nextWeekBtn.setOnAction(e -> exchange.advance()); // observer handles the UI update + + Button exitBtn = new Button("Exit"); + exitBtn.getStyleClass().add("exit-button"); + exitBtn.setOnAction(e -> { + if (confirm("Exit Game?", "Final Net Worth: $" + formatMoney(getNetWorth()))) { + exchange.removeObserver(this); // clean up before closing + onExit.run(); + } + }); + + topBar.getChildren().addAll(statusLabel, sep, nextWeekBtn, exitBtn); + + // MAIN CONTENT TABS + TabPane tabs = new TabPane(); + tabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE); + tabs.getTabs().addAll( + new Tab("Stocks", createStocksPanel()), + new Tab("Portfolio", createPortfolioPanel()), + new Tab("Trade", createTradePanel()), + new Tab("History", createHistoryPanel()) + ); + + VBox.setVgrow(tabs, Priority.ALWAYS); + root.getChildren().addAll(topBar, tabs); + + Scene scene = new Scene(root, 1000, 700); + scene.getStylesheets().add( + getClass().getResource("/Style/Global.css").toExternalForm() + ); + return scene; + } + + /** + * Creates the stocks panel displaying available stocks. + * + * @return the stocks panel + */ + private VBox createStocksPanel() { + VBox panel = new VBox(10); + panel.getStyleClass().add("content-area"); + + HBox searchBox = new HBox(8); + searchBox.setAlignment(Pos.CENTER_LEFT); + + TextField search = new TextField(); + search.setPromptText("Search symbol or company..."); + HBox.setHgrow(search, Priority.ALWAYS); + + Button searchBtn = new Button("Search"); + searchBtn.getStyleClass().add("action-button"); + + ComboBox filter = new ComboBox<>(); + filter.setItems(FXCollections.observableArrayList("All", "Gainers", "Losers")); + filter.setValue("All"); + + TableView table = new TableView<>(); + addStockColumns(table); + + Runnable loadStocks = () -> { + List stocks; + String filterVal = filter.getValue(); + String searchVal = search.getText().trim(); + + if (null == filterVal) { + stocks = exchange.findStocks(searchVal); + } else { + stocks = switch (filterVal) { + case "Gainers" -> exchange.getGainers(20); + case "Losers" -> exchange.getLosers(20); + default -> exchange.findStocks(searchVal); }; - - searchBtn.setOnAction(e -> loadStocks.run()); - filter.setOnAction(e -> loadStocks.run()); - search.setOnAction(e -> loadStocks.run()); - - searchBox.getChildren().addAll(search, searchBtn, filter); - loadStocks.run(); - - // Double-click a row to view price statistics - table.setOnMouseClicked(event -> { - if (event.getClickCount() == 2) { - StockRow selected = table.getSelectionModel().getSelectedItem(); - if (selected != null) { - showStockStats(selected.s); - } - } - }); - - VBox.setVgrow(table, Priority.ALWAYS); - - Label hint = new Label("Double-click a row to view price history"); - hint.getStyleClass().add("file-label"); - - panel.getChildren().addAll(searchBox, table, hint); - return panel; - } - - private VBox createPortfolioPanel() { - VBox panel = new VBox(10); - panel.getStyleClass().add("content-area"); - - Label heading = new Label("Holdings:"); - - portfolioTable = new TableView<>(); - addPortfolioColumns(portfolioTable); - updatePortfolio(portfolioTable); - - Button refresh = new Button("Refresh"); - refresh.getStyleClass().add("action-button"); - refresh.setOnAction(e -> updatePortfolio(portfolioTable)); - - Button sellSelected = new Button("Sell Selected"); - sellSelected.getStyleClass().add("action-button"); - sellSelected.setOnAction(e -> { - PortfolioRow selected = portfolioTable.getSelectionModel().getSelectedItem(); - if (selected == null) { - alert("Error", "Select a holding to sell."); - return; - } - Transaction trans = exchange.sell(selected.s, player); // observer fires refresh - if (trans != null && trans.isCommitted()) { - showConfirmation("Sale successful", trans); - } else { - alert("Failed", "Could not complete the sale."); - } - }); - - HBox buttons = new HBox(10, refresh, sellSelected); - buttons.setAlignment(Pos.CENTER_LEFT); - - VBox.setVgrow(portfolioTable, Priority.ALWAYS); - panel.getChildren().addAll(heading, portfolioTable, buttons); - return panel; - } - - private VBox createTradePanel() { - VBox panel = new VBox(0); - panel.getStyleClass().add("content-area"); - - TabPane tradeTabs = new TabPane(); - tradeTabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE); - tradeTabs.getTabs().addAll( - new Tab("Buy", createBuyTab()), - new Tab("Sell", createSellTab()) - ); - - VBox.setVgrow(tradeTabs, Priority.ALWAYS); - panel.getChildren().add(tradeTabs); - return panel; - } - - private VBox createBuyTab() { - VBox box = new VBox(10); - box.getStyleClass().add("content-area"); - - Label heading = new Label("Buy Stocks:"); - - GridPane form = new GridPane(); - form.setHgap(10); - form.setVgap(10); - - TextField stockField = new TextField(); - stockField.setPromptText("Stock symbol (e.g. AAPL)"); - - TextField qtyField = new TextField(); - qtyField.setPromptText("Quantity"); - - form.add(new Label("Symbol:"), 0, 0); - form.add(stockField, 1, 0); - form.add(new Label("Quantity:"), 0, 1); - form.add(qtyField, 1, 1); - - Label infoLabel = new Label(); - - stockField.textProperty().addListener((obs, old, val) -> updateBuyInfo(val, qtyField, infoLabel)); - qtyField.textProperty().addListener((obs, old, val) -> updateBuyInfo(stockField.getText(), qtyField, infoLabel)); - - Button buyBtn = new Button("Buy"); - buyBtn.getStyleClass().add("action-button"); - buyBtn.setOnAction(e -> { - try { - Stock s = exchange.getStock(stockField.getText().toUpperCase()); - if (s == null) { - alert("Error", "Stock not found"); - return; - } - BigDecimal qty = new BigDecimal(qtyField.getText()); - Transaction trans = exchange.buy(s.getSymbol(), qty, player); // observer fires refresh - if (trans != null && trans.isCommitted()) { - showConfirmation("Purchase successful", trans); - stockField.clear(); - qtyField.clear(); - infoLabel.setText(""); - } else { - alert("Failed", "Insufficient funds or error"); - } - } catch (Exception ex) { - alert("Error", ex.getMessage()); - } - }); - - box.getChildren().addAll(heading, form, infoLabel, buyBtn); - return box; - } - - private VBox createSellTab() { - VBox box = new VBox(10); - box.getStyleClass().add("content-area"); - - Label heading = new Label("Your Holdings:"); - - holdingsList = new ListView<>(); - holdingsList.setPrefHeight(400); - - holdingsList.setCellFactory(lv -> new ListCell() { - @Override - protected void updateItem(Share s, boolean empty) { - super.updateItem(s, empty); - if (empty || s == null) { - setText(null); - } else { - setText( - s.getStock().getSymbol() + " - " + - s.getQuantity() + " @ $" + - formatMoney(s.getStock().getSalesPrice()) - ); - } - } - }); - - updateHoldingsList(holdingsList); - - // Quantity row - HBox quantityRow = new HBox(8); - quantityRow.setAlignment(Pos.CENTER_LEFT); - TextField quantityField = new TextField(); - quantityField.setPromptText("Antall"); - quantityField.setPrefWidth(120); - quantityRow.getChildren().addAll(new Label("Quantity to sell:"), quantityField); - - // Automatically fills in the maximum quantity when a holding is selected - holdingsList.getSelectionModel().selectedItemProperty().addListener((obs, old, selected) -> { - if (selected != null) { - quantityField.setText(selected.getQuantity().toPlainString()); - } - }); - - Button sellBtn = new Button("Sell Selected"); - sellBtn.setOnAction(e -> { - Share selected = holdingsList.getSelectionModel().getSelectedItem(); - if (selected == null) { - alert("Error", "Select a holding to sell"); - return; + } + + ObservableList data = FXCollections.observableArrayList(); + for (Stock s : stocks) { + data.add(new StockRow(s)); + } + table.setItems(data); + }; + + searchBtn.setOnAction(e -> loadStocks.run()); + filter.setOnAction(e -> loadStocks.run()); + search.setOnAction(e -> loadStocks.run()); + + searchBox.getChildren().addAll(search, searchBtn, filter); + loadStocks.run(); + + VBox.setVgrow(table, Priority.ALWAYS); + panel.getChildren().addAll(searchBox, table); + return panel; + } + + /** + * Creates the portfolio panel showing held shares and net worth. + * + * @return the portfolio panel + */ + private VBox createPortfolioPanel() { + VBox panel = new VBox(10); + panel.getStyleClass().add("content-area"); + + Label heading = new Label("Holdings:"); + + portfolioTable = new TableView<>(); + addPortfolioColumns(portfolioTable); + updatePortfolio(portfolioTable); + + Button refresh = new Button("Refresh"); + refresh.getStyleClass().add("action-button"); + refresh.setOnAction(e -> updatePortfolio(portfolioTable)); + + Button sellSelected = new Button("Sell Selected"); + sellSelected.getStyleClass().add("action-button"); + sellSelected.setOnAction(e -> { + PortfolioRow selected = portfolioTable.getSelectionModel().getSelectedItem(); + if (selected == null) { + alert("Error", "Select a holding to sell."); + return; + } + Transaction trans = exchange.sell(selected.s, player); // observer fires refresh + if (trans != null && trans.isCommitted()) { + showConfirmation("Sale successful", trans); + } else { + alert("Failed", "Could not complete the sale."); + } + }); + + HBox buttons = new HBox(10, refresh, sellSelected); + buttons.setAlignment(Pos.CENTER_LEFT); + + VBox.setVgrow(portfolioTable, Priority.ALWAYS); + panel.getChildren().addAll(heading, portfolioTable, buttons); + return panel; + } + + /** + * Creates the trading panel with buy and sell tabs. + * + * @return the trading panel + */ + private VBox createTradePanel() { + VBox panel = new VBox(0); + panel.getStyleClass().add("content-area"); + + TabPane tradeTabs = new TabPane(); + tradeTabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE); + tradeTabs.getTabs().addAll( + new Tab("Buy", createBuyTab()), + new Tab("Sell", createSellTab()) + ); + + VBox.setVgrow(tradeTabs, Priority.ALWAYS); + panel.getChildren().add(tradeTabs); + return panel; + } + + /** + * Creates the buy transaction tab. + * + * @return the buy tab content + */ + private VBox createBuyTab() { + VBox box = new VBox(10); + box.getStyleClass().add("content-area"); + + Label heading = new Label("Buy Stocks:"); + + GridPane form = new GridPane(); + form.setHgap(10); + form.setVgap(10); + + TextField stockField = new TextField(); + stockField.setPromptText("Stock symbol (e.g. AAPL)"); + + TextField qtyField = new TextField(); + qtyField.setPromptText("Quantity"); + + form.add(new Label("Symbol:"), 0, 0); + form.add(stockField, 1, 0); + form.add(new Label("Quantity:"), 0, 1); + form.add(qtyField, 1, 1); + + Label infoLabel = new Label(); + + stockField.textProperty() + .addListener((obs, old, val) -> updateBuyInfo(val, qtyField, infoLabel)); + qtyField.textProperty() + .addListener((obs, old, val) -> updateBuyInfo(stockField.getText(), qtyField, infoLabel)); + + Button buyBtn = new Button("Buy"); + buyBtn.getStyleClass().add("action-button"); + buyBtn.setOnAction(e -> { + try { + Stock s = exchange.getStock(stockField.getText().toUpperCase()); + if (s == null) { + alert("Error", "Stock not found"); + return; + } + BigDecimal qty = new BigDecimal(qtyField.getText()); + Transaction trans = exchange.buy(s.getSymbol(), qty, player); // observer fires refresh + if (trans != null && trans.isCommitted()) { + showConfirmation("Purchase successful", trans); + stockField.clear(); + qtyField.clear(); + infoLabel.setText(""); + } else { + alert("Failed", "Insufficient funds or error"); + } + } catch (Exception ex) { + alert("Error", ex.getMessage()); + } + }); + + box.getChildren().addAll(heading, form, infoLabel, buyBtn); + return box; + } + + /** + * Creates the sell transaction tab. + * + * @return the sell tab content + */ + private VBox createSellTab() { + VBox box = new VBox(10); + box.getStyleClass().add("content-area"); + + Label heading = new Label("Your Holdings:"); + + holdingsList = new ListView<>(); + holdingsList.setPrefHeight(400); + + holdingsList.setCellFactory(lv -> new ListCell() { + @Override + protected void updateItem(Share s, boolean empty) { + super.updateItem(s, empty); + if (empty || s == null) { + setText(null); + } else { + setText( + s.getStock().getSymbol() + " - " + + s.getQuantity() + " @ $" + + formatMoney(s.getStock().getSalesPrice()) + ); } + } + }); + + updateHoldingsList(holdingsList); + + // Numer of rows + HBox quantityRow = new HBox(8); + quantityRow.setAlignment(Pos.CENTER_LEFT); + TextField quantityField = new TextField(); + quantityField.setPromptText("Antall"); + quantityField.setPrefWidth(120); + quantityRow.getChildren().addAll(new Label("Quantity to sell:"), quantityField); + + // Fill in the maximum number automatically when selecting a holding + holdingsList.getSelectionModel().selectedItemProperty().addListener((obs, old, selected) -> { + if (selected != null) { + quantityField.setText(selected.getQuantity().toPlainString()); + } + }); + + Button sellBtn = new Button("Sell Selected"); + sellBtn.setOnAction(e -> { + Share selected = holdingsList.getSelectionModel().getSelectedItem(); + if (selected == null) { + alert("Error", "Select a holding to sell"); + return; + } - try { - BigDecimal quantity = new BigDecimal(quantityField.getText().trim()); - - if (quantity.compareTo(BigDecimal.ZERO) <= 0) { - alert("Error", "Quantity must be greater than zero"); - return; - } + try { + BigDecimal quantity = new BigDecimal(quantityField.getText().trim()); - Transaction trans = exchange.sell(selected, quantity, player); // observer fires refresh + if (quantity.compareTo(BigDecimal.ZERO) <= 0) { + alert("Error", "Quantity must be greater than zero"); + return; + } - if (trans != null && trans.isCommitted()) { - showConfirmation("Sale successful", trans); - } + Transaction trans = exchange.sell(selected, quantity, player); // observer fires refresh - else { - alert("Failed", "Could not complete sale. Check quantity."); - } - } - - catch (NumberFormatException ex) { - alert("Error", "Enter a valid number for quantity"); - } - }); - - box.getChildren().addAll(heading, holdingsList, quantityRow, sellBtn); - - return box; + if (trans != null && trans.isCommitted()) { + showConfirmation("Sale successful", trans); + } else { + alert("Failed", "Could not complete sale. Check quantity."); + } + } catch (NumberFormatException ex) { + alert("Error", "Enter a valid number for quantity"); + } + }); + + box.getChildren().addAll(heading, holdingsList, quantityRow, sellBtn); + + return box; + } + + /** + * Creates the transaction history panel. + * + * @return the history panel + */ + private VBox createHistoryPanel() { + VBox panel = new VBox(10); + panel.getStyleClass().add("content-area"); + + weekFilterCombo = new ComboBox<>(); + updateWeekCombo(weekFilterCombo); + + historyTable = new TableView<>(); + addHistoryColumns(historyTable); + updateHistory(historyTable, null); + + weekFilterCombo.setOnAction(e -> updateHistory( + historyTable, + weekFilterCombo.getValue() == null || weekFilterCombo.getValue() == 0 + ? null : weekFilterCombo.getValue() + )); + + HBox filterRow = new HBox(8); + filterRow.setAlignment(Pos.CENTER_LEFT); + filterRow.getChildren().addAll(new Label("Week:"), weekFilterCombo); + + VBox.setVgrow(historyTable, Priority.ALWAYS); + panel.getChildren().addAll(filterRow, historyTable); + return panel; + } + + // ---- Column helpers ---- + + @SuppressWarnings("unchecked") +private void addStockColumns(TableView table) { + table.getColumns().addAll( + createCol("Symbol", 90, "symbol"), + createCol("Company", 180, "company"), + createCol("Price", 90, "price"), + createCol("Change", 90, "change"), + createCol("High", 90, "high"), + createCol("Low", 90, "low") + ); + } + + @SuppressWarnings("unchecked") + private void addPortfolioColumns(TableView table) { + table.getColumns().addAll( + createCol("Symbol", 90, "symbol"), + createCol("Qty", 70, "qty"), + createCol("Buy Price", 100, "buyPrice"), + createCol("Current Price", 120, "currentPrice"), + createCol("Gain/Loss", 100, "gainLoss") + ); + } + + @SuppressWarnings("unchecked") + private void addHistoryColumns(TableView table) { + table.getColumns().addAll( + createCol("Week", 70, "week"), + createCol("Type", 70, "type"), + createCol("Symbol", 90, "symbol"), + createCol("Qty", 70, "qty"), + createCol("Total", 110, "total") + ); + } + + private TableColumn createCol(String header, int width, String property) { + TableColumn col = new TableColumn<>(header); + col.setPrefWidth(width); + col.setCellValueFactory(cellData -> { + try { + return new javafx.beans.property.SimpleStringProperty( + cellData.getValue().getClass() + .getMethod("get" + capitalize(property)) + .invoke(cellData.getValue()).toString() + ); + } catch (Exception e) { + return new javafx.beans.property.SimpleStringProperty(""); + } + }); + return col; + } + + private String capitalize(String s) { + return s.substring(0, 1).toUpperCase() + s.substring(1); + } + + // ---- Data updaters ---- + + private void updatePortfolio(TableView table) { + ObservableList data = FXCollections.observableArrayList(); + for (Share s : player.getPortfolio().getShares()) { + data.add(new PortfolioRow(s)); + } + table.setItems(data); + } + + private void updateHoldingsList(ListView list) { + ObservableList items = FXCollections.observableArrayList(); + items.addAll(player.getPortfolio().getShares()); + list.setItems(items); + } + + private void updateHistory(TableView table, Integer week) { + ObservableList data = FXCollections.observableArrayList(); + List trans = week == null + ? player.getTransactionArchive().getAllTransactions() + : player.getTransactionArchive().getTransactions(week); + for (Transaction t : trans) { + data.add(new HistoryRow(t)); + } + table.setItems(data); + } + + private void updateWeekCombo(ComboBox combo) { + ObservableList weeks = FXCollections.observableArrayList(); + weeks.add(0); + int total = player.getTransactionArchive().countDistinctWeeks(); + for (int i = 1; i <= total; i++) { + weeks.add(i); + } + combo.setItems(weeks); + combo.setValue(0); + } + + private void updateBuyInfo(String symbol, TextField qtyField, Label label) { + try { + if (symbol == null || symbol.isEmpty()) { + return; + } + Stock s = exchange.getStock(symbol.toUpperCase()); + if (s == null) { + return; + } + BigDecimal qty = new BigDecimal(qtyField.getText()); + BigDecimal gross = s.getSalesPrice().multiply(qty); + BigDecimal comm = gross.multiply(new BigDecimal("0.005")); + label.setText("Gross: $" + formatMoney(gross) + + " | Commission: $" + formatMoney(comm) + + " | Total: $" + formatMoney(gross.add(comm))); + } catch (Exception e) { + // ignore partial input + } + } + + private void updateStatus() { + PlayerStatus playerStatus = player.getStatus(); + statusLabel.setText( + "Player: " + player.getName() + + " | Status: " + playerStatus.getDisplayName() + + " | Week: " + exchange.getWeek() + + " | Cash: $" + formatMoney(player.getMoney()) + + " | Net Worth: $" + formatMoney(getNetWorth()) + ); + } + + private void refreshAllUI() { + if (portfolioTable != null) { + updatePortfolio(portfolioTable); + } + if (holdingsList != null) { + updateHoldingsList(holdingsList); + } + if (historyTable != null) { + updateHistory(historyTable, + weekFilterCombo != null && weekFilterCombo.getValue() != null + ? weekFilterCombo.getValue() : null); + if (weekFilterCombo != null) { + updateWeekCombo(weekFilterCombo); + } } + } - private VBox createHistoryPanel() { - VBox panel = new VBox(10); - panel.getStyleClass().add("content-area"); + private BigDecimal getNetWorth() { + return player.getMoney().add(player.getPortfolio().getNetWorth()); + } - weekFilterCombo = new ComboBox<>(); - updateWeekCombo(weekFilterCombo); + private void showConfirmation(String title, Transaction t) { + StringBuilder msg = new StringBuilder(); + if (t instanceof Purchase) { + Purchase p = (Purchase) t; + msg.append("Bought ").append(formatMoney(p.getShare().getQuantity())) + .append(" @ $").append(formatMoney(p.getShare().getPurchasePrice())) + .append("\nCost: $").append(formatMoney(p.getCalculator().calculateTotal())); + } else { + Sale s = (Sale) t; + msg.append("Sold ").append(formatMoney(s.getShare().getQuantity())) + .append(" @ $").append(formatMoney(s.getShare().getStock().getSalesPrice())) + .append("\nProceeds: $").append(formatMoney(s.getCalculator().calculateTotal())); + } + alert(title, msg.toString()); + } - historyTable = new TableView<>(); - addHistoryColumns(historyTable); - updateHistory(historyTable, null); + private void alert(String title, String msg) { + Alert a = new Alert(Alert.AlertType.INFORMATION); + a.setTitle(title); + a.setHeaderText(null); + a.setContentText(msg); + a.showAndWait(); + } - weekFilterCombo.setOnAction(e -> updateHistory( - historyTable, - weekFilterCombo.getValue() == null || weekFilterCombo.getValue() == 0 - ? null : weekFilterCombo.getValue() - )); + private boolean confirm(String title, String msg) { + Alert a = new Alert(Alert.AlertType.CONFIRMATION); + a.setTitle(title); + a.setHeaderText(null); + a.setContentText(msg); + return a.showAndWait().get() == ButtonType.OK; + } - HBox filterRow = new HBox(8); - filterRow.setAlignment(Pos.CENTER_LEFT); - filterRow.getChildren().addAll(new Label("Week:"), weekFilterCombo); + private String formatMoney(BigDecimal n) { + return n.setScale(2, java.math.RoundingMode.HALF_UP).toString(); + } - VBox.setVgrow(historyTable, Priority.ALWAYS); - panel.getChildren().addAll(filterRow, historyTable); - return panel; - } + public Scene getScene() { + return scene; + } - // ---- Column helpers ---- + // ---- Data row classes ---- - private void addStockColumns(TableView table) { - table.getColumns().addAll( - createCol("Symbol", 90, "symbol"), - createCol("Company", 180, "company"), - createCol("Price", 90, "price"), - createCol("Change", 90, "change"), - createCol("High", 90, "high"), - createCol("Low", 90, "low") - ); - } + static class StockRow { + Stock s; - private void addPortfolioColumns(TableView table) { - table.getColumns().addAll( - createCol("Symbol", 90, "symbol"), - createCol("Qty", 70, "qty"), - createCol("Buy Price", 100, "buyPrice"), - createCol("Current Price", 120, "currentPrice"), - createCol("Gain/Loss", 100, "gainLoss") - ); + StockRow(Stock s) { + this.s = s; } - private void addHistoryColumns(TableView table) { - table.getColumns().addAll( - createCol("Week", 70, "week"), - createCol("Type", 70, "type"), - createCol("Symbol", 90, "symbol"), - createCol("Qty", 70, "qty"), - createCol("Total", 110, "total") - ); + public String getSymbol() { + return s.getSymbol(); } - private TableColumn createCol(String header, int width, String property) { - TableColumn col = new TableColumn<>(header); - col.setPrefWidth(width); - col.setCellValueFactory(cellData -> { - try { - return new javafx.beans.property.SimpleStringProperty( - cellData.getValue().getClass() - .getMethod("get" + capitalize(property)) - .invoke(cellData.getValue()).toString() - ); - } catch (Exception e) { - return new javafx.beans.property.SimpleStringProperty(""); - } - }); - return col; + public String getCompany() { + return s.getCompany(); } - private String capitalize(String s) { - return s.substring(0, 1).toUpperCase() + s.substring(1); + public String getPrice() { + return "$" + s.getSalesPrice(); } - // ---- Data updaters ---- - - private void updatePortfolio(TableView table) { - ObservableList data = FXCollections.observableArrayList(); - for (Share s : player.getPortfolio().getShares()) { - data.add(new PortfolioRow(s)); - } - table.setItems(data); + public String getChange() { + return "$" + s.getLatestPriceChange(); } - - private void updateHoldingsList(ListView list) { - ObservableList items = FXCollections.observableArrayList(); - items.addAll(player.getPortfolio().getShares()); - list.setItems(items); + + public String getHigh() { + return "$" + s.getHighestPrice(); } - private void updateHistory(TableView table, Integer week) { - ObservableList data = FXCollections.observableArrayList(); - List trans = week == null - ? player.getTransactionArchive().getAllTransactions() - : player.getTransactionArchive().getTransactions(week); - for (Transaction t : trans) { - data.add(new HistoryRow(t)); - } - table.setItems(data); + public String getLow() { + return "$" + s.getLowestPrice(); } - private void updateWeekCombo(ComboBox combo) { - ObservableList weeks = FXCollections.observableArrayList(); - weeks.add(0); - int total = player.getTransactionArchive().countDistinctWeeks(); - for (int i = 1; i <= total; i++) weeks.add(i); - combo.setItems(weeks); - combo.setValue(0); - } + } - private void updateBuyInfo(String symbol, TextField qtyField, Label label) { - try { - if (symbol == null || symbol.isEmpty()) return; - Stock s = exchange.getStock(symbol.toUpperCase()); - if (s == null) return; - BigDecimal qty = new BigDecimal(qtyField.getText()); - BigDecimal gross = s.getSalesPrice().multiply(qty); - BigDecimal comm = gross.multiply(new BigDecimal("0.005")); - label.setText("Gross: $" + formatMoney(gross) + - " | Commission: $" + formatMoney(comm) + - " | Total: $" + formatMoney(gross.add(comm))); - } catch (Exception e) { - // ignore partial input - } - } + static class PortfolioRow { + Share s; - private void updateStatus() { - PlayerStatus playerStatus = player.getStatus(); - statusLabel.setText( - "Player: " + player.getName() + - " | Status: " + playerStatus.getDisplayName() + - " | Week: " + exchange.getWeek() + - " | Cash: $" + formatMoney(player.getMoney()) + - " | Net Worth: $" + formatMoney(getNetWorth()) - ); + PortfolioRow(Share s) { + this.s = s; } - - private void refreshAllUI() { - if (portfolioTable != null) { - updatePortfolio(portfolioTable); - } - if (holdingsList != null) { - updateHoldingsList(holdingsList); - } - if (historyTable != null) { - updateHistory(historyTable, - weekFilterCombo != null && weekFilterCombo.getValue() != null - ? weekFilterCombo.getValue() : null); - if (weekFilterCombo != null) { - updateWeekCombo(weekFilterCombo); - } - } + + public String getSymbol() { + return s.getStock().getSymbol(); } - private BigDecimal getNetWorth() { - return player.getMoney().add(player.getPortfolio().getNetWorth()); + public String getQty() { + return s.getQuantity().toString(); } - private void showStockStats(Stock stock) { - Dialog dialog = new Dialog<>(); - dialog.setTitle(stock.getSymbol() + " — " + stock.getCompany()); - dialog.getDialogPane().getButtonTypes().add(ButtonType.CLOSE); - - VBox content = new VBox(12); - content.setPrefWidth(340); - - // --- Number Sumary --- - BigDecimal change = stock.getLatestPriceChange(); - String changeSign = change.compareTo(BigDecimal.ZERO) >= 0 ? "+" : ""; - - GridPane summary = new GridPane(); - summary.setHgap(16); - summary.setVgap(6); - addStatRow(summary, 0, "Current price:", "$" + formatMoney(stock.getSalesPrice())); - addStatRow(summary, 1, "Week change:", changeSign + formatMoney(change)); - addStatRow(summary, 2, "All-time high:", "$" + formatMoney(stock.getHighestPrice())); - addStatRow(summary, 3, "All-time low:", "$" + formatMoney(stock.getLowestPrice())); - addStatRow(summary, 4, "Weeks tracked:", String.valueOf(stock.getHistoricalPrices().size())); - - // --- Price history list --- - Label histHeading = new Label("Price history:"); - histHeading.setStyle("-fx-font-weight: bold;"); - - ListView histList = new ListView<>(); - histList.setPrefHeight(180); - List prices = stock.getHistoricalPrices(); - ObservableList rows = FXCollections.observableArrayList(); - for (int i = 0; i < prices.size(); i++) { - String change_i = ""; - if (i > 0) { - BigDecimal diff = prices.get(i).subtract(prices.get(i - 1)); - change_i = " (" + (diff.compareTo(BigDecimal.ZERO) >= 0 ? "+" : "") + - formatMoney(diff) + ")"; - } - rows.add("Week " + (i + 1) + ": $" + formatMoney(prices.get(i)) + change_i); - } - histList.setItems(rows); - - // Scroll to the most recent week - if (!rows.isEmpty()) histList.scrollTo(rows.size() - 1); - - content.getChildren().addAll(summary, new Separator(), histHeading, histList); - dialog.getDialogPane().setContent(content); - dialog.showAndWait(); + public String getBuyPrice() { + return "$" + s.getPurchasePrice(); } - /** Adds a label and value to the given row. */ - private void addStatRow(GridPane grid, int row, String labelText, String valueText) { - Label lbl = new Label(labelText); - lbl.setStyle("-fx-font-weight: bold;"); - Label val = new Label(valueText); - grid.add(lbl, 0, row); - grid.add(val, 1, row); + public String getCurrentPrice() { + return "$" + s.getStock().getSalesPrice(); } - private void showConfirmation(String title, Transaction t) { - StringBuilder msg = new StringBuilder(); - if (t instanceof Purchase) { - Purchase p = (Purchase) t; - msg.append("Bought ").append(formatMoney(p.getShare().getQuantity())) - .append(" @ $").append(formatMoney(p.getShare().getPurchasePrice())) - .append("\nCost: $").append(formatMoney(p.getCalculator().calculateTotal())); - } else { - Sale s = (Sale) t; - msg.append("Sold ").append(formatMoney(s.getShare().getQuantity())) - .append(" @ $").append(formatMoney(s.getShare().getStock().getSalesPrice())) - .append("\nProceeds: $").append(formatMoney(s.getCalculator().calculateTotal())); - } - alert(title, msg.toString()); + public String getGainLoss() { + SaleCalculator calc = new SaleCalculator(s); + BigDecimal val = calc.calculateTotal(); + BigDecimal cost = s.getPurchasePrice().multiply(s.getQuantity()); + return "$" + val.subtract(cost); } + } - private void alert(String title, String msg) { - Alert a = new Alert(Alert.AlertType.INFORMATION); - a.setTitle(title); - a.setHeaderText(null); - a.setContentText(msg); - a.showAndWait(); - } + static class HistoryRow { + Transaction t; - private boolean confirm(String title, String msg) { - Alert a = new Alert(Alert.AlertType.CONFIRMATION); - a.setTitle(title); - a.setHeaderText(null); - a.setContentText(msg); - return a.showAndWait().get() == ButtonType.OK; + HistoryRow(Transaction t) { + this.t = t; } - private String formatMoney(BigDecimal n) { - return n.setScale(2, java.math.RoundingMode.HALF_UP).toString(); + public String getWeek() { + return String.valueOf(t.getWeek()); } - public Scene getScene() { - return scene; + public String getType() { + return t instanceof Purchase ? "BUY" : "SELL"; } - // ---- Data row classes ---- - - static class StockRow { - Stock s; - StockRow(Stock s) { this.s = s; } - public String getSymbol() { return s.getSymbol(); } - public String getCompany() { return s.getCompany(); } - public String getPrice() { return "$" + s.getSalesPrice(); } - public String getChange() { return "$" + s.getLatestPriceChange(); } - public String getHigh() { return "$" + s.getHighestPrice(); } - public String getLow() { return "$" + s.getLowestPrice(); } + public String getSymbol() { + return t.getShare().getStock().getSymbol(); } - static class PortfolioRow { - Share s; - PortfolioRow(Share s) { this.s = s; } - public String getSymbol() { return s.getStock().getSymbol(); } - public String getQty() { return s.getQuantity().toString(); } - public String getBuyPrice() { return "$" + s.getPurchasePrice(); } - public String getCurrentPrice() { return "$" + s.getStock().getSalesPrice(); } - public String getGainLoss() { - SaleCalculator calc = new SaleCalculator(s); - BigDecimal val = calc.calculateTotal(); - BigDecimal cost = s.getPurchasePrice().multiply(s.getQuantity()); - return "$" + val.subtract(cost); - } + public String getQty() { + return t.getShare().getQuantity().toString(); } - static class HistoryRow { - Transaction t; - HistoryRow(Transaction t) { this.t = t; } - public String getWeek() { return String.valueOf(t.getWeek()); } - public String getType() { return t instanceof Purchase ? "BUY" : "SELL"; } - public String getSymbol() { return t.getShare().getStock().getSymbol(); } - public String getQty() { return t.getShare().getQuantity().toString(); } - public String getTotal() { return "$" + t.getCalculator().calculateTotal(); } + public String getTotal() { + return "$" + t.getCalculator().calculateTotal(); } + } } diff --git a/src/main/java/View/StockTradingGameApp.java b/src/main/java/View/StockTradingGameApp.java index a2d6543..b1ab987 100644 --- a/src/main/java/View/StockTradingGameApp.java +++ b/src/main/java/View/StockTradingGameApp.java @@ -1,49 +1,73 @@ package View; -import javafx.application.Application; -import javafx.stage.Stage; import Controller.StockFileHandler; import Model.*; +import javafx.application.Application; +import javafx.stage.Stage; -import java.math.BigDecimal; - +/** + * Main JavaFX application class for the Stock Trading Game. + * Manages the application lifecycle, window setup, scene transitions, + * and exchanges between the game setup and main game views. + */ public class StockTradingGameApp extends Application { - private Stage primaryStage; - private Exchange exchange; - private Player player; - private StockFileHandler fileHandler; + private Stage primaryStage; + private Exchange exchange; + private Player player; + private StockFileHandler fileHandler; - @Override - public void start(Stage primaryStage) { - this.primaryStage = primaryStage; - this.fileHandler = new StockFileHandler(); + /** + * Initializes and displays the primary stage. + * + * @param primaryStage the primary window stage provided by JavaFX + */ + @Override + public void start(Stage primaryStage) { + this.primaryStage = primaryStage; + this.fileHandler = new StockFileHandler(); - primaryStage.setTitle("Stock Trading Game"); - primaryStage.setWidth(1200); - primaryStage.setHeight(800); + primaryStage.setTitle("Stock Trading Game"); + primaryStage.setWidth(1200); + primaryStage.setHeight(800); - showGameSetup(); - primaryStage.show(); - } + showGameSetup(); + primaryStage.show(); + } - private void showGameSetup() { - GameSetupScene setupScene = new GameSetupScene(this::startGame); - primaryStage.setScene(setupScene.getScene()); - } + /** + * Displays the game setup scene where players configure their game. + */ + private void showGameSetup() { + GameSetupScene setupScene = new GameSetupScene(this::startGame); + primaryStage.setScene(setupScene.getScene()); + } - private void startGame(GameSetupScene.StartGameData gameData) { - this.exchange = gameData.exchange; - this.player = new Player(gameData.playerName, gameData.startingCapital); + /** + * Starts the main game scene after setup is complete. + * + * @param gameData the game configuration data from the setup scene + */ + private void startGame(GameSetupScene.StartGameData gameData) { + this.exchange = gameData.exchange; + this.player = new Player(gameData.playerName, gameData.startingCapital); - MainGameScene gameScene = new MainGameScene(exchange, player, this::endGame); - primaryStage.setScene(gameScene.getScene()); - } + MainGameScene gameScene = new MainGameScene(exchange, player, this::endGame); + primaryStage.setScene(gameScene.getScene()); + } - private void endGame() { - primaryStage.close(); - } + /** + * Ends the game and closes the application. + */ + private void endGame() { + primaryStage.close(); + } - public static void main(String[] args) { - launch(args); - } + /** + * Main method to launch the JavaFX application. + * + * @param args command-line arguments (not used) + */ + public static void main(String[] args) { + launch(args); + } } diff --git a/src/test/java/ExchangeTest.java b/src/test/java/ExchangeTest.java index 9261bd6..cc55f5d 100644 --- a/src/test/java/ExchangeTest.java +++ b/src/test/java/ExchangeTest.java @@ -1,265 +1,365 @@ -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; import Model.Exchange; import Model.Player; +import Model.Purchase; +import Model.Sale; import Model.Share; import Model.Stock; import Model.Transaction; -import Model.Purchase; -import Model.Sale; - -import static org.junit.jupiter.api.Assertions.*; - import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +/** + * Unit tests for the Exchange class. + * Tests core exchange functionality including stock management, buying/selling, + * market data retrieval, and error handling. + */ public class ExchangeTest { - private Exchange exchange; - private Stock apple; - private Stock google; - private Player player; - - @BeforeEach - void setUp() { - apple = new Stock("AAPL", "Apple", new BigDecimal("100")); - google = new Stock("GOOGL", "Google", new BigDecimal("200")); - - List stocks = new ArrayList<>(); - stocks.add(apple); - stocks.add(google); - - exchange = new Exchange("TestExchange", stocks); - player = new Player("Jane", new BigDecimal("500000")); - } - - // ---- Positive tests ---- - - @Test - void testGetName() { - assertEquals("TestExchange", exchange.getName()); - } - - @Test - void testInitialWeekIsOne() { - assertEquals(1, exchange.getWeek()); - } - - @Test - void testAdvanceIncrementsWeek() { - exchange.advance(); - assertEquals(2, exchange.getWeek()); - } - - @Test - void testHasStockReturnsTrue() { - assertTrue(exchange.hasStock("AAPL")); - } - - @Test - void testHasStockReturnsFalse() { - assertFalse(exchange.hasStock("MSFT")); - } - - @Test - void testGetStockReturnsCorrectStock() { - assertEquals(apple, exchange.getStock("AAPL")); - } - - @Test - void testGetStockReturnsNullForUnknown() { - assertNull(exchange.getStock("MSFT")); - } - - @Test - void testFindStocksBySymbol() { - List result = exchange.findStocks("AAPL"); - assertEquals(1, result.size()); - assertEquals("AAPL", result.get(0).getSymbol()); - } - - @Test - void testFindStocksByCompanyName() { - List result = exchange.findStocks("e"); - assertEquals(2, result.size()); - } - - @Test - void testFindStocksNoMatch() { - List result = exchange.findStocks("Samsung"); - assertEquals(0, result.size()); - } - - @Test - void testFindStocksEmptyTermReturnsAll() { - List result = exchange.findStocks(""); - assertEquals(2, result.size()); - } - - @Test - void testBuyReturnsCommittedPurchase() { - Transaction t = exchange.buy("AAPL", new BigDecimal("5"), player); - assertNotNull(t); - assertTrue(t.isCommitted()); - assertInstanceOf(Purchase.class, t); - } - - @Test - void testBuyDeductsMoneyFromPlayer() { - BigDecimal before = player.getMoney(); - exchange.buy("AAPL", new BigDecimal("5"), player); - assertTrue(player.getMoney().compareTo(before) < 0); - } - - @Test - void testBuyAddsShareToPortfolio() { - exchange.buy("AAPL", new BigDecimal("5"), player); - assertFalse(player.getPortfolio().getShares("AAPL").isEmpty()); - } - - @Test - void testSellReturnsCommittedSale() { - exchange.buy("AAPL", new BigDecimal("5"), player); - Share share = player.getPortfolio().getShares("AAPL").get(0); - - Transaction t = exchange.sell(share, player); - assertNotNull(t); - assertTrue(t.isCommitted()); - assertInstanceOf(Sale.class, t); - } - - @Test - void testSellAddsMoney() { - exchange.buy("AAPL", new BigDecimal("5"), player); - Share share = player.getPortfolio().getShares("AAPL").get(0); - BigDecimal before = player.getMoney(); - - exchange.sell(share, player); - assertTrue(player.getMoney().compareTo(before) > 0); - } - - @Test - void testSellRemovesShareFromPortfolio() { - exchange.buy("AAPL", new BigDecimal("5"), player); - Share share = player.getPortfolio().getShares("AAPL").get(0); - - exchange.sell(share, player); - assertTrue(player.getPortfolio().getShares("AAPL").isEmpty()); - } - - @Test - void testGetGainersReturnsRequestedLimit() { - exchange.advance(); // prices must have changed at least once for a meaningful sort - List gainers = exchange.getGainers(1); - assertEquals(1, gainers.size()); - } - - @Test - void testGetLosersReturnsRequestedLimit() { - exchange.advance(); - List losers = exchange.getLosers(1); - assertEquals(1, losers.size()); - } - - // ---- Negative tests ---- - - @Test - void testConstructorNullNameThrows() { - assertThrows(IllegalArgumentException.class, () -> - new Exchange(null, List.of(apple)) - ); - } - - @Test - void testConstructorBlankNameThrows() { - assertThrows(IllegalArgumentException.class, () -> - new Exchange(" ", List.of(apple)) - ); - } - - @Test - void testConstructorNullStockListThrows() { - assertThrows(IllegalArgumentException.class, () -> - new Exchange("X", null) - ); - } - - @Test - void testConstructorNullStockEntryThrows() { - List withNull = new ArrayList<>(); - withNull.add(apple); - withNull.add(null); - assertThrows(IllegalArgumentException.class, () -> - new Exchange("X", withNull) - ); - } - - @Test - void testBuyNullSymbolThrows() { - assertThrows(IllegalArgumentException.class, () -> - exchange.buy(null, new BigDecimal("5"), player) - ); - } - - @Test - void testBuyUnknownSymbolThrows() { - assertThrows(IllegalArgumentException.class, () -> - exchange.buy("MSFT", new BigDecimal("5"), player) - ); - } - - @Test - void testBuyZeroQuantityThrows() { - assertThrows(IllegalArgumentException.class, () -> - exchange.buy("AAPL", BigDecimal.ZERO, player) - ); - } - - @Test - void testBuyNegativeQuantityThrows() { - assertThrows(IllegalArgumentException.class, () -> - exchange.buy("AAPL", new BigDecimal("-1"), player) - ); - } - - @Test - void testBuyNullPlayerThrows() { - assertThrows(IllegalArgumentException.class, () -> - exchange.buy("AAPL", new BigDecimal("5"), null) - ); - } - - @Test - void testBuyInsufficientFundsThrows() { - Player poorPlayer = new Player("Broke", new BigDecimal("1")); - assertThrows(IllegalStateException.class, () -> - exchange.buy("AAPL", new BigDecimal("5"), poorPlayer) - ); - } - - @Test - void testSellNullPlayerThrows() { - exchange.buy("AAPL", new BigDecimal("5"), player); - Share share = player.getPortfolio().getShares("AAPL").get(0); - assertThrows(IllegalArgumentException.class, () -> - exchange.sell(share, null) - ); - } - - @Test - void testSellShareNotInPortfolioThrows() { - Share unowned = new Share(apple, new BigDecimal("1"), new BigDecimal("100")); - assertThrows(IllegalStateException.class, () -> - exchange.sell(unowned, player) - ); - } - - @Test - void testAddNullObserverThrows() { - assertThrows(IllegalArgumentException.class, () -> - exchange.addObserver(null) - ); - } + private Exchange exchange; + private Stock apple; + private Stock google; + private Player player; + + @BeforeEach + void setUp() { + apple = new Stock("AAPL", "Apple", new BigDecimal("100")); + google = new Stock("GOOGL", "Google", new BigDecimal("200")); + + List stocks = new ArrayList<>(); + stocks.add(apple); + stocks.add(google); + + exchange = new Exchange("TestExchange", stocks); + player = new Player("Jane", new BigDecimal("500000")); + } + + // ---- Positive tests ---- + + /** + * Tests that exchange returns its correct name. + */ + @Test + void testGetName() { + assertEquals("TestExchange", exchange.getName()); + } + + /** + * Tests that exchange starts at week 1. + */ + @Test + void testInitialWeekIsOne() { + assertEquals(1, exchange.getWeek()); + } + + /** + * Tests that calling advance increments the week counter. + */ + @Test + void testAdvanceIncrementsWeek() { + exchange.advance(); + assertEquals(2, exchange.getWeek()); + } + + /** + * Tests that hasStock returns true for a known stock symbol. + */ + @Test + void testHasStockReturnsTrue() { + assertTrue(exchange.hasStock("AAPL")); + } + + /** + * Tests that hasStock returns false for an unknown stock symbol. + */ + @Test + void testHasStockReturnsFalse() { + assertFalse(exchange.hasStock("MSFT")); + } + + /** + * Tests that getStock returns the correct Stock object for a given symbol. + */ + @Test + void testGetStockReturnsCorrectStock() { + assertEquals(apple, exchange.getStock("AAPL")); + } + + /** + * Tests that getStock returns null for an unknown symbol. + */ + @Test + void testGetStockReturnsNullForUnknown() { + assertNull(exchange.getStock("MSFT")); + } + + /** + * Tests that findStocks correctly locates stocks by symbol. + */ + @Test + void testFindStocksBySymbol() { + List result = exchange.findStocks("AAPL"); + assertEquals(1, result.size()); + assertEquals("AAPL", result.get(0).getSymbol()); + } + + /** + * Tests that findStocks correctly locates stocks by partial company name match. + */ + @Test + void testFindStocksByCompanyName() { + List result = exchange.findStocks("e"); + assertEquals(2, result.size()); + } + + /** + * Tests that findStocks returns empty list when no stocks match search term. + */ + @Test + void testFindStocksNoMatch() { + List result = exchange.findStocks("Samsung"); + assertEquals(0, result.size()); + } + + /** + * Tests that findStocks with empty search term returns all stocks. + */ + @Test + void testFindStocksEmptyTermReturnsAll() { + List result = exchange.findStocks(""); + assertEquals(2, result.size()); + } + + /** + * Tests that buying a stock returns a committed Purchase transaction. + */ + @Test + void testBuyReturnsCommittedPurchase() { + Transaction t = exchange.buy("AAPL", new BigDecimal("5"), player); + assertNotNull(t); + assertTrue(t.isCommitted()); + assertInstanceOf(Purchase.class, t); + } + + /** + * Tests that buying stock deducts the correct amount from player's balance. + */ + @Test + void testBuyDeductsMoneyFromPlayer() { + BigDecimal before = player.getMoney(); + exchange.buy("AAPL", new BigDecimal("5"), player); + assertTrue(player.getMoney().compareTo(before) < 0); + } + + /** + * Tests that buying stock adds the share to player's portfolio. + */ + @Test + void testBuyAddsShareToPortfolio() { + exchange.buy("AAPL", new BigDecimal("5"), player); + assertFalse(player.getPortfolio().getShares("AAPL").isEmpty()); + } + + /** + * Tests that selling a share returns a committed Sale transaction. + */ + @Test + void testSellReturnsCommittedSale() { + exchange.buy("AAPL", new BigDecimal("5"), player); + Share share = player.getPortfolio().getShares("AAPL").get(0); + + Transaction t = exchange.sell(share, player); + assertNotNull(t); + assertTrue(t.isCommitted()); + assertInstanceOf(Sale.class, t); + } + + /** + * Tests that selling stock adds the correct amount to player's balance. + */ + @Test + void testSellAddsMoney() { + exchange.buy("AAPL", new BigDecimal("5"), player); + Share share = player.getPortfolio().getShares("AAPL").get(0); + BigDecimal before = player.getMoney(); + + exchange.sell(share, player); + assertTrue(player.getMoney().compareTo(before) > 0); + } + + /** + * Tests that selling stock removes the share from player's portfolio. + */ + @Test + void testSellRemovesShareFromPortfolio() { + exchange.buy("AAPL", new BigDecimal("5"), player); + Share share = player.getPortfolio().getShares("AAPL").get(0); + + exchange.sell(share, player); + assertTrue(player.getPortfolio().getShares("AAPL").isEmpty()); + } + + /** + * Tests that getGainers returns the requested number of gaining stocks. + */ + @Test + void testGetGainersReturnsRequestedLimit() { + exchange.advance(); // prices must have changed at least once for a meaningful sort + List gainers = exchange.getGainers(1); + assertEquals(1, gainers.size()); + } + + /** + * Tests that getLosers returns the requested number of losing stocks. + */ + @Test + void testGetLosersReturnsRequestedLimit() { + exchange.advance(); + List losers = exchange.getLosers(1); + assertEquals(1, losers.size()); + } + + // ---- Negative tests ---- + + /** + * Tests that Exchange constructor throws when given a null name. + */ + @Test + void testConstructorNullNameThrows() { + assertThrows(IllegalArgumentException.class, () -> + new Exchange(null, List.of(apple)) + ); + } + + /** + * Tests that Exchange constructor throws when given a blank name. + */ + @Test + void testConstructorBlankNameThrows() { + assertThrows(IllegalArgumentException.class, () -> + new Exchange(" ", List.of(apple)) + ); + } + + /** + * Tests that Exchange constructor throws when given a null stock list. + */ + @Test + void testConstructorNullStockListThrows() { + assertThrows(IllegalArgumentException.class, () -> + new Exchange("X", null) + ); + } + + /** + * Tests that Exchange constructor throws when stock list contains null entry. + */ + @Test + void testConstructorNullStockEntryThrows() { + List withNull = new ArrayList<>(); + withNull.add(apple); + withNull.add(null); + assertThrows(IllegalArgumentException.class, () -> + new Exchange("X", withNull) + ); + } + + /** + * Tests that buy throws when given a null symbol. + */ + @Test + void testBuyNullSymbolThrows() { + assertThrows(IllegalArgumentException.class, () -> + exchange.buy(null, new BigDecimal("5"), player) + ); + } + + /** + * Tests that buy throws when given an unknown stock symbol. + */ + @Test + void testBuyUnknownSymbolThrows() { + assertThrows(IllegalArgumentException.class, () -> + exchange.buy("MSFT", new BigDecimal("5"), player) + ); + } + + /** + * Tests that buy throws when given zero quantity. + */ + @Test + void testBuyZeroQuantityThrows() { + assertThrows(IllegalArgumentException.class, () -> + exchange.buy("AAPL", BigDecimal.ZERO, player) + ); + } + + /** + * Tests that buy throws when given negative quantity. + */ + @Test + void testBuyNegativeQuantityThrows() { + assertThrows(IllegalArgumentException.class, () -> + exchange.buy("AAPL", new BigDecimal("-1"), player) + ); + } + + /** + * Tests that buy throws when given a null player. + */ + @Test + void testBuyNullPlayerThrows() { + assertThrows(IllegalArgumentException.class, () -> + exchange.buy("AAPL", new BigDecimal("5"), null) + ); + } + + /** + * Tests that buy throws when player has insufficient funds. + */ + @Test + void testBuyInsufficientFundsThrows() { + Player poorPlayer = new Player("Broke", new BigDecimal("1")); + assertThrows(IllegalStateException.class, () -> + exchange.buy("AAPL", new BigDecimal("5"), poorPlayer) + ); + } + + /** + * Tests that sell throws when given a null player. + */ + @Test + void testSellNullPlayerThrows() { + exchange.buy("AAPL", new BigDecimal("5"), player); + Share share = player.getPortfolio().getShares("AAPL").get(0); + assertThrows(IllegalArgumentException.class, () -> + exchange.sell(share, null) + ); + } + + /** + * Tests that sell throws when share is not in player's portfolio. + */ + @Test + void testSellShareNotInPortfolioThrows() { + Share unowned = new Share(apple, new BigDecimal("1"), new BigDecimal("100")); + assertThrows(IllegalStateException.class, () -> + exchange.sell(unowned, player) + ); + } + + /** + * Tests that addObserver throws when given a null observer. + */ + @Test + void testAddNullObserverThrows() { + assertThrows(IllegalArgumentException.class, () -> + exchange.addObserver(null) + ); + } } + diff --git a/src/test/java/PlayerTest.java b/src/test/java/PlayerTest.java index ac1f95a..cedf182 100644 --- a/src/test/java/PlayerTest.java +++ b/src/test/java/PlayerTest.java @@ -1,128 +1,183 @@ import static org.junit.jupiter.api.Assertions.*; +import Model.Player; +import java.math.BigDecimal; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import Model.Player; - -import java.math.BigDecimal; +/** + * Unit tests for the Player class. + * Tests player creation, money management, portfolio, and error handling. + */ public class PlayerTest { - private Player player; - - @BeforeEach - void setUp() { - player = new Player("Jane", new BigDecimal("1000")); - } - - // ---- Positive tests ---- - - @Test - void testGetName() { - assertEquals("Jane", player.getName()); - } - - @Test - void testGetMoneyMatchesStartingCapital() { - assertEquals(new BigDecimal("1000"), player.getMoney()); - } - - @Test - void testAddMoney() { - player.addMoney(new BigDecimal("500")); - assertEquals(new BigDecimal("1500"), player.getMoney()); - } - - @Test - void testAddMoneyZero() { - player.addMoney(BigDecimal.ZERO); - assertEquals(new BigDecimal("1000"), player.getMoney()); - } - - @Test - void testWithdrawMoney() { - player.withdrawMoney(new BigDecimal("400")); - assertEquals(new BigDecimal("600"), player.getMoney()); - } - - @Test - void testWithdrawMoneyZero() { - player.withdrawMoney(BigDecimal.ZERO); - assertEquals(new BigDecimal("1000"), player.getMoney()); - } - - @Test - void testPortfolioInitiallyEmpty() { - assertTrue(player.getPortfolio().getShares().isEmpty()); - } - - @Test - void testTransactionArchiveInitiallyEmpty() { - assertTrue(player.getTransactionArchive().isEmpty()); - } - - @Test - void testStartingMoneyZeroAllowed() { - Player broke = new Player("Broke", BigDecimal.ZERO); - assertEquals(BigDecimal.ZERO, broke.getMoney()); - } - - // ---- Negative tests ---- - - @Test - void testNullNameThrows() { - assertThrows(IllegalArgumentException.class, () -> - new Player(null, new BigDecimal("1000")) - ); - } - - @Test - void testBlankNameThrows() { - assertThrows(IllegalArgumentException.class, () -> - new Player(" ", new BigDecimal("1000")) - ); - } - - @Test - void testNullStartingMoneyThrows() { - assertThrows(IllegalArgumentException.class, () -> - new Player("Jane", null) - ); - } - - @Test - void testNegativeStartingMoneyThrows() { - assertThrows(IllegalArgumentException.class, () -> - new Player("Jane", new BigDecimal("-1")) - ); - } - - @Test - void testAddNullAmountThrows() { - assertThrows(IllegalArgumentException.class, () -> - player.addMoney(null) - ); - } - - @Test - void testAddNegativeAmountThrows() { - assertThrows(IllegalArgumentException.class, () -> - player.addMoney(new BigDecimal("-100")) - ); - } - - @Test - void testWithdrawNullAmountThrows() { - assertThrows(IllegalArgumentException.class, () -> - player.withdrawMoney(null) - ); - } - - @Test - void testWithdrawNegativeAmountThrows() { - assertThrows(IllegalArgumentException.class, () -> - player.withdrawMoney(new BigDecimal("-100")) - ); - } + private Player player; + + @BeforeEach + void setUp() { + player = new Player("Jane", new BigDecimal("1000")); + } + + // ---- Positive tests ---- + + /** + * Tests that player name is stored and retrieved correctly. + */ + @Test + void testGetName() { + assertEquals("Jane", player.getName()); + } + + /** + * Tests that player's initial money equals starting capital. + */ + @Test + void testGetMoneyMatchesStartingCapital() { + assertEquals(new BigDecimal("1000"), player.getMoney()); + } + + /** + * Tests that adding money increases player's balance. + */ + @Test + void testAddMoney() { + player.addMoney(new BigDecimal("500")); + assertEquals(new BigDecimal("1500"), player.getMoney()); + } + + /** + * Tests that adding zero money does not change balance. + */ + @Test + void testAddMoneyZero() { + player.addMoney(BigDecimal.ZERO); + assertEquals(new BigDecimal("1000"), player.getMoney()); + } + + /** + * Tests that withdrawing money decreases player's balance. + */ + @Test + void testWithdrawMoney() { + player.withdrawMoney(new BigDecimal("400")); + assertEquals(new BigDecimal("600"), player.getMoney()); + } + + /** + * Tests that withdrawing zero money does not change balance. + */ + @Test + void testWithdrawMoneyZero() { + player.withdrawMoney(BigDecimal.ZERO); + assertEquals(new BigDecimal("1000"), player.getMoney()); + } + + /** + * Tests that player's portfolio is initially empty. + */ + @Test + void testPortfolioInitiallyEmpty() { + assertTrue(player.getPortfolio().getShares().isEmpty()); + } + + /** + * Tests that player's transaction archive is initially empty. + */ + @Test + void testTransactionArchiveInitiallyEmpty() { + assertTrue(player.getTransactionArchive().isEmpty()); + } + + /** + * Tests that player can be created with zero starting money. + */ + @Test + void testStartingMoneyZeroAllowed() { + Player broke = new Player("Broke", BigDecimal.ZERO); + assertEquals(BigDecimal.ZERO, broke.getMoney()); + } + + // ---- Negative tests ---- + + /** + * Tests that Player constructor throws when given a null name. + */ + @Test + void testNullNameThrows() { + assertThrows(IllegalArgumentException.class, () -> + new Player(null, new BigDecimal("1000")) + ); + } + + /** + * Tests that Player constructor throws when given a blank name. + */ + @Test + void testBlankNameThrows() { + assertThrows(IllegalArgumentException.class, () -> + new Player(" ", new BigDecimal("1000")) + ); + } + + /** + * Tests that Player constructor throws when given null starting money. + */ + @Test + void testNullStartingMoneyThrows() { + assertThrows(IllegalArgumentException.class, () -> + new Player("Jane", null) + ); + } + + /** + * Tests that Player constructor throws when given negative starting money. + */ + @Test + void testNegativeStartingMoneyThrows() { + assertThrows(IllegalArgumentException.class, () -> + new Player("Jane", new BigDecimal("-1")) + ); + } + + /** + * Tests that addMoney throws when given null amount. + */ + @Test + void testAddNullAmountThrows() { + assertThrows(IllegalArgumentException.class, () -> + player.addMoney(null) + ); + } + + /** + * Tests that addMoney throws when given negative amount. + */ + @Test + void testAddNegativeAmountThrows() { + assertThrows(IllegalArgumentException.class, () -> + player.addMoney(new BigDecimal("-100")) + ); + } + + /** + * Tests that withdrawMoney throws when given null amount. + */ + @Test + void testWithdrawNullAmountThrows() { + assertThrows(IllegalArgumentException.class, () -> + player.withdrawMoney(null) + ); + } + + /** + * Tests that withdrawMoney throws when given negative amount. + */ + @Test + void testWithdrawNegativeAmountThrows() { + assertThrows(IllegalArgumentException.class, () -> + player.withdrawMoney(new BigDecimal("-100")) + ); + } } + diff --git a/src/test/java/PortfolioTest.java b/src/test/java/PortfolioTest.java index 34e46aa..359f3c5 100644 --- a/src/test/java/PortfolioTest.java +++ b/src/test/java/PortfolioTest.java @@ -1,155 +1,210 @@ import static org.junit.jupiter.api.Assertions.*; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - import Model.Portfolio; import Model.Share; import Model.Stock; - import java.math.BigDecimal; import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +/** + * Unit tests for the Portfolio class. + * Tests share management, portfolio queries, net worth calculation, and error + * handling. + */ public class PortfolioTest { - private Portfolio portfolio; - private Stock stock; - private Share share; - - @BeforeEach - void setUp() { - portfolio = new Portfolio(); - stock = new Stock("AAPL", "Apple", new BigDecimal("150")); - share = new Share(stock, new BigDecimal("10"), new BigDecimal("140")); - } - - // ---- Positive tests ---- - - @Test - void testAddShare() { - boolean result = portfolio.addShare(share); - assertTrue(result); - assertTrue(portfolio.contains(share)); - } - - @Test - void testAddMultipleShares() { - Stock stock2 = new Stock("GOOGL", "Google", new BigDecimal("200")); - Share share2 = new Share(stock2, new BigDecimal("5"), new BigDecimal("190")); - - portfolio.addShare(share); - portfolio.addShare(share2); - - assertEquals(2, portfolio.getShares().size()); - } - - @Test - void testRemoveShare() { - portfolio.addShare(share); - boolean result = portfolio.removeShare(share); - assertTrue(result); - assertFalse(portfolio.contains(share)); - } - - @Test - void testRemoveShareNotInPortfolioReturnsFalse() { - boolean result = portfolio.removeShare(share); - assertFalse(result); - } - - @Test - void testGetSharesReturnsAll() { - portfolio.addShare(share); - List shares = portfolio.getShares(); - assertEquals(1, shares.size()); - assertTrue(shares.contains(share)); - } - - @Test - void testGetSharesIsDefensiveCopy() { - portfolio.addShare(share); - List copy = portfolio.getShares(); - copy.clear(); - assertEquals(1, portfolio.getShares().size()); - } - - @Test - void testGetSharesBySymbol() { - Stock stock2 = new Stock("GOOGL", "Google", new BigDecimal("200")); - Share share2 = new Share(stock2, new BigDecimal("5"), new BigDecimal("190")); - - portfolio.addShare(share); - portfolio.addShare(share2); - - List result = portfolio.getShares("AAPL"); - assertEquals(1, result.size()); - assertTrue(result.contains(share)); - } - - @Test - void testGetSharesBySymbolNoMatch() { - portfolio.addShare(share); - List result = portfolio.getShares("MSFT"); - assertTrue(result.isEmpty()); - } - - @Test - void testContainsReturnsTrueWhenPresent() { - portfolio.addShare(share); - assertTrue(portfolio.contains(share)); - } - - @Test - void testContainsReturnsFalseWhenAbsent() { - assertFalse(portfolio.contains(share)); - } - - @Test - void testGetNetWorthEmptyPortfolio() { - assertEquals(BigDecimal.ZERO, portfolio.getNetWorth()); - } - - @Test - void testGetNetWorthWithShares() { - portfolio.addShare(share); - // net worth should be positive when portfolio has holdings - assertTrue(portfolio.getNetWorth().compareTo(BigDecimal.ZERO) > 0); - } - - // ---- Negative tests ---- - - @Test - void testAddNullShareThrows() { - assertThrows(IllegalArgumentException.class, () -> - portfolio.addShare(null) - ); - } - - @Test - void testRemoveNullShareThrows() { - assertThrows(IllegalArgumentException.class, () -> - portfolio.removeShare(null) - ); - } - - @Test - void testContainsNullShareThrows() { - assertThrows(IllegalArgumentException.class, () -> - portfolio.contains(null) - ); - } - - @Test - void testGetSharesByNullSymbolThrows() { - assertThrows(IllegalArgumentException.class, () -> - portfolio.getShares((String) null) - ); - } - - @Test - void testGetSharesByBlankSymbolThrows() { - assertThrows(IllegalArgumentException.class, () -> - portfolio.getShares(" ") - ); - } + private Portfolio portfolio; + private Stock stock; + private Share share; + + @BeforeEach + void setUp() { + portfolio = new Portfolio(); + stock = new Stock("AAPL", "Apple", new BigDecimal("150")); + share = new Share(stock, new BigDecimal("10"), new BigDecimal("140")); + } + + // ---- Positive tests ---- + + /** + * Tests that adding a share to portfolio returns true and succeeds. + */ + @Test + void testAddShare() { + boolean result = portfolio.addShare(share); + assertTrue(result); + assertTrue(portfolio.contains(share)); + } + + /** + * Tests that portfolio can store multiple shares from different stocks. + */ + @Test + void testAddMultipleShares() { + Stock stock2 = new Stock("GOOGL", "Google", new BigDecimal("200")); + Share share2 = new Share(stock2, new BigDecimal("5"), new BigDecimal("190")); + + portfolio.addShare(share); + portfolio.addShare(share2); + + assertEquals(2, portfolio.getShares().size()); + } + + /** + * Tests that removing a share from portfolio returns true and succeeds. + */ + @Test + void testRemoveShare() { + portfolio.addShare(share); + boolean result = portfolio.removeShare(share); + assertTrue(result); + assertFalse(portfolio.contains(share)); + } + + /** + * Tests that removing a share not in portfolio returns false. + */ + @Test + void testRemoveShareNotInPortfolioReturnsFalse() { + boolean result = portfolio.removeShare(share); + assertFalse(result); + } + + /** + * Tests that getShares returns all shares in portfolio. + */ + @Test + void testGetSharesReturnsAll() { + portfolio.addShare(share); + List shares = portfolio.getShares(); + assertEquals(1, shares.size()); + assertTrue(shares.contains(share)); + } + + /** + * Tests that getShares returns a defensive copy that cannot modify internal + * state. + */ + @Test + void testGetSharesIsDefensiveCopy() { + portfolio.addShare(share); + List copy = portfolio.getShares(); + copy.clear(); + assertEquals(1, portfolio.getShares().size()); + } + + /** + * Tests that getShares by symbol returns only shares of that stock. + */ + @Test + void testGetSharesBySymbol() { + Stock stock2 = new Stock("GOOGL", "Google", new BigDecimal("200")); + Share share2 = new Share(stock2, new BigDecimal("5"), new BigDecimal("190")); + + portfolio.addShare(share); + portfolio.addShare(share2); + + List result = portfolio.getShares("AAPL"); + assertEquals(1, result.size()); + assertTrue(result.contains(share)); + } + + /** + * Tests that getShares by symbol returns empty list when no matches. + */ + @Test + void testGetSharesBySymbolNoMatch() { + portfolio.addShare(share); + List result = portfolio.getShares("MSFT"); + assertTrue(result.isEmpty()); + } + + /** + * Tests that contains returns true for shares in portfolio. + */ + @Test + void testContainsReturnsTrueWhenPresent() { + portfolio.addShare(share); + assertTrue(portfolio.contains(share)); + } + + /** + * Tests that contains returns false for shares not in portfolio. + */ + @Test + void testContainsReturnsFalseWhenAbsent() { + assertFalse(portfolio.contains(share)); + } + + /** + * Tests that empty portfolio has zero net worth. + */ + @Test + void testGetNetWorthEmptyPortfolio() { + assertEquals(BigDecimal.ZERO, portfolio.getNetWorth()); + } + + /** + * Tests that portfolio with shares has positive net worth. + */ + @Test + void testGetNetWorthWithShares() { + portfolio.addShare(share); + // net worth should be positive when portfolio has holdings + assertTrue(portfolio.getNetWorth().compareTo(BigDecimal.ZERO) > 0); + } + + // ---- Negative tests ---- + + /** + * Tests that addShare throws when given null share. + */ + @Test + void testAddNullShareThrows() { + assertThrows(IllegalArgumentException.class, () -> + portfolio.addShare(null) + ); + } + + /** + * Tests that removeShare throws when given null share. + */ + @Test + void testRemoveNullShareThrows() { + assertThrows(IllegalArgumentException.class, () -> + portfolio.removeShare(null) + ); + } + + /** + * Tests that contains throws when given null share. + */ + @Test + void testContainsNullShareThrows() { + assertThrows(IllegalArgumentException.class, () -> + portfolio.contains(null) + ); + } + + /** + * Tests that getShares throws when given null symbol. + */ + @Test + void testGetSharesByNullSymbolThrows() { + assertThrows(IllegalArgumentException.class, () -> + portfolio.getShares((String) null) + ); + } + + /** + * Tests that getShares throws when given blank symbol. + */ + @Test + void testGetSharesByBlankSymbolThrows() { + assertThrows(IllegalArgumentException.class, () -> + portfolio.getShares(" ") + ); + } } diff --git a/src/test/java/PurchaseTest.java b/src/test/java/PurchaseTest.java index d41e71a..de1c2ca 100644 --- a/src/test/java/PurchaseTest.java +++ b/src/test/java/PurchaseTest.java @@ -1,103 +1,139 @@ import static org.junit.jupiter.api.Assertions.*; -import java.math.BigDecimal; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - import Model.Player; import Model.Purchase; import Model.Share; import Model.Stock; +import java.math.BigDecimal; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +/** + * Unit tests for the Purchase class. + * Tests purchase transaction handling, commitment, and error handling. + */ public class PurchaseTest { - private Stock stock; - private Share share; - private Purchase purchase; - private Player player; - - @BeforeEach - void setUp() { - stock = new Stock("AAPL", "Apple", new BigDecimal("100")); - share = new Share(stock, new BigDecimal("20"), new BigDecimal("50")); - purchase = new Purchase(share, 1); - player = new Player("Jane", new BigDecimal("500000")); - } - - // ---- Positive tests ---- - - @Test - void testCommitDeductsMoneyFromPlayer() { - BigDecimal before = player.getMoney(); - purchase.commit(player); - assertTrue(player.getMoney().compareTo(before) < 0); - } - - @Test - void testCommitAddsShareToPortfolio() { - purchase.commit(player); - assertTrue(player.getPortfolio().contains(share)); - } - - @Test - void testCommitRecordedInArchive() { - purchase.commit(player); - assertTrue(player.getTransactionArchive().getAllTransactions().contains(purchase)); - } - - @Test - void testCommitSetsCommittedFlag() { - purchase.commit(player); - assertTrue(purchase.isCommitted()); - } - - @Test - void testGetShareReturnsCorrectShare() { - assertEquals(share, purchase.getShare()); - } - - @Test - void testGetWeekReturnsCorrectWeek() { - assertEquals(1, purchase.getWeek()); - } - - // ---- Negative tests ---- - - @Test - void testCommitNullPlayerThrows() { - assertThrows(IllegalArgumentException.class, () -> - purchase.commit(null) - ); - } - - @Test - void testCommitInsufficientFundsThrows() { - Player poorPlayer = new Player("Broke", new BigDecimal("1")); - assertThrows(IllegalStateException.class, () -> - purchase.commit(poorPlayer) - ); - } - - @Test - void testCommitAlreadyCommittedThrows() { - purchase.commit(player); - assertThrows(IllegalStateException.class, () -> - purchase.commit(player) - ); - } - - @Test - void testConstructorWeekZeroThrows() { - assertThrows(IllegalArgumentException.class, () -> - new Purchase(share, 0) - ); - } - - @Test - void testConstructorNegativeWeekThrows() { - assertThrows(IllegalArgumentException.class, () -> - new Purchase(share, -5) - ); - } + private Stock stock; + private Share share; + private Purchase purchase; + private Player player; + + @BeforeEach + void setUp() { + stock = new Stock("AAPL", "Apple", new BigDecimal("100")); + share = new Share(stock, new BigDecimal("20"), new BigDecimal("50")); + purchase = new Purchase(share, 1); + player = new Player("Jane", new BigDecimal("500000")); + } + + // ---- Positive tests ---- + + /** + * Tests that committing a purchase deducts money from player's balance. + */ + @Test + void testCommitDeductsMoneyFromPlayer() { + BigDecimal before = player.getMoney(); + purchase.commit(player); + assertTrue(player.getMoney().compareTo(before) < 0); + } + + /** + * Tests that committing a purchase adds the share to player's portfolio. + */ + @Test + void testCommitAddsShareToPortfolio() { + purchase.commit(player); + assertTrue(player.getPortfolio().contains(share)); + } + + /** + * Tests that committed purchase is recorded in transaction archive. + */ + @Test + void testCommitRecordedInArchive() { + purchase.commit(player); + assertTrue(player.getTransactionArchive().getAllTransactions().contains(purchase)); + } + + /** + * Tests that committing a purchase sets the committed flag. + */ + @Test + void testCommitSetsCommittedFlag() { + purchase.commit(player); + assertTrue(purchase.isCommitted()); + } + + /** + * Tests that getShare returns the correct share object. + */ + @Test + void testGetShareReturnsCorrectShare() { + assertEquals(share, purchase.getShare()); + } + + /** + * Tests that getWeek returns the purchase week. + */ + @Test + void testGetWeekReturnsCorrectWeek() { + assertEquals(1, purchase.getWeek()); + } + + // ---- Negative tests ---- + + /** + * Tests that commit throws when given null player. + */ + @Test + void testCommitNullPlayerThrows() { + assertThrows(IllegalArgumentException.class, () -> + purchase.commit(null) + ); + } + + /** + * Tests that commit throws when player has insufficient funds. + */ + @Test + void testCommitInsufficientFundsThrows() { + Player poorPlayer = new Player("Broke", new BigDecimal("1")); + assertThrows(IllegalStateException.class, () -> + purchase.commit(poorPlayer) + ); + } + + /** + * Tests that commit throws when purchase was already committed. + */ + @Test + void testCommitAlreadyCommittedThrows() { + purchase.commit(player); + assertThrows(IllegalStateException.class, () -> + purchase.commit(player) + ); + } + + /** + * Tests that Purchase constructor throws when given zero week. + */ + @Test + void testConstructorWeekZeroThrows() { + assertThrows(IllegalArgumentException.class, () -> + new Purchase(share, 0) + ); + } + + /** + * Tests that Purchase constructor throws when given negative week. + */ + @Test + void testConstructorNegativeWeekThrows() { + assertThrows(IllegalArgumentException.class, () -> + new Purchase(share, -5) + ); + } } + diff --git a/src/test/java/SaleTest.java b/src/test/java/SaleTest.java index 622f262..31efe4d 100644 --- a/src/test/java/SaleTest.java +++ b/src/test/java/SaleTest.java @@ -1,104 +1,141 @@ -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; import Model.Player; import Model.Sale; import Model.Share; import Model.Stock; - -import static org.junit.jupiter.api.Assertions.*; - import java.math.BigDecimal; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for the Sale class. + * Tests sale transaction handling, commitment, and error handling. + */ public class SaleTest { - private Stock stock; - private Share share; - private Sale sale; - private Player player; - - @BeforeEach - void setUp() { - stock = new Stock("AAPL", "Apple", new BigDecimal("100")); - share = new Share(stock, new BigDecimal("20"), new BigDecimal("80")); - sale = new Sale(share, 1); - player = new Player("Jane", new BigDecimal("500")); - player.getPortfolio().addShare(share); - } - - // ---- Positive tests ---- - - @Test - void testCommitAddsMoney() { - BigDecimal before = player.getMoney(); - sale.commit(player); - assertTrue(player.getMoney().compareTo(before) > 0); - } - - @Test - void testCommitRemovesShareFromPortfolio() { - sale.commit(player); - assertFalse(player.getPortfolio().contains(share)); - } - - @Test - void testCommitRecordedInArchive() { - sale.commit(player); - assertTrue(player.getTransactionArchive().getAllTransactions().contains(sale)); - } - - @Test - void testCommitSetsCommittedFlag() { - sale.commit(player); - assertTrue(sale.isCommitted()); - } - - @Test - void testGetShareReturnsCorrectShare() { - assertEquals(share, sale.getShare()); - } - - @Test - void testGetWeekReturnsCorrectWeek() { - assertEquals(1, sale.getWeek()); - } - - // ---- Negative tests ---- - - @Test - void testCommitNullPlayerThrows() { - assertThrows(IllegalArgumentException.class, () -> - sale.commit(null) - ); - } - - @Test - void testCommitAlreadyCommittedThrows() { - sale.commit(player); - assertThrows(IllegalStateException.class, () -> - sale.commit(player) - ); - } - - @Test - void testCommitShareNotInPortfolioThrows() { - Player otherPlayer = new Player("Bob", new BigDecimal("500")); - assertThrows(IllegalStateException.class, () -> - sale.commit(otherPlayer) - ); - } - - @Test - void testConstructorWeekZeroThrows() { - assertThrows(IllegalArgumentException.class, () -> - new Sale(share, 0) - ); - } - - @Test - void testConstructorNegativeWeekThrows() { - assertThrows(IllegalArgumentException.class, () -> - new Sale(share, -3) - ); - } + private Stock stock; + private Share share; + private Sale sale; + private Player player; + + @BeforeEach + void setUp() { + stock = new Stock("AAPL", "Apple", new BigDecimal("100")); + share = new Share(stock, new BigDecimal("20"), new BigDecimal("80")); + sale = new Sale(share, 1); + player = new Player("Jane", new BigDecimal("500")); + player.getPortfolio().addShare(share); + } + + // ---- Positive tests ---- + + /** + * Tests that committing a sale adds money to player's balance. + */ + @Test + void testCommitAddsMoney() { + BigDecimal before = player.getMoney(); + sale.commit(player); + assertTrue(player.getMoney().compareTo(before) > 0); + } + + /** + * Tests that committing a sale removes the share from player's portfolio. + */ + @Test + void testCommitRemovesShareFromPortfolio() { + sale.commit(player); + assertFalse(player.getPortfolio().contains(share)); + } + + /** + * Tests that committed sale is recorded in transaction archive. + */ + @Test + void testCommitRecordedInArchive() { + sale.commit(player); + assertTrue(player.getTransactionArchive().getAllTransactions().contains(sale)); + } + + /** + * Tests that committing a sale sets the committed flag. + */ + @Test + void testCommitSetsCommittedFlag() { + sale.commit(player); + assertTrue(sale.isCommitted()); + } + + /** + * Tests that getShare returns the correct share object. + */ + @Test + void testGetShareReturnsCorrectShare() { + assertEquals(share, sale.getShare()); + } + + /** + * Tests that getWeek returns the sale week. + */ + @Test + void testGetWeekReturnsCorrectWeek() { + assertEquals(1, sale.getWeek()); + } + + // ---- Negative tests ---- + + /** + * Tests that commit throws when given null player. + */ + @Test + void testCommitNullPlayerThrows() { + assertThrows(IllegalArgumentException.class, () -> + sale.commit(null) + ); + } + + /** + * Tests that commit throws when sale was already committed. + */ + @Test + void testCommitAlreadyCommittedThrows() { + sale.commit(player); + assertThrows(IllegalStateException.class, () -> + sale.commit(player) + ); + } + + /** + * Tests that commit throws when share is not in player's portfolio. + */ + @Test + void testCommitShareNotInPortfolioThrows() { + Player otherPlayer = new Player("Bob", new BigDecimal("500")); + assertThrows(IllegalStateException.class, () -> + sale.commit(otherPlayer) + ); + } + + /** + * Tests that Sale constructor throws when given zero week. + */ + @Test + void testConstructorWeekZeroThrows() { + assertThrows(IllegalArgumentException.class, () -> + new Sale(share, 0) + ); + } + + /** + * Tests that Sale constructor throws when given negative week. + */ + @Test + void testConstructorNegativeWeekThrows() { + assertThrows(IllegalArgumentException.class, () -> + new Sale(share, -3) + ); + } } + diff --git a/src/test/java/ShareTest.java b/src/test/java/ShareTest.java index fbc018d..37b5422 100644 --- a/src/test/java/ShareTest.java +++ b/src/test/java/ShareTest.java @@ -1,96 +1,134 @@ import static org.junit.jupiter.api.Assertions.*; +import Model.Share; +import Model.Stock; +import java.math.BigDecimal; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import Model.Share; -import Model.Stock; -import java.math.BigDecimal; +/** + * Unit tests for the Share class. + * Tests share creation, property access, and error handling. + */ public class ShareTest { - private Stock stock; - - @BeforeEach - void setUp() { - stock = new Stock("AAPL", "Apple", new BigDecimal("150")); - } - - // ---- Positive tests ---- - - @Test - void testShareConstructorNotNull() { - Share share = new Share(stock, new BigDecimal("10"), new BigDecimal("140")); - assertNotNull(share); - } - - @Test - void testGetStock() { - Share share = new Share(stock, new BigDecimal("10"), new BigDecimal("140")); - assertEquals(stock, share.getStock()); - } - - @Test - void testGetQuantity() { - Share share = new Share(stock, new BigDecimal("10"), new BigDecimal("140")); - assertEquals(new BigDecimal("10"), share.getQuantity()); - } - - @Test - void testGetPurchasePrice() { - Share share = new Share(stock, new BigDecimal("10"), new BigDecimal("140")); - assertEquals(new BigDecimal("140"), share.getPurchasePrice()); - } - - @Test - void testPurchasePriceZeroAllowed() { - // buying at price 0 is an edge case but not invalid - Share share = new Share(stock, new BigDecimal("5"), BigDecimal.ZERO); - assertEquals(BigDecimal.ZERO, share.getPurchasePrice()); - } - - // ---- Negative tests ---- - - @Test - void testNullStockThrows() { - assertThrows(IllegalArgumentException.class, () -> - new Share(null, new BigDecimal("10"), new BigDecimal("100")) - ); - } - - @Test - void testNullQuantityThrows() { - assertThrows(IllegalArgumentException.class, () -> - new Share(stock, null, new BigDecimal("100")) - ); - } - - @Test - void testZeroQuantityThrows() { - assertThrows(IllegalArgumentException.class, () -> - new Share(stock, BigDecimal.ZERO, new BigDecimal("100")) - ); - } - - @Test - void testNegativeQuantityThrows() { - assertThrows(IllegalArgumentException.class, () -> - new Share(stock, new BigDecimal("-5"), new BigDecimal("100")) - ); - } - - @Test - void testNullPurchasePriceThrows() { - assertThrows(IllegalArgumentException.class, () -> - new Share(stock, new BigDecimal("10"), null) - ); - } - - @Test - void testNegativePurchasePriceThrows() { - assertThrows(IllegalArgumentException.class, () -> - new Share(stock, new BigDecimal("10"), new BigDecimal("-50")) - ); - } + private Stock stock; + + @BeforeEach + void setUp() { + stock = new Stock("AAPL", "Apple", new BigDecimal("150")); + } + + // ---- Positive tests ---- + + /** + * Tests that Share constructor creates a non-null object. + */ + @Test + void testShareConstructorNotNull() { + Share share = new Share(stock, new BigDecimal("10"), new BigDecimal("140")); + assertNotNull(share); + } + + /** + * Tests that getStock returns the correct stock object. + */ + @Test + void testGetStock() { + Share share = new Share(stock, new BigDecimal("10"), new BigDecimal("140")); + assertEquals(stock, share.getStock()); + } + + /** + * Tests that getQuantity returns the correct quantity. + */ + @Test + void testGetQuantity() { + Share share = new Share(stock, new BigDecimal("10"), new BigDecimal("140")); + assertEquals(new BigDecimal("10"), share.getQuantity()); + } + + /** + * Tests that getPurchasePrice returns the correct purchase price. + */ + @Test + void testGetPurchasePrice() { + Share share = new Share(stock, new BigDecimal("10"), new BigDecimal("140")); + assertEquals(new BigDecimal("140"), share.getPurchasePrice()); + } + + /** + * Tests that share can be created with zero purchase price. + */ + @Test + void testPurchasePriceZeroAllowed() { + // buying at price 0 is an edge case but not invalid + Share share = new Share(stock, new BigDecimal("5"), BigDecimal.ZERO); + assertEquals(BigDecimal.ZERO, share.getPurchasePrice()); + } + + // ---- Negative tests ---- + + /** + * Tests that Share constructor throws when given null stock. + */ + @Test + void testNullStockThrows() { + assertThrows(IllegalArgumentException.class, () -> + new Share(null, new BigDecimal("10"), new BigDecimal("100")) + ); + } + + /** + * Tests that Share constructor throws when given null quantity. + */ + @Test + void testNullQuantityThrows() { + assertThrows(IllegalArgumentException.class, () -> + new Share(stock, null, new BigDecimal("100")) + ); + } + + /** + * Tests that Share constructor throws when given zero quantity. + */ + @Test + void testZeroQuantityThrows() { + assertThrows(IllegalArgumentException.class, () -> + new Share(stock, BigDecimal.ZERO, new BigDecimal("100")) + ); + } + + /** + * Tests that Share constructor throws when given negative quantity. + */ + @Test + void testNegativeQuantityThrows() { + assertThrows(IllegalArgumentException.class, () -> + new Share(stock, new BigDecimal("-5"), new BigDecimal("100")) + ); + } + + /** + * Tests that Share constructor throws when given null purchase price. + */ + @Test + void testNullPurchasePriceThrows() { + assertThrows(IllegalArgumentException.class, () -> + new Share(stock, new BigDecimal("10"), null) + ); + } + + /** + * Tests that Share constructor throws when given negative purchase price. + */ + @Test + void testNegativePurchasePriceThrows() { + assertThrows(IllegalArgumentException.class, () -> + new Share(stock, new BigDecimal("10"), new BigDecimal("-50")) + ); + } } + diff --git a/src/test/java/StockTest.java b/src/test/java/StockTest.java index 6a6a477..c07c75b 100644 --- a/src/test/java/StockTest.java +++ b/src/test/java/StockTest.java @@ -4,161 +4,231 @@ import Model.Stock; import static org.junit.jupiter.api.Assertions.*; - import java.math.BigDecimal; import java.util.List; +/** + * Unit tests for the Stock class. + * Tests stock creation, price tracking, and error handling. + */ public class StockTest { - private Stock stock; - - @BeforeEach - void setUp() { - stock = new Stock("AAPL", "Apple", new BigDecimal("1000")); - } - - // ---- Positive tests ---- - - @Test - void testGetSymbol() { - assertEquals("AAPL", stock.getSymbol()); - } - - @Test - void testGetCompany() { - assertEquals("Apple", stock.getCompany()); - } - - @Test - void testGetSalesPrice() { - assertEquals(new BigDecimal("1000"), stock.getSalesPrice()); - } - - @Test - void testGetSalesPriceZero() { - Stock zeroStock = new Stock("ZERO", "ZeroCorp", new BigDecimal("0")); - assertEquals(new BigDecimal("0"), zeroStock.getSalesPrice()); - } - - @Test - void testAddNewSalesPrice() { - stock.addNewSalesPrice(new BigDecimal("1200")); - assertEquals(new BigDecimal("1200"), stock.getSalesPrice()); - } - - @Test - void testAddNewSalesPriceZero() { - stock.addNewSalesPrice(new BigDecimal("0")); - assertEquals(new BigDecimal("0"), stock.getSalesPrice()); - } - - @Test - void testGetHistoricalPricesInitial() { - List history = stock.getHistoricalPrices(); - assertEquals(1, history.size()); - assertEquals(new BigDecimal("1000"), history.get(0)); - } - - @Test - void testGetHistoricalPricesAfterUpdates() { - stock.addNewSalesPrice(new BigDecimal("1100")); - stock.addNewSalesPrice(new BigDecimal("1200")); - assertEquals(3, stock.getHistoricalPrices().size()); - } - - @Test - void testGetHighestPrice() { - stock.addNewSalesPrice(new BigDecimal("1500")); - stock.addNewSalesPrice(new BigDecimal("800")); - assertEquals(new BigDecimal("1500"), stock.getHighestPrice()); - } - - @Test - void testGetLowestPrice() { - stock.addNewSalesPrice(new BigDecimal("1500")); - stock.addNewSalesPrice(new BigDecimal("800")); - assertEquals(new BigDecimal("800"), stock.getLowestPrice()); - } - - @Test - void testGetLatestPriceChangePositive() { - stock.addNewSalesPrice(new BigDecimal("1100")); - assertEquals(new BigDecimal("100"), stock.getLatestPriceChange()); - } - - @Test - void testGetLatestPriceChangeNegative() { - stock.addNewSalesPrice(new BigDecimal("900")); - assertEquals(new BigDecimal("-100"), stock.getLatestPriceChange()); - } - - @Test - void testGetLatestPriceChangeOnlyOnePrice() { - assertEquals(BigDecimal.ZERO, stock.getLatestPriceChange()); - } - - @Test - void testHistoricalPricesIsDefensiveCopy() { - List history = stock.getHistoricalPrices(); - history.add(new BigDecimal("9999")); - assertEquals(1, stock.getHistoricalPrices().size()); - } - - // ---- Negative tests ---- - - @Test - void testNullSymbolThrows() { - assertThrows(IllegalArgumentException.class, () -> - new Stock(null, "Apple", new BigDecimal("100")) - ); - } - - @Test - void testBlankSymbolThrows() { - assertThrows(IllegalArgumentException.class, () -> - new Stock(" ", "Apple", new BigDecimal("100")) - ); - } - - @Test - void testNullCompanyThrows() { - assertThrows(IllegalArgumentException.class, () -> - new Stock("AAPL", null, new BigDecimal("100")) - ); - } - - @Test - void testBlankCompanyThrows() { - assertThrows(IllegalArgumentException.class, () -> - new Stock("AAPL", " ", new BigDecimal("100")) - ); - } - - @Test - void testNullInitialPriceThrows() { - assertThrows(IllegalArgumentException.class, () -> - new Stock("AAPL", "Apple", null) - ); - } - - @Test - void testNegativeInitialPriceThrows() { - assertThrows(IllegalArgumentException.class, () -> - new Stock("AAPL", "Apple", new BigDecimal("-1")) - ); - } - - @Test - void testAddNullPriceThrows() { - assertThrows(IllegalArgumentException.class, () -> - stock.addNewSalesPrice(null) - ); - } - - @Test - void testAddNegativePriceThrows() { - assertThrows(IllegalArgumentException.class, () -> - stock.addNewSalesPrice(new BigDecimal("-10")) - ); - } + private Stock stock; + + @BeforeEach + void setUp() { + stock = new Stock("AAPL", "Apple", new BigDecimal("1000")); + } + + // ---- Positive tests ---- + + /** + * Tests that getSymbol returns the correct stock symbol. + */ + @Test + void testGetSymbol() { + assertEquals("AAPL", stock.getSymbol()); + } + + /** + * Tests that getCompany returns the correct company name. + */ + @Test + void testGetCompany() { + assertEquals("Apple", stock.getCompany()); + } + + /** + * Tests that getSalesPrice returns the current stock price. + */ + @Test + void testGetSalesPrice() { + assertEquals(new BigDecimal("1000"), stock.getSalesPrice()); + } + + /** + * Tests that stock can have zero sales price. + */ + @Test + void testGetSalesPriceZero() { + Stock zeroStock = new Stock("ZERO", "ZeroCorp", new BigDecimal("0")); + assertEquals(new BigDecimal("0"), zeroStock.getSalesPrice()); + } + + /** + * Tests that adding a new sales price updates the current price. + */ + @Test + void testAddNewSalesPrice() { + stock.addNewSalesPrice(new BigDecimal("1200")); + assertEquals(new BigDecimal("1200"), stock.getSalesPrice()); + } + + /** + * Tests that zero price can be added to price history. + */ + @Test + void testAddNewSalesPriceZero() { + stock.addNewSalesPrice(new BigDecimal("0")); + assertEquals(new BigDecimal("0"), stock.getSalesPrice()); + } + + /** + * Tests that historical prices initially contain only the initial price. + */ + @Test + void testGetHistoricalPricesInitial() { + List history = stock.getHistoricalPrices(); + assertEquals(1, history.size()); + assertEquals(new BigDecimal("1000"), history.get(0)); + } + + /** + * Tests that historical prices accumulates all price changes. + */ + @Test + void testGetHistoricalPricesAfterUpdates() { + stock.addNewSalesPrice(new BigDecimal("1100")); + stock.addNewSalesPrice(new BigDecimal("1200")); + assertEquals(3, stock.getHistoricalPrices().size()); + } + + /** + * Tests that getHighestPrice returns the maximum price from history. + */ + @Test + void testGetHighestPrice() { + stock.addNewSalesPrice(new BigDecimal("1500")); + stock.addNewSalesPrice(new BigDecimal("800")); + assertEquals(new BigDecimal("1500"), stock.getHighestPrice()); + } + + /** + * Tests that getLowestPrice returns the minimum price from history. + */ + @Test + void testGetLowestPrice() { + stock.addNewSalesPrice(new BigDecimal("1500")); + stock.addNewSalesPrice(new BigDecimal("800")); + assertEquals(new BigDecimal("800"), stock.getLowestPrice()); + } + + /** + * Tests that getLatestPriceChange returns positive change for price increase. + */ + @Test + void testGetLatestPriceChangePositive() { + stock.addNewSalesPrice(new BigDecimal("1100")); + assertEquals(new BigDecimal("100"), stock.getLatestPriceChange()); + } + + /** + * Tests that getLatestPriceChange returns negative change for price decrease. + */ + @Test + void testGetLatestPriceChangeNegative() { + stock.addNewSalesPrice(new BigDecimal("900")); + assertEquals(new BigDecimal("-100"), stock.getLatestPriceChange()); + } + + /** + * Tests that price change is zero when only one price exists. + */ + @Test + void testGetLatestPriceChangeOnlyOnePrice() { + assertEquals(BigDecimal.ZERO, stock.getLatestPriceChange()); + } + + /** + * Tests that getHistoricalPrices returns a defensive copy. + */ + @Test + void testHistoricalPricesIsDefensiveCopy() { + List history = stock.getHistoricalPrices(); + history.add(new BigDecimal("9999")); + assertEquals(1, stock.getHistoricalPrices().size()); + } + + // ---- Negative tests ---- + + /** + * Tests that Stock constructor throws when given null symbol. + */ + @Test + void testNullSymbolThrows() { + assertThrows(IllegalArgumentException.class, () -> + new Stock(null, "Apple", new BigDecimal("100")) + ); + } + + /** + * Tests that Stock constructor throws when given blank symbol. + */ + @Test + void testBlankSymbolThrows() { + assertThrows(IllegalArgumentException.class, () -> + new Stock(" ", "Apple", new BigDecimal("100")) + ); + } + + /** + * Tests that Stock constructor throws when given null company name. + */ + @Test + void testNullCompanyThrows() { + assertThrows(IllegalArgumentException.class, () -> + new Stock("AAPL", null, new BigDecimal("100")) + ); + } + + /** + * Tests that Stock constructor throws when given blank company name. + */ + @Test + void testBlankCompanyThrows() { + assertThrows(IllegalArgumentException.class, () -> + new Stock("AAPL", " ", new BigDecimal("100")) + ); + } + + /** + * Tests that Stock constructor throws when given null initial price. + */ + @Test + void testNullInitialPriceThrows() { + assertThrows(IllegalArgumentException.class, () -> + new Stock("AAPL", "Apple", null) + ); + } + + /** + * Tests that Stock constructor throws when given negative initial price. + */ + @Test + void testNegativeInitialPriceThrows() { + assertThrows(IllegalArgumentException.class, () -> + new Stock("AAPL", "Apple", new BigDecimal("-1")) + ); + } + + /** + * Tests that addNewSalesPrice throws when given null price. + */ + @Test + void testAddNullPriceThrows() { + assertThrows(IllegalArgumentException.class, () -> + stock.addNewSalesPrice(null) + ); + } + + /** + * Tests that addNewSalesPrice throws when given negative price. + */ + @Test + void testAddNegativePriceThrows() { + assertThrows(IllegalArgumentException.class, () -> + stock.addNewSalesPrice(new BigDecimal("-10")) + ); + } } + diff --git a/src/test/java/TransactionArchiveTest.java b/src/test/java/TransactionArchiveTest.java index 14ff009..df7ed0c 100644 --- a/src/test/java/TransactionArchiveTest.java +++ b/src/test/java/TransactionArchiveTest.java @@ -1,148 +1,196 @@ import static org.junit.jupiter.api.Assertions.*; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - import Model.Player; import Model.Purchase; import Model.Sale; import Model.Share; import Model.Stock; import Model.TransactionArchive; - import java.math.BigDecimal; import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +/** + * Unit tests for the TransactionArchive class. + * Tests transaction recording, retrieval, filtering, and error handling. + */ public class TransactionArchiveTest { - private TransactionArchive archive; - private Stock stock; - private Share share; - private Purchase purchase; - private Sale sale; - - @BeforeEach - void setUp() { - archive = new TransactionArchive(); - stock = new Stock("AAPL", "Apple", new BigDecimal("100")); - share = new Share(stock, new BigDecimal("10"), new BigDecimal("90")); - purchase = new Purchase(share, 1); - sale = new Sale(share, 2); - } - - // ---- Positive tests ---- - - @Test - void testIsEmptyOnCreation() { - assertTrue(archive.isEmpty()); - } - - @Test - void testIsEmptyAfterAdd() { - archive.add(purchase); - assertFalse(archive.isEmpty()); - } - - @Test - void testAddReturnsTrue() { - assertTrue(archive.add(purchase)); - } - - @Test - void testGetAllTransactions() { - archive.add(purchase); - archive.add(sale); - assertEquals(2, archive.getAllTransactions().size()); - } - - @Test - void testGetAllTransactionsIsDefensiveCopy() { - archive.add(purchase); - List copy = archive.getAllTransactions(); - copy.clear(); - assertEquals(1, archive.getAllTransactions().size()); - } - - @Test - void testGetTransactionsByWeek() { - archive.add(purchase); // week 1 - archive.add(sale); // week 2 - - List week1 = archive.getTransactions(1); - assertEquals(1, week1.size()); - assertTrue(week1.contains(purchase)); - } - - @Test - void testGetTransactionsByWeekNoMatch() { - archive.add(purchase); // week 1 - assertTrue(archive.getTransactions(99).isEmpty()); - } - - @Test - void testGetPurchaseByWeek() { - archive.add(purchase); // week 1 - archive.add(sale); // week 2 - - List purchases = archive.getPurchase(1); - assertEquals(1, purchases.size()); - assertTrue(purchases.contains(purchase)); - } - - @Test - void testGetPurchaseByWeekExcludesSales() { - // Add a sale at week 1 — should not appear in getPurchase(1) - Sale saleWeek1 = new Sale(share, 1); - archive.add(saleWeek1); - - assertTrue(archive.getPurchase(1).isEmpty()); - } - - @Test - void testGetSaleByWeek() { - archive.add(purchase); // week 1 - archive.add(sale); // week 2 - - List sales = archive.getSale(2); - assertEquals(1, sales.size()); - assertTrue(sales.contains(sale)); - } - - @Test - void testGetSaleByWeekExcludesPurchases() { - // Add a purchase at week 2 — should not appear in getSale(2) - Purchase purchaseWeek2 = new Purchase(share, 2); - archive.add(purchaseWeek2); - - assertTrue(archive.getSale(2).isEmpty()); - } - - @Test - void testCountDistinctWeeksEmpty() { - assertEquals(0, archive.countDistinctWeeks()); - } - - @Test - void testCountDistinctWeeksMultipleTransactionsSameWeek() { - Purchase purchase2 = new Purchase(share, 1); - archive.add(purchase); - archive.add(purchase2); - assertEquals(1, archive.countDistinctWeeks()); - } - - @Test - void testCountDistinctWeeksAcrossWeeks() { - archive.add(purchase); // week 1 - archive.add(sale); // week 2 - assertEquals(2, archive.countDistinctWeeks()); - } - - // ---- Negative tests ---- - - @Test - void testAddNullThrows() { - assertThrows(IllegalArgumentException.class, () -> - archive.add(null) - ); - } + private TransactionArchive archive; + private Stock stock; + private Share share; + private Purchase purchase; + private Sale sale; + + @BeforeEach + void setUp() { + archive = new TransactionArchive(); + stock = new Stock("AAPL", "Apple", new BigDecimal("100")); + share = new Share(stock, new BigDecimal("10"), new BigDecimal("90")); + purchase = new Purchase(share, 1); + sale = new Sale(share, 2); + } + + // ---- Positive tests ---- + + /** + * Tests that archive is empty immediately after creation. + */ + @Test + void testIsEmptyOnCreation() { + assertTrue(archive.isEmpty()); + } + + /** + * Tests that isEmpty returns false after adding a transaction. + */ + @Test + void testIsEmptyAfterAdd() { + archive.add(purchase); + assertFalse(archive.isEmpty()); + } + + /** + * Tests that add method returns true for successful addition. + */ + @Test + void testAddReturnsTrue() { + assertTrue(archive.add(purchase)); + } + + /** + * Tests that getAllTransactions returns all stored transactions. + */ + @Test + void testGetAllTransactions() { + archive.add(purchase); + archive.add(sale); + assertEquals(2, archive.getAllTransactions().size()); + } + + /** + * Tests that getAllTransactions returns a defensive copy. + */ + @Test + void testGetAllTransactionsIsDefensiveCopy() { + archive.add(purchase); + List copy = archive.getAllTransactions(); + copy.clear(); + assertEquals(1, archive.getAllTransactions().size()); + } + + /** + * Tests that getTransactions filters transactions by week number. + */ + @Test + void testGetTransactionsByWeek() { + archive.add(purchase); // week 1 + archive.add(sale); // week 2 + + List week1 = archive.getTransactions(1); + assertEquals(1, week1.size()); + assertTrue(week1.contains(purchase)); + } + + /** + * Tests that getTransactions returns empty list for non-existent week. + */ + @Test + void testGetTransactionsByWeekNoMatch() { + archive.add(purchase); // week 1 + assertTrue(archive.getTransactions(99).isEmpty()); + } + + /** + * Tests that getPurchase returns only purchase transactions for week. + */ + @Test + void testGetPurchaseByWeek() { + archive.add(purchase); // week 1 + archive.add(sale); // week 2 + + List purchases = archive.getPurchase(1); + assertEquals(1, purchases.size()); + assertTrue(purchases.contains(purchase)); + } + + /** + * Tests that getPurchase does not return sale transactions. + */ + @Test + void testGetPurchaseByWeekExcludesSales() { + // Add a sale at week 1 — should not appear in getPurchase(1) + Sale saleWeek1 = new Sale(share, 1); + archive.add(saleWeek1); + + assertTrue(archive.getPurchase(1).isEmpty()); + } + + /** + * Tests that getSale returns only sale transactions for week. + */ + @Test + void testGetSaleByWeek() { + archive.add(purchase); // week 1 + archive.add(sale); // week 2 + + List sales = archive.getSale(2); + assertEquals(1, sales.size()); + assertTrue(sales.contains(sale)); + } + + /** + * Tests that getSale does not return purchase transactions. + */ + @Test + void testGetSaleByWeekExcludesPurchases() { + // Add a purchase at week 2 — should not appear in getSale(2) + Purchase purchaseWeek2 = new Purchase(share, 2); + archive.add(purchaseWeek2); + + assertTrue(archive.getSale(2).isEmpty()); + } + + /** + * Tests that countDistinctWeeks returns zero for empty archive. + */ + @Test + void testCountDistinctWeeksEmpty() { + assertEquals(0, archive.countDistinctWeeks()); + } + + /** + * Tests that countDistinctWeeks counts only distinct weeks. + */ + @Test + void testCountDistinctWeeksMultipleTransactionsSameWeek() { + Purchase purchase2 = new Purchase(share, 1); + archive.add(purchase); + archive.add(purchase2); + assertEquals(1, archive.countDistinctWeeks()); + } + + /** + * Tests that countDistinctWeeks correctly counts across multiple weeks. + */ + @Test + void testCountDistinctWeeksAcrossWeeks() { + archive.add(purchase); // week 1 + archive.add(sale); // week 2 + assertEquals(2, archive.countDistinctWeeks()); + } + + // ---- Negative tests ---- + + /** + * Tests that add throws when given null transaction. + */ + @Test + void testAddNullThrows() { + assertThrows(IllegalArgumentException.class, () -> + archive.add(null) + ); + } } + diff --git a/target/test-classes/ExchangeTest.class b/target/test-classes/ExchangeTest.class index 238ca9d..c301cc2 100644 Binary files a/target/test-classes/ExchangeTest.class and b/target/test-classes/ExchangeTest.class differ diff --git a/target/test-classes/PortfolioTest.class b/target/test-classes/PortfolioTest.class index 679e0c4..6929863 100644 Binary files a/target/test-classes/PortfolioTest.class and b/target/test-classes/PortfolioTest.class differ diff --git a/target/test-classes/ShareTest.class b/target/test-classes/ShareTest.class index abf202d..bdbf4a6 100644 Binary files a/target/test-classes/ShareTest.class and b/target/test-classes/ShareTest.class differ diff --git a/target/test-classes/StockTest.class b/target/test-classes/StockTest.class index 0008700..c71a54d 100644 Binary files a/target/test-classes/StockTest.class and b/target/test-classes/StockTest.class differ