personalBests = new LinkedHashMap<>();
+
+ for (DailyRecord dailyRecord : history.values()) {
+ // Skip this record if it does not have a Day (workout data)
+ if (dailyRecord.getDayFromRecord() == null) {
+ continue;
+ }
+
+ int exercisesCount = dailyRecord.getDayFromRecord().getExercisesCount();
+ for (int i = 0; i < exercisesCount; i++) {
+ Exercise exercise = dailyRecord.getDayFromRecord().getExercise(i);
+ String exerciseName = exercise.getName();
+
+ if (!personalBests.containsKey(exerciseName) || isBetter(exercise, personalBests.get(exerciseName))) {
+ personalBests.put(exerciseName, exercise);
+ }
+ }
+ }
+ return personalBests;
+ }
+
+ /**
+ * Compares two exercises to determine if the current exercise is better based on weight.
+ *
+ * @param current the current exercise to evaluate
+ * @param best the existing best exercise to compare against
+ * @return {@code true} if the current exercise is better, {@code false} otherwise
+ */
+ private boolean isBetter(Exercise current, Exercise best) {
+ return current.getWeight() > best.getWeight();
+ }
+
+ /**
+ * Retrieves a formatted personal best entry for a specific exercise.
+ *
+ * Filters out any {@code DailyRecord} entries without a valid workout {@code Day} to avoid
+ * null pointer exceptions.
+ *
+ * @param exerciseName the name of the exercise to look up
+ * @return a formatted string showing the personal best for the specified exercise, or a message if not found
+ */
+ public String getPersonalBestForExercise(String exerciseName) {
+ Exercise personalBest = null;
+
+ for (DailyRecord dailyRecord : history.values()) {
+ // Skip this record if it does not have a Day (workout data)
+ if (dailyRecord.getDayFromRecord() == null) {
+ continue;
+ }
+
+ int exercisesCount = dailyRecord.getDayFromRecord().getExercisesCount();
+ for (int i = 0; i < exercisesCount; i++) {
+ Exercise exercise = dailyRecord.getDayFromRecord().getExercise(i);
+
+ if (exercise.getName().equalsIgnoreCase(exerciseName)) {
+ if (personalBest == null || isBetter(exercise, personalBest)) {
+ personalBest = exercise;
+ }
+ }
+ }
+ }
+
+ if (personalBest != null) {
+ return String.format("Personal best for %s: %s", exerciseName, personalBest.toStringPb());
+ } else {
+ return String.format("No personal best found for %s", exerciseName);
+ }
+ }
+
+ //@@author Bev-Low
+ /**
+ * Converts the {@code history} map, which stores {@link DailyRecord} objects associated with their
+ * completion dates, into a formatted string representation for printing.
+ *
+ * This method iterates over each entry in the {@code history} map and formats each
+ * {@code LocalDate} key and corresponding {@code DailyRecord} value into a readable string.
+ * Each record is separated by a line of equals signs to improve readability.
+ *
+ * If the history is empty, this method returns a message indicating that no history is available.
+ *
+ * @return a formatted string representation of the history, displaying each {@code DailyRecord}
+ * with its completion date. If the history is empty, returns "No history available."
+ */
+ @Override
+ public String toString() {
+ StringBuilder historyString = new StringBuilder();
+ int count = 0;
+ int size = history.size();
+
+ if (history.isEmpty()) {
+ return "No history available.";
+ }
+
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy");
+
+ for (LocalDate date : history.keySet()) {
+ historyString.append("\n");
+ historyString.append(String.format("Completed On: %s%n%n", date.format(formatter)));
+ DailyRecord dailyRecord = history.get(date);
+ historyString.append(dailyRecord.toString());
+ count++;
+ if (count < size) {
+ historyString.append("\n\n==============\n");
+ }
+ }
+
+ return historyString.toString();
+ }
+ //@@author
+}
+
diff --git a/src/main/java/meal/Meal.java b/src/main/java/meal/Meal.java
new file mode 100644
index 0000000000..e72bdf8dce
--- /dev/null
+++ b/src/main/java/meal/Meal.java
@@ -0,0 +1,71 @@
+// @@author Atulteja
+
+package meal;
+
+import java.util.Objects;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Represents a meal with a name and calorie count.
+ */
+public class Meal {
+
+ private static final Logger logger = Logger.getLogger(Meal.class.getName());
+
+ private int calories;
+ private String name;
+
+ /**
+ * Constructs a Meal with the specified name and calorie count.
+ *
+ * @param name the name of the meal
+ * @param calories the calorie count of the meal
+ * @throws AssertionError if the name is null, empty, or if the calories are negative
+ */
+ public Meal(String name, int calories) {
+ assert name != null && !name.isEmpty() : "Meal name cannot be null or empty";
+ assert calories >= 0 : "Calories cannot be negative";
+
+ this.name = name;
+ this.calories = calories;
+
+ logger.log(Level.INFO, "Meal created: {0} with {1} kcal", new Object[]{name, calories});
+ }
+
+ public int getCalories() {
+ return calories;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(calories, name);
+ }
+
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ Meal meal = (Meal) o;
+ return calories == meal.calories &&
+ Objects.equals(name, meal.name);
+ }
+
+ /**
+ * Overridden to return a string representation of the meal, including its name and calorie count.
+ *
+ * @return a string representation of the meal
+ */
+ @Override
+ public String toString() {
+ return name + " | " + calories + "kcal";
+ }
+}
diff --git a/src/main/java/meal/MealList.java b/src/main/java/meal/MealList.java
new file mode 100644
index 0000000000..0db1cdd93a
--- /dev/null
+++ b/src/main/java/meal/MealList.java
@@ -0,0 +1,107 @@
+// @@author Atulteja
+
+package meal;
+
+import exceptions.MealException;
+
+import java.util.ArrayList;
+import java.util.Objects;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+
+/**
+ * Represents a list of meals, providing functionality to add, delete, and retrieve meals.
+ */
+public class MealList {
+ private static final Logger logger = Logger.getLogger(MealList.class.getName());
+ private ArrayList meals;
+
+ public MealList() {
+ meals = new ArrayList<>();
+ logger.log(Level.INFO, "MealList created with an empty list.");
+ }
+
+ public boolean isEmpty() {
+ return meals.isEmpty();
+ }
+
+ public int getSize() {
+ return meals.size();
+ }
+
+ /**
+ * Adds a meal to the list.
+ *
+ * @param meal the meal to add to the list
+ * @throws AssertionError if the meal is null
+ */
+ public void addMeal(Meal meal) {
+ assert meal != null : "Meal cannot be null";
+
+
+ meals.add(meal);
+ logger.log(Level.INFO, "Added meal: {0}. Current list: {1}", new Object[]{meal, meals});
+ }
+
+ /**
+ * Deletes a meal from the list at the specified index.
+ *
+ * @param index the index of the meal to delete
+ * @return the meal that was deleted
+ * @throws IndexOutOfBoundsException if the index is out of bounds
+ */
+ public Meal deleteMeal(int index) {
+ if (index < 0 || index >= meals.size()) {
+ logger.log(Level.WARNING, "Invalid index for deletion: {0}", index);
+ throw MealException.doesNotExist();
+ }
+
+ Meal mealToBeDeleted = meals.get(index);
+ meals.remove(index);
+ logger.log(Level.INFO, "Deleted meal: {0} at index {1}. Current list: {2}",
+ new Object[]{mealToBeDeleted, index, meals});
+ return mealToBeDeleted;
+ }
+
+ public ArrayList getMeals() {
+ logger.log(Level.INFO, "Retrieved meal list: {0}", meals);
+ return meals;
+ }
+
+ /**
+ * Overrides the toString to returns a string representation of the meal list.
+ * Each meal is represented by its index and details on a new line.
+ *
+ * @return a string representation of the meal list
+ */
+ @Override
+ public String toString() {
+ StringBuilder output = new StringBuilder();
+ int count = 1;
+
+ for (Meal meal : meals) {
+ output.append(count).append(": ").append(meal).append("\n");
+ count++;
+ }
+
+ return output.toString().trim(); // Trim to remove trailing newline
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(meals);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ MealList meallist = (MealList) o;
+ return meals == meallist.meals;
+ }
+}
diff --git a/src/main/java/parser/FlagDefinitions.java b/src/main/java/parser/FlagDefinitions.java
new file mode 100644
index 0000000000..0c28560ae3
--- /dev/null
+++ b/src/main/java/parser/FlagDefinitions.java
@@ -0,0 +1,49 @@
+//@@author nirala-ts
+package parser;
+import java.util.HashSet;
+
+public class FlagDefinitions {
+ public static final String DATE_FLAG = "/t";
+
+ public static final String PROGRAMME_FLAG = "/p";
+ public static final String DAY_FLAG = "/d";
+ public static final String EXERCISE_FLAG = "/e";
+
+ public static final String NAME_FLAG = "/n";
+ public static final String SETS_FLAG = "/s";
+ public static final String REPS_FLAG = "/r";
+ public static final String WEIGHT_FLAG = "/w";
+ public static final String CALORIES_FLAG = "/c";
+
+ public static final String REMOVE_EXERCISE_FLAG = "/xe";
+ public static final String ADD_EXERCISE_FLAG = "/ae";
+ public static final String UPDATE_EXERCISE_FLAG = "/ue";
+
+ public static final String ADD_DAY_FLAG = "/ad";
+ public static final String REMOVE_DAY_FLAG = "/xd";
+
+ public static final String MEAL_INDEX = "/m";
+ public static final String WATER_INDEX = "/w";
+ public static final String VOLUME_FLAG = "/v";
+
+ public static final HashSet VALID_FLAGS = new HashSet<>();
+
+ static {
+ VALID_FLAGS.add(DATE_FLAG);
+ VALID_FLAGS.add(PROGRAMME_FLAG);
+ VALID_FLAGS.add(DAY_FLAG);
+ VALID_FLAGS.add(EXERCISE_FLAG);
+ VALID_FLAGS.add(NAME_FLAG);
+ VALID_FLAGS.add(SETS_FLAG);
+ VALID_FLAGS.add(REPS_FLAG);
+ VALID_FLAGS.add(WEIGHT_FLAG);
+ VALID_FLAGS.add(CALORIES_FLAG);
+ VALID_FLAGS.add(REMOVE_EXERCISE_FLAG);
+ VALID_FLAGS.add(ADD_EXERCISE_FLAG);
+ VALID_FLAGS.add(UPDATE_EXERCISE_FLAG);
+ VALID_FLAGS.add(ADD_DAY_FLAG);
+ VALID_FLAGS.add(REMOVE_DAY_FLAG);
+ VALID_FLAGS.add(MEAL_INDEX);
+ VALID_FLAGS.add(VOLUME_FLAG);
+ }
+}
diff --git a/src/main/java/parser/FlagParser.java b/src/main/java/parser/FlagParser.java
new file mode 100644
index 0000000000..839870ad81
--- /dev/null
+++ b/src/main/java/parser/FlagParser.java
@@ -0,0 +1,309 @@
+//@@author nirala-ts
+
+package parser;
+
+import exceptions.FlagException;
+
+import java.time.LocalDate;
+import java.util.HashMap;
+import java.util.Map;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import static common.Utils.isNull;
+
+import static parser.FlagDefinitions.DAY_FLAG;
+import static parser.FlagDefinitions.DATE_FLAG;
+import static parser.FlagDefinitions.EXERCISE_FLAG;
+import static parser.FlagDefinitions.NAME_FLAG;
+import static parser.FlagDefinitions.CALORIES_FLAG;
+import static parser.FlagDefinitions.REPS_FLAG;
+import static parser.FlagDefinitions.SETS_FLAG;
+import static parser.FlagDefinitions.WEIGHT_FLAG;
+import static parser.FlagDefinitions.PROGRAMME_FLAG;
+import static parser.FlagDefinitions.UPDATE_EXERCISE_FLAG;
+import static parser.FlagDefinitions.ADD_DAY_FLAG;
+import static parser.FlagDefinitions.ADD_EXERCISE_FLAG;
+import static parser.FlagDefinitions.REMOVE_DAY_FLAG;
+import static parser.FlagDefinitions.REMOVE_EXERCISE_FLAG;
+import static parser.FlagDefinitions.MEAL_INDEX;
+import static parser.FlagDefinitions.WATER_INDEX;
+import static parser.FlagDefinitions.VOLUME_FLAG;
+
+import static parser.ParserUtils.parseInteger;
+import static parser.ParserUtils.parseIndex;
+import static parser.ParserUtils.parseFloat;
+import static parser.ParserUtils.parseDate;
+import static parser.ParserUtils.splitArguments;
+
+/**
+ * The {@code FlagParser} class simplifies the parsing of flagged argument strings. The values can be retrieved in
+ * various formats, such as Integer, Date, String, or Index.
+ * This class also supports aliasing for easy retrieval of data in different formats.
+ *
+ * @author nirala-ts
+ */
+public class FlagParser {
+ private static final String DEFAULT_SPLIT_BY = "(?=/)";
+ private static final String SPLIT_BY_START = "(?=/(?!(";
+ private static final String SPLIT_BY_DELIMITER = "|";
+ private static final String SPLIT_BY_END = ")\\b))";
+
+ private final Logger logger = Logger.getLogger(FlagParser.class.getName());
+
+ private final Map parsedFlags = new HashMap<>();
+ private final Map aliasMap = new HashMap<>();
+
+ /**
+ * Constructs a {@code FlagParser} with the given argument string, setting up aliases and ignored flags.
+ * This parser is essential for processing commands with multiple flags.
+ *
+ * @param argumentString The argument string to parse.
+ * @param ignoredFlags Flags that are optional for this instance.
+ * @throws FlagException if {@code argumentString} is null.
+ */
+ public FlagParser(String argumentString, String... ignoredFlags) {
+ if (isNull(argumentString)){
+ throw FlagException.missingArguments();
+ }
+
+ initializeAliasMap();
+ parse(argumentString, generateSplitBy(ignoredFlags));
+ }
+
+ /**
+ * Generates a regex for splitting the argument string by specified flags and aliases.
+ *
+ * @param ignoredFlags Flags to ignore while generating the regex split pattern.
+ * @return A regex string to split the argument string by flags.
+ */
+ private String generateSplitBy(String... ignoredFlags){
+ if (ignoredFlags.length == 0){
+ return DEFAULT_SPLIT_BY;
+ }
+
+ // Starts building the regex pattern, initializing with specific syntax for ignored flags
+ StringBuilder splitBy = new StringBuilder(SPLIT_BY_START);
+ for (String ignoredFlag: ignoredFlags) {
+ splitBy.append(ignoredFlag.substring(1)).append(SPLIT_BY_DELIMITER);
+
+ // Checks for aliases and adds them to the pattern if they map to the ignored flag
+ for (Map.Entry entry: aliasMap.entrySet()){
+ String canonicalFlag = entry.getValue();
+ String aliasFlag = entry.getKey();
+ if (canonicalFlag.equals(ignoredFlag)){
+ splitBy.append(aliasFlag.substring(1)).append(SPLIT_BY_DELIMITER);
+ }
+ }
+ }
+
+ // Removes the trailing delimiter added after the last flag to prevent invalid regex syntax
+ splitBy.setLength(splitBy.length() - 1);
+
+ return splitBy.append(SPLIT_BY_END).toString();
+ }
+
+ /**
+ * Sets up flag aliases to allow flexible parsing by recognizing alternative names for flags.
+ */
+ private void initializeAliasMap() {
+ aliasMap.put("/programme", PROGRAMME_FLAG);
+ aliasMap.put("/prog", PROGRAMME_FLAG);
+
+ aliasMap.put("/day", DAY_FLAG);
+ aliasMap.put("/date", DATE_FLAG);
+
+ aliasMap.put("/name", NAME_FLAG);
+
+ aliasMap.put("/exercise", EXERCISE_FLAG);
+ aliasMap.put("/ex", EXERCISE_FLAG);
+ aliasMap.put("/set", SETS_FLAG);
+ aliasMap.put("/sets", SETS_FLAG);
+ aliasMap.put("/rep", REPS_FLAG);
+ aliasMap.put("/reps", REPS_FLAG);
+ aliasMap.put("/weight", WEIGHT_FLAG);
+ aliasMap.put("/calories", CALORIES_FLAG);
+
+ aliasMap.put("/addEx", ADD_EXERCISE_FLAG);
+ aliasMap.put("/updateEx", UPDATE_EXERCISE_FLAG);
+ aliasMap.put("/removeEx", REMOVE_EXERCISE_FLAG);
+
+ aliasMap.put("/addDay", ADD_DAY_FLAG);
+ aliasMap.put("/removeDay", REMOVE_DAY_FLAG);
+
+ aliasMap.put("/meal", MEAL_INDEX);
+
+ aliasMap.put("/water", WATER_INDEX);
+ aliasMap.put("/volume", VOLUME_FLAG);
+ aliasMap.put("/vol", VOLUME_FLAG);
+ }
+
+ /**
+ * Parses the argument string by splitting it based on the given regex and populates
+ * the {@code parsedFlags} map with flag-value pairs.
+ *
+ * @param argumentString The string to parse.
+ * @param splitBy The regex used to split the argument string by flags.
+ */
+ private void parse(String argumentString, String splitBy) {
+ assert argumentString != null : "Argument string must not be null";
+
+ String[] args = argumentString.split(splitBy);
+ for (String arg : args) {
+
+ String[] argParts = splitArguments(arg);
+ String flag = argParts[0].trim();
+ String value = argParts[1].trim();
+ flag = resolveAlias(flag);
+
+ if (hasFlag(flag)) {
+ throw FlagException.duplicateFlag(flag);
+ }
+
+ if (!FlagDefinitions.VALID_FLAGS.contains(flag)) {
+ throw FlagException.invalidFlag(flag);
+ }
+
+ logger.log(Level.INFO, "Successfully parsed flag: {0} with value: {1}", new Object[]{flag, value});
+ parsedFlags.put(flag, value);
+ }
+ }
+
+ /**
+ * Resolves the alias for a given flag, returning the canonical flag if an alias is found.
+ *
+ * @param flag The flag or alias to resolve.
+ * @return The canonical flag, or the original flag if no alias is found.
+ */
+ private String resolveAlias(String flag) {
+ if (aliasMap.containsKey(flag)) {
+ return aliasMap.get(flag);
+ }
+ return flag;
+ }
+
+ /**
+ * Checks if a flag is present in the parsed flags.
+ *
+ * @param flag The flag to check.
+ * @return {@code true} if the flag is present, {@code false} otherwise.
+ */
+ public boolean hasFlag(String flag) {
+ assert flag != null && !flag.isEmpty() : "Flag must not be null or empty";
+
+ flag = resolveAlias(flag);
+ boolean hasFlag = parsedFlags.containsKey(flag);
+
+ logger.log(Level.INFO, "Flag {0} presence: {1}", new Object[]{flag, hasFlag});
+ return hasFlag;
+ }
+
+ /**
+ * Validates that all required flags are present in the parsed flags and contains a non-null value.
+ *
+ * @param requiredFlags The required flags to validate.
+ * @throws FlagException if any required flag is missing.
+ */
+ public void validateRequiredFlags(String... requiredFlags) {
+ assert requiredFlags != null : "Required flags string must not be null";
+
+ for (String flag : requiredFlags) {
+
+ flag = resolveAlias(flag);
+
+ if (!hasFlag(flag)) {
+ logger.log(Level.WARNING, "Missing required flag: {0}", flag);
+ throw FlagException.missingFlag(flag);
+ }
+
+ String value = getStringByFlag(flag);
+
+ if (isNull(value)) {
+ logger.log(Level.WARNING, "Required flag has null value: {0}", flag);
+ throw FlagException.missingRequiredArguments(flag);
+ }
+ }
+ }
+
+ public void validateUniqueFlag(String... uniqueFlags){
+ int count = 0;
+ StringBuilder seenFlags = new StringBuilder();
+
+ for (String flag : uniqueFlags) {
+ if (hasFlag(flag)) {
+ count++;
+ seenFlags.append(flag).append(" ");
+ }
+ }
+
+ if (count > 1) {
+ throw FlagException.nonUniqueFlag(seenFlags.toString());
+ }
+ }
+
+ /**
+ * Retrieves the string value associated with a flag.
+ *
+ * @param flag The flag whose value to retrieve.
+ * @return The value associated with the flag, or {@code null} if the flag is absent.
+ */
+ public String getStringByFlag(String flag) {
+ assert flag != null && !flag.isEmpty() : "Flag must not be null or empty";
+
+ flag = resolveAlias(flag);
+
+ if (!parsedFlags.containsKey(flag)) {
+ logger.log(Level.INFO, "Flag {0} not found; returning null", flag);
+ return null;
+ }
+
+ String value = parsedFlags.get(flag);
+ logger.log(Level.INFO, "Successfully retrieved value for flag {0}: {1}", new Object[]{flag, value});
+ return value.trim();
+ }
+
+ /**
+ * Retrieves the zero-based index associated with a flag.
+ *
+ * @param flag The flag whose index to retrieve.
+ * @return The zero-based index parsed from the flag's value.
+ */
+ public int getIndexByFlag(String flag) {
+ String indexString = getStringByFlag(flag);
+ return parseIndex(indexString);
+ }
+
+ /**
+ * Retrieves the integer value associated with a flag.
+ *
+ * @param flag The flag whose integer value to retrieve.
+ * @return The integer parsed from the flag's value.
+ */
+ public int getIntegerByFlag(String flag){
+ String intString = getStringByFlag(flag);
+ return parseInteger(intString);
+ }
+
+ /**
+ * Retrieves the float value associated with a flag.
+ *
+ * @param flag The flag whose float value to retrieve.
+ * @return The float parsed from the flag's value.
+ */
+ public float getFloatByFlag(String flag) {
+ String floatString = getStringByFlag(flag);
+ return parseFloat(floatString);
+ }
+
+ /**
+ * Retrieves the date value associated with a flag.
+ *
+ * @param flag The flag whose date value to retrieve.
+ * @return The {@code LocalDate} parsed from the flag's value.
+ */
+ public LocalDate getDateByFlag(String flag){
+ String dateString = getStringByFlag(flag);
+ return parseDate(dateString);
+ }
+}
diff --git a/src/main/java/parser/Parser.java b/src/main/java/parser/Parser.java
new file mode 100644
index 0000000000..5f0d18d054
--- /dev/null
+++ b/src/main/java/parser/Parser.java
@@ -0,0 +1,65 @@
+//@@author nirala-ts
+
+package parser;
+
+import command.Command;
+import exceptions.ParserException;
+import parser.command.factory.CommandFactory;
+
+import java.util.logging.Logger;
+import java.util.logging.Level;
+
+import static parser.ParserUtils.splitArguments;
+
+/**
+ * The {@code Parser} class serves as an intermediary between user input and command execution
+ * within the BuffBuddy application. It interprets user-provided strings and generates appropriate
+ * {@code Command} objects for execution.
+ *
+ */
+public class Parser {
+ private final Logger logger = Logger.getLogger(this.getClass().getName());
+ private final CommandFactory commandFactory;
+
+ /**
+ * Constructs a new {@code Parser} instance, initializing its associated {@code CommandFactory}.
+ */
+ public Parser() {
+ this.commandFactory = new CommandFactory();
+ }
+
+ /**
+ * Constructs a new {@code Parser} instance, using the provided {@code CommandFactory}.
+ * This constructor is primarily for testing purposes.
+ */
+ public Parser(CommandFactory commandFactory) {
+ this.commandFactory = commandFactory;
+ }
+
+ /**
+ * Parses the given command string and returns the corresponding {@code Command} object.
+ *
+ * @param fullCommand The complete user input, containing the command and any arguments.
+ * @return A {@code Command} object that represents the parsed command.
+ * @throws ParserException if the input is null or empty.
+ */
+ public Command parse(String fullCommand) {
+ if (fullCommand == null || fullCommand.trim().isEmpty()) {
+ logger.log(Level.WARNING, "Command is empty");
+ throw ParserException.missingCommand();
+ }
+
+ /*
+ * Splits the full command input into the primary command and its associated arguments,
+ * enabling identification of the command category.
+ */
+ String[] inputArguments = splitArguments(fullCommand);
+ String commandString = inputArguments[0];
+ String argumentString = inputArguments[1];
+
+ logger.log(Level.INFO, "Successfully parsed command: {0}, with arguments: {1}",
+ new Object[]{commandString, argumentString});
+
+ return commandFactory.createCommand(commandString, argumentString);
+ }
+}
diff --git a/src/main/java/parser/ParserUtils.java b/src/main/java/parser/ParserUtils.java
new file mode 100644
index 0000000000..8d23f649e9
--- /dev/null
+++ b/src/main/java/parser/ParserUtils.java
@@ -0,0 +1,188 @@
+// @@author nirala-ts
+
+package parser;
+
+import exceptions.ParserException;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import static common.Utils.DATE_FORMAT;
+import static common.Utils.NULL_INTEGER;
+import static common.Utils.NULL_FLOAT;
+import static common.Utils.validate;
+
+
+/**
+ * The {@code ParserUtils} class is a utility class containing common methods used across all parsing functions.
+ * These methods handle tasks such as splitting arguments, parsing integers and floats, validating indices,
+ * and formatting dates.
+ */
+public class ParserUtils {
+ private static final Logger logger = Logger.getLogger(ParserUtils.class.getName());
+
+ /**
+ * Splits the argument string into the primary command and its arguments.
+ *
+ * @param argumentString The full argument string provided by the user.
+ * @return A string array containing the command as the first element and the remaining arguments as the second.
+ * @throws AssertionError if {@code argumentString} is null.
+ */
+ public static String[] splitArguments(String argumentString) {
+ assert argumentString != null : "Argument string must not be null";
+
+ String[] inputArguments = argumentString.split(" ", 2);
+ String command = inputArguments[0].trim();
+ String args = (inputArguments.length > 1) ? inputArguments[1].trim() : "";
+
+ logger.log(Level.INFO, "Successfully split arguments. Command: {0}, Arguments: {1}",
+ new Object[]{command, args});
+ return new String[]{command, args};
+ }
+
+ /**
+ * Trims the input string to remove leading and trailing whitespace.
+ *
+ * @param argumentString The string to trim.
+ * @return The trimmed version of {@code argumentString}.
+ * @throws ParserException if {@code argumentString} is empty after trimming.
+ */
+ static String trimInput(String argumentString) {
+ assert argumentString != null : "Argument string must not be null";
+ String trimmedString = argumentString.trim();
+
+ if (trimmedString.isEmpty()){
+ logger.log(Level.WARNING, "Trimmed input is empty");
+ throw ParserException.missingArguments();
+ }
+
+ logger.log(Level.INFO, "Successfully trimmed input: {0}", trimmedString);
+ return trimmedString;
+ }
+
+ /**
+ * Parses a string as an integer, returning a default value if the string is null.
+ *
+ * @param intString The string to parse as an integer.
+ * @return The parsed integer, or {@code NULL_INTEGER} if {@code intString} is null.
+ * @throws ParserException if {@code intString} cannot be parsed as an integer.
+ */
+ public static int parseInteger(String intString){
+ if (intString == null) {
+ logger.log(Level.INFO, "Integer string is null. Returning default value: {0}", NULL_INTEGER);
+ return NULL_INTEGER;
+ }
+
+ String trimmedIntString = trimInput(intString);
+ int result = -1;
+
+ if (trimmedIntString.length() > 10) { // 10 digits is the maximum for Integer.MAX_VALUE (2,147,483,647)
+ throw ParserException.infinityInt(trimmedIntString);
+ }
+
+ try{
+ result = Integer.parseInt(trimmedIntString);
+ logger.log(Level.INFO, "Successfully parsed integer: {0}", result);
+ } catch (NumberFormatException e){
+ logger.log(Level.WARNING, "Failed to parse integer from string: {0}", intString);
+ throw ParserException.invalidInt(result);
+ }
+
+ validate(result);
+
+ return result;
+ }
+
+ /**
+ * Parses a string as a float, returning a default value if the string is null.
+ *
+ * @param floatString The string to parse as a float.
+ * @return The parsed float, or {@code NULL_FLOAT} if {@code floatString} is null.
+ * @throws ParserException if {@code floatString} cannot be parsed as a float.
+ */
+ public static float parseFloat(String floatString) {
+ if (floatString == null) {
+ logger.log(Level.INFO, "Float string is null. Returning default value: {0}", NULL_FLOAT);
+ return NULL_FLOAT;
+ }
+
+ String trimmedFloatString = trimInput(floatString);
+ float result = -1;
+
+ try {
+ result = Float.parseFloat(trimmedFloatString);
+ logger.log(Level.INFO, "Successfully parsed float: {0}", result);
+ } catch (NumberFormatException e) {
+ logger.log(Level.WARNING, "Failed to parse float from string: {0}", floatString);
+ throw ParserException.invalidFloat(result);
+ }
+
+ validate(result);
+
+ return result;
+ }
+
+ /**
+ * Parses a string as an index, adjusting it to zero-based and returning a default if null.
+ *
+ * @param indexString The string to parse as an index.
+ * @return The zero-based index, or {@code NULL_INTEGER} if {@code indexString} is null.
+ * @throws ParserException if the index is less than zero.
+ */
+ public static int parseIndex(String indexString) {
+ if (indexString == null) {
+ logger.log(Level.INFO, "Index string is null. Returning default value: {0}", NULL_INTEGER);
+ return NULL_INTEGER;
+ }
+
+ int index = parseInteger(indexString);
+ validate(index);
+ index--;
+
+ logger.log(Level.INFO, "Successfully parsed index: {0}", index);
+ return index;
+
+ }
+
+ /**
+ * Parses a string as a date using the specified date format. If null, returns the current date.
+ *
+ * @param dateString The string to parse as a date.
+ * @return The parsed {@code LocalDate} object, or today's date if {@code dateString} is null.
+ * @throws ParserException if the date format is invalid.
+ */
+ public static LocalDate parseDate(String dateString) {
+ if (dateString == null || dateString.trim().isEmpty()) {
+ LocalDate today = LocalDate.now();
+ logger.log(Level.INFO, "Date string is null/empty. Returning current date: {0}", today);
+ return today;
+ }
+
+ String trimmedDateString = trimInput(dateString);
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_FORMAT);
+
+ try {
+ LocalDate date = LocalDate.parse(trimmedDateString, formatter);
+
+ String[] parts = trimmedDateString.split("-");
+ int inputDay = Integer.parseInt(parts[0]);
+ int inputMonth = Integer.parseInt(parts[1]);
+
+ // Check if the parsed date matches the input values as LocalDate.parse
+ // automatically adjusts invalid dates to the nearest valid one
+ if (date.getDayOfMonth() != inputDay || date.getMonthValue() != inputMonth) {
+ throw new DateTimeParseException("Invalid date: " + dateString, dateString, 0);
+ }
+
+ logger.log(Level.INFO, "Successfully parsed date: {0}", date);
+ return date;
+ } catch (DateTimeParseException e) {
+ logger.log(Level.WARNING, "Invalid date format: {0}. Expected format: {1}",
+ new Object[]{dateString, DATE_FORMAT});
+ throw ParserException.invalidDate(trimmedDateString);
+ }
+ }
+}
diff --git a/src/main/java/parser/command/factory/CommandFactory.java b/src/main/java/parser/command/factory/CommandFactory.java
new file mode 100644
index 0000000000..0f646f1696
--- /dev/null
+++ b/src/main/java/parser/command/factory/CommandFactory.java
@@ -0,0 +1,82 @@
+// @@author nirala-ts
+
+package parser.command.factory;
+
+import command.Command;
+import command.ExitCommand;
+import command.InvalidCommand;
+
+/**
+ * The {@code CommandFactory} class is responsible for creating instances of various {@code Command} objects
+ * based on the specified command string. It serves as a central factory that distributes command creation
+ * requests to specific factories for subcommands like "prog," "history," and others.
+ * For unsupported commands, it returns an {@code InvalidCommand} to handle erroneous input.
+ *
+ * Supported Commands:
+ *
+ * - Program Commands - handled by {@code ProgCommandFactory}
+ * - Meal Commands - handled by {@code MealCommandFactory}
+ * - Water Commands - handled by {@code WaterCommandFactory}
+ * - History Commands - handled by {@code HistoryCommandFactory}
+ * - Exit Command - creates an {@code ExitCommand} directly
+ * - Invalid Command - returns an {@code InvalidCommand} for unknown inputs
+ *
+ *
+ * @author nirala-ts
+ */
+public class CommandFactory {
+ private final ProgrammeCommandFactory progFactory;
+ private final MealCommandFactory mealFactory;
+ private final WaterCommandFactory waterFactory;
+ private final HistoryCommandFactory historyFactory;
+
+ /**
+ * Constructs a {@code CommandFactory} and initializes subcommand factories for handling
+ * specific types of commands.
+ */
+ public CommandFactory() {
+ this.progFactory = new ProgrammeCommandFactory();
+ this.mealFactory = new MealCommandFactory();
+ this.waterFactory = new WaterCommandFactory();
+ this.historyFactory = new HistoryCommandFactory();
+ }
+
+ /**
+ * Constructor for dependency injection, allowing custom instances of subcommand factories
+ * for testing with mock objects.
+ *
+ * @param progFactory the {@code ProgCommandFactory} instance to handle "prog" commands
+ * @param mealFactory the {@code MealCommandFactory} instance to handle "meal" commands
+ * @param waterFactory the {@code WaterCommandFactory} instance to handle "water" commands
+ * @param historyFactory the {@code HistoryCommandFactory} instance to handle "history" commands
+ */
+ public CommandFactory(ProgrammeCommandFactory progFactory, MealCommandFactory mealFactory,
+ WaterCommandFactory waterFactory, HistoryCommandFactory historyFactory) {
+ this.progFactory = progFactory;
+ this.mealFactory = mealFactory;
+ this.waterFactory = waterFactory;
+ this.historyFactory = historyFactory;
+ }
+
+ /**
+ * Creates and returns the appropriate {@code Command} object based on the provided command string.
+ * Delegates command parsing to the relevant subcommand factory when available. Returns an
+ * {@code ExitCommand} for exit requests and an {@code InvalidCommand} for unsupported commands.
+ *
+ * @param commandString The main command word indicating the type of command.
+ * @param argumentString The arguments provided for the command, if any.
+ * @return A {@code Command} object corresponding to the parsed command.
+ */
+ public Command createCommand(String commandString, String argumentString) {
+ assert commandString != null;
+
+ return switch (commandString) {
+ case ProgrammeCommandFactory.COMMAND_WORD -> progFactory.parse(argumentString);
+ case MealCommandFactory.COMMAND_WORD -> mealFactory.parse(argumentString);
+ case WaterCommandFactory.COMMAND_WORD -> waterFactory.parse(argumentString);
+ case HistoryCommandFactory.COMMAND_WORD -> historyFactory.parse(argumentString);
+ case ExitCommand.COMMAND_WORD -> new ExitCommand();
+ default -> new InvalidCommand();
+ };
+ }
+}
diff --git a/src/main/java/parser/command/factory/HistoryCommandFactory.java b/src/main/java/parser/command/factory/HistoryCommandFactory.java
new file mode 100644
index 0000000000..92555cedcb
--- /dev/null
+++ b/src/main/java/parser/command/factory/HistoryCommandFactory.java
@@ -0,0 +1,104 @@
+// @@author andreusxcarvalho
+package parser.command.factory;
+
+import command.Command;
+import command.history.ListPersonalBestsCommand;
+import command.history.ViewHistoryCommand;
+import command.history.DeleteHistoryCommand;
+import command.history.ListHistoryCommand;
+import command.history.ViewPersonalBestCommand;
+import command.history.WeeklySummaryCommand;
+import command.InvalidCommand;
+
+import java.time.LocalDate;
+
+import static parser.ParserUtils.parseDate;
+import static parser.ParserUtils.splitArguments;
+
+/**
+ * Factory class responsible for creating instances of history-related commands.
+ *
+ * The {@code HistoryCommandFactory} parses the input arguments and generates
+ * specific command objects based on the type of history command requested.
+ * Supported commands include viewing, listing, and deleting history, as well as
+ * managing personal bests.
+ *
+ */
+public class HistoryCommandFactory {
+ public static final String COMMAND_WORD = "history";
+
+ /**
+ * Parses the given argument string and creates the corresponding {@link Command} object.
+ *
+ * Based on the subcommand specified in the argument string, this method determines
+ * the appropriate command type and returns an instance of that command.
+ *
+ *
+ * @param argumentString the argument string containing the subcommand and any additional arguments
+ * @return the created {@link Command} object, or an {@link InvalidCommand} if the subcommand is unrecognized
+ */
+ public Command parse(String argumentString) {
+ assert argumentString != null : "Argument string must not be null";
+
+ String[] inputArguments = splitArguments(argumentString);
+ String subCommandString = inputArguments[0];
+ String arguments = inputArguments[1];
+
+ return switch (subCommandString) {
+ case ViewHistoryCommand.COMMAND_WORD -> prepareViewHistoryCommand(arguments);
+ case ListHistoryCommand.COMMAND_WORD -> new ListHistoryCommand();
+ case ListPersonalBestsCommand.COMMAND_WORD -> preparePersonalBestCommand(arguments);
+ case WeeklySummaryCommand.COMMAND_WORD -> new WeeklySummaryCommand();
+ case DeleteHistoryCommand.COMMAND_WORD -> prepareDeleteHistoryCommand(arguments);
+ default -> new InvalidCommand();
+ };
+ }
+
+ /**
+ * Prepares a {@link ViewHistoryCommand} for a specific date.
+ *
+ * This method parses the argument string to extract the date and creates a {@link ViewHistoryCommand}
+ * for viewing the history record on that date.
+ *
+ *
+ * @param argumentString the argument string containing the date
+ * @return a {@link ViewHistoryCommand} for the specified date
+ */
+ private Command prepareViewHistoryCommand(String argumentString) {
+ LocalDate date = parseDate(argumentString);
+ return new ViewHistoryCommand(date);
+ }
+
+ /**
+ * Prepares a command to handle personal bests, either listing all or viewing a specific exercise.
+ *
+ * If the argument string is empty, a {@link ListPersonalBestsCommand} is created.
+ * Otherwise, a {@link ViewPersonalBestCommand} is created for the specified exercise.
+ *
+ *
+ * @param argumentString the argument string specifying an exercise name, or empty to list all personal bests
+ * @return a {@link ListPersonalBestsCommand} or {@link ViewPersonalBestCommand}, depending on the argument
+ */
+ private Command preparePersonalBestCommand(String argumentString) {
+ if (argumentString.isEmpty()) {
+ return new ListPersonalBestsCommand();
+ }
+ return new ViewPersonalBestCommand(argumentString);
+ }
+
+ /**
+ * Prepares a {@link DeleteHistoryCommand} to delete a record for a specific date.
+ *
+ * This method parses the argument string to extract the date and creates a {@link DeleteHistoryCommand}
+ * to delete the history record on that date.
+ *
+ *
+ * @param argumentString the argument string containing the date to delete
+ * @return a {@link DeleteHistoryCommand} for the specified date
+ */
+ private Command prepareDeleteHistoryCommand(String argumentString) {
+ LocalDate date = parseDate(argumentString);
+ return new DeleteHistoryCommand(date);
+ }
+}
+
diff --git a/src/main/java/parser/command/factory/MealCommandFactory.java b/src/main/java/parser/command/factory/MealCommandFactory.java
new file mode 100644
index 0000000000..194992e47d
--- /dev/null
+++ b/src/main/java/parser/command/factory/MealCommandFactory.java
@@ -0,0 +1,103 @@
+// @@author Atulteja
+package parser.command.factory;
+
+import command.Command;
+import command.InvalidCommand;
+import command.meals.AddMealCommand;
+import command.meals.DeleteMealCommand;
+import command.meals.ViewMealCommand;
+import exceptions.MealException;
+import meal.Meal;
+import parser.FlagParser;
+
+import java.time.LocalDate;
+
+import static parser.FlagDefinitions.MEAL_INDEX;
+import static parser.ParserUtils.parseDate;
+import static parser.ParserUtils.splitArguments;
+
+
+/**
+ * Factory class for parsing meal-related commands.
+ * This class processes command strings and creates appropriate meal command instances.
+ */
+public class MealCommandFactory {
+ public static final String COMMAND_WORD = "meal";
+
+ /**
+ * Parses the argument string and returns the appropriate meal command based on the subcommand.
+ *
+ * @param argumentString the argument string containing the subcommand and flags
+ * @return the corresponding Command object based on the subcommand;
+ * InvalidCommand if the subcommand is unrecognized
+ * @throws AssertionError if the argument string is null
+ */
+ public Command parse(String argumentString) {
+ assert argumentString != null : "Argument string must not be null";
+
+ String[] inputArguments = splitArguments(argumentString);
+ String subCommandString = inputArguments[0];
+ String arguments = inputArguments[1];
+
+ return switch (subCommandString) {
+ case AddMealCommand.COMMAND_WORD -> prepareAddCommand(arguments);
+ case DeleteMealCommand.COMMAND_WORD -> prepareDeleteCommand(arguments);
+ case ViewMealCommand.COMMAND_WORD -> prepareViewCommand(arguments);
+ default -> new InvalidCommand();
+ };
+ }
+
+ /**
+ * Prepares an AddMealCommand by parsing the argument string for the meal name, calories, and date.
+ *
+ * @param argumentString the argument string containing flags for name, calories, and date
+ * @return an AddMealCommand containing the parsed meal details
+ * @throws AssertionError if required flags are missing or invalid
+ */
+ public Command prepareAddCommand(String argumentString) {
+ FlagParser flagParser = new FlagParser(argumentString);
+
+ flagParser.validateRequiredFlags("/n", "/c");
+
+ String mealName = flagParser.getStringByFlag("/n");
+ int mealCalories = flagParser.getIntegerByFlag("/c");
+
+ if (mealCalories < 0){
+ throw MealException.caloriesOutOfBounds();
+ }
+
+ LocalDate date = flagParser.getDateByFlag("/t");
+
+ Meal mealToAdd = new Meal(mealName, mealCalories);
+
+ return new AddMealCommand(mealToAdd, date);
+ }
+
+ /**
+ * Prepares a DeleteMealCommand by parsing the argument string for the meal index and date.
+ *
+ * @param argumentString the argument string containing flags for meal index and date
+ * @return a DeleteMealCommand containing the parsed meal index and date
+ * @throws AssertionError if the required flag is missing or invalid
+ */
+ public Command prepareDeleteCommand(String argumentString) {
+ FlagParser flagParser = new FlagParser(argumentString);
+
+ flagParser.validateRequiredFlags(MEAL_INDEX);
+
+ int mealIndexToDelete = flagParser.getIndexByFlag(MEAL_INDEX);
+ LocalDate date = flagParser.getDateByFlag("/t");
+
+ return new DeleteMealCommand(mealIndexToDelete, date);
+ }
+
+ /**
+ * Prepares a ViewMealCommand by parsing the argument string for the date.
+ * @param argumentString the argument string containing the date
+ * @return a ViewMealCommand containing the parsed date
+ */
+ public Command prepareViewCommand(String argumentString) {
+ LocalDate date = parseDate(argumentString);
+ return new ViewMealCommand(date);
+ }
+}
diff --git a/src/main/java/parser/command/factory/ProgrammeCommandFactory.java b/src/main/java/parser/command/factory/ProgrammeCommandFactory.java
new file mode 100644
index 0000000000..e319479795
--- /dev/null
+++ b/src/main/java/parser/command/factory/ProgrammeCommandFactory.java
@@ -0,0 +1,434 @@
+// @@author nirala-ts
+
+package parser.command.factory;
+
+import command.Command;
+import command.InvalidCommand;
+import command.programme.CreateProgrammeCommand;
+import command.programme.DeleteProgrammeCommand;
+import command.programme.ViewProgrammeCommand;
+import command.programme.ListProgrammeCommand;
+import command.programme.StartProgrammeCommand;
+import command.programme.LogProgrammeCommand;
+import command.programme.edit.EditProgrammeCommand;
+import command.programme.edit.EditExerciseProgrammeCommand;
+import command.programme.edit.CreateExerciseProgrammeCommand;
+import command.programme.edit.DeleteExerciseProgrammeCommand;
+import command.programme.edit.CreateDayProgrammeCommand;
+import command.programme.edit.DeleteDayProgrammeCommand;
+
+import exceptions.FlagException;
+import exceptions.ProgrammeException;
+import parser.FlagParser;
+import programme.Day;
+import programme.Exercise;
+import programme.ExerciseUpdate;
+
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import static parser.FlagDefinitions.DAY_FLAG;
+import static parser.FlagDefinitions.DATE_FLAG;
+import static parser.FlagDefinitions.EXERCISE_FLAG;
+import static parser.FlagDefinitions.NAME_FLAG;
+import static parser.FlagDefinitions.CALORIES_FLAG;
+import static parser.FlagDefinitions.REPS_FLAG;
+import static parser.FlagDefinitions.SETS_FLAG;
+import static parser.FlagDefinitions.WEIGHT_FLAG;
+import static parser.FlagDefinitions.PROGRAMME_FLAG;
+import static parser.FlagDefinitions.UPDATE_EXERCISE_FLAG;
+import static parser.FlagDefinitions.ADD_DAY_FLAG;
+import static parser.FlagDefinitions.ADD_EXERCISE_FLAG;
+import static parser.FlagDefinitions.REMOVE_DAY_FLAG;
+import static parser.FlagDefinitions.REMOVE_EXERCISE_FLAG;
+
+import static parser.ParserUtils.parseIndex;
+import static parser.ParserUtils.splitArguments;
+
+/**
+ * The {@code ProgCommandFactory} class is a factory responsible for creating all program-related commands.
+ * This class includes helper methods such as {@code parseDay} and {@code parseExercise} to handle
+ * common parsing tasks across program-related commands, centralizing command creation and
+ * simplifying command handling.
+ *
+ * Supported Commands:
+ *
+ * - Create program command - handled by {@link #prepareCreateCommand}
+ * - View program command - handled by {@link #prepareViewCommand}
+ * - Edit program and exercises command - handled by {@link #prepareEditCommand}
+ * - Start program command - handled by {@link #prepareStartCommand}
+ * - Delete program command - handled by {@link #prepareDeleteCommand}
+ * - Log program activity command - handled by {@link #prepareLogCommand}
+ *
+ *
+ */
+
+public class ProgrammeCommandFactory {
+ public static final String COMMAND_WORD = "prog";
+ private final Logger logger = Logger.getLogger(this.getClass().getName());
+
+ /**
+ * Parses the provided argument string to identify and create the appropriate command.
+ *
+ * @param argumentString The command's argument string, which contains the subcommand and arguments.
+ * @return The created {@code Command} object based on the subcommand specified in the argument string.
+ */
+ public Command parse(String argumentString) {
+ assert argumentString != null : "Argument string must not be null";
+
+ /*
+ * Splits the full command input into the primary command and its associated arguments,
+ * enabling identification of the command category.
+ */
+ String[] inputArguments = splitArguments(argumentString);
+ String subCommandString = inputArguments[0];
+ String arguments = inputArguments[1];
+
+ logger.log(Level.INFO, "Successfully parsed sub-command: {0}, with arguments: {1}",
+ new Object[]{subCommandString, arguments});
+
+ return switch (subCommandString) {
+ case CreateProgrammeCommand.COMMAND_WORD -> prepareCreateCommand(arguments);
+ case ViewProgrammeCommand.COMMAND_WORD -> prepareViewCommand(arguments);
+ case ListProgrammeCommand.COMMAND_WORD -> new ListProgrammeCommand();
+ case EditProgrammeCommand.COMMAND_WORD -> prepareEditCommand(arguments);
+ case StartProgrammeCommand.COMMAND_WORD -> prepareStartCommand(arguments);
+ case DeleteProgrammeCommand.COMMAND_WORD -> prepareDeleteCommand(arguments);
+ case LogProgrammeCommand.COMMAND_WORD -> prepareLogCommand(arguments);
+ default -> new InvalidCommand();
+ };
+ }
+
+ /**
+ * Prepares and returns a {@link CreateProgrammeCommand} to create a new program with a specified name and days.
+ *
+ * @param argumentString The argument string containing program details, including days and exercises.
+ * @return A {@link CreateProgrammeCommand} object that represents the request to create a new program.
+ * @throws ProgrammeException when programme is missing name.
+ */
+ private Command prepareCreateCommand(String argumentString) {
+ assert argumentString != null : "Argument string must not be null";
+
+ ArrayList days = new ArrayList<>();
+
+ // Splits the argument by DAY_FLAG to separate each day.
+ // The first element represents the program name.
+ String[] progParts = argumentString.split(DAY_FLAG);
+ String progName = progParts[0].trim();
+ if (progName.isEmpty()) {
+ logger.log(Level.WARNING, "Programme name is empty");
+ throw ProgrammeException.programmeMissingName();
+ }
+
+ for (int i = 1; i < progParts.length; i++) {
+ String dayString = progParts[i].trim();
+ Day day = parseDay(dayString);
+ days.add(day);
+ }
+
+ logger.log(Level.INFO, "Successfully prepared CreateCommand with programme: {0}", progName);
+ return new CreateProgrammeCommand(progName, days);
+ }
+
+ /**
+ * Prepares and returns a {@link ViewProgrammeCommand} to view a specific program by its index.
+ *
+ * @param argumentString The string containing the program index to view.
+ * @return A {@link ViewProgrammeCommand} initialized with the specified program index.
+ */
+ private Command prepareViewCommand(String argumentString) {
+ if (argumentString.isEmpty()) {
+ argumentString = null;
+ }
+
+ int progIndex = parseIndex(argumentString);
+
+ logger.log(Level.INFO, "Successfully prepared ViewCommand");
+ return new ViewProgrammeCommand(progIndex);
+ }
+
+ /**
+ * Prepares and returns a {@link StartProgrammeCommand} to activate a specific program by its index.
+ *
+ * @param argumentString The string containing the program index to start.
+ * @return A {@link StartProgrammeCommand} initialized with the specified program index.
+ */
+ private Command prepareStartCommand(String argumentString) {
+ assert argumentString != null : "Argument string must not be null";
+
+ int progIndex = parseIndex(argumentString);
+
+ logger.log(Level.INFO, "Successfully prepared StartCommand");
+ return new StartProgrammeCommand(progIndex);
+ }
+
+ /**
+ * Prepares and returns a {@link DeleteProgrammeCommand} to remove a specific program by its index.
+ *
+ * @param argumentString The string containing the program index to delete.
+ * @return A {@link DeleteProgrammeCommand} initialized with the specified program index.
+ */
+ private Command prepareDeleteCommand(String argumentString){
+ if (argumentString.isEmpty()) {
+ argumentString = null;
+ }
+
+ int progIndex = parseIndex(argumentString);
+
+ logger.log(Level.INFO, "Successfully prepared DeleteCommand");
+ return new DeleteProgrammeCommand(progIndex);
+ }
+
+ //@@author andreusxcarvalho
+ /**
+ * Prepares and returns a {@link LogProgrammeCommand} to log activity in a specific program on a particular date.
+ *
+ * @param argumentString The string containing flags for the date, program index, and day index.
+ * @return A {@link LogProgrammeCommand} initialized with the specified date and indices.
+ * @throws FlagException If required flags are missing.
+ */
+ public Command prepareLogCommand(String argumentString) {
+ FlagParser flagParser = new FlagParser(argumentString);
+
+ flagParser.validateRequiredFlags(DAY_FLAG);
+
+ LocalDate date = flagParser.getDateByFlag(DATE_FLAG);
+ int progIndex = flagParser.getIndexByFlag(PROGRAMME_FLAG);
+ int dayIndex = flagParser.getIndexByFlag(DAY_FLAG);
+
+ logger.log(Level.INFO, "Successfully prepared LogCommand with Date: {0}, " +
+ "Programme index: {1}, Day index: {2}", new Object[]{progIndex, dayIndex, date});
+
+ return new LogProgrammeCommand(progIndex, dayIndex, date);
+ }
+
+ // @@author TVageesan
+ /**
+ * Prepares and returns an appropriate {@link EditProgrammeCommand} based on the flags parsed
+ * from the provided argument string.
+ *
+ * @param argumentString A {@link String} containing arguments to parse.
+ * @return The specific {@link EditProgrammeCommand} object that corresponds to the flag detected.
+ * @throws FlagException If required flags are missing or unique flags are supplied more than once
+ */
+ private EditProgrammeCommand prepareEditCommand(String argumentString) {
+ assert argumentString != null : "Argument string must not be null";
+
+ /*
+ * Parse by flags except for all exercise related flags,
+ * ignoring exercise related flags for later parsing if needed
+ */
+ FlagParser flagParser = new FlagParser (
+ argumentString,
+ NAME_FLAG, REPS_FLAG,SETS_FLAG,WEIGHT_FLAG,EXERCISE_FLAG,CALORIES_FLAG
+ );
+
+ // Validate that the command flags appear at most once in the argument string
+ flagParser.validateUniqueFlag(
+ UPDATE_EXERCISE_FLAG,
+ ADD_EXERCISE_FLAG,
+ REMOVE_EXERCISE_FLAG,
+ ADD_DAY_FLAG,
+ REMOVE_DAY_FLAG
+ );
+
+ if (flagParser.hasFlag(UPDATE_EXERCISE_FLAG)) {
+ return prepareEditExerciseCommand(flagParser);
+ }
+
+ if (flagParser.hasFlag(ADD_EXERCISE_FLAG)) {
+ return prepareCreateExerciseCommand(flagParser);
+ }
+
+ if (flagParser.hasFlag(REMOVE_EXERCISE_FLAG)) {
+ return prepareDeleteExerciseCommand(flagParser);
+ }
+
+ if (flagParser.hasFlag(ADD_DAY_FLAG)) {
+ return prepareCreateDayCommand(flagParser);
+ }
+
+ if (flagParser.hasFlag(REMOVE_DAY_FLAG)) {
+ return prepareDeleteDayCommand(flagParser);
+ }
+
+ // If none of the command flags are present, throw exception
+ throw FlagException.missingArguments();
+ }
+
+ /**
+ * Creates and returns an {@link EditExerciseProgrammeCommand} for updating an exercise in a program.
+ *
+ * @author TVageesan
+ * @param flagParser A {@link FlagParser} containing parsed flags and arguments.
+ * @return An {@link EditExerciseProgrammeCommand} object to edit an existing exercise.
+ * @throws FlagException If required flags are missing.
+ */
+ private EditExerciseProgrammeCommand prepareEditExerciseCommand(FlagParser flagParser) {
+ assert flagParser != null: "flagParser must not be null";
+
+ flagParser.validateRequiredFlags(DAY_FLAG);
+ String editString = flagParser.getStringByFlag(UPDATE_EXERCISE_FLAG);
+
+ String[] editParts = splitArguments(editString);
+ int exerciseIndex = parseIndex(editParts[0]);
+ String exerciseString = editParts[1];
+
+ return new EditExerciseProgrammeCommand(
+ flagParser.getIndexByFlag(PROGRAMME_FLAG),
+ flagParser.getIndexByFlag(DAY_FLAG),
+ exerciseIndex,
+ parseExerciseUpdate(exerciseString)
+ );
+ }
+
+ /**
+ * Creates and returns a {@link CreateExerciseProgrammeCommand} for adding a new exercise to a day in a program.
+ *
+ * @param flagParser A {@link FlagParser} containing parsed flags and arguments.
+ * @return A {@link CreateExerciseProgrammeCommand} object to create a new exercise.
+ * @throws FlagException If required flags are missing.
+ */
+ private CreateExerciseProgrammeCommand prepareCreateExerciseCommand(FlagParser flagParser) {
+ assert flagParser != null: "flagParser must not be null";
+
+ flagParser.validateRequiredFlags(DAY_FLAG);
+ String exerciseString = flagParser.getStringByFlag(ADD_EXERCISE_FLAG);
+ return new CreateExerciseProgrammeCommand(
+ flagParser.getIndexByFlag(PROGRAMME_FLAG),
+ flagParser.getIndexByFlag(DAY_FLAG),
+ parseExercise(exerciseString)
+ );
+ }
+
+ /**
+ * Creates and returns a {@link DeleteExerciseProgrammeCommand} for removing an exercise from a day in a program.
+ *
+ * @param flagParser A {@link FlagParser} containing parsed flags and arguments.
+ * @return A {@link DeleteExerciseProgrammeCommand} object to delete an existing exercise.
+ * @throws FlagException If required flags are missing.
+ */
+ private DeleteExerciseProgrammeCommand prepareDeleteExerciseCommand(FlagParser flagParser) {
+ assert flagParser != null: "flagParser must not be null";
+
+ flagParser.validateRequiredFlags(DAY_FLAG, REMOVE_EXERCISE_FLAG);
+ return new DeleteExerciseProgrammeCommand(
+ flagParser.getIndexByFlag(PROGRAMME_FLAG),
+ flagParser.getIndexByFlag(DAY_FLAG),
+ flagParser.getIndexByFlag(REMOVE_EXERCISE_FLAG)
+ );
+ }
+
+ /**
+ * Creates and returns a {@link CreateDayProgrammeCommand} for adding a new day to a program.
+ *
+ * @param flagParser A {@link FlagParser} containing parsed flags and arguments.
+ * @return A {@link CreateDayProgrammeCommand} object to create a new day.
+ * @throws FlagException If required flags are missing.
+ */
+ private CreateDayProgrammeCommand prepareCreateDayCommand(FlagParser flagParser) {
+ assert flagParser != null: "flagParser must not be null";
+
+ String dayString = flagParser.getStringByFlag(ADD_DAY_FLAG);
+ return new CreateDayProgrammeCommand(
+ flagParser.getIndexByFlag(PROGRAMME_FLAG),
+ parseDay(dayString)
+ );
+ }
+
+ /**
+ * Creates and returns a {@link DeleteDayProgrammeCommand} for removing a day from a program.
+ *
+ * @param flagParser A {@link FlagParser} containing parsed flags and arguments.
+ * @return A {@link DeleteDayProgrammeCommand} object to delete an existing day.
+ * @throws FlagException If required flags are missing.
+ */
+ private DeleteDayProgrammeCommand prepareDeleteDayCommand(FlagParser flagParser) {
+ assert flagParser != null: "flagParser must not be null";
+
+ flagParser.validateRequiredFlags(REMOVE_DAY_FLAG);
+ return new DeleteDayProgrammeCommand(
+ flagParser.getIndexByFlag(PROGRAMME_FLAG),
+ flagParser.getIndexByFlag(REMOVE_DAY_FLAG)
+ );
+ }
+
+ // @@author nirala-ts
+ /**
+ * Parses a string of day related arguments and returns a Day object.
+ *
+ * @param dayString the input string representing a day and its exercises, not null.
+ * @return a Day object representing the parsed day and its exercises.
+ * @throws ProgrammeException if there are missing arguments to create a day.
+ */
+ private Day parseDay(String dayString) {
+ assert dayString != null : "Day string must not be null";
+
+ String[] dayParts = dayString.split(EXERCISE_FLAG);
+ String dayName = dayParts[0].trim();
+ if (dayName.isEmpty()) {
+ throw ProgrammeException.missingDayName();
+ }
+
+ Day day = new Day(dayName);
+
+ for (int j = 1; j < dayParts.length; j++) {
+ String exerciseString = dayParts[j].trim();
+ Exercise exercise = parseExercise(exerciseString);
+ day.insertExercise(exercise);
+ }
+
+ logger.log(Level.INFO, "Parsed day successfully: {0}", dayName);
+ return day;
+ }
+
+ // @@author nirala-ts
+ /**
+ * Parses an exercise string to create an {@link Exercise} object with required attributes.
+ *
+ * @param argumentString The string containing exercise details and flags.
+ * @return An {@link Exercise} object initialized with the specified attributes.
+ * @throws FlagException If required flags are missing.
+ * */
+ private Exercise parseExercise(String argumentString) {
+ assert argumentString != null : "Argument string must not be null";
+
+ FlagParser flagParser = new FlagParser(argumentString);
+
+ // Ensures the exercise contains all necessary information before creating a new Exercise
+ flagParser.validateRequiredFlags(SETS_FLAG, REPS_FLAG, WEIGHT_FLAG, CALORIES_FLAG, NAME_FLAG);
+
+ return new Exercise(
+ flagParser.getIntegerByFlag(SETS_FLAG),
+ flagParser.getIntegerByFlag(REPS_FLAG),
+ flagParser.getIntegerByFlag(WEIGHT_FLAG),
+ flagParser.getIntegerByFlag(CALORIES_FLAG),
+ flagParser.getStringByFlag(NAME_FLAG)
+ );
+ }
+
+ // @@author TVageesan
+ /**
+ * Parses an exercise string to create an {@link ExerciseUpdate} object with required attributes.
+ *
+ * @param argumentString The string containing exercise details and flags.
+ * @return An {@link ExerciseUpdate} object initialized with the specified attributes.
+ * @throws FlagException If required flags are missing.
+ * */
+ private ExerciseUpdate parseExerciseUpdate(String argumentString){
+ assert argumentString != null : "Argument string must not be null";
+
+ FlagParser flagParser = new FlagParser(argumentString);
+
+ return new ExerciseUpdate(
+ flagParser.getIntegerByFlag(SETS_FLAG),
+ flagParser.getIntegerByFlag(REPS_FLAG),
+ flagParser.getIntegerByFlag(WEIGHT_FLAG),
+ flagParser.getIntegerByFlag(CALORIES_FLAG),
+ flagParser.getStringByFlag(NAME_FLAG)
+ );
+ }
+}
+
diff --git a/src/main/java/parser/command/factory/WaterCommandFactory.java b/src/main/java/parser/command/factory/WaterCommandFactory.java
new file mode 100644
index 0000000000..ecec32b81e
--- /dev/null
+++ b/src/main/java/parser/command/factory/WaterCommandFactory.java
@@ -0,0 +1,100 @@
+//@@author Bev-low
+package parser.command.factory;
+
+import command.Command;
+import command.InvalidCommand;
+import command.water.AddWaterCommand;
+import command.water.DeleteWaterCommand;
+import command.water.ViewWaterCommand;
+import exceptions.WaterException;
+import parser.FlagParser;
+
+import java.time.LocalDate;
+
+import static parser.FlagDefinitions.VOLUME_FLAG;
+import static parser.FlagDefinitions.WATER_INDEX;
+import static parser.FlagDefinitions.DATE_FLAG;
+import static parser.ParserUtils.parseDate;
+import static parser.ParserUtils.splitArguments;
+
+/**
+ * A factory class to create and return specific water-related commands based on input arguments.
+ */
+public class WaterCommandFactory {
+ public static final String COMMAND_WORD = "water";
+
+ /**
+ * Parses the argument string to determine which water-related command to create.
+ * The first word of the argument string is used to identify the specific command (e.g., add, delete, view).
+ *
+ * @param argumentString the input arguments provided by the user
+ * @return a {@code Command} object representing the water command to be executed
+ * @throws AssertionError if {@code argumentString} is null
+ */
+ public Command parse(String argumentString) {
+ assert argumentString != null : "Argument string must not be null";
+
+ String[] inputArguments = splitArguments(argumentString);
+ String subCommandString = inputArguments[0];
+ String arguments = inputArguments[1];
+
+ return switch (subCommandString) {
+ case AddWaterCommand.COMMAND_WORD -> prepareAddCommand(arguments);
+ case DeleteWaterCommand.COMMAND_WORD -> prepareDeleteCommand(arguments);
+ case ViewWaterCommand.COMMAND_WORD -> prepareViewCommand(arguments);
+ default -> new InvalidCommand();
+ };
+ }
+
+ /**
+ * Prepares the AddWaterCommand by parsing the volume of water to be added and date.
+ *
+ * @param argumentString the argument string containing the flags and values
+ * @return an {@code AddWaterCommand} with the parsed water volume and date
+ * @throws IllegalArgumentException if required flags are missing
+ */
+ public Command prepareAddCommand(String argumentString) {
+ FlagParser flagParser = new FlagParser(argumentString);
+
+ flagParser.validateRequiredFlags(VOLUME_FLAG);
+
+ float water = flagParser.getFloatByFlag(VOLUME_FLAG);
+
+ if (water < 0){
+ throw WaterException.volumeOutOfBounds();
+ }
+
+ LocalDate date = flagParser.getDateByFlag(DATE_FLAG);
+
+ return new AddWaterCommand(water, date);
+ }
+
+ /**
+ * Prepares the DeleteWaterCommand by parsing the index of water to be deleted and the date.
+ *
+ * @param argumentString the argument string containing the flags and values
+ * @return a {@code DeleteWaterCommand} with the parsed index and date
+ * @throws IllegalArgumentException if required flags are missing
+ */
+ public Command prepareDeleteCommand(String argumentString) {
+ FlagParser flagParser = new FlagParser(argumentString);
+
+ flagParser.validateRequiredFlags(WATER_INDEX);
+
+ int waterIndexToDelete = flagParser.getIndexByFlag(WATER_INDEX);
+ LocalDate date = flagParser.getDateByFlag(DATE_FLAG);
+
+ return new DeleteWaterCommand(waterIndexToDelete, date);
+ }
+
+ /**
+ * Prepares the ViewWaterCommand to view water logs for a specific date.
+ *
+ * @param argumentString the argument string containing the date
+ * @return a {@code ViewWaterCommand} with the parsed date
+ */
+ public Command prepareViewCommand(String argumentString) {
+ LocalDate date = parseDate(argumentString);
+ return new ViewWaterCommand(date);
+ }
+}
diff --git a/src/main/java/programme/Day.java b/src/main/java/programme/Day.java
new file mode 100644
index 0000000000..397dc3eb1b
--- /dev/null
+++ b/src/main/java/programme/Day.java
@@ -0,0 +1,162 @@
+// @@author Atulteja
+package programme;
+
+import exceptions.ProgrammeException;
+
+import java.util.ArrayList;
+import java.util.Objects;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+
+/**
+ * Represents a day in a workout programme, containing a list of exercises.
+ * Provides methods to add, delete, and retrieve exercises, as well as calculate the total calories burnt.
+ */
+public class Day {
+ private static final Logger logger = Logger.getLogger(Day.class.getName());
+
+ private final String name;
+ private final ArrayList exercises;
+
+ /**
+ * Deep copies a new Day by copying the name and exercises from an existing Day.
+ *
+ * @param day the Day to copy
+ */
+ public Day(Day day) {
+ this.name = day.name;
+ this.exercises = new ArrayList<>(day.exercises);
+ }
+
+ /**
+ * Constructs a Day with a specified name and an empty list of exercises.
+ *
+ * @param name the name of the day
+ * @throws AssertionError if the name is null or empty
+ */
+ public Day(String name){
+ assert name != null && !name.isEmpty() : "Name cannot be null or empty";
+
+ this.name = name;
+ this.exercises = new ArrayList<>();
+
+ logger.log(Level.INFO, "Day created with empty exercise list: {0}", this);
+ }
+
+ /**
+ * Constructs a Day with a specified name and a list of exercises.
+ *
+ * @param name the name of the day
+ * @param exercises the list of exercises for the day
+ * @throws AssertionError if the name is null or empty, or if exercises is null
+ */
+ public Day(String name, ArrayList exercises) {
+ assert name != null && !name.isEmpty() : "Name cannot be null or empty";
+ assert exercises != null : "Exercises list cannot be null";
+
+ this.exercises = exercises;
+ this.name = name;
+
+ logger.log(Level.INFO, "Day created: {0}", this);
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public int getExercisesCount() {
+ logger.log(Level.INFO, "Number of exercises: {0}", exercises.size());
+ return exercises.size();
+ }
+
+ /**
+ * Returns the exercise at the specified index.
+ *
+ * @param index the index of the exercise to retrieve
+ * @return the exercise at the specified index
+ * @throws ProgrammeException if the index is out of bounds
+ */
+ public Exercise getExercise(int index){
+ if (index < 0 || index >= exercises.size()) {
+ throw ProgrammeException.doesNotExist("exercise");
+ }
+
+ logger.log(Level.INFO, "Retrieving exercise at index {0}: {1}", new Object[]{index, exercises.get(index)});
+ return exercises.get(index);
+ }
+
+ /**
+ * Inserts an exercise into the day's exercise list.
+ *
+ * @param exercise the exercise to insert
+ * @throws AssertionError if the exercise is null
+ */
+ public void insertExercise(Exercise exercise) {
+ assert exercise != null : "Exercise to insert cannot be null";
+ exercises.add(exercise);
+ logger.log(Level.INFO, "Inserted exercise {0}", exercise);
+ }
+
+ /**
+ * Deletes the exercise at the specified index.
+ *
+ * @param index the index of the exercise to delete
+ * @return the deleted exercise
+ * @throws ProgrammeException if the index is out of bounds
+ */
+ public Exercise deleteExercise(int index) {
+ if (index < 0 || index >= exercises.size()) {
+ throw ProgrammeException.doesNotExist("exercise");
+ }
+ Exercise toBeDeleted = exercises.get(index);
+ exercises.remove(index);
+ logger.log(Level.INFO, "Deleted exercise at index {0}: {1}", new Object[]{index, toBeDeleted});
+ return toBeDeleted;
+ }
+
+ /**
+ * Calculates the total calories burnt from all exercises in the day.
+ *
+ * @return the total calories burnt
+ */
+ public int getTotalCaloriesBurnt(){
+ int totalCalories = 0;
+ for (Exercise exercise : exercises) {
+ totalCalories += exercise.getCalories();
+ }
+ return totalCalories;
+ }
+
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ result.append(name).append("\n");
+
+ for (int i = 0; i < exercises.size(); i++) {
+ Exercise exercise = exercises.get(i);
+ result.append(String.format("%d. %s%n", i + 1, exercise));
+ }
+
+ return result.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, exercises);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Day day = (Day) o;
+ return Objects.equals(name, day.name) &&
+ Objects.equals(exercises, day.exercises);
+ }
+}
diff --git a/src/main/java/programme/Exercise.java b/src/main/java/programme/Exercise.java
new file mode 100644
index 0000000000..614f634339
--- /dev/null
+++ b/src/main/java/programme/Exercise.java
@@ -0,0 +1,182 @@
+// @@author Atulteja
+package programme;
+
+import java.util.Objects;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import static common.Utils.isNull;
+
+/**
+ * Represents an exercise with sets, reps, weight, calories burned, and a name.
+ * Provides methods to update individual fields and retrieve key details.
+ */
+public class Exercise {
+ private static final Logger logger = Logger.getLogger(Exercise.class.getName());
+
+ private int sets;
+ private int reps;
+ private int weight;
+ private int calories;
+ private String name;
+
+
+ /**
+ * Constructs an Exercise with the specified details.
+ *
+ * @param sets the number of sets for the exercise
+ * @param reps the number of reps for each set
+ * @param weight the weight used for the exercise
+ * @param calories the calories burned by the exercise
+ * @param name the name of the exercise
+ */
+ public Exercise(int sets, int reps, int weight, int calories, String name) {
+ this.sets = sets;
+ this.reps = reps;
+ this.weight = weight;
+ this.calories = calories;
+ this.name = name;
+
+ logger.log(Level.INFO, "Exercise created: {0}", this);
+ }
+
+ // @@author TVageesan
+
+ /**
+ * Updates the current Exercise's fields based on the non-null values in the provided ExerciseUpdate object.
+ *
+ * For each non-null field in the UpdateExercise object, the corresponding field in this Exercise is updated.
+ *
+ *
+ * @param update the ExerciseUpdate containing fields to be updated in this Exercise.
+ */
+ public void updateExercise(ExerciseUpdate update) {
+ assert update != null : "ExerciseUpdate object must be provided";
+
+ updateSets(update.sets);
+ updateReps(update.reps);
+ updateWeight(update.weight);
+ updateName(update.name);
+ updateCalories(update.calories);
+ }
+
+ /**
+ * Updates the number of sets for this exercise.
+ *
+ * @param newSets the new number of sets; if null, the sets are not updated
+ */
+ private void updateSets(Integer newSets) {
+ if (isNull(newSets)) {
+ return;
+ }
+ logger.log(Level.INFO, "Updating sets from {0} to {1}", new Object[]{sets, newSets});
+ sets = newSets;
+ }
+
+ /**
+ * Updates the number of reps for this exercise.
+ *
+ * @param newReps the new number of reps; if null, the reps are not updated
+ */
+ private void updateReps(Integer newReps) {
+ if (isNull(newReps)) {
+ return;
+ }
+ logger.log(Level.INFO, "Updating reps from {0} to {1}", new Object[]{reps, newReps});
+ reps = newReps;
+ }
+
+ /**
+ * Updates the weight for this exercise.
+ *
+ * @param newWeight the new weight; if null, the weight is not updated
+ */
+ private void updateWeight(Integer newWeight) {
+ if (isNull(newWeight)) {
+ return;
+ }
+ logger.log(Level.INFO, "Updating weight from {0} to {1}", new Object[]{weight, newWeight});
+ weight = newWeight;
+ }
+
+ /**
+ * Updates the name of this exercise.
+ *
+ * @param newName the new name; if null, the name is not updated
+ */
+ private void updateName(String newName) {
+ if (isNull(newName)) {
+ return;
+ }
+ logger.log(Level.INFO, "Updating name from {0} to {1}", new Object[]{name, newName});
+ name = newName;
+ }
+
+ /**
+ * Updates the calories burned for this exercise.
+ *
+ * @param newCalories the new calorie count for the exercise; if null, the calories are not updated
+ */
+ private void updateCalories(Integer newCalories) {
+ if (isNull(newCalories)) {
+ return;
+ }
+ logger.log(Level.INFO, "Updating calories from {0} to {1}", new Object[]{calories, newCalories});
+ calories = newCalories;
+ }
+
+ // @@author Atulteja
+
+ public int getCalories() {
+ return calories;
+ }
+
+ public int getWeight(){
+ return weight;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns a string representation of the exercise, including name, sets, reps, weight, and calories burned.
+ *
+ * @return a string representation of the exercise
+ */
+ @Override
+ public String toString() {
+ return String.format("%s: %d sets of %d at %dkg | Burnt %d kcal", name, sets, reps, weight, calories);
+ }
+
+ /**
+ * Returns a string representation of the exercise, sets, reps, weight.
+ *
+ * @return a string representation of the exercise for printing personal best
+ */
+ public String toStringPb() {
+ return String.format("%d sets of %d at %dkg", sets, reps, weight);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ Exercise exercise = (Exercise) o;
+ return sets == exercise.sets &&
+ reps == exercise.reps &&
+ weight == exercise.weight &&
+ calories == exercise.calories &&
+ Objects.equals(name, exercise.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(sets, reps, weight, calories, name);
+ }
+}
diff --git a/src/main/java/programme/ExerciseUpdate.java b/src/main/java/programme/ExerciseUpdate.java
new file mode 100644
index 0000000000..74bae4e24e
--- /dev/null
+++ b/src/main/java/programme/ExerciseUpdate.java
@@ -0,0 +1,37 @@
+// @@author TVageesan
+
+package programme;
+
+/**
+ * Represents an immutable set of updates for an Exercise object.
+ *
+ * This class encapsulates the data fields to update specific attributes of an Exercise.
+ * Any field with a non-null value indicates a field to be updated in the target Exercise.
+ *
+ */
+public class ExerciseUpdate {
+
+ protected final int sets;
+ protected final int reps;
+ protected final int weight;
+ protected final int calories;
+ protected final String name;
+
+ /**
+ * Constructs an ExerciseUpdate with specified fields. Each non-null parameter
+ * represents a field intended to be updated in the target Exercise.
+ *
+ * @param sets the updated number of sets, or NULL_INTEGER if not updating
+ * @param reps the updated number of reps, or NULL_INTEGER if not updating
+ * @param weight the updated weight, or NULL_INTEGER if not updating
+ * @param calories the updated calorie count, or NULL_INTEGER if not updating
+ * @param name the updated name, or NULL_STRING if not updating
+ */
+ public ExerciseUpdate(int sets, int reps, int weight, int calories, String name) {
+ this.sets = sets;
+ this.reps = reps;
+ this.weight = weight;
+ this.calories = calories;
+ this.name = name;
+ }
+}
diff --git a/src/main/java/programme/Programme.java b/src/main/java/programme/Programme.java
new file mode 100644
index 0000000000..8cd3989650
--- /dev/null
+++ b/src/main/java/programme/Programme.java
@@ -0,0 +1,113 @@
+// @@author Atulteja
+
+package programme;
+
+import exceptions.ProgrammeException;
+
+import java.util.ArrayList;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+
+/**
+ * Represents a Programme consisting of multiple days, each containing exercises.
+ * Provides functionality to add, retrieve, and delete days, as well as access programme information.
+ */
+public class Programme {
+ private static final Logger logger = Logger.getLogger(Programme.class.getName());
+
+ private final String programmeName;
+ private final ArrayList dayList;
+
+ /**
+ * Constructs a Programme with the specified name and a list of days.
+ *
+ * @param programmeName the name of the programme
+ * @param dayList the list of days in the programme
+ * @throws AssertionError if the programme name is null/empty or dayList is null
+ */
+ public Programme(String programmeName, ArrayList dayList) {
+ assert programmeName != null && !programmeName.isEmpty() : "Programme name cannot be null or empty";
+ assert dayList != null : "Day list cannot be null";
+
+ this.programmeName = programmeName;
+ this.dayList = dayList;
+
+ logger.log(Level.INFO, "Programme created: {0}", this);
+ }
+
+ public String getProgrammeName() {
+ return programmeName;
+ }
+
+ /**
+ * Retrieves the day at the specified index.
+ *
+ * @param index the index of the day to retrieve
+ * @return the Day at the specified index
+ * @throws ProgrammeException if the index is out of bounds for the day list
+ */
+ public Day getDay(int index){
+ if (index < 0 || index >= dayList.size()) {
+ logger.log(Level.WARNING, "Invalid index: {0} for getDay()", index);
+ throw ProgrammeException.doesNotExist("day");
+ }
+ logger.log(Level.INFO, "Retrieving day at index {0}: {1}", new Object[]{index, dayList.get(index)});
+ return dayList.get(index);
+ }
+
+ /**
+ * Inserts a new day into the programme.
+ *
+ * @param day the day to insert
+ * @throws AssertionError if the day is null
+ */
+ public void insertDay(Day day) {
+ assert day != null : "Day to insert cannot be null";
+ logger.log(Level.INFO, "Inserting day: {0}", day);
+
+ dayList.add(day);
+ }
+
+ public int getDayCount() {
+ logger.log(Level.INFO, "Getting day count: {0}", dayList.size());
+ return dayList.size();
+ }
+
+
+ /**
+ * Deletes the day at the specified index.
+ *
+ * @param index the index of the day to delete
+ * @return the Day that was deleted
+ * @throws ProgrammeException if the index is out of bounds for the day list
+ */
+ public Day deleteDay(int index){
+ if (index < 0 || index >= dayList.size()) {
+ logger.log(Level.WARNING, "Invalid index: {0} for deleteDay()", index);
+ throw ProgrammeException.doesNotExist("day");
+ }
+ Day toBeDeleted = dayList.get(index);
+ dayList.remove(index);
+ logger.log(Level.INFO, "Deleted day at index {0}: {1}", new Object[]{index, toBeDeleted});
+ return toBeDeleted;
+ }
+
+
+ /**
+ * Returns a string representation of the programme, including its name and all days.
+ *
+ * @return a string representation of the programme
+ */
+ @Override
+ public String toString() {
+ StringBuilder str = new StringBuilder();
+ str.append(programmeName).append("\n\n");
+
+ for (int i = 0; i < dayList.size(); i++) {
+ str.append("Day ").append(i+1).append(": ").append(dayList.get(i)).append("\n");
+ }
+
+ return str.toString();
+ }
+}
diff --git a/src/main/java/programme/ProgrammeList.java b/src/main/java/programme/ProgrammeList.java
new file mode 100644
index 0000000000..15d53db5a0
--- /dev/null
+++ b/src/main/java/programme/ProgrammeList.java
@@ -0,0 +1,208 @@
+// @@author Atulteja
+
+package programme;
+
+import exceptions.ProgrammeException;
+
+import java.util.ArrayList;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import static common.Utils.NULL_INTEGER;
+
+
+/**
+ * Represents a list of Programmes, providing functionality to add, retrieve, delete, and start a Programme.
+ * Maintains the current active Programme and supports basic Programme management operations.
+ */
+public class ProgrammeList {
+
+ private static final Logger logger = Logger.getLogger(ProgrammeList.class.getName());
+
+ int currentActiveProgramme;
+ private final ArrayList programmeList;
+
+ /**
+ * Constructs an empty ProgrammeList.
+ */
+ public ProgrammeList() {
+ programmeList = new ArrayList<>();
+ currentActiveProgramme = NULL_INTEGER;
+ logger.log(Level.INFO, "ProgrammeList created with an empty list.");
+ }
+
+ public ArrayList getProgrammeList() {
+ return programmeList;
+ }
+
+ public int getProgrammeListSize(){
+ logger.log(Level.INFO, "Getting programme list size: {0}", programmeList.size());
+ return programmeList.size();
+ }
+
+ /**
+ * Checks if the ProgrammeList is empty.
+ *
+ * @return true if it is empty
+ */
+ public boolean isEmpty() {
+ return programmeList.isEmpty();
+ }
+
+ /**
+ * Deactivates the current active programme by setting the active programme index to NULL_INTEGER.
+ * If there are no programmes in the list, it logs a warning and ensures the index remains as NULL_INTEGER.
+ */
+ public void deactivateCurrentProgramme() {
+ if (programmeList.isEmpty()) {
+ logger.log(Level.WARNING, "Attempted to deactivate programme, but no programmes exist in the list.");
+ currentActiveProgramme = NULL_INTEGER; // Defensive: ensure it's set correctly even if empty
+ } else {
+ logger.log(Level.INFO, "Deactivating current active programme.");
+ currentActiveProgramme = NULL_INTEGER;
+ }
+ }
+
+ /**
+ * Inserts a new Programme into the Programme list with the specified name and days.
+ *
+ * @param programmeName the name of the Programme
+ * @param days the list of days associated with the Programme
+ * @return the Programme that was added
+ */
+ public Programme insertProgramme(String programmeName, ArrayList days) {
+ Programme programmeToAdd = new Programme(programmeName, days);
+ programmeList.add(programmeToAdd);
+
+ if (programmeList.size() == 1) {
+ currentActiveProgramme = 0;
+ }
+
+ return programmeToAdd;
+ }
+
+ /**
+ * Deletes a Programme at the specified index or at the current active Programme if index is NULL_INTEGER.
+ *
+ * @param index the index of the Programme to delete, or NULL_INTEGER to delete the active Programme
+ * @return the Programme that was deleted
+ * @throws IndexOutOfBoundsException if the index is out of bounds for the Programme list
+ */
+ public Programme deleteProgram(int index){
+ if (index == NULL_INTEGER){
+ index = currentActiveProgramme;
+ }
+
+ if (index < 0 || index >= programmeList.size()) {
+ logger.log(Level.WARNING, "Invalid index: {0} for deleteProgram()", index);
+ throw ProgrammeException.doesNotExist("programme");
+ }
+
+ Programme programmeToDelete = programmeList.get(index);
+ programmeList.remove(index);
+
+ if (programmeList.isEmpty()) {
+ deactivateCurrentProgramme();
+ // currentActiveProgramme = NULL_INTEGER;
+ } else if (index == currentActiveProgramme) {
+ // Reset `currentActiveProgramme` to 0 if the deleted programme was the active one
+ currentActiveProgramme = 0;
+ }
+
+ logger.log(Level.INFO, "Deleted programme at index {0}: {1}", new Object[]{index, programmeToDelete});
+
+ return programmeToDelete;
+ }
+
+ /**
+ * Retrieves a Programme at the specified index or at the current active Programme if index is NULL_INTEGER.
+ *
+ * @param index the index of the Programme to retrieve, or NULL_INTEGER to retrieve the active Programme
+ * @return the Programme at the specified index
+ * @throws IndexOutOfBoundsException if the index is out of bounds for the Programme list
+ */
+ public Programme getProgramme(int index){
+ if (index == NULL_INTEGER){
+ index = currentActiveProgramme;
+ }
+
+ if(index < 0) {
+ throw ProgrammeException.indexOutOfBounds();
+ }
+
+ if (index >= programmeList.size()) {
+ logger.log(Level.WARNING, "Invalid index: {0} for getProgramme()", index);
+ throw ProgrammeException.doesNotExist("programme");
+ }
+
+ logger.log(Level.INFO, "Retrieving programme at index {0}: {1}", new Object[]{index, programmeList.get(index)});
+ return programmeList.get(index);
+ }
+
+ /**
+ * Retrieves the current active Programme index.
+ *
+ * @return the current active Programme index, or {@code NULL_INTEGER} if no Programme is active.
+ */
+ public int getCurrentActiveProgramme(){
+ logger.log(Level.INFO, "Retrieving index of current program: {1}", new Object[]{currentActiveProgramme});
+ return currentActiveProgramme;
+ }
+
+ /**
+ * Sets a Programme at the specified index as the current active Programme.
+ *
+ * @param startIndex the index of the Programme to start
+ * @return the Programme that was started
+ * @throws IndexOutOfBoundsException if the startIndex is out of bounds for the Programme list
+ */
+ public Programme startProgramme(int startIndex) {
+ if (programmeList.isEmpty()){
+ deactivateCurrentProgramme();
+ //currentActiveProgramme = NULL_INTEGER;
+ logger.log(Level.WARNING, "Attempted to start a programme but the list is empty");
+ throw ProgrammeException.programmeListEmpty();
+ }
+
+ if (startIndex < 0) {
+ throw ProgrammeException.indexOutOfBounds();
+ }
+ if (startIndex >= programmeList.size()) {
+ logger.log(Level.WARNING, "Invalid index: {0} for startProgramme()", startIndex);
+ throw ProgrammeException.doesNotExist("programme");
+ }
+
+ if (currentActiveProgramme == startIndex) {
+ throw ProgrammeException.programmeAlreadyActive(startIndex);
+ }
+
+ currentActiveProgramme = startIndex;
+ Programme activeProgramme = programmeList.get(currentActiveProgramme);
+ logger.log(Level.INFO, "Started programme at index {0}: {1}", new Object[]{startIndex, activeProgramme});
+ return activeProgramme;
+ }
+
+ /**
+ * Returns a string representation of the Programme list, indicating the active Programme.
+ *
+ * @return a string representation of the Programme list
+ */
+ @Override
+ public String toString() {
+ if (programmeList.isEmpty()){
+ return "No programmes found.";
+ }
+
+ StringBuilder str = new StringBuilder();
+ for (int i = 0; i < programmeList.size(); i++) {
+ Programme programme = programmeList.get(i);
+ str.append(i + 1).append(". ").append(programme.getProgrammeName());
+ if (i == currentActiveProgramme) {
+ str.append(" -- Active");
+ }
+ str.append("\n");
+ }
+ return str.toString();
+ }
+
+}
diff --git a/src/main/java/seedu/duke/Duke.java b/src/main/java/seedu/duke/Duke.java
deleted file mode 100644
index 5c74e68d59..0000000000
--- a/src/main/java/seedu/duke/Duke.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package seedu.duke;
-
-import java.util.Scanner;
-
-public class Duke {
- /**
- * Main entry-point for the java.duke.Duke application.
- */
- public static void main(String[] args) {
- String logo = " ____ _ \n"
- + "| _ \\ _ _| | _____ \n"
- + "| | | | | | | |/ / _ \\\n"
- + "| |_| | |_| | < __/\n"
- + "|____/ \\__,_|_|\\_\\___|\n";
- System.out.println("Hello from\n" + logo);
- System.out.println("What is your name?");
-
- Scanner in = new Scanner(System.in);
- System.out.println("Hello " + in.nextLine());
- }
-}
diff --git a/src/main/java/storage/DateSerializer.java b/src/main/java/storage/DateSerializer.java
new file mode 100644
index 0000000000..38dfa6bbda
--- /dev/null
+++ b/src/main/java/storage/DateSerializer.java
@@ -0,0 +1,51 @@
+//@@author Bev-low
+
+package storage;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonSerializer;
+import com.google.gson.JsonDeserializationContext;
+import java.lang.reflect.Type;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * A custom serializer and deserializer for {@link LocalDate} objects, formatting dates as
+ * {@code dd-MM-yyyy} for JSON serialization and deserialization.
+ *
+ * This class implements {@link JsonSerializer} and {@link JsonDeserializer} to convert
+ * {@code LocalDate} objects to and from JSON strings in a specific date format.
+ *
+ */
+public class DateSerializer implements JsonSerializer, JsonDeserializer {
+ private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy");
+
+ /**
+ * Serializes a {@code LocalDate} object into a JSON string using the {@code dd-MM-yyyy} format.
+ *
+ * @param src the {@code LocalDate} to serialize, must not be {@code null}
+ * @param typeOfSrc the type of the source object
+ * @param context the serialization context
+ * @return a {@link JsonElement} containing the formatted date string
+ */
+ @Override
+ public JsonElement serialize(LocalDate src, Type typeOfSrc, JsonSerializationContext context) {
+ assert src != null;
+ return new JsonPrimitive(src.format(formatter));
+ }
+
+ /**
+ * Deserializes a JSON string into a {@code LocalDate} object using the {@code dd-MM-yyyy} format.
+ *
+ * @param json the JSON element containing the date string
+ * @param typeOfT the type of the object to deserialize
+ * @param context the deserialization context
+ * @return a {@code LocalDate} parsed from the JSON string
+ */
+ @Override
+ public LocalDate deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) {
+ return LocalDate.parse(json.getAsString(), formatter);
+ }
+}
diff --git a/src/main/java/storage/FileManager.java b/src/main/java/storage/FileManager.java
new file mode 100644
index 0000000000..2e37fe6f5c
--- /dev/null
+++ b/src/main/java/storage/FileManager.java
@@ -0,0 +1,176 @@
+//@@author Bev-low
+
+package storage;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonElement;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonParser;
+import exceptions.StorageException;
+
+import java.util.logging.Logger;
+import java.util.logging.Level;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.FileReader;
+import java.io.IOException;
+
+/**
+ * Represents the FileManager system for saving and loading tasks.
+ * The FileManager
class handles reading from and writing to the file specified by the user.
+ */
+public class FileManager {
+ private final String path;
+ private final Logger logger = Logger.getLogger(this.getClass().getName());
+
+ /**
+ * Constructs a FileManager with a specified file path.
+ *
+ * @param path the path to the file for storing data
+ */
+ public FileManager(String path) {
+ this.path = path;
+ }
+
+ /**
+ * Loads the JSON object containing the programme list from the data loaded by the {@link #load()} method.
+ *
+ * This method retrieves the "programmeList" section from the JSON data. If the "programmeList" section
+ * is not found, returns an empty JSON object.
+ *
+ * @return the JSON object containing the programme list if available, or an empty JSON object if not found
+ */
+ public JsonObject loadProgrammeList() {
+ try {
+ JsonObject jsonObject = load();
+ if (jsonObject == null || !jsonObject.has("programmeList")) {
+ logger.log(Level.INFO, "No programme list found.");
+ return new JsonObject();
+ }
+ logger.log(Level.INFO, "Programme list Loaded");
+ return jsonObject.getAsJsonObject("programmeList");
+ } catch (RuntimeException e) {
+ logger.log(Level.WARNING, "Failed to load programme list: " + e.getMessage());
+ return new JsonObject();
+ }
+ }
+
+ /**
+ * Loads the JSON object containing the history from the data loaded by the {@link #load()} method.
+ *
+ * This method retrieves the "history" section from the JSON data. If the "history" section
+ * is not found, returns an empty JSON object.
+ *
+ * @return the JSON object containing the history if available, or an empty JSON object if not found
+ */
+ public JsonObject loadHistory() {
+ try {
+ JsonObject jsonObject = load();
+ if (jsonObject == null || !jsonObject.has("history")) {
+ logger.log(Level.INFO, "No history found.");
+ return new JsonObject();
+ }
+ logger.log(Level.INFO, "History Loaded");
+ return jsonObject.getAsJsonObject("history");
+ } catch (RuntimeException e) {
+ logger.log(Level.WARNING, "Failed to load history: " + e.getMessage());
+ return new JsonObject();
+ }
+ }
+
+ /**
+ * Loads data from the file specified by the path and converts it to a JSON object.
+ *
+ * This method attempts to read the file at the specified path and parse its contents as a JSON object.
+ * If the file is empty or contains no data, an empty JSON object is returned. Any I/O errors encountered
+ * during file reading will result in a {@link StorageException}.
+ *
+ * @return a JSON object containing the data loaded from the file, or an empty JSON object if the file is empty
+ * @throws StorageException if an I/O error occurs during loading
+ */
+ private JsonObject load() {
+ logger.info("Attempting to load data from file: " + path);
+ try (FileReader reader = new FileReader(path)){
+ JsonElement element = JsonParser.parseReader(reader);
+ if(element == null || element.isJsonNull()) {
+ logger.info("No data found");
+ return new JsonObject();
+ }
+ logger.info("Data successfully loaded from file");
+ return element.getAsJsonObject();
+ } catch(Exception e){
+ logger.log(Level.WARNING, "Failed to load data from file: " + path, e);
+ return new JsonObject();
+ }
+ }
+
+ /**
+ * Saves the specified JSON object to the file.
+ *
+ * This method writes the contents of the provided JSON object to the file specified by the path.
+ * If the directory or file does not exist, it attempts to create them before writing. The data
+ * is saved in a pretty-printed JSON format for readability.
+ *
+ * @param data the JSON object containing the data to be saved
+ * @throws IOException if an I/O error occurs during saving
+ */
+ public void save(JsonObject data) throws IOException {
+ Gson gson = new GsonBuilder().setPrettyPrinting().create();
+
+ createDirIfNotExist();
+ createFileIfNotExist();
+
+ try (FileWriter writer = new FileWriter(path)) {
+ gson.toJson(data, writer);
+ logger.info("Data successfully saved to file.");
+ } catch (StorageException e) {
+ logger.log(Level.WARNING, "Failed to save data to file: " + path, e);
+ throw StorageException.unableToSave();
+ }
+ }
+
+ /**
+ * Creates the directory for the file if it does not exist.
+ *
+ * @throws IOException if the directory creation fails
+ */
+ private void createDirIfNotExist() throws IOException {
+ File dir = new File(path).getParentFile();
+
+ if (dir == null || dir.exists()){
+ logger.log(Level.INFO, "Directory exists");
+ return;
+ }
+
+ boolean isSuccess = dir.mkdirs();
+
+ if (!isSuccess){
+ logger.log(Level.WARNING, "Failed to create directory.");
+ throw StorageException.unableToCreateDirectory();
+ }
+ logger.log(Level.INFO, "Directory created");
+ }
+
+ /**
+ * Creates the file if it does not exist.
+ *
+ * @throws IOException if the file creation fails
+ */
+ private void createFileIfNotExist() throws IOException {
+ File file = new File(path);
+ if (file.exists()) {
+ logger.log(Level.INFO, "File exists");
+ return;
+ }
+
+ boolean isSuccess = file.createNewFile();
+
+ if (!isSuccess) {
+ logger.log(Level.WARNING, "Failed to create file.");
+ throw StorageException.unableToCreateFile();
+ }
+ logger.log(Level.INFO, "File created");
+ }
+}
diff --git a/src/main/java/storage/Storage.java b/src/main/java/storage/Storage.java
new file mode 100644
index 0000000000..0a43e3e8be
--- /dev/null
+++ b/src/main/java/storage/Storage.java
@@ -0,0 +1,394 @@
+//@@author Bev-low
+
+package storage;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonElement;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import exceptions.ParserException;
+import exceptions.StorageException;
+import history.DailyRecord;
+import history.History;
+import programme.ProgrammeList;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import static common.Utils.validate;
+
+/*
+ Storage acts as an adapter layer between the FileManager and BuffBuddy classes,
+ translating between JSON and programmeList or History objects
+*/
+public class Storage {
+
+ private static final Logger logger = Logger.getLogger(Storage.class.getName());
+ private FileManager fileManager;
+ private String message;
+ private boolean isProgrammeListEmpty = false;
+ private boolean isProgrammeListCorrupted = false;
+ private boolean isHistoryEmpty = false;
+ private boolean isHistoryCorrupted = false;
+ /**
+ * Constructs a Storage object with a specified file path.
+ *
+ * @param path the path to the file used for storing data
+ */
+ public Storage(String path) {
+ this.fileManager = new FileManager(path);
+ message = null;
+ }
+
+ /**
+ * Loads the programme list from the JSON object obtained via the FileManager.
+ *
+ * This method retrieves the JSON data containing the programme list from the FileManager. If no
+ * programme list data is found, it initializes an empty ProgrammeList.
+ *
+ * @return the ProgrammeList object containing programme data, or an empty ProgrammeList if not found
+ */
+ public ProgrammeList loadProgrammeList() {
+ try {
+ JsonObject programmeListJson = fileManager.loadProgrammeList();
+
+ if(programmeListJson == null || programmeListJson.size() == 0) {
+ isProgrammeListEmpty = true;
+ return new ProgrammeList();
+ }
+
+ try {
+ validateProgrammeList(programmeListJson);
+ } catch (ParserException e) {
+ throw StorageException.corruptedFile("Programme List.");
+ }
+
+ logger.info("Loading programmeList");
+ message = "Welcome back!";
+ return programmeListFromJson(programmeListJson);
+ } catch (StorageException e ) {
+ logger.info("Programme list corrupted, empty list initialised");
+ isProgrammeListCorrupted = true;
+ return new ProgrammeList();
+ }
+ }
+
+ /**
+ * Converts json Object containing history to from the JSON object obtained via the FileManager.
+ *
+ * This method retrieves the JSON data containing the history from the FileManager. If no
+ * history data is found, it initializes an empty History.
+ *
+ * @return the history object containing programme data, or an empty history if not found
+ */
+ public History loadHistory() {
+ try {
+ JsonObject historyJson = fileManager.loadHistory();
+ if (historyJson == null || historyJson.size() == 0) {
+ isHistoryEmpty = true;
+ return new History();
+ }
+
+ try {
+ validateHistory(historyJson);
+ } catch (ParserException e) {
+ throw StorageException.corruptedFile("History.");
+ }
+
+ logger.info("Loading history");
+ return historyFromJson(historyJson);
+ } catch (StorageException e) {
+ logger.info("history corrupted, empty history initialised");
+ isHistoryCorrupted = true;
+ return new History();
+ }
+ }
+
+ /**
+ * Saves the programme list and history data to the file.
+ *
+ * @param programmeList the ProgrammeList object to be saved
+ * @param history the History object to be saved
+ */
+ public void saveData(ProgrammeList programmeList, History history) {
+ assert programmeList != null : "programmeList must not be null";
+ assert history != null : "history must not be null";
+
+ JsonObject jsonObject = createJSON(programmeList, history);
+ logger.info("JsonObject containing programme list and history created.");
+
+ try{
+ fileManager.save(jsonObject);
+ } catch (Exception e) {
+ logger.info("Failed to save data");
+ }
+ }
+
+ /**
+ * To set message about status of data
+ *
+ * @return String value that to tell user what is the state of the file.
+ */
+ public String getMessage() {
+ if(isProgrammeListEmpty && isHistoryEmpty) {
+ message = "First time here, welcome to BuffBuddy!";
+ } else if (isHistoryCorrupted && isProgrammeListCorrupted) {
+ message = "data is corrupted, initialise new ProgrammeList and History";
+ } else if (isHistoryCorrupted) {
+ message = "History is corrupted, initialise new History, loaded ProgrammeList";
+ } else if (isProgrammeListCorrupted) {
+ message = "ProgrammeList is corrupted, initialise new ProgrammeList, loaded History";
+ } else {
+ message = "Welcome back!";
+ }
+ return message;
+ }
+
+ /**
+ * Creates a JSON object containing the programme list and history data.
+ *
+ * @param programmeList the ProgrammeList object to be added to JSON
+ * @param history the History object to be added to JSON
+ * @return a JSON object containing the programme list and history data
+ */
+ private JsonObject createJSON(ProgrammeList programmeList, History history) {
+ JsonObject jsonObject = new JsonObject();
+
+ assert programmeList != null : "programmeList must not be null";
+ assert history != null : "history must not be null";
+
+ jsonObject.add("programmeList", programmeListToJson(programmeList));
+ logger.info("Programme list converted to JsonObject.");
+ jsonObject.add("history", historyToJson(history));
+ logger.info("History converted to JsonObject.");
+ return jsonObject;
+ }
+
+ /**
+ * Converts a ProgrammeList object to a JSON object.
+ *
+ * @param programmeList the ProgrammeList object to convert
+ * @return a JSON object representing the programme list
+ */
+ private JsonObject programmeListToJson(ProgrammeList programmeList) {
+ Gson gson = new Gson();
+ logger.log(Level.INFO, "Programme list converted to Json for saving.");
+ return gson.toJsonTree(programmeList).getAsJsonObject();
+ }
+
+ /**
+ * Converts a JSON object to a ProgrammeList object.
+ *
+ * @param jsonObject the JSON object representing the programme list
+ * @return the ProgrammeList object created from the JSON data
+ */
+ private ProgrammeList programmeListFromJson(JsonObject jsonObject) {
+ Gson gson = new Gson();
+ logger.log(Level.INFO, "Programme list converted from Json for loading.");
+ return gson.fromJson(jsonObject, ProgrammeList.class);
+ }
+
+ /**
+ * Converts a History object to a JSON object.
+ *
+ * @param history the History object to convert
+ * @return a JSON object representing the history
+ */
+ private JsonObject historyToJson(History history) {
+ Gson gson = new GsonBuilder()
+ .registerTypeAdapter(LocalDate.class, new DateSerializer()) // Custom serializer for LocalDate
+ .setPrettyPrinting()
+ .create();
+
+ JsonObject historyJson = new JsonObject();
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy");
+ LinkedHashMap historyMap = history.getHistory(); //To access the Hashmap
+
+ for (LocalDate date : historyMap.keySet()) {
+ DailyRecord dailyRecord = historyMap.get(date);
+ historyJson.add(date.format(formatter), gson.toJsonTree(dailyRecord));
+ }
+ logger.log(Level.INFO, "History converted to Json for saving.");
+ return historyJson;
+ }
+
+ /**
+ * Converts a JSON object to a History object.
+ *
+ * @param jsonObject the JSON object representing the history
+ * @return the History object created from the JSON data
+ */
+ private History historyFromJson(JsonObject jsonObject) {
+ Gson gson = new GsonBuilder()
+ .registerTypeAdapter(LocalDate.class, new DateSerializer()) // Custom deserializer for LocalDate
+ .create();
+ History history = new History();
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy");
+ LinkedHashMap historyMap = history.getHistory(); //To access the Hashmap
+
+
+ for (Map.Entry entry : jsonObject.entrySet()) {
+ LocalDate date = LocalDate.parse(entry.getKey(), formatter);
+ DailyRecord dailyRecord = gson.fromJson(entry.getValue(), DailyRecord.class);
+ historyMap.put(date, dailyRecord);
+ }
+ logger.log(Level.INFO, "historyJson converted from Json for loading.");
+ return history;
+ }
+
+ /**
+ * Sets the FileManager instance for testing purposes.
+ *
+ * @param mockFileManager the mocked FileManager to be used for testing.
+ */
+ public void setFileManager(FileManager mockFileManager) {
+ this.fileManager = mockFileManager;
+ }
+
+ /**
+ * Validates the given JSON object representing a programme list.
+ *
+ * @param programmeList the JSON object containing the programme data to be validated
+ * @throws ParserException if any validation fails for the integer, float, or string fields
+ */
+ private void validateProgrammeList(JsonObject programmeList) {
+ JsonArray programmeArray = programmeList.getAsJsonArray("programmeList");
+ validateProgramme(programmeArray);
+ }
+
+ /**
+ * Validates the given JSON object representing a history.
+ *
+ * @param history the JSON object containing the history data to be validated
+ * @throws ParserException if any validation fails for the date, day, exercise, meal, or water fields
+ */
+ private void validateHistory(JsonObject history) {
+ for (Map.Entry entry : history.entrySet()) {
+ String date = entry.getKey();
+ JsonObject record = entry.getValue().getAsJsonObject();
+
+ validateDate(date);
+
+ JsonObject day = record.getAsJsonObject("day");
+ String dayName = day.get("name").getAsString();
+ validate(dayName);
+
+ JsonArray exercises = day.getAsJsonArray("exercises");
+ validateExercise(exercises);
+
+ JsonObject mealList = record.getAsJsonObject("mealList");
+ JsonArray meals = mealList.getAsJsonArray("meals");
+ validateMeal(meals);
+
+ JsonObject water = record.getAsJsonObject("water");
+ JsonArray waterList = water.getAsJsonArray("waterList");
+ validateWater(waterList);
+ }
+ }
+
+ /**
+ * Validates a date string.
+ *
+ * @param dateString the date string to be validated in the format "dd-MM-yyyy"
+ * @throws ParserException if the date string is invalid or cannot be parsed
+ */
+ private void validateDate(String dateString) {
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy");
+ LocalDate date = LocalDate.parse(dateString, formatter);
+ validate(date);
+ }
+
+ /**
+ * Validates an array of programmes.
+ *
+ * @param programmeList the JSON array of programme objects to be validated
+ * @throws ParserException if any validation fails for the programme or its nested day data
+ */
+ private void validateProgramme(JsonArray programmeList) {
+ for (JsonElement programmeElement : programmeList) {
+ JsonObject programme = programmeElement.getAsJsonObject();
+ String programmeName = programme.get("programmeName").getAsString();
+ validate(programmeName);
+
+ JsonArray dayList = programme.getAsJsonArray("dayList");
+ validateDay(dayList);
+ }
+
+ }
+
+ /**
+ * Validates an array of days.
+ *
+ * @param dayList the JSON array of day objects to be validated
+ * @throws ParserException if any validation fails for the day or its nested exercise data
+ */
+ private void validateDay(JsonArray dayList) {
+ for (JsonElement dayElement : dayList) {
+ JsonObject day = dayElement.getAsJsonObject();
+ String dayName = day.get("name").getAsString();
+ validate(dayName);
+
+ JsonArray exercises = day.getAsJsonArray("exercises");
+ validateExercise(exercises);
+ }
+ }
+
+
+ /**
+ * Validates an array of exercises.
+ *
+ * @param exercises the JSON array of exercise objects to be validated
+ * @throws ParserException if any validation fails for the sets, reps, weight, calories, or name of exercises
+ */
+ private void validateExercise(JsonArray exercises) {
+ for (JsonElement exerciseElement : exercises) {
+ JsonObject exercise = exerciseElement.getAsJsonObject();
+ int sets = exercise.get("sets").getAsInt();
+ int reps = exercise.get("reps").getAsInt();
+ float weight = exercise.get("weight").getAsInt();
+ int calories = exercise.get("calories").getAsInt();
+ String exerciseName = exercise.get("name").getAsString();
+
+ validate(sets);
+ validate(reps);
+ validate(weight);
+ validate(calories);
+ validate(exerciseName);
+ }
+ }
+
+ /**
+ * Validates an array of meals.
+ *
+ * @param meals the JSON array of meal strings to be validated
+ * @throws ParserException if any validation fails for the meal name or calories
+ */
+ private void validateMeal(JsonArray meals) {
+ for (JsonElement mealElement : meals) {
+ JsonObject mealObject = mealElement.getAsJsonObject();
+ String mealName = mealObject.get("name").getAsString();
+ int calories = mealObject.get("calories").getAsInt();
+
+ validate(mealName);
+ validate(calories);
+ }
+ }
+
+ /**
+ * Validates an array of water amounts.
+ *
+ * @param waterList the JSON array of water amounts to be validated
+ * @throws ParserException if any validation fails for the water amount
+ */
+ private void validateWater(JsonArray waterList) {
+ for (JsonElement waterElement : waterList) {
+ float waterAmount = waterElement.getAsFloat();
+ validate(waterAmount);
+ }
+ }
+}
diff --git a/src/main/java/ui/Ui.java b/src/main/java/ui/Ui.java
new file mode 100644
index 0000000000..460d580cf9
--- /dev/null
+++ b/src/main/java/ui/Ui.java
@@ -0,0 +1,109 @@
+// @@author nirala-ts
+
+package ui;
+
+import command.CommandResult;
+
+import java.io.PrintStream;
+import java.util.Scanner;
+
+/**
+ * Represents the user interface for the task management system.
+ * This class handles user input and output, providing methods to read commands,
+ * display messages, and show welcome or farewell messages.
+ */
+public class Ui {
+ private static final String ERROR_HEADER = "Error: ";
+ private static final String LINE_CHAR = "=";
+ private static final int LINE_LENGTH = 50;
+
+ private static final String PROMPT = "What can I do for you?";
+ private static final String FAREWELL ="Bye. Hope to see you again soon!";
+ private static final String GREETING = "Hello! I'm...";
+ private static final String LOGO = """
+ ____ __ __ ____ _ _ \s
+ | _ \\ / _|/ _| _ \\ | | | | \s
+ | |_) |_ _| |_| |_| |_) |_ _ __| | __| |_ _\s
+ | _ <| | | | _| _| _ <| | | |/ _` |/ _` | | | |
+ | |_) | |_| | | | | | |_) | |_| | (_| | (_| | |_| |
+ |____/ \\__,_|_| |_| |____/ \\__,_|\\__,_|\\__,_|\\__, |
+ __/ |
+ |___/\s
+ """;
+ private final Scanner in;
+ private final PrintStream out;
+
+ /**
+ * Constructs an Ui object, initializing the input and output streams.
+ */
+ public Ui() {
+ in = new Scanner(System.in);
+ out = new PrintStream(System.out);
+ }
+
+ /**
+ * Reads a command input from the user.
+ *
+ * @return the input command as a string
+ */
+ public String readCommand() {
+ return in.nextLine();
+ }
+
+ /**
+ * Displays a line for visual separation in the output.
+ */
+ public void showLine() {
+ out.println(LINE_CHAR.repeat(LINE_LENGTH));
+ }
+
+ /**
+ * Displays a message to the user.
+ *
+ * @param msg the message to be displayed
+ */
+ public void showMessage(String msg) {
+ showLine();
+ String strippedMsg = msg.trim();
+ out.println(strippedMsg);
+ showLine();
+ }
+
+ /**
+ * Displays an error message to the user.
+ *
+ * @param e the exception to be displayed
+ */
+ public void showMessage(Exception e) {
+ showMessage(ERROR_HEADER + e.getMessage());
+ }
+
+ /**
+ * Displays the success/failure message of a Command to the user.
+ *
+ * @param result the CommandResult to be displayed
+ */
+ public void showMessage(CommandResult result){
+ showMessage(result.getMessage());
+ }
+
+ /**
+ * Displays a welcome message to the user.
+ */
+ public void showWelcome() {
+ out.println(GREETING);
+ out.println(LOGO);
+ out.println(PROMPT);
+ }
+
+ /**
+ * Displays a farewell message to the user.
+ */
+ public void showFarewell() {
+ out.println(FAREWELL);
+ }
+
+ public void showFirstTime(){
+ out.println("First time here, lets get started!");
+ }
+}
diff --git a/src/main/java/water/Water.java b/src/main/java/water/Water.java
new file mode 100644
index 0000000000..c526552aca
--- /dev/null
+++ b/src/main/java/water/Water.java
@@ -0,0 +1,96 @@
+// @@author Atulteja
+package water;
+
+import exceptions.WaterException;
+import java.util.ArrayList;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Represents a list of water intake records, allowing addition, deletion, and retrieval of records.
+ */
+public class Water {
+
+ private static final Logger logger = Logger.getLogger(Water.class.getName());
+ private final ArrayList waterList;
+
+ /**
+ * Constructs a new Water instance with an empty list of water entries.
+ */
+ public Water() {
+ waterList = new ArrayList<>();
+ logger.log(Level.INFO, "Water instance created with an empty list.");
+ }
+
+ /**
+ * Adds a specified amount of water to the list.
+ *
+ * @param water the amount of water in liters to add; must be positive
+ * @throws AssertionError if the amount of water is not positive
+ */
+ public void addWater(float water) {
+ assert water > 0 : "Water amount must be positive";
+
+ waterList.add(water);
+ logger.log(Level.INFO, "{0} liters of water added. Current list: {1}", new Object[]{water, waterList});
+ }
+
+ /**
+ * Checks if the water list is empty.
+ *
+ * @return true if the water list contains no entries; false otherwise
+ */
+ public boolean isEmpty() {
+ return waterList.isEmpty();
+ }
+
+ /**
+ * Deletes a water entry at the specified index.
+ *
+ * @param index the index of the water entry to delete
+ * @return the amount of water deleted
+ * @throws IndexOutOfBoundsException if the index is out of bounds for the water list
+ */
+ public float deleteWater(int index) {
+ if (index < 0 || index >= waterList.size()) {
+ logger.log(Level.WARNING, "Invalid index for deletion: {0}", index);
+ throw WaterException.doesNotExist();
+ }
+
+ float waterToBeDeleted = waterList.get(index);
+ waterList.remove(index);
+ logger.log(Level.INFO, "Deleted {0} liters of water at index {1}. Current list: {2}",
+ new Object[]{waterToBeDeleted, index, waterList});
+ return waterToBeDeleted;
+ }
+
+ /**
+ * Retrieves the list of water entries.
+ *
+ * @return an ArrayList containing all water entries in liters
+ */
+ public ArrayList getWaterList() {
+ logger.log(Level.INFO, "Retrieved water list: {0}", waterList);
+ return waterList;
+ }
+
+ /**
+ * Returns a string representation of the water entries.
+ *
+ * @return a string listing all water entries or "No record" if the list is empty
+ */
+ @Override
+ public String toString() {
+ StringBuilder output = new StringBuilder();
+
+ if(waterList.isEmpty()) {
+ return "No record.";
+ }
+
+ for (int i = 0; i < waterList.size(); i++) {
+ output.append(i + 1).append(": ").append(waterList.get(i)).append("\n");
+ }
+
+ return output.toString().trim();
+ }
+}
diff --git a/src/test/java/seedu/duke/DukeTest.java b/src/test/java/BuffBuddyTest.java
similarity index 81%
rename from src/test/java/seedu/duke/DukeTest.java
rename to src/test/java/BuffBuddyTest.java
index 2dda5fd651..17c984b807 100644
--- a/src/test/java/seedu/duke/DukeTest.java
+++ b/src/test/java/BuffBuddyTest.java
@@ -1,10 +1,7 @@
-package seedu.duke;
-
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertTrue;
-class DukeTest {
+public class BuffBuddyTest {
@Test
public void sampleTest() {
assertTrue(true);
diff --git a/src/test/java/command/CommandResultTest.java b/src/test/java/command/CommandResultTest.java
new file mode 100644
index 0000000000..00f0d352d1
--- /dev/null
+++ b/src/test/java/command/CommandResultTest.java
@@ -0,0 +1,33 @@
+package command;
+
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+public class CommandResultTest {
+
+ @Test
+ public void testCommandResult_initialization() {
+ String message = "Operation successful";
+ CommandResult commandResult = new CommandResult(message);
+
+ assertNotNull(commandResult, "CommandResult should be initialized");
+ assertEquals(message, commandResult.getMessage(), "The message should match the initialized value");
+ }
+
+ @Test
+ public void testCommandResult_emptyMessage() {
+ CommandResult commandResult = new CommandResult("");
+
+ assertNotNull(commandResult, "CommandResult should be initialized");
+ assertEquals("", commandResult.getMessage(), "The message should be an empty string");
+ }
+
+ @Test
+ public void testCommandResult_nullMessage() {
+ CommandResult commandResult = new CommandResult(null);
+
+ assertNotNull(commandResult, "CommandResult should be initialized");
+ assertEquals(null, commandResult.getMessage(), "The message should be null");
+ }
+}
diff --git a/src/test/java/command/CommandTest.java b/src/test/java/command/CommandTest.java
new file mode 100644
index 0000000000..c56e83a0b3
--- /dev/null
+++ b/src/test/java/command/CommandTest.java
@@ -0,0 +1,29 @@
+package command;
+
+import history.History;
+import org.junit.jupiter.api.Test;
+import programme.ProgrammeList;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.Mockito.mock;
+
+public class CommandTest {
+
+ private static class TestCommand extends Command {
+ @Override
+ public CommandResult execute(ProgrammeList programmes, History history) {
+ return new CommandResult("Test executed");
+ }
+ }
+
+ @Test
+ public void testExecute_returnsCommandResult() {
+ ProgrammeList mockProgrammes = mock(ProgrammeList.class);
+ History mockHistory = mock(History.class);
+
+ TestCommand testCommand = new TestCommand();
+ CommandResult result = testCommand.execute(mockProgrammes, mockHistory);
+
+ assertNotNull(result, "The execute method should return a non-null CommandResult");
+ }
+}
diff --git a/src/test/java/command/ExitCommandTest.java b/src/test/java/command/ExitCommandTest.java
new file mode 100644
index 0000000000..e76d608947
--- /dev/null
+++ b/src/test/java/command/ExitCommandTest.java
@@ -0,0 +1,39 @@
+package command;
+
+import history.History;
+import org.junit.jupiter.api.Test;
+import programme.ProgrammeList;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.mockito.Mockito.mock;
+
+public class ExitCommandTest {
+
+ @Test
+ public void testExecute_doesNotThrowException() {
+ ProgrammeList mockProgrammes = mock(ProgrammeList.class);
+ History mockHistory = mock(History.class);
+
+ ExitCommand exitCommand = new ExitCommand();
+
+ assertDoesNotThrow(() -> exitCommand.execute(mockProgrammes, mockHistory));
+ }
+
+ @Test
+ public void testExecute_withNullProgrammeList() {
+ History mockHistory = mock(History.class);
+
+ ExitCommand exitCommand = new ExitCommand();
+
+ assertDoesNotThrow(() -> exitCommand.execute(null, mockHistory));
+ }
+
+ @Test
+ public void testExecute_withNullHistory() {
+ ProgrammeList mockProgrammes = mock(ProgrammeList.class);
+
+ ExitCommand exitCommand = new ExitCommand();
+
+ assertDoesNotThrow( () -> exitCommand.execute(mockProgrammes, null));
+ }
+}
diff --git a/src/test/java/command/InvalidCommandTest.java b/src/test/java/command/InvalidCommandTest.java
new file mode 100644
index 0000000000..1b8859142a
--- /dev/null
+++ b/src/test/java/command/InvalidCommandTest.java
@@ -0,0 +1,22 @@
+package command;
+
+import history.History;
+import org.junit.jupiter.api.Test;
+import programme.ProgrammeList;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.Mockito.mock;
+
+
+public class InvalidCommandTest {
+
+ @Test
+ public void testExecute_showsInvalidCommandMessage() {
+ ProgrammeList mockprogrammes = mock(ProgrammeList.class);
+ History mockHistory = mock(History.class);
+
+ InvalidCommand invalidCommand = new InvalidCommand();
+ CommandResult result = invalidCommand.execute(mockprogrammes, mockHistory);
+ assertNotNull(result);
+ }
+}
diff --git a/src/test/java/command/history/DeleteHistoryCommandTest.java b/src/test/java/command/history/DeleteHistoryCommandTest.java
new file mode 100644
index 0000000000..220ed4e850
--- /dev/null
+++ b/src/test/java/command/history/DeleteHistoryCommandTest.java
@@ -0,0 +1,72 @@
+package command.history;
+
+import command.CommandResult;
+import history.DailyRecord;
+import history.History;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDate;
+
+import static common.Utils.formatDate;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class DeleteHistoryCommandTest {
+
+ private DeleteHistoryCommand deleteHistoryCommand;
+ private History history;
+ private LocalDate testDate;
+
+ @BeforeEach
+ public void setUp() {
+ history = new History();
+ testDate = LocalDate.now();
+ deleteHistoryCommand = new DeleteHistoryCommand(testDate);
+ }
+
+ @Test
+ public void testExecuteHappyPathDeleteExistingRecord() {
+ // Happy Path: Record exists and is deleted successfully
+ DailyRecord dailyRecord = new DailyRecord();
+ history.logRecord(testDate, dailyRecord);
+
+ CommandResult expectedResult = new CommandResult("Deleted record: \n" + dailyRecord.toString());
+
+ // Act
+ CommandResult result = deleteHistoryCommand.execute(history);
+
+ // Assert
+ assertEquals(expectedResult, result, "Execution should return the deleted record for the specified date.");
+ }
+
+ @Test
+ public void testExecuteEdgeCaseNoRecordForDate() {
+ // Edge Case: No record exists for the specified date
+ CommandResult expectedResult = new CommandResult("No record found at " + formatDate(testDate));
+
+ // Act
+ CommandResult result = deleteHistoryCommand.execute(history);
+
+ // Assert
+ assertEquals(expectedResult, result,
+ "Execution should return 'No record found' when no record exists for the specified date.");
+ }
+
+ @Test
+ public void testExecuteEdgeCaseRecordExistsForDifferentDate() {
+ // Edge Case: Record exists, but for a different date
+ LocalDate differentDate = testDate.minusDays(1);
+ DailyRecord dailyRecord = new DailyRecord();
+ history.logRecord(differentDate, dailyRecord);
+
+ CommandResult expectedResult = new CommandResult("No record found at " + formatDate(testDate));
+
+ // Act
+ CommandResult result = deleteHistoryCommand.execute(history);
+
+ // Assert
+ assertEquals(expectedResult, result,
+ "Execution should return 'No record found' if the record exists only for a different date.");
+ }
+}
+
diff --git a/src/test/java/command/history/ListHistoryCommandTest.java b/src/test/java/command/history/ListHistoryCommandTest.java
new file mode 100644
index 0000000000..cfc60bd81e
--- /dev/null
+++ b/src/test/java/command/history/ListHistoryCommandTest.java
@@ -0,0 +1,76 @@
+package command.history;
+
+import command.CommandResult;
+import history.DailyRecord;
+import history.History;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import programme.ProgrammeList;
+
+import java.time.LocalDate;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class ListHistoryCommandTest {
+
+ private ListHistoryCommand listHistoryCommand;
+ private History history;
+ private ProgrammeList programmeList;
+
+ @BeforeEach
+ public void setUp() {
+ listHistoryCommand = new ListHistoryCommand();
+ history = new History();
+ programmeList = new ProgrammeList(); // Assuming ProgrammeList is initialized this way
+ }
+
+ @Test
+ public void testExecuteHappyPathSingleRecord() {
+ // Happy Path: Single Record in History
+ LocalDate date = LocalDate.now();
+ DailyRecord dailyRecord = new DailyRecord();
+ history.logRecord(date, dailyRecord);
+
+ CommandResult expectedResult = new CommandResult(history.toString());
+
+ // Act
+ CommandResult result = listHistoryCommand.execute(programmeList, history);
+
+ // Assert
+ assertEquals(expectedResult, result, "Execution with a single record should output the correct history.");
+ }
+
+ @Test
+ public void testExecuteEdgeCaseEmptyHistory() {
+ // Edge Case: Empty History
+ CommandResult expectedResult = new CommandResult("No history available.");
+
+ // Act
+ CommandResult result = listHistoryCommand.execute(programmeList, history);
+
+ // Assert
+ assertEquals(expectedResult, result, "Execution with an empty history should return 'No history available.'");
+ }
+
+ @Test
+ public void testExecuteEdgeCaseMultipleRecordsInHistory() {
+ // Edge Case: Multiple Records in History
+ LocalDate date1 = LocalDate.now().minusDays(1);
+ LocalDate date2 = LocalDate.now();
+
+ DailyRecord record1 = new DailyRecord();
+ DailyRecord record2 = new DailyRecord();
+
+ history.logRecord(date1, record1);
+ history.logRecord(date2, record2);
+
+ CommandResult expectedResult = new CommandResult(history.toString());
+
+ // Act
+ CommandResult result = listHistoryCommand.execute(programmeList, history);
+
+ // Assert
+ assertEquals(expectedResult, result, "Execution with multiple records should output the correct history.");
+ }
+}
+
diff --git a/src/test/java/command/history/ListPersonalBestsCommandTest.java b/src/test/java/command/history/ListPersonalBestsCommandTest.java
new file mode 100644
index 0000000000..3a171a4ade
--- /dev/null
+++ b/src/test/java/command/history/ListPersonalBestsCommandTest.java
@@ -0,0 +1,90 @@
+package command.history;
+
+import command.CommandResult;
+import history.DailyRecord;
+import history.History;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import programme.ProgrammeList;
+import programme.Exercise;
+
+import java.time.LocalDate;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class ListPersonalBestsCommandTest {
+
+ private ListPersonalBestsCommand listPersonalBestsCommand;
+ private History history;
+ private ProgrammeList programmeList;
+
+ @BeforeEach
+ public void setUp() {
+ listPersonalBestsCommand = new ListPersonalBestsCommand();
+ history = new History();
+ programmeList = new ProgrammeList(); // Assuming ProgrammeList can be initialized this way
+ }
+
+ @Test
+ public void testExecuteHappyPathSingleExercise() {
+ // Happy Path: Single Exercise Record
+ LocalDate date = LocalDate.now();
+ DailyRecord dailyRecord = new DailyRecord();
+ Exercise exercise = new Exercise(3, 12, 50, 200, "Bench Press");
+ dailyRecord.logDayToRecord(new programme.Day("Day 1"));
+ dailyRecord.getDayFromRecord().insertExercise(exercise);
+ history.logRecord(date, dailyRecord);
+
+ CommandResult expectedResult = new CommandResult(history.getFormattedPersonalBests());
+
+ // Act
+ CommandResult result = listPersonalBestsCommand.execute(programmeList, history);
+
+ // Assert
+ assertEquals(expectedResult, result,
+ "Execution with a single exercise should return the correct personal best.");
+ }
+
+ @Test
+ public void testExecuteEdgeCaseNoExercises() {
+ // Edge Case: No Exercises in History
+ CommandResult expectedResult = new CommandResult("No personal bests found.");
+
+ // Act
+ CommandResult result = listPersonalBestsCommand.execute(programmeList, history);
+
+ // Assert
+ assertEquals(expectedResult, result, "Execution with no exercises should return 'No personal bests found.'");
+ }
+
+ @Test
+ public void testExecuteEdgeCaseMultipleExercises() {
+ // Edge Case: Multiple Exercises with different dates
+ LocalDate date1 = LocalDate.now().minusDays(1);
+ LocalDate date2 = LocalDate.now();
+
+ DailyRecord record1 = new DailyRecord();
+ DailyRecord record2 = new DailyRecord();
+
+ Exercise exercise1 = new Exercise(3, 12, 60, 250, "Squat");
+ Exercise exercise2 = new Exercise(4, 10, 70, 300, "Deadlift");
+
+ record1.logDayToRecord(new programme.Day("Day 1"));
+ record1.getDayFromRecord().insertExercise(exercise1);
+ record2.logDayToRecord(new programme.Day("Day 2"));
+ record2.getDayFromRecord().insertExercise(exercise2);
+
+ history.logRecord(date1, record1);
+ history.logRecord(date2, record2);
+
+ CommandResult expectedResult = new CommandResult(history.getFormattedPersonalBests());
+
+ // Act
+ CommandResult result = listPersonalBestsCommand.execute(programmeList, history);
+
+ // Assert
+ assertEquals(expectedResult, result,
+ "Execution with multiple exercises should return the correct personal bests.");
+ }
+}
+
diff --git a/src/test/java/command/history/ViewHistoryCommandTest.java b/src/test/java/command/history/ViewHistoryCommandTest.java
new file mode 100644
index 0000000000..8e7252e975
--- /dev/null
+++ b/src/test/java/command/history/ViewHistoryCommandTest.java
@@ -0,0 +1,72 @@
+package command.history;
+
+import command.CommandResult;
+import history.DailyRecord;
+import history.History;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDate;
+
+import static common.Utils.formatDate;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class ViewHistoryCommandTest {
+
+ private ViewHistoryCommand viewHistoryCommand;
+ private History history;
+ private LocalDate testDate;
+
+ @BeforeEach
+ public void setUp() {
+ history = new History();
+ testDate = LocalDate.now();
+ viewHistoryCommand = new ViewHistoryCommand(testDate);
+ }
+
+ @Test
+ public void testExecuteHappyPathSingleRecordForDate() {
+ // Happy Path: Record exists for the specified date
+ DailyRecord dailyRecord = new DailyRecord();
+ history.logRecord(testDate, dailyRecord);
+
+ CommandResult expectedResult = new CommandResult(dailyRecord.toString());
+
+ // Act
+ CommandResult result = viewHistoryCommand.execute(history);
+
+ // Assert
+ assertEquals(expectedResult, result, "Execution should return the correct record for the specified date.");
+ }
+
+ @Test
+ public void testExecuteEdgeCaseNoRecordForDate() {
+ // Edge Case: No record exists for the specified date
+ CommandResult expectedResult = new CommandResult("No record found for " + formatDate(testDate));
+
+ // Act
+ CommandResult result = viewHistoryCommand.execute(history);
+
+ // Assert
+ assertEquals(expectedResult, result,
+ "Execution should return 'No record found' when no record exists for the specified date.");
+ }
+
+ @Test
+ public void testExecuteEdgeCaseRecordExistsForDifferentDate() {
+ // Edge Case: Record exists, but for a different date
+ LocalDate differentDate = testDate.minusDays(1);
+ DailyRecord dailyRecord = new DailyRecord();
+ history.logRecord(differentDate, dailyRecord);
+
+ CommandResult expectedResult = new CommandResult("No record found for " + formatDate(testDate));
+
+ // Act
+ CommandResult result = viewHistoryCommand.execute(history);
+
+ // Assert
+ assertEquals(expectedResult, result,
+ "Execution should return 'No record found' if the record exists only for a different date.");
+ }
+}
+
diff --git a/src/test/java/command/history/ViewPersonalBestCommandTest.java b/src/test/java/command/history/ViewPersonalBestCommandTest.java
new file mode 100644
index 0000000000..621f882a0c
--- /dev/null
+++ b/src/test/java/command/history/ViewPersonalBestCommandTest.java
@@ -0,0 +1,81 @@
+package command.history;
+
+import command.CommandResult;
+import history.History;
+import programme.ProgrammeList;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class ViewPersonalBestCommandTest {
+
+ private ProgrammeList programmeList;
+ private History history;
+
+ @BeforeEach
+ public void setUp() {
+ programmeList = new ProgrammeList();
+ history = new History();
+ }
+
+ @Test
+ public void testExecuteHappyPathSingleExercise() {
+ // Arrange
+ String exerciseName = "Bench Press";
+ history.getRecordByDate(java.time.LocalDate.now()).logDayToRecord(new programme.Day("Day 1"));
+ history.getRecordByDate(java.time.LocalDate.now()).getDayFromRecord().insertExercise(
+ new programme.Exercise(3, 12, 50, 200, exerciseName)
+ );
+
+ ViewPersonalBestCommand command = new ViewPersonalBestCommand(exerciseName);
+
+ // Act
+ CommandResult result = command.execute(programmeList, history);
+
+ // Assert
+ String expectedMessage = "Personal best for " + exerciseName + ": 3 sets of 12 at 50kg";
+ assertEquals(expectedMessage, result.getMessage(), "Execution should return correct personal best message.");
+ }
+
+ @Test
+ public void testExecuteEdgeCaseExerciseNotFound() {
+ // Arrange
+ String exerciseName = "Squat";
+ ViewPersonalBestCommand command = new ViewPersonalBestCommand(exerciseName);
+
+ // Act
+ CommandResult result = command.execute(programmeList, history);
+
+ // Assert
+ String expectedMessage = "No personal best found for " + exerciseName;
+ assertEquals(expectedMessage, result.getMessage(),
+ "Execution should return message indicating no personal best found.");
+ }
+
+ @Test
+ public void testExecuteEdgeCaseSimilarExerciseNames() {
+ // Arrange
+ String exerciseName1 = "Bench Press";
+ String exerciseName2 = "Bench Press Incline";
+
+ history.getRecordByDate(java.time.LocalDate.now()).logDayToRecord(new programme.Day("Day 1"));
+ history.getRecordByDate(java.time.LocalDate.now()).getDayFromRecord().insertExercise(
+ new programme.Exercise(3, 12, 50, 200, exerciseName1)
+ );
+ history.getRecordByDate(java.time.LocalDate.now()).getDayFromRecord().insertExercise(
+ new programme.Exercise(3, 10, 45, 180, exerciseName2)
+ );
+
+ ViewPersonalBestCommand command = new ViewPersonalBestCommand(exerciseName2);
+
+ // Act
+ CommandResult result = command.execute(programmeList, history);
+
+ // Assert
+ String expectedMessage = "Personal best for " + exerciseName2 + ": 3 sets of 10 at 45kg";
+ assertEquals(expectedMessage, result.getMessage(),
+ "Execution should return the correct personal best for a similarly named exercise.");
+ }
+}
+
diff --git a/src/test/java/command/history/WeeklySummaryCommandTest.java b/src/test/java/command/history/WeeklySummaryCommandTest.java
new file mode 100644
index 0000000000..ff538d7ecf
--- /dev/null
+++ b/src/test/java/command/history/WeeklySummaryCommandTest.java
@@ -0,0 +1,81 @@
+package command.history;
+
+import command.CommandResult;
+import history.DailyRecord;
+import history.History;
+import programme.ProgrammeList;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDate;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class WeeklySummaryCommandTest {
+
+ private ProgrammeList programmeList;
+ private History history;
+ private WeeklySummaryCommand weeklySummaryCommand;
+
+ @BeforeEach
+ public void setUp() {
+ programmeList = new ProgrammeList();
+ history = new History();
+ weeklySummaryCommand = new WeeklySummaryCommand();
+ }
+
+ @Test
+ public void testExecuteHappyPathWithWeeklyData() {
+ // Arrange
+ LocalDate today = LocalDate.now();
+ LocalDate threeDaysAgo = today.minusDays(3);
+ DailyRecord todayRecord = new DailyRecord();
+ DailyRecord threeDaysAgoRecord = new DailyRecord();
+
+ todayRecord.logDayToRecord(new programme.Day("Day 1"));
+ todayRecord.getDayFromRecord().insertExercise(new programme.Exercise(3, 12, 50, 200, "Bench Press"));
+
+ threeDaysAgoRecord.logDayToRecord(new programme.Day("Day 2"));
+ threeDaysAgoRecord.getDayFromRecord().insertExercise(new programme.Exercise(3, 10, 45, 150, "Squat"));
+
+ history.logRecord(today, todayRecord);
+ history.logRecord(threeDaysAgo, threeDaysAgoRecord);
+
+ // Act
+ CommandResult result = weeklySummaryCommand.execute(programmeList, history);
+
+ // Assert
+ String expectedMessage = "Your weekly workout summary: \n" + history.getWeeklyWorkoutSummary();
+ assertEquals(expectedMessage, result.getMessage(), "Execution should return correct weekly workout summary.");
+ }
+
+ @Test
+ public void testExecuteEdgeCaseNoWorkoutHistory() {
+ // Act
+ CommandResult result = weeklySummaryCommand.execute(programmeList, history);
+
+ // Assert
+ String expectedMessage = "Your weekly workout summary: \nNo workout history available.";
+ assertEquals(expectedMessage, result.getMessage(), "Execution should indicate no workout history available.");
+ }
+
+ @Test
+ public void testExecuteEdgeCaseOnlyOldRecords() {
+ // Arrange
+ LocalDate tenDaysAgo = LocalDate.now().minusDays(10);
+ DailyRecord oldRecord = new DailyRecord();
+
+ oldRecord.logDayToRecord(new programme.Day("Day 1"));
+ oldRecord.getDayFromRecord().insertExercise(new programme.Exercise(3, 10, 40, 120, "Deadlift"));
+
+ history.logRecord(tenDaysAgo, oldRecord);
+
+ // Act
+ CommandResult result = weeklySummaryCommand.execute(programmeList, history);
+
+ // Assert
+ String expectedMessage = "Your weekly workout summary: \nNo workout history available for the past week.";
+ assertEquals(expectedMessage, result.getMessage(),
+ "Execution should indicate no recent workout history available.");
+ }
+}
diff --git a/src/test/java/command/meals/AddMealCommandTest.java b/src/test/java/command/meals/AddMealCommandTest.java
new file mode 100644
index 0000000000..97f862463a
--- /dev/null
+++ b/src/test/java/command/meals/AddMealCommandTest.java
@@ -0,0 +1,79 @@
+package command.meals;
+
+import command.CommandResult;
+import history.DailyRecord;
+import history.History;
+import meal.Meal;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.time.LocalDate;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.verify;
+
+public class AddMealCommandTest {
+
+ private AddMealCommand addMealCommand;
+ private Meal sampleMeal;
+ private LocalDate date;
+
+ @Mock
+ private History mockHistory;
+
+ @Mock
+ private DailyRecord mockDailyRecord;
+
+ @BeforeEach
+ public void setUp() {
+ // Initialize the mocks
+ MockitoAnnotations.openMocks(this);
+
+ sampleMeal = new Meal("Sample Meal", 500);
+ date = LocalDate.now();
+
+ // Set up History mock to return a DailyRecord for the specified date
+ when(mockHistory.getRecordByDate(date)).thenReturn(mockDailyRecord);
+
+ addMealCommand = new AddMealCommand(sampleMeal, date);
+ }
+
+ @Test
+ public void testExecuteHappyPath() {
+ // Arrange
+ CommandResult expectedResult = new CommandResult(sampleMeal.toString() + " has been added");
+
+ // Act
+ CommandResult result = addMealCommand.execute(mockHistory);
+
+ // Assert
+ verify(mockDailyRecord).addMealToRecord(sampleMeal);
+ assertEquals(expectedResult, result, "Execution should " +
+ "return a CommandResult with the correct success message.");
+ }
+
+ @Test
+ public void testExecuteEdgeCaseNullDailyRecord() {
+ // Set up History mock to return null for the DailyRecord
+ when(mockHistory.getRecordByDate(date)).thenReturn(null);
+
+ // If assertions are enabled, an AssertionError is expected; otherwise,
+ // replace with a custom exception if defined
+ assertThrows(AssertionError.class, () -> addMealCommand.execute(mockHistory), "Executing " +
+ "AddMealCommand without a valid DailyRecord should throw an AssertionError.");
+ }
+
+ @Test
+ public void testExecuteEdgeCaseNullMeal() {
+ // Attempting to create a command with a null meal should throw an AssertionError
+ // If a specific custom exception is defined for null meal, replace AssertionError with it
+ assertThrows(AssertionError.class, () -> new AddMealCommand(null, date), "Creating " +
+ "AddMealCommand with null Meal should throw AssertionError.");
+ }
+}
+
+
diff --git a/src/test/java/command/meals/DeleteMealCommandTest.java b/src/test/java/command/meals/DeleteMealCommandTest.java
new file mode 100644
index 0000000000..3f2b9b93be
--- /dev/null
+++ b/src/test/java/command/meals/DeleteMealCommandTest.java
@@ -0,0 +1,88 @@
+package command.meals;
+
+import command.CommandResult;
+import history.DailyRecord;
+import history.History;
+import meal.Meal;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.time.LocalDate;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.verify;
+
+public class DeleteMealCommandTest {
+
+ private DeleteMealCommand deleteMealCommand;
+ private Meal sampleMeal;
+ private LocalDate date;
+
+ @Mock
+ private History mockHistory;
+
+ @Mock
+ private DailyRecord mockDailyRecord;
+
+ @BeforeEach
+ public void setUp() {
+ // Initialize the mocks
+ MockitoAnnotations.openMocks(this);
+
+ sampleMeal = new Meal("Sample Meal", 500);
+ date = LocalDate.now();
+
+ // Set up History mock to return a DailyRecord for the specified date
+ when(mockHistory.getRecordByDate(date)).thenReturn(mockDailyRecord);
+
+ // Set up DailyRecord mock to return the sample meal when deleting by index
+ when(mockDailyRecord.deleteMealFromRecord(0)).thenReturn(sampleMeal);
+
+ deleteMealCommand = new DeleteMealCommand(0, date);
+ }
+
+ @Test
+ public void testExecuteHappyPath() {
+ // Arrange
+ CommandResult expectedResult = new CommandResult(sampleMeal + " has been deleted");
+
+ // Act
+ CommandResult result = deleteMealCommand.execute(mockHistory);
+
+ // Assert
+ verify(mockDailyRecord).deleteMealFromRecord(0);
+ assertEquals(expectedResult, result, "Execution should " +
+ "return a CommandResult with the correct success message.");
+ }
+
+ @Test
+ public void testExecuteEdgeCaseNullDailyRecord() {
+ // Set up History mock to return null for the DailyRecord
+ when(mockHistory.getRecordByDate(date)).thenReturn(null);
+
+ assertThrows(AssertionError.class, () -> deleteMealCommand.execute(mockHistory), "Executing " +
+ "DeleteMealCommand without a valid DailyRecord should throw an AssertionError.");
+ }
+
+ @Test
+ public void testExecuteEdgeCaseInvalidIndex() {
+ // Set up DailyRecord mock to throw IndexOutOfBoundsException when an invalid index is used
+ when(mockDailyRecord.deleteMealFromRecord(5)).thenThrow(new IndexOutOfBoundsException("Invalid index"));
+
+ DeleteMealCommand invalidIndexCommand = new DeleteMealCommand(5, date);
+
+ assertThrows(IndexOutOfBoundsException.class, () -> invalidIndexCommand.execute(mockHistory), "Executing " +
+ "DeleteMealCommand with an invalid index should throw IndexOutOfBoundsException.");
+ }
+
+ @Test
+ public void testConstructorEdgeCaseNegativeIndex() {
+ // Attempting to create a command with a negative index should throw an AssertionError
+ assertThrows(AssertionError.class, () -> new DeleteMealCommand(-1, date), "Creating " +
+ "DeleteMealCommand with negative index should throw AssertionError.");
+ }
+}
diff --git a/src/test/java/command/meals/MealCommandTest.java b/src/test/java/command/meals/MealCommandTest.java
new file mode 100644
index 0000000000..be5f7b17af
--- /dev/null
+++ b/src/test/java/command/meals/MealCommandTest.java
@@ -0,0 +1,73 @@
+package command.meals;
+
+import command.CommandResult;
+import history.History;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import programme.ProgrammeList;
+
+import java.time.LocalDate;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.verify;
+
+public class MealCommandTest {
+
+ private MealCommand mealCommand;
+ private LocalDate date;
+
+ @Mock
+ private History mockHistory;
+
+ @Mock
+ private ProgrammeList mockProgrammeList;
+
+ @BeforeEach
+ public void setUp() {
+ //initializes the @Mock annotated fields in the test class
+ MockitoAnnotations.openMocks(this);
+ date = LocalDate.now();
+ mealCommand = new TestMealCommand(date); // Using a concrete subclass for testing
+ }
+
+ @Test
+ public void testConstructorHappyPath() {
+ assertEquals(date, mealCommand.date, "Date should be " +
+ "initialized correctly in MealCommand.");
+ }
+
+ @Test
+ public void testConstructorEdgeCaseNullDate() {
+ assertThrows(AssertionError.class, () -> new TestMealCommand(null), "Creating " +
+ "MealCommand with null date should throw an AssertionError.");
+ }
+
+ @Test
+ public void testExecuteWithProgrammeListAndHistoryHappyPath() {
+ CommandResult expected = new CommandResult("Executed with history");
+
+ CommandResult result = mealCommand.execute(mockProgrammeList, mockHistory);
+
+ assertEquals(expected, result, "execute should return a " +
+ "CommandResult with the correct message.");
+ // Assuming some method interaction with mockHistory in the subclass
+ verify(mockHistory).getRecordByDate(date);
+ }
+
+ // Concrete subclass of MealCommand for testing purposes
+ private static class TestMealCommand extends MealCommand {
+ public TestMealCommand(LocalDate date) {
+ super(date);
+ }
+
+ @Override
+ public CommandResult execute(History history) {
+ // Ensure getRecordByDate is called as part of the execution
+ history.getRecordByDate(date);
+ return new CommandResult("Executed with history");
+ }
+ }
+}
diff --git a/src/test/java/command/meals/ViewMealCommandTest.java b/src/test/java/command/meals/ViewMealCommandTest.java
new file mode 100644
index 0000000000..10ee523d96
--- /dev/null
+++ b/src/test/java/command/meals/ViewMealCommandTest.java
@@ -0,0 +1,83 @@
+package command.meals;
+
+import command.CommandResult;
+import history.DailyRecord;
+import history.History;
+import meal.MealList;
+import java.time.format.DateTimeFormatter;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.time.LocalDate;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.verify;
+
+public class ViewMealCommandTest {
+
+ private ViewMealCommand viewMealCommand;
+ private LocalDate date;
+
+ @Mock
+ private History mockHistory;
+
+ @Mock
+ private DailyRecord mockDailyRecord;
+
+ @Mock
+ private MealList mockMealList;
+
+ @BeforeEach
+ public void setUp() {
+ MockitoAnnotations.openMocks(this);
+ date = LocalDate.now();
+
+ // Set up History mock to return a DailyRecord for the specified date
+ when(mockHistory.getRecordByDate(date)).thenReturn(mockDailyRecord);
+
+ // Set up DailyRecord mock to return a MealList
+ when(mockDailyRecord.getMealListFromRecord()).thenReturn(mockMealList);
+
+ viewMealCommand = new ViewMealCommand(date);
+ }
+
+ @Test
+ public void testExecuteHappyPath() {
+ // Arrange
+ when(mockMealList.toString()).thenReturn("Sample Meal List");
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy");
+ String formattedDate = date.format(formatter);
+
+ CommandResult expectedResult = new CommandResult("Meals for " + formattedDate + ": \n\nSample Meal List");
+
+ // Act
+ CommandResult result = viewMealCommand.execute(mockHistory);
+
+ // Assert
+ verify(mockHistory).getRecordByDate(date);
+ verify(mockDailyRecord).getMealListFromRecord();
+ assertEquals(expectedResult, result, "Execution should return a " +
+ "CommandResult with the correct meal list output.");
+ }
+
+ @Test
+ public void testExecuteEdgeCaseNullDailyRecord() {
+ // Arrange
+ when(mockHistory.getRecordByDate(date)).thenReturn(null);
+
+ // Act & Assert
+ assertThrows(AssertionError.class, () -> viewMealCommand.execute(mockHistory), "Executing " +
+ "ViewMealCommand with a null DailyRecord should throw an AssertionError.");
+ }
+
+ @Test
+ public void testConstructorEdgeCaseNullDate() {
+ // Act & Assert
+ assertThrows(AssertionError.class, () -> new ViewMealCommand(null), "Creating " +
+ "ViewMealCommand with a null date should throw an AssertionError.");
+ }
+}
diff --git a/src/test/java/command/programme/CreateProgrammeCommandTest.java b/src/test/java/command/programme/CreateProgrammeCommandTest.java
new file mode 100644
index 0000000000..e731b1f945
--- /dev/null
+++ b/src/test/java/command/programme/CreateProgrammeCommandTest.java
@@ -0,0 +1,65 @@
+// @@author nirala-ts
+
+package command.programme;
+
+import command.CommandResult;
+import history.History;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import programme.Day;
+import programme.Programme;
+import programme.ProgrammeList;
+
+import java.util.ArrayList;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class CreateProgrammeCommandTest {
+
+ private static final String VALID_PROGRAMME_NAME = "New Programme";
+ private static final String EMPTY_PROGRAMME_NAME = "";
+ private static final ArrayList VALID_PROGRAMME_CONTENTS = new ArrayList<>();
+ private static final ArrayList NULL_PROGRAMME_CONTENTS = null;
+ private ProgrammeList programmeList;
+ private History history;
+
+ @BeforeEach
+ void setUp() {
+ programmeList = new ProgrammeList();
+ history = new History();
+ }
+
+ @Test
+ void constructor_initializesWithValidParameters(){
+ assertDoesNotThrow(() -> new CreateProgrammeCommand(VALID_PROGRAMME_NAME, VALID_PROGRAMME_CONTENTS));
+ }
+
+ @Test
+ void constructor_throwsAssertionErrorIfProgrammesIsNull() {
+ assertThrows(AssertionError.class, () -> new CreateProgrammeCommand(
+ VALID_PROGRAMME_NAME, NULL_PROGRAMME_CONTENTS)
+ );
+ }
+
+ @Test
+ void constructor_createsProgrammeWithEmptyName() {
+ assertThrows(AssertionError.class, () -> new CreateProgrammeCommand(
+ EMPTY_PROGRAMME_NAME, VALID_PROGRAMME_CONTENTS)
+ );
+ }
+
+ @Test
+ void execute_createsProgrammeSuccessfully_returnsSuccessMessage() {
+ CreateProgrammeCommand command = new CreateProgrammeCommand(VALID_PROGRAMME_NAME, VALID_PROGRAMME_CONTENTS);
+ String expectedMessage = String.format(CreateProgrammeCommand.SUCCESS_MESSAGE_FORMAT,
+ new Programme(VALID_PROGRAMME_NAME, VALID_PROGRAMME_CONTENTS)
+ );
+ CommandResult expectedResult = new CommandResult(expectedMessage);
+
+ CommandResult actualResult = command.execute(programmeList, history);
+ assertEquals(expectedResult, actualResult);
+ }
+}
+
diff --git a/src/test/java/command/programme/DeleteProgrammeCommandTest.java b/src/test/java/command/programme/DeleteProgrammeCommandTest.java
new file mode 100644
index 0000000000..e41790fa5b
--- /dev/null
+++ b/src/test/java/command/programme/DeleteProgrammeCommandTest.java
@@ -0,0 +1,69 @@
+// @@author nirala-ts
+
+package command.programme;
+
+import command.CommandResult;
+import exceptions.ProgrammeException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import programme.ProgrammeList;
+import programme.Day;
+import programme.Programme;
+import history.History;
+
+import java.util.ArrayList;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class DeleteProgrammeCommandTest {
+
+ private static final int VALID_PROGRAMME_ID = 0;
+ private static final int INVALID_PROGRAMME_ID = -2;
+ private static final int OUT_OF_RANGE_PROGRAMME_ID = 999;
+
+ private ProgrammeList programmeList;
+ private Programme expectedProgramme;
+ private DeleteProgrammeCommand command;
+ private History history;
+
+ @BeforeEach
+ void setUp() {
+ programmeList = new ProgrammeList();
+ history = new History();
+ Day day = new Day("Day 1", new ArrayList<>());
+ ArrayList days = new ArrayList<>();
+ days.add(day);
+ programmeList.insertProgramme("Mock Programme", days);
+
+ expectedProgramme = new Programme("Mock Programme", days);
+ command = new DeleteProgrammeCommand(VALID_PROGRAMME_ID);
+ }
+
+ @Test
+ void constructor_initializesWithValidParameters() {
+ assertDoesNotThrow(() -> new DeleteProgrammeCommand(VALID_PROGRAMME_ID));
+ }
+
+ @Test
+ void constructor_throwsAssertionErrorIfProgrammeIdIsNegative() {
+ assertThrows(AssertionError.class, () -> new DeleteProgrammeCommand(INVALID_PROGRAMME_ID));
+ }
+
+ @Test
+ void execute_deletesDayFromProgramme_returnsSuccessMessage() {
+ String expectedMessage = String.format(DeleteProgrammeCommand.SUCCESS_MESSAGE_FORMAT, expectedProgramme);
+ CommandResult expectedResult = new CommandResult(expectedMessage);
+
+ CommandResult actualResult = command.execute(programmeList, history);
+ assertEquals(expectedResult, actualResult);
+ }
+
+ @Test
+ void execute_throwsIndexOutOfBoundsIfProgrammeIdDoesNotExist() {
+ DeleteProgrammeCommand invalidCommand = new DeleteProgrammeCommand(OUT_OF_RANGE_PROGRAMME_ID);
+ assertThrows(ProgrammeException.class, () -> invalidCommand.execute(programmeList, history),
+ "Expected IndexOutOfBoundsBuffBuddyException for an out-of-range programme ID");
+ }
+}
diff --git a/src/test/java/command/programme/ListProgrammeCommandTest.java b/src/test/java/command/programme/ListProgrammeCommandTest.java
new file mode 100644
index 0000000000..9843400a99
--- /dev/null
+++ b/src/test/java/command/programme/ListProgrammeCommandTest.java
@@ -0,0 +1,67 @@
+// @@author nirala-ts
+
+package command.programme;
+
+import command.CommandResult;
+import history.History;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import programme.Day;
+import programme.ProgrammeList;
+
+import java.util.ArrayList;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+public class ListProgrammeCommandTest {
+
+ private ProgrammeList programmeList;
+ private History history;
+ private ListProgrammeCommand command;
+
+ @BeforeEach
+ void setUp() {
+ programmeList = new ProgrammeList();
+ history = new History();
+ command = new ListProgrammeCommand();
+
+ // Add some programmes to the list for testing
+ ArrayList days1 = new ArrayList<>();
+ days1.add(new Day("Day 1", new ArrayList<>()));
+ programmeList.insertProgramme("Programme 1", days1);
+
+ ArrayList days2 = new ArrayList<>();
+ days2.add(new Day("Day 2", new ArrayList<>()));
+ programmeList.insertProgramme("Programme 2", days2);
+ }
+
+ @Test
+ void execute_returnsListOfProgrammes() {
+ CommandResult result = command.execute(programmeList, history);
+ assertNotNull(result);
+ String expectedMessage = String.format(
+ ListProgrammeCommand.SUCCESS_MESSAGE_FORMAT,
+ programmeList
+ );
+ assertEquals(expectedMessage, result.getMessage());
+ }
+
+ @Test
+ void execute_returnsEmptyListWhenNoProgrammes() {
+ programmeList = new ProgrammeList(); // Reset to empty list
+ CommandResult result = command.execute(programmeList, history);
+ assertNotNull(result);
+ String expectedMessage = String.format(
+ ListProgrammeCommand.SUCCESS_MESSAGE_FORMAT,
+ "No programmes found."
+ );
+ assertEquals(expectedMessage, result.getMessage());
+ }
+
+ @Test
+ void execute_handlesNullProgrammeList() {
+ assertThrows(AssertionError.class, () -> command.execute(null, history));
+ }
+}
diff --git a/src/test/java/command/programme/LogProgrammeCommandTest.java b/src/test/java/command/programme/LogProgrammeCommandTest.java
new file mode 100644
index 0000000000..407a50fbd2
--- /dev/null
+++ b/src/test/java/command/programme/LogProgrammeCommandTest.java
@@ -0,0 +1,75 @@
+// @@ author andreusxcarvalho
+
+package command.programme;
+import org.junit.jupiter.api.Test;
+
+import command.CommandResult;
+import history.History;
+import history.DailyRecord;
+import programme.Day;
+import programme.ProgrammeList;
+import programme.Programme;
+
+import java.time.LocalDate;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+
+public class LogProgrammeCommandTest {
+
+ @Test
+ public void testExecute_logsDayIntoHistory() {
+ ProgrammeList mockProgrammes = mock(ProgrammeList.class);
+ History mockHistory = mock(History.class);
+ Day mockDay = mock(Day.class);
+ DailyRecord mockDailyRecord = mock(DailyRecord.class);
+ Programme mockProgramme = mock(Programme.class);
+
+ int progIndex = 0;
+ int dayIndex = 0;
+ LocalDate date = LocalDate.of(2024, 12, 12);
+
+ // Mock ProgrammeList behavior to return the mock Programme when getProgramme() is called
+ when(mockProgrammes.getProgramme(progIndex)).thenReturn(mockProgramme);
+ when(mockProgramme.getDay(dayIndex)).thenReturn(mockDay);
+ when(mockHistory.getRecordByDate(date)).thenReturn(mockDailyRecord);
+
+ LogProgrammeCommand logCommand = new LogProgrammeCommand(progIndex, dayIndex, date);
+
+ CommandResult result = logCommand.execute(mockProgrammes, mockHistory);
+
+ verify(mockProgrammes).getProgramme(progIndex);
+ verify(mockProgramme).getDay(dayIndex);
+ verify(mockDailyRecord).logDayToRecord(mockDay);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void testExecute_edgeCase_invalidProgrammeIndex() {
+ ProgrammeList mockProgrammes = mock(ProgrammeList.class);
+ History mockHistory = mock(History.class);
+ int invalidProgIndex = -1;
+ int dayIndex = 0;
+ LocalDate date = LocalDate.of(2024, 12, 12);
+
+ LogProgrammeCommand logCommand = new LogProgrammeCommand(invalidProgIndex, dayIndex, date);
+
+ assertThrows(NullPointerException.class, () -> logCommand.execute(mockProgrammes, mockHistory));
+ }
+
+ @Test
+ public void testExecute_edgeCase_nullHistory() {
+ ProgrammeList mockProgrammes = mock(ProgrammeList.class);
+ int progIndex = 0;
+ int dayIndex = 0;
+ LocalDate date = LocalDate.of(2024, 12, 12);
+
+ LogProgrammeCommand logCommand = new LogProgrammeCommand(progIndex, dayIndex, date);
+
+ assertThrows(AssertionError.class, () -> logCommand.execute(mockProgrammes, null));
+ }
+}
diff --git a/src/test/java/command/programme/StartProgrammeCommandTest.java b/src/test/java/command/programme/StartProgrammeCommandTest.java
new file mode 100644
index 0000000000..7aebad4914
--- /dev/null
+++ b/src/test/java/command/programme/StartProgrammeCommandTest.java
@@ -0,0 +1,65 @@
+// @@author nirala-ts
+
+package command.programme;
+
+import command.CommandResult;
+import exceptions.ProgrammeException;
+import history.History;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import programme.Day;
+import programme.Programme;
+import programme.ProgrammeList;
+
+import java.util.ArrayList;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+public class StartProgrammeCommandTest {
+
+ private ProgrammeList programmeList;
+ private History history;
+ private StartProgrammeCommand command;
+
+ @BeforeEach
+ void setUp() {
+ programmeList = new ProgrammeList();
+ history = new History();
+
+ ArrayList days1 = new ArrayList<>();
+ days1.add(new Day("Day 1", new ArrayList<>()));
+ programmeList.insertProgramme("Programme 1", days1);
+
+ ArrayList days2 = new ArrayList<>();
+ days2.add(new Day("Day 2", new ArrayList<>()));
+ programmeList.insertProgramme("Programme 2", days2);
+
+ programmeList.deactivateCurrentProgramme();
+ }
+
+ @Test
+ void execute_startsProgrammeSuccessfully() {
+ command = new StartProgrammeCommand(0);
+ CommandResult result = command.execute(programmeList, history);
+ assertNotNull(result);
+ Programme startedProgramme = programmeList.getProgramme(0);
+ String expectedMessage = String.format(StartProgrammeCommand.SUCCESS_MESSAGE_FORMAT, startedProgramme);
+ assertEquals(expectedMessage, result.getMessage());
+ }
+
+
+ @Test
+ void execute_invalidProgrammeIndex_throwsException() {
+ command = new StartProgrammeCommand(5);
+ assertThrows(ProgrammeException.class, () -> command.execute(programmeList, history));
+ }
+
+ @Test
+ void execute_nullProgrammeList_throwsException() {
+ command = new StartProgrammeCommand(0);
+ assertThrows(AssertionError.class, () -> command.execute(null, history));
+ }
+}
+
diff --git a/src/test/java/command/programme/ViewProgrammeCommandTest.java b/src/test/java/command/programme/ViewProgrammeCommandTest.java
new file mode 100644
index 0000000000..093b405309
--- /dev/null
+++ b/src/test/java/command/programme/ViewProgrammeCommandTest.java
@@ -0,0 +1,62 @@
+// @@author nirala-ts
+
+package command.programme;
+
+import command.CommandResult;
+import exceptions.ProgrammeException;
+import history.History;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import programme.Day;
+import programme.Programme;
+import programme.ProgrammeList;
+
+import java.util.ArrayList;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+public class ViewProgrammeCommandTest {
+
+ private ProgrammeList programmeList;
+ private History history;
+ private ViewProgrammeCommand command;
+
+ @BeforeEach
+ void setUp() {
+ programmeList = new ProgrammeList();
+ history = new History();
+
+ ArrayList days1 = new ArrayList<>();
+ days1.add(new Day("Day 1", new ArrayList<>()));
+ programmeList.insertProgramme("Programme 1", days1);
+
+ ArrayList days2 = new ArrayList<>();
+ days2.add(new Day("Day 2", new ArrayList<>()));
+ programmeList.insertProgramme("Programme 2", days2);
+ }
+
+ @Test
+ void execute_viewsProgrammeSuccessfully() {
+ command = new ViewProgrammeCommand(0);
+ CommandResult result = command.execute(programmeList, history);
+ assertNotNull(result);
+
+ Programme viewedProgramme = programmeList.getProgramme(0);
+ String expectedMessage = String.format(ViewProgrammeCommand.SUCCESS_MESSAGE_FORMAT, viewedProgramme);
+ assertEquals(expectedMessage, result.getMessage());
+ }
+
+ @Test
+ void execute_invalidProgrammeIndex_throwsException() {
+ command = new ViewProgrammeCommand(5); // Invalid index
+ assertThrows(ProgrammeException.class, () -> command.execute(programmeList, history));
+ }
+
+ @Test
+ void execute_nullProgrammeList_throwsException() {
+ command = new ViewProgrammeCommand(0);
+ assertThrows(AssertionError.class, () -> command.execute(null, history));
+ }
+}
diff --git a/src/test/java/command/programme/edit/CreateDayCommandTest.java b/src/test/java/command/programme/edit/CreateDayCommandTest.java
new file mode 100644
index 0000000000..ba6ce79bcc
--- /dev/null
+++ b/src/test/java/command/programme/edit/CreateDayCommandTest.java
@@ -0,0 +1,75 @@
+package command.programme.edit;
+
+import command.CommandResult;
+import exceptions.ProgrammeException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import programme.Day;
+import programme.ProgrammeList;
+
+import java.util.ArrayList;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class CreateDayCommandTest {
+
+ private static final int VALID_PROGRAMME_ID = 0;
+ private static final int INVALID_PROGRAMME_ID = -2;
+ private static final int OUT_OF_RANGE_PROGRAMME_ID = 999;
+ private ProgrammeList programme;
+ private Day day;
+ private CreateDayProgrammeCommand command;
+
+ @BeforeEach
+ void setUp() {
+ // Creates a programmeList with one programme with no days
+ programme = new ProgrammeList();
+ programme.insertProgramme("Mock programme", new ArrayList<>());
+ day = new Day("Day 1", new ArrayList<>());
+ command = new CreateDayProgrammeCommand(VALID_PROGRAMME_ID, day);
+ }
+
+ // Test for the constructor with a happy path case
+ @Test
+ void constructor_initializesWithValidProgrammeIdAndDay() {
+ assertDoesNotThrow(() -> new CreateDayProgrammeCommand(VALID_PROGRAMME_ID, day));
+ }
+
+ // Edge case for the constructor: Negative programme ID
+ @Test
+ void constructor_throwsExceptionIfProgrammeIdIsNegative() {
+ assertThrows(AssertionError.class, () -> new CreateDayProgrammeCommand(INVALID_PROGRAMME_ID, day));
+ }
+
+ // Edge case for the constructor: Day is null
+ @Test
+ void constructor_throwsExceptionIfDayIsNull() {
+ assertThrows(AssertionError.class, () -> new CreateDayProgrammeCommand(VALID_PROGRAMME_ID, null));
+ }
+
+ // Test for the "execute" method with a happy path case
+ @Test
+ void execute_addsDayToProgrammeList_returnsSuccessMessage() {
+ String expectedMessage = String.format(CreateDayProgrammeCommand.SUCCESS_MESSAGE_FORMAT, day);
+ CommandResult expectedResult = new CommandResult(expectedMessage);
+
+ CommandResult actualResult = command.execute(programme);
+ assertEquals(expectedResult, actualResult);
+ }
+
+ // Edge case for the "execute" method: Programmes list is null
+ @Test
+ void execute_throwsAssertionErrorIfProgrammesIsNull() {
+ assertThrows(AssertionError.class, () -> command.execute(null));
+ }
+
+ // Edge case for the "execute" method: Programme list does not contain programme ID
+ @Test
+ void execute_handlesNonexistentProgrammeIdGracefully() {
+ CreateDayProgrammeCommand invalidCommand = new CreateDayProgrammeCommand(OUT_OF_RANGE_PROGRAMME_ID, day);
+ assertThrows(ProgrammeException.class, () -> invalidCommand.execute(programme));
+ }
+}
+
diff --git a/src/test/java/command/programme/edit/CreateExerciseCommandTest.java b/src/test/java/command/programme/edit/CreateExerciseCommandTest.java
new file mode 100644
index 0000000000..f6afb3033b
--- /dev/null
+++ b/src/test/java/command/programme/edit/CreateExerciseCommandTest.java
@@ -0,0 +1,109 @@
+package command.programme.edit;
+
+import command.CommandResult;
+import exceptions.ProgrammeException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import programme.Day;
+import programme.Exercise;
+import programme.ProgrammeList;
+
+import java.util.ArrayList;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class CreateExerciseCommandTest {
+
+ private static final int VALID_PROGRAMME_ID = 0;
+ private static final int VALID_DAY_ID = 0;
+
+ private static final int INVALID_PROGRAMME_ID = -2;
+ private static final int INVALID_DAY_ID = -1;
+ private static final int OUT_OF_RANGE_PROGRAMME_ID = 999;
+ private static final int OUT_OF_RANGE_DAY_ID = 999;
+
+ private ProgrammeList programmeList;
+ private Exercise exercise;
+ private CreateExerciseProgrammeCommand command;
+
+ @BeforeEach
+ void setUp() {
+ // Creates a ProgrammeList with a single programme and one day
+ programmeList = new ProgrammeList();
+ ArrayList days = new ArrayList<>();
+ Day day = new Day("Day 1", new ArrayList<>());
+ days.add(day);
+ programmeList.insertProgramme("Mock Programme", days);
+
+ // Initialize the Exercise and CreateExerciseCommand with valid IDs
+ exercise = new Exercise(3, 10, 100, 200, "Deadlift");
+ command = new CreateExerciseProgrammeCommand(VALID_PROGRAMME_ID, VALID_DAY_ID, exercise);
+ }
+
+ // Test for the constructor with valid programme and day IDs and exercise
+ @Test
+ void constructor_initializesWithValidParameters() {
+ assertDoesNotThrow(() -> new CreateExerciseProgrammeCommand(VALID_PROGRAMME_ID, VALID_DAY_ID, exercise));
+ }
+
+ // Edge case for the constructor: Negative programme ID
+ @Test
+ void constructor_throwsAssertionErrorIfProgrammeIdIsInvalid() {
+ assertThrows(AssertionError.class, () ->
+ new CreateExerciseProgrammeCommand(INVALID_PROGRAMME_ID, VALID_DAY_ID, exercise)
+ );
+ }
+
+ // Edge case for the constructor: Negative day ID
+ @Test
+ void constructor_throwsAssertionErrorIfdayIndexIsNegative() {
+ assertThrows(AssertionError.class, () ->
+ new CreateExerciseProgrammeCommand(VALID_PROGRAMME_ID, INVALID_DAY_ID, exercise)
+ );
+ }
+
+ // Edge case for the constructor: Exercise is null
+ @Test
+ void constructor_throwsAssertionErrorIfExerciseIsNull() {
+ assertThrows(AssertionError.class, () ->
+ new CreateExerciseProgrammeCommand(VALID_PROGRAMME_ID, VALID_DAY_ID, null)
+ );
+ }
+
+ // Test for the execute method with a valid programme and day ID
+ @Test
+ void execute_addsExerciseToDay_returnsSuccessMessage() {
+ String expectedMessage = String.format(CreateExerciseProgrammeCommand.SUCCESS_MESSAGE_FORMAT, exercise);
+ CommandResult expectedResult = new CommandResult(expectedMessage);
+
+ CommandResult actualResult = command.execute(programmeList);
+ assertEquals(expectedResult, actualResult);
+ }
+
+ // Edge case for the execute method: Programme list is null
+ @Test
+ void execute_throwsAssertionErrorIfProgrammesIsNull() {
+ assertThrows(AssertionError.class, () -> command.execute(null));
+ }
+
+ // Edge case for the execute method: Nonexistent programme ID
+ @Test
+ void execute_throwsIndexOutOfBoundsIfProgrammeIdDoesNotExist() {
+ CreateExerciseProgrammeCommand invalidCommand = new CreateExerciseProgrammeCommand(
+ OUT_OF_RANGE_PROGRAMME_ID, VALID_DAY_ID, exercise
+ );
+ assertThrows(ProgrammeException.class, () -> invalidCommand.execute(programmeList));
+ }
+
+ // Edge case for the execute method: Nonexistent day ID within an existing programme
+ @Test
+ void execute_throwsIndexOutOfBoundsIfdayIndexDoesNotExist() {
+ CreateExerciseProgrammeCommand invalidCommand = new CreateExerciseProgrammeCommand(
+ VALID_PROGRAMME_ID, OUT_OF_RANGE_DAY_ID, exercise
+ );
+ assertThrows(ProgrammeException.class, () -> invalidCommand.execute(programmeList));
+ }
+}
+
diff --git a/src/test/java/command/programme/edit/DeleteDayCommandTest.java b/src/test/java/command/programme/edit/DeleteDayCommandTest.java
new file mode 100644
index 0000000000..d967f9e5df
--- /dev/null
+++ b/src/test/java/command/programme/edit/DeleteDayCommandTest.java
@@ -0,0 +1,94 @@
+package command.programme.edit;
+
+import command.CommandResult;
+import exceptions.ProgrammeException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import programme.Day;
+import programme.ProgrammeList;
+
+import java.util.ArrayList;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class DeleteDayCommandTest {
+
+ private static final int VALID_PROGRAMME_ID = 0;
+ private static final int VALID_DAY_ID = 0;
+ private static final int INVALID_PROGRAMME_ID = -2;
+ private static final int INVALID_DAY_ID = -1;
+ private static final int OUT_OF_RANGE_PROGRAMME_ID = 999;
+ private static final int OUT_OF_RANGE_DAY_ID = 999;
+
+ private ProgrammeList programmeList;
+ private Day day;
+ private DeleteDayProgrammeCommand command;
+
+ @BeforeEach
+ void setUp() {
+ // Set up a ProgrammeList with one programme and one day
+ programmeList = new ProgrammeList();
+ day = new Day("Day 1", new ArrayList<>());
+ ArrayList days = new ArrayList<>();
+ days.add(day);
+ programmeList.insertProgramme("Mock Programme", days);
+
+ // Initialize DeleteDayCommand with valid IDs
+ command = new DeleteDayProgrammeCommand(VALID_PROGRAMME_ID, VALID_DAY_ID);
+ }
+
+ // Test for constructor with valid inputs
+ @Test
+ void constructor_initializesWithValidParameters() {
+ assertDoesNotThrow(() -> new DeleteDayProgrammeCommand(VALID_PROGRAMME_ID, VALID_DAY_ID));
+ }
+
+ // Edge case for constructor: Negative programme ID
+ @Test
+ void constructor_throwsAssertionErrorIfProgrammeIdIsNegative() {
+ assertThrows(AssertionError.class, () -> new DeleteDayProgrammeCommand(INVALID_PROGRAMME_ID, VALID_DAY_ID));
+ }
+
+ // Edge case for constructor: Negative day ID
+ @Test
+ void constructor_throwsAssertionErrorIfdayIndexIsNegative() {
+ assertThrows(AssertionError.class, () -> new DeleteDayProgrammeCommand(VALID_PROGRAMME_ID, INVALID_DAY_ID));
+ }
+
+ // Test for execute method: successfully deletes day and returns success message
+ @Test
+ void execute_deletesDayFromProgramme_returnsSuccessMessage() {
+ String expectedMessage = String.format(DeleteDayProgrammeCommand.SUCCESS_MESSAGE_FORMAT, day);
+ CommandResult expectedResult = new CommandResult(expectedMessage);
+
+ CommandResult actualResult = command.execute(programmeList);
+ assertEquals(expectedResult, actualResult);
+ }
+
+ // Edge case for execute: Programme list is null
+ @Test
+ void execute_throwsAssertionErrorIfProgrammesIsNull() {
+ assertThrows(AssertionError.class, () -> command.execute(null));
+ }
+
+ // Edge case for execute: Nonexistent programme ID
+ @Test
+ void execute_throwsIndexOutOfBoundsIfProgrammeIdDoesNotExist() {
+ DeleteDayProgrammeCommand invalidCommand = new DeleteDayProgrammeCommand(
+ OUT_OF_RANGE_PROGRAMME_ID, VALID_DAY_ID
+ );
+ assertThrows(ProgrammeException.class, () -> invalidCommand.execute(programmeList));
+ }
+
+ // Edge case for execute: Nonexistent day ID within existing programme
+ @Test
+ void execute_throwsIndexOutOfBoundsIfdayIndexDoesNotExist() {
+ DeleteDayProgrammeCommand invalidCommand = new DeleteDayProgrammeCommand(
+ VALID_PROGRAMME_ID, OUT_OF_RANGE_DAY_ID
+ );
+ assertThrows(ProgrammeException.class, () -> invalidCommand.execute(programmeList));
+ }
+}
+
diff --git a/src/test/java/command/programme/edit/DeleteExerciseCommandTest.java b/src/test/java/command/programme/edit/DeleteExerciseCommandTest.java
new file mode 100644
index 0000000000..6b81304c24
--- /dev/null
+++ b/src/test/java/command/programme/edit/DeleteExerciseCommandTest.java
@@ -0,0 +1,126 @@
+package command.programme.edit;
+
+import command.CommandResult;
+import exceptions.ProgrammeException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import programme.Day;
+import programme.Exercise;
+import programme.ProgrammeList;
+
+import java.util.ArrayList;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class DeleteExerciseCommandTest {
+
+ private static final int VALID_PROGRAMME_ID = 0;
+ private static final int VALID_DAY_ID = 0;
+ private static final int VALID_EXERCISE_ID = 0;
+ private static final int INVALID_PROGRAMME_ID = -2;
+ private static final int INVALID_DAY_ID = -1;
+ private static final int INVALID_EXERCISE_ID = -1;
+ private static final int OUT_OF_RANGE_PROGRAMME_ID = 999;
+ private static final int OUT_OF_RANGE_DAY_ID = 999;
+ private static final int OUT_OF_RANGE_EXERCISE_ID = 999;
+
+ private ProgrammeList programmeList;
+ private Exercise exercise;
+ private DeleteExerciseProgrammeCommand command;
+
+ @BeforeEach
+ void setUp() {
+ // Set up a ProgrammeList with one programme, one day, and one exercise
+ programmeList = new ProgrammeList();
+ exercise = new Exercise(3, 10, 100, 200, "Deadlift");
+ ArrayList exercises = new ArrayList<>();
+ exercises.add(exercise);
+ Day day = new Day("Day 1", exercises);
+
+ ArrayList days = new ArrayList<>();
+ days.add(day);
+ programmeList.insertProgramme("Mock Programme", days);
+
+ // Initialize DeleteExerciseCommand with valid IDs
+ command = new DeleteExerciseProgrammeCommand(VALID_PROGRAMME_ID, VALID_DAY_ID, VALID_EXERCISE_ID);
+ }
+
+ // Test for constructor with valid inputs
+ @Test
+ void constructor_initializesWithValidParameters() {
+ assertDoesNotThrow(() -> new DeleteExerciseProgrammeCommand(
+ VALID_PROGRAMME_ID, VALID_DAY_ID, VALID_EXERCISE_ID)
+ );
+ }
+
+ // Edge case for constructor: Negative programme ID
+ @Test
+ void constructor_throwsAssertionErrorIfProgrammeIdIsNegative() {
+ assertThrows(AssertionError.class, () ->
+ new DeleteExerciseProgrammeCommand(INVALID_PROGRAMME_ID, VALID_DAY_ID, VALID_EXERCISE_ID)
+ );
+ }
+
+ // Edge case for constructor: Negative day ID
+ @Test
+ void constructor_throwsAssertionErrorIfdayIndexIsNegative() {
+ assertThrows(AssertionError.class, () ->
+ new DeleteExerciseProgrammeCommand(VALID_PROGRAMME_ID, INVALID_DAY_ID, VALID_EXERCISE_ID)
+ );
+ }
+
+ // Edge case for constructor: Negative exercise ID
+ @Test
+ void constructor_throwsAssertionErrorIfexerciseIndexIsNegative() {
+ assertThrows(AssertionError.class, () ->
+ new DeleteExerciseProgrammeCommand(VALID_PROGRAMME_ID, VALID_DAY_ID, INVALID_EXERCISE_ID)
+ );
+ }
+
+ // Test for execute method: successfully deletes exercise and returns success message
+ @Test
+ void execute_deletesExerciseFromDay_returnsSuccessMessage() {
+ String expectedMessage = String.format(
+ DeleteExerciseProgrammeCommand.SUCCESS_MESSAGE_FORMAT, VALID_EXERCISE_ID + 1, exercise
+ );
+ CommandResult expectedResult = new CommandResult(expectedMessage);
+
+ CommandResult actualResult = command.execute(programmeList);
+ assertEquals(expectedResult, actualResult);
+ }
+
+ // Edge case for execute: Programme list is null
+ @Test
+ void execute_throwsAssertionErrorIfProgrammesIsNull() {
+ assertThrows(AssertionError.class, () -> command.execute(null));
+ }
+
+ // Edge case for execute: Nonexistent programme ID
+ @Test
+ void execute_throwsIndexOutOfBoundsIfProgrammeIdDoesNotExist() {
+ DeleteExerciseProgrammeCommand invalidCommand = new DeleteExerciseProgrammeCommand(
+ OUT_OF_RANGE_PROGRAMME_ID, VALID_DAY_ID, VALID_EXERCISE_ID
+ );
+ assertThrows(ProgrammeException.class, () -> invalidCommand.execute(programmeList));
+ }
+
+ // Edge case for execute: Nonexistent day ID within existing programme
+ @Test
+ void execute_throwsIndexOutOfBoundsIfdayIndexDoesNotExist() {
+ DeleteExerciseProgrammeCommand invalidCommand = new DeleteExerciseProgrammeCommand(
+ VALID_PROGRAMME_ID, OUT_OF_RANGE_DAY_ID, VALID_EXERCISE_ID
+ );
+ assertThrows(ProgrammeException.class, () -> invalidCommand.execute(programmeList));
+ }
+
+ // Edge case for execute: Nonexistent exercise ID within existing day
+ @Test
+ void execute_handlesNonexistentexerciseIndexGracefully() {
+ DeleteExerciseProgrammeCommand invalidCommand = new DeleteExerciseProgrammeCommand(
+ VALID_PROGRAMME_ID, VALID_DAY_ID, OUT_OF_RANGE_EXERCISE_ID
+ );
+ assertThrows(ProgrammeException.class, () -> invalidCommand.execute(programmeList));
+ }
+}
diff --git a/src/test/java/command/programme/edit/EditExerciseCommandTest.java b/src/test/java/command/programme/edit/EditExerciseCommandTest.java
new file mode 100644
index 0000000000..a8fc6ba4f6
--- /dev/null
+++ b/src/test/java/command/programme/edit/EditExerciseCommandTest.java
@@ -0,0 +1,143 @@
+package command.programme.edit;
+
+import command.CommandResult;
+import exceptions.ProgrammeException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import programme.Day;
+import programme.Exercise;
+import programme.ExerciseUpdate;
+import programme.ProgrammeList;
+
+import java.util.ArrayList;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class EditExerciseCommandTest {
+
+ private static final int VALID_PROGRAMME_ID = 0;
+ private static final int VALID_DAY_ID = 0;
+ private static final int VALID_EXERCISE_ID = 0;
+
+ //Since programme_id is optional, -1 (NULL_INTEGER) is a valid input.
+ private static final int INVALID_PROGRAMME_ID = -2;
+
+ private static final int INVALID_DAY_ID = -1;
+ private static final int INVALID_EXERCISE_ID = -1;
+ private static final int OUT_OF_RANGE_PROGRAMME_ID = 999;
+ private static final int OUT_OF_RANGE_DAY_ID = 999;
+ private static final int OUT_OF_RANGE_EXERCISE_ID = 999;
+
+ private ProgrammeList programmeList;
+ private ExerciseUpdate update;
+ private Exercise expectedExercise;
+ private EditExerciseProgrammeCommand command;
+
+ @BeforeEach
+ void setUp() {
+ // Set up a ProgrammeList with one programme, one day, and one exercise
+ programmeList = new ProgrammeList();
+ Exercise originalExercise = new Exercise(3, 10, 100, 200, "Deadlift");
+ update = new ExerciseUpdate(3, 12, 105, 205, "Deadlift Updated");
+ expectedExercise = new Exercise(3, 12, 105, 205, "Deadlift Updated");
+
+ ArrayList exercises = new ArrayList<>();
+ exercises.add(originalExercise);
+ Day day = new Day("Day 1", exercises);
+
+ ArrayList days = new ArrayList<>();
+ days.add(day);
+ programmeList.insertProgramme("Mock Programme", days);
+
+ // Initialize EditExerciseCommand with valid IDs and the updated exercise
+ command = new EditExerciseProgrammeCommand(VALID_PROGRAMME_ID, VALID_DAY_ID, VALID_EXERCISE_ID, update);
+ }
+
+ // Test for constructor with valid inputs
+ @Test
+ void constructor_initializesWithValidParameters() {
+ assertDoesNotThrow(() ->
+ new EditExerciseProgrammeCommand(VALID_PROGRAMME_ID, VALID_DAY_ID, VALID_EXERCISE_ID, update)
+ );
+ }
+
+ // Edge case for constructor: Negative programme ID
+ @Test
+ void constructor_throwsAssertionErrorIfProgrammeIdIsNegative() {
+ assertThrows(AssertionError.class, () ->
+ new EditExerciseProgrammeCommand(INVALID_PROGRAMME_ID, VALID_DAY_ID, VALID_EXERCISE_ID, update)
+ );
+ }
+
+ // Edge case for constructor: Negative day ID
+ @Test
+ void constructor_throwsAssertionErrorIfdayIndexIsNegative() {
+ assertThrows(AssertionError.class, () ->
+ new EditExerciseProgrammeCommand(VALID_PROGRAMME_ID, INVALID_DAY_ID, VALID_EXERCISE_ID, update)
+ );
+ }
+
+ // Edge case for constructor: Negative exercise ID
+ @Test
+ void constructor_throwsAssertionErrorIfexerciseIndexIsNegative() {
+ assertThrows(AssertionError.class, () ->
+ new EditExerciseProgrammeCommand(VALID_PROGRAMME_ID, VALID_DAY_ID, INVALID_EXERCISE_ID, update)
+ );
+ }
+
+ // Edge case for constructor: Updated exercise is null
+ @Test
+ void constructor_throwsAssertionErrorIfUpdatedExerciseIsNull() {
+ assertThrows(AssertionError.class, () ->
+ new EditExerciseProgrammeCommand(VALID_PROGRAMME_ID, VALID_DAY_ID, VALID_EXERCISE_ID, null)
+ );
+ }
+
+ // Test for execute method: successfully updates exercise and returns success message
+ @Test
+ void execute_updatesExerciseInDay_returnsSuccessMessage() {
+ String expectedMessage = String.format(
+ EditExerciseProgrammeCommand.SUCCESS_MESSAGE_FORMAT, expectedExercise
+ );
+
+ CommandResult expectedResult = new CommandResult(expectedMessage);
+ CommandResult actualResult = command.execute(programmeList);
+
+ assertEquals(expectedResult, actualResult);
+ }
+
+ // Edge case for execute: Programme list is null
+ @Test
+ void execute_throwsAssertionErrorIfProgrammesIsNull() {
+ assertThrows(AssertionError.class, () -> command.execute(null));
+ }
+
+ // Edge case for execute: Nonexistent programme ID
+ @Test
+ void execute_throwsIndexOutOfBoundsIfProgrammeIdDoesNotExist() {
+ EditExerciseProgrammeCommand invalidCommand = new EditExerciseProgrammeCommand(
+ OUT_OF_RANGE_PROGRAMME_ID, VALID_DAY_ID, VALID_EXERCISE_ID, update
+ );
+ assertThrows(ProgrammeException.class, () -> invalidCommand.execute(programmeList));
+ }
+
+ // Edge case for execute: Nonexistent day ID within existing programme
+ @Test
+ void execute_throwsIndexOutOfBoundsIfdayIndexDoesNotExist() {
+ EditExerciseProgrammeCommand invalidCommand = new EditExerciseProgrammeCommand(
+ VALID_PROGRAMME_ID, OUT_OF_RANGE_DAY_ID, VALID_EXERCISE_ID, update
+ );
+ assertThrows(ProgrammeException.class, () -> invalidCommand.execute(programmeList));
+ }
+
+ // Edge case for execute: Nonexistent exercise ID within existing day
+ @Test
+ void execute_handlesNonexistentexerciseIndexGracefully() {
+ EditExerciseProgrammeCommand invalidCommand = new EditExerciseProgrammeCommand(
+ VALID_PROGRAMME_ID, VALID_DAY_ID, OUT_OF_RANGE_EXERCISE_ID, update
+ );
+ assertThrows(ProgrammeException.class, () -> invalidCommand.execute(programmeList));
+ }
+}
diff --git a/src/test/java/command/programme/edit/EditProgrammeCommandTest.java b/src/test/java/command/programme/edit/EditProgrammeCommandTest.java
new file mode 100644
index 0000000000..bcd0c0ac34
--- /dev/null
+++ b/src/test/java/command/programme/edit/EditProgrammeCommandTest.java
@@ -0,0 +1,54 @@
+package command.programme.edit;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class EditProgrammeCommandTest {
+
+ private static final int VALID_PROGRAMME_ID = 0;
+ private static final int INVALID_PROGRAMME_ID = -2;
+ private static final int VALID_DAY_ID = 0;
+ private static final int VALID_EXERCISE_ID = 0;
+ private static final int INVALID_EXERCISE_ID = -1;
+ private static final int INVALID_DAY_ID = -1;
+
+ // Test for the constructor with valid parameters
+ @Test
+ void constructor_withValidParameters_initializesCorrectly() {
+ assertDoesNotThrow(() -> new TestEditProgrammeCommand(VALID_PROGRAMME_ID, VALID_DAY_ID, VALID_EXERCISE_ID));
+ }
+
+ // Edge case for constructor with exerciseIndex: Negative exercise ID
+ @Test
+ void constructor_withNegativeexerciseIndex_throwsAssertionError() {
+ assertThrows(AssertionError.class, () ->
+ new TestEditProgrammeCommand(VALID_PROGRAMME_ID, VALID_DAY_ID, INVALID_EXERCISE_ID)
+ );
+ }
+
+ // Happy path for constructor without exerciseIndex
+ @Test
+ void constructor_withoutexerciseIndex_initializesCorrectly() {
+ assertDoesNotThrow(() -> new TestEditProgrammeCommand(VALID_PROGRAMME_ID, VALID_DAY_ID));
+ }
+
+ // Edge case for constructor without exerciseIndex: Invalid day ID
+ @Test
+ void constructor_withNegativedayIndex_throwsAssertionError() {
+ assertThrows(AssertionError.class, () -> new TestEditProgrammeCommand(VALID_PROGRAMME_ID, INVALID_DAY_ID));
+ }
+
+ // Happy path for constructor with only programme index
+ @Test
+ void constructor_withProgrammeIndex_initializesCorrectly() {
+ assertDoesNotThrow(() -> new TestEditProgrammeCommand(VALID_PROGRAMME_ID));
+ }
+
+ // Edge case for constructor with programme index: Negative programme index
+ @Test
+ void constructor_withNegativeProgrammeIndex_throwsAssertionError() {
+ assertThrows(AssertionError.class, () -> new TestEditProgrammeCommand(INVALID_PROGRAMME_ID));
+ }
+}
diff --git a/src/test/java/command/programme/edit/TestEditProgrammeCommand.java b/src/test/java/command/programme/edit/TestEditProgrammeCommand.java
new file mode 100644
index 0000000000..452f8d3e51
--- /dev/null
+++ b/src/test/java/command/programme/edit/TestEditProgrammeCommand.java
@@ -0,0 +1,24 @@
+package command.programme.edit;
+
+import command.CommandResult;
+import programme.ProgrammeList;
+
+// A concrete subclass of EditCommand to help testing abstract EditCommand class
+class TestEditProgrammeCommand extends EditProgrammeCommand {
+ public TestEditProgrammeCommand(int programmeIndex, int dayIndex, int exerciseIndex) {
+ super(programmeIndex, dayIndex, exerciseIndex);
+ }
+
+ public TestEditProgrammeCommand(int programmeIndex, int dayIndex) {
+ super(programmeIndex, dayIndex);
+ }
+
+ public TestEditProgrammeCommand(int programmeIndex) {
+ super(programmeIndex);
+ }
+
+ @Override
+ public CommandResult execute(ProgrammeList programmes) {
+ return new CommandResult("Executed");
+ }
+}
diff --git a/src/test/java/command/water/AddWaterCommandTest.java b/src/test/java/command/water/AddWaterCommandTest.java
new file mode 100644
index 0000000000..9613dc09a1
--- /dev/null
+++ b/src/test/java/command/water/AddWaterCommandTest.java
@@ -0,0 +1,68 @@
+package command.water;
+
+import command.CommandResult;
+import history.DailyRecord;
+import history.History;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.time.LocalDate;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class AddWaterCommandTest {
+
+ private AddWaterCommand addWaterCommand;
+ private float waterAmount;
+ private LocalDate date;
+
+ @Mock
+ private History mockHistory;
+
+ @Mock
+ private DailyRecord mockDailyRecord;
+
+ @BeforeEach
+ public void setUp() {
+ // Initialize the mocks
+ MockitoAnnotations.openMocks(this);
+
+ waterAmount = 1.5f;
+ date = LocalDate.now();
+
+ when(mockHistory.getRecordByDate(date)).thenReturn(mockDailyRecord);
+
+ addWaterCommand = new AddWaterCommand(waterAmount, date);
+ }
+
+ @Test
+ public void testExecuteHappyPath() {
+ CommandResult expectedResult = new CommandResult(waterAmount + " liters of water has been added");
+
+ CommandResult result = addWaterCommand.execute(mockHistory);
+
+ verify(mockDailyRecord).addWaterToRecord(waterAmount);
+ assertEquals(expectedResult, result,
+ "Execution should return a CommandResult with the correct success message.");
+ }
+
+ @Test
+ public void testExecuteEdgeCaseNullDailyRecord() {
+ // Set up History mock to return null for the DailyRecord
+ when(mockHistory.getRecordByDate(date)).thenReturn(null);
+
+ assertThrows(AssertionError.class, () -> addWaterCommand.execute(mockHistory),
+ "Executing AddWaterCommand without a valid DailyRecord should throw an AssertionError.");
+ }
+
+ @Test
+ public void testExecuteEdgeCaseNegativeWaterAmount() {
+ assertThrows(AssertionError.class, () -> new AddWaterCommand(-1.0f, date),
+ "Creating AddWaterCommand with a negative water amount should throw AssertionError.");
+ }
+}
diff --git a/src/test/java/command/water/DeleteWaterCommandTest.java b/src/test/java/command/water/DeleteWaterCommandTest.java
new file mode 100644
index 0000000000..83ca1161c6
--- /dev/null
+++ b/src/test/java/command/water/DeleteWaterCommandTest.java
@@ -0,0 +1,81 @@
+package command.water;
+
+import command.CommandResult;
+import history.DailyRecord;
+import history.History;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.time.LocalDate;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class DeleteWaterCommandTest {
+
+ private DeleteWaterCommand deleteWaterCommand;
+ private float sampleWaterAmount;
+ private LocalDate date;
+
+ @Mock
+ private History mockHistory;
+
+ @Mock
+ private DailyRecord mockDailyRecord;
+
+ @BeforeEach
+ public void setUp() {
+ // Initialize the mocks
+ MockitoAnnotations.openMocks(this);
+
+ sampleWaterAmount = 1.5f;
+ date = LocalDate.now();
+
+ when(mockHistory.getRecordByDate(date)).thenReturn(mockDailyRecord);
+
+ when(mockDailyRecord.removeWaterFromRecord(0)).thenReturn(sampleWaterAmount);
+
+ deleteWaterCommand = new DeleteWaterCommand(0, date);
+ }
+
+ @Test
+ public void testExecuteHappyPath() {
+ CommandResult expectedResult = new CommandResult(sampleWaterAmount +
+ " liters of water has been deleted");
+
+ CommandResult result = deleteWaterCommand.execute(mockHistory);
+
+ verify(mockDailyRecord).removeWaterFromRecord(0);
+ assertEquals(expectedResult, result,
+ "Execution should return a CommandResult with the correct success message.");
+ }
+
+ @Test
+ public void testExecuteEdgeCaseNullDailyRecord() {
+ when(mockHistory.getRecordByDate(date)).thenReturn(null);
+
+ assertThrows(AssertionError.class, () -> deleteWaterCommand.execute(mockHistory), "Executing " +
+ "DeleteWaterCommand without a valid DailyRecord should throw an AssertionError.");
+ }
+
+ @Test
+ public void testExecuteEdgeCaseInvalidIndex() {
+ // Set up DailyRecord mock to throw IndexOutOfBoundsException when an invalid index is used
+ when(mockDailyRecord.removeWaterFromRecord(5)).thenThrow(new IndexOutOfBoundsException("Invalid index"));
+
+ DeleteWaterCommand invalidIndexCommand = new DeleteWaterCommand(5, date);
+
+ assertThrows(IndexOutOfBoundsException.class, () -> invalidIndexCommand.execute(mockHistory), "Executing " +
+ "DeleteWaterCommand with an invalid index should throw IndexOutOfBoundsException.");
+ }
+
+ @Test
+ public void testConstructorEdgeCaseNegativeIndex() {
+ assertThrows(AssertionError.class, () -> new DeleteWaterCommand(-1, date), "Creating " +
+ "DeleteWaterCommand with negative index should throw AssertionError.");
+ }
+}
diff --git a/src/test/java/command/water/ViewWaterCommandTest.java b/src/test/java/command/water/ViewWaterCommandTest.java
new file mode 100644
index 0000000000..f65ab8e2ef
--- /dev/null
+++ b/src/test/java/command/water/ViewWaterCommandTest.java
@@ -0,0 +1,80 @@
+package command.water;
+
+import command.CommandResult;
+import history.DailyRecord;
+import history.History;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import water.Water;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class ViewWaterCommandTest {
+
+ private ViewWaterCommand viewWaterCommand;
+ private LocalDate date;
+
+ @Mock
+ private History mockHistory;
+
+ @Mock
+ private DailyRecord mockDailyRecord;
+
+ @Mock
+ private Water mockWater;
+
+ @BeforeEach
+ public void setUp() {
+ MockitoAnnotations.openMocks(this);
+ date = LocalDate.now();
+
+ // Set up History mock to return a DailyRecord for the specified date
+ when(mockHistory.getRecordByDate(date)).thenReturn(mockDailyRecord);
+
+ // Set up DailyRecord mock to return a Water object
+ when(mockDailyRecord.getWaterFromRecord()).thenReturn(mockWater);
+
+ when(mockWater.toString()).thenReturn("Sample Water Record");
+
+ viewWaterCommand = new ViewWaterCommand(date);
+ }
+
+ @Test
+ public void testExecuteHappyPath() {
+ String expectedWaterMessage = "Water intake for " + date.format(DateTimeFormatter.ofPattern("dd-MM-yyyy"))
+ + ": \n\n" + "Sample Water Record";
+ CommandResult expectedResult = new CommandResult(expectedWaterMessage);
+
+ CommandResult result = viewWaterCommand.execute(mockHistory);
+
+ verify(mockHistory).getRecordByDate(date);
+ verify(mockDailyRecord).getWaterFromRecord();
+
+ assertEquals(expectedResult, result,
+ "Execution should return a CommandResult with the correct water record output.");
+ }
+
+
+
+ @Test
+ public void testExecuteEdgeCaseNullDailyRecord() {
+ when(mockHistory.getRecordByDate(date)).thenReturn(null);
+
+ assertThrows(AssertionError.class, () -> viewWaterCommand.execute(mockHistory), "Executing " +
+ "ViewWaterCommand with a null DailyRecord should throw an AssertionError.");
+ }
+
+ @Test
+ public void testConstructorEdgeCaseNullDate() {
+ assertThrows(AssertionError.class, () -> new ViewWaterCommand(null), "Creating " +
+ "ViewWaterCommand with a null date should throw an AssertionError.");
+ }
+}
diff --git a/src/test/java/command/water/WaterCommandTest.java b/src/test/java/command/water/WaterCommandTest.java
new file mode 100644
index 0000000000..dfc3142b9d
--- /dev/null
+++ b/src/test/java/command/water/WaterCommandTest.java
@@ -0,0 +1,68 @@
+package command.water;
+
+import command.CommandResult;
+import history.History;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import programme.ProgrammeList;
+
+import java.time.LocalDate;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.verify;
+
+public class WaterCommandTest {
+
+ private WaterCommand waterCommand;
+ private LocalDate date;
+
+ @Mock
+ private History mockHistory;
+
+ @Mock
+ private ProgrammeList mockProgrammeList;
+
+ @BeforeEach
+ public void setUp() {
+ MockitoAnnotations.openMocks(this);
+ date = LocalDate.now();
+ waterCommand = new TestWaterCommand(date); // Using a concrete subclass for testing
+ }
+
+ @Test
+ public void testConstructorHappyPath() {
+ assertEquals(date, waterCommand.date, "Date should be initialized correctly in WaterCommand.");
+ }
+
+ @Test
+ public void testConstructorEdgeCaseNullDate() {
+ assertThrows(AssertionError.class, () -> new TestWaterCommand(null),
+ "Creating WaterCommand with null date should throw an AssertionError.");
+ }
+
+ @Test
+ public void testExecuteWithProgrammeListAndHistoryHappyPath() {
+ CommandResult expected = new CommandResult("Executed with history");
+
+ CommandResult result = waterCommand.execute(mockProgrammeList, mockHistory);
+
+ assertEquals(expected, result, "Execute should return a CommandResult with the correct message.");
+ verify(mockHistory).getRecordByDate(date);
+ }
+
+ // Concrete subclass of WaterCommand for testing purposes
+ private static class TestWaterCommand extends WaterCommand {
+ public TestWaterCommand(LocalDate date) {
+ super(date);
+ }
+
+ @Override
+ public CommandResult execute(History history) {
+ history.getRecordByDate(date);
+ return new CommandResult("Executed with history");
+ }
+ }
+}
diff --git a/src/test/java/history/DailyRecordTest.java b/src/test/java/history/DailyRecordTest.java
new file mode 100644
index 0000000000..e30b25c342
--- /dev/null
+++ b/src/test/java/history/DailyRecordTest.java
@@ -0,0 +1,283 @@
+//@@author Bev-low
+
+package history;
+
+import exceptions.MealException;
+import exceptions.WaterException;
+import meal.Meal;
+import meal.MealList;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import programme.Day;
+import programme.Exercise;
+import water.Water;
+
+import java.util.ArrayList;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.when;
+
+public class DailyRecordTest {
+ private DailyRecord dailyRecord;
+ private Day validDay;
+ private Meal meal1;
+ private Meal meal2;
+
+ @BeforeEach
+ public void setUp() {
+ validDay = new Day("validDay");
+ validDay.insertExercise(new Exercise(3, 12, 50, 120, "Bench_Press"));
+ validDay.insertExercise(new Exercise(3, 12, 80, 200, "Squat"));
+
+ meal1 = new Meal("potato", 100);
+ meal2 = new Meal("pasta", 900);
+ }
+
+ @AfterEach
+ public void tearDown() {
+ dailyRecord = null;
+ validDay = null;
+ meal1 = null;
+ meal2 = null;
+ }
+
+ @Test
+ public void testConstructor_default() {
+ dailyRecord = new DailyRecord();
+ assertNotNull(dailyRecord.getWaterFromRecord());
+ assertNotNull(dailyRecord.getMealListFromRecord());
+ }
+
+ @Test
+ public void logDay_validDay() {
+ dailyRecord = new DailyRecord();
+ dailyRecord.logDayToRecord(validDay);
+
+ assertEquals(validDay, dailyRecord.getDayFromRecord());
+ }
+
+ @Test
+ public void logDay_emptyDay() {
+ dailyRecord = new DailyRecord();
+ Day emptyDay = new Day("Empty Day");
+ dailyRecord.logDayToRecord(emptyDay);
+ assertEquals(emptyDay, dailyRecord.getDayFromRecord());
+ }
+
+ @Test
+ public void addMealToRecord_validMeals() {
+ dailyRecord = new DailyRecord();
+ dailyRecord.addMealToRecord(meal1);
+ dailyRecord.addMealToRecord(meal2);
+ assertFalse(dailyRecord.getMealListFromRecord().isEmpty());
+ assertEquals(2, dailyRecord.getMealListFromRecord().getSize());
+ assertEquals("pasta", dailyRecord.getMealListFromRecord().getMeals().get(1).getName());
+ }
+
+ @Test
+ public void addMealToRecord_negativeCaloriesMeal() {
+ dailyRecord = new DailyRecord();
+ assertThrows(AssertionError.class, () -> dailyRecord.addMealToRecord(new Meal("potato", -100)));
+ }
+
+ @Test
+ public void addMealToRecord_nullMeal() {
+ dailyRecord = new DailyRecord();
+ assertThrows(AssertionError.class, () -> dailyRecord.addMealToRecord(null));
+ }
+
+ @Test
+ public void deleteMealFromRecord_validIndex() {
+ dailyRecord = new DailyRecord();
+ dailyRecord.addMealToRecord(meal1);
+ dailyRecord.addMealToRecord(meal2);
+ dailyRecord.deleteMealFromRecord(0);
+ assertEquals(1, dailyRecord.getMealListFromRecord().getSize());
+ assertEquals("pasta", dailyRecord.getMealListFromRecord().getMeals().get(0).getName());
+ }
+
+ @Test
+ public void deleteMealFromRecord_negativeIndex() {
+ dailyRecord = new DailyRecord();
+ dailyRecord.addMealToRecord(meal1);
+ assertThrows(MealException.class, () -> dailyRecord.deleteMealFromRecord(-1),
+ "Expected MealException for negative index in meal list.");
+ }
+
+ @Test
+ public void deleteMealFromRecord_outOfBoundsIndex() {
+ dailyRecord = new DailyRecord();
+ dailyRecord.addMealToRecord(meal1);
+ assertThrows(MealException.class, () -> dailyRecord.deleteMealFromRecord(10),
+ "Expected MealException for out-of-bounds index in meal list.");
+ }
+
+ @Test
+ public void addWaterToRecord_validWater() {
+ dailyRecord = new DailyRecord();
+ dailyRecord.addWaterToRecord(100.0f);
+ dailyRecord.addWaterToRecord(400.0f);
+ assertFalse(dailyRecord.getWaterFromRecord().isEmpty());
+ assertEquals(2, dailyRecord.getWaterFromRecord().getWaterList().size());
+ assertEquals(100.0f, dailyRecord.getWaterFromRecord().getWaterList().get(0));
+ }
+
+ @Test
+ public void addWaterToRecord_negativeWater() {
+ dailyRecord = new DailyRecord();
+ assertThrows(AssertionError.class, () -> dailyRecord.addWaterToRecord(-500.0f));
+ }
+
+ @Test
+ public void removeWaterFromRecord_validIndex() {
+ dailyRecord = new DailyRecord();
+ dailyRecord.addWaterToRecord(100.0f);
+ dailyRecord.addWaterToRecord(400.0f);
+ dailyRecord.removeWaterFromRecord(0);
+ assertEquals(1, dailyRecord.getWaterFromRecord().getWaterList().size());
+ assertEquals(400.0f, dailyRecord.getWaterFromRecord().getWaterList().get(0));
+ }
+
+ @Test
+ public void removeWaterFromRecord_negativeIndex() {
+ dailyRecord = new DailyRecord();
+ dailyRecord.addWaterToRecord(100.0f);
+ assertThrows(WaterException.class, () -> dailyRecord.removeWaterFromRecord(-1),
+ "Expected WaterExceptions for negative index in water list.");
+ }
+
+ @Test
+ public void removeWaterFromRecord_outOfBoundsIndex() {
+ dailyRecord = new DailyRecord();
+ dailyRecord.addWaterToRecord(100.0f);
+ assertThrows(WaterException.class, () -> dailyRecord.removeWaterFromRecord(10),
+ "Expected WaterExceptions for out-of-bounds index in water list.");
+ }
+
+ @Test
+ public void getDayFromRecord_initialDay() {
+ dailyRecord = new DailyRecord();
+ assertNull(dailyRecord.getDayFromRecord());
+ }
+
+ @Test
+ public void getDayFromRecord_afterLogDay() {
+ dailyRecord = new DailyRecord();
+ dailyRecord.logDayToRecord(validDay);
+ assertEquals("validDay", dailyRecord.getDayFromRecord().getName());
+ }
+
+ @Test
+ public void getMealList_initialMealList() {
+ dailyRecord = new DailyRecord();
+ MealList mealList = dailyRecord.getMealListFromRecord();
+ assertNotNull(mealList);
+ assertTrue(mealList.getMeals().isEmpty());
+ }
+
+ @Test
+ public void getMealList_afterAddMeal() {
+ dailyRecord = new DailyRecord();
+ dailyRecord.addMealToRecord(meal1);
+ dailyRecord.addMealToRecord(meal2);
+ MealList mealList = dailyRecord.getMealListFromRecord();
+ assertEquals("potato", mealList.getMeals().get(0).getName());
+ assertEquals("pasta", mealList.getMeals().get(1).getName());
+ }
+
+ @Test
+ public void getMealList_afterDeleteMeal() {
+ dailyRecord = new DailyRecord();
+ dailyRecord.addMealToRecord(meal1);
+ dailyRecord.addMealToRecord(meal2);
+ dailyRecord.deleteMealFromRecord(0);
+ assertEquals(1, dailyRecord.getMealListFromRecord().getSize());
+ }
+
+ @Test
+ public void getWater_initialWater() {
+ dailyRecord = new DailyRecord();
+ Water water = dailyRecord.getWaterFromRecord();
+ assertNotNull(water);
+ assertTrue(water.getWaterList().isEmpty());
+ }
+
+ @Test
+ public void getWater_afterAddWater() {
+ dailyRecord = new DailyRecord();
+ dailyRecord.addWaterToRecord(100.0f);
+ dailyRecord.addWaterToRecord(400.0f);
+ Water water = dailyRecord.getWaterFromRecord();
+ assertEquals(100.0f, water.getWaterList().get(0));
+ assertEquals(400.0f, water.getWaterList().get(1));
+ }
+
+ @Test
+ public void getWater_afterRemoveWater() {
+ dailyRecord = new DailyRecord();
+ dailyRecord.addWaterToRecord(100.0f);
+ dailyRecord.addWaterToRecord(400.0f);
+ dailyRecord.removeWaterFromRecord(0);
+ assertEquals(1, dailyRecord.getWaterFromRecord().getWaterList().size());
+ }
+
+ @Test
+ public void toString_emptyRecord() {
+ MealList mockMealList = mock(MealList.class);
+ Water mockWater = mock(Water.class);
+ Day mockDay = mock(Day.class);
+
+ dailyRecord = spy(new DailyRecord());
+
+ when(mockMealList.getMeals()).thenReturn(new ArrayList());
+ when(mockWater.getWaterList()).thenReturn(new ArrayList());
+ when(mockDay.getExercisesCount()).thenReturn(0);
+
+ doReturn(mockMealList).when(dailyRecord).getMealListFromRecord();
+ doReturn(mockDay).when(dailyRecord).getDayFromRecord();
+ doReturn(mockWater).when(dailyRecord).getWaterFromRecord();
+
+ String result = dailyRecord.toString();
+
+ assertTrue(result.contains("No Day"));
+ assertTrue(result.contains("No Water"));
+ assertTrue(result.contains("No Meals"));
+ }
+
+ @Test
+ public void toString_callsGetCaloriesFromMeal() {
+ dailyRecord = new DailyRecord();
+ dailyRecord.addMealToRecord(meal1);
+ dailyRecord.addMealToRecord(meal2);
+ String result = dailyRecord.toString();
+ assertTrue(result.contains("Total Calories from Meals:"));
+ }
+
+ @Test
+ public void toString_callsGetTotalWater() {
+ dailyRecord = new DailyRecord();
+ dailyRecord.addWaterToRecord(100.0f);
+ dailyRecord.addWaterToRecord(400.0f);
+ String result = dailyRecord.toString();
+ assertTrue(result.contains("Total Water Intake:"));
+ }
+
+ @Test
+ public void toString_testDayToString() {
+ dailyRecord = new DailyRecord();
+ dailyRecord.logDayToRecord(validDay);
+ String result = dailyRecord.toString();
+ assertFalse(result.contains("No Day"));
+ }
+}
+
diff --git a/src/test/java/history/HistoryTest.java b/src/test/java/history/HistoryTest.java
new file mode 100644
index 0000000000..a012586000
--- /dev/null
+++ b/src/test/java/history/HistoryTest.java
@@ -0,0 +1,108 @@
+package history;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import programme.Day;
+import programme.Exercise;
+
+import java.time.LocalDate;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class HistoryTest {
+
+ private History history;
+ private Day sampleDay;
+
+ @BeforeEach
+ public void setUp() {
+ history = new History();
+ sampleDay = new Day("Leg Day");
+ Exercise sampleExercise = new Exercise(3, 10, 100, 200, "Squat");
+ sampleDay.insertExercise(sampleExercise);
+ }
+
+ @Test
+ public void testLogRecordAndGetRecordByDate() {
+ LocalDate date = LocalDate.now();
+ DailyRecord dailyRecord = new DailyRecord();
+ dailyRecord.logDayToRecord(sampleDay);
+
+ history.logRecord(date, dailyRecord);
+
+ DailyRecord retrievedRecord = history.getRecordByDate(date);
+ assertNotNull(retrievedRecord, "Retrieved record should not be null.");
+ assertEquals(dailyRecord, retrievedRecord, "Retrieved record should match the logged record.");
+ }
+
+ @Test
+ public void testGetWeeklyWorkoutSummaryWithData() {
+ LocalDate date = LocalDate.now();
+ DailyRecord dailyRecord = new DailyRecord();
+ dailyRecord.logDayToRecord(sampleDay);
+
+ history.logRecord(date, dailyRecord);
+
+ String weeklySummary = history.getWeeklyWorkoutSummary();
+ assertTrue(weeklySummary.contains("Leg Day"), "Weekly summary should contain the day's name.");
+ assertTrue(weeklySummary.contains("Squat"), "Weekly summary should contain the exercise name.");
+ }
+
+ @Test
+ public void testGetWeeklyWorkoutSummaryWithoutData() {
+ String weeklySummary = history.getWeeklyWorkoutSummary();
+ assertEquals("No workout history available.", weeklySummary,
+ "Weekly summary should indicate no data available.");
+ }
+
+ @Test
+ public void testGetPersonalBestForExercise() {
+ LocalDate date = LocalDate.now();
+ DailyRecord dailyRecord = new DailyRecord();
+ dailyRecord.logDayToRecord(sampleDay);
+
+ history.logRecord(date, dailyRecord);
+
+ String personalBest = history.getPersonalBestForExercise("Squat");
+ assertTrue(personalBest.contains("Personal best for Squat"),
+ "Personal best output should contain exercise name.");
+ assertTrue(personalBest.contains("100kg"), "Personal best output should contain correct weight.");
+ }
+
+ @Test
+ public void testGetFormattedPersonalBests() {
+ LocalDate date = LocalDate.now();
+ DailyRecord dailyRecord = new DailyRecord();
+ dailyRecord.logDayToRecord(sampleDay);
+
+ history.logRecord(date, dailyRecord);
+
+ String formattedPersonalBests = history.getFormattedPersonalBests();
+ assertTrue(formattedPersonalBests.contains("Personal bests for all exercises:"),
+ "Output should contain header.");
+ assertTrue(formattedPersonalBests.contains("Squat: 3 sets of 10 at 100kg"),
+ "Output should contain exercise details.");
+ }
+
+ @Test
+ public void testToStringNoHistory() {
+ String historyString = history.toString();
+ assertEquals("No history available.", historyString, "Output should indicate no history available.");
+ }
+
+ @Test
+ public void testToStringWithHistory() {
+ LocalDate date = LocalDate.now();
+ DailyRecord dailyRecord = new DailyRecord();
+ dailyRecord.logDayToRecord(sampleDay);
+
+ history.logRecord(date, dailyRecord);
+
+ String historyString = history.toString();
+ assertTrue(historyString.contains("Completed On:"), "Output should contain 'Completed On' date.");
+ assertTrue(historyString.contains("Leg Day"), "Output should contain the day's name.");
+ }
+}
+
diff --git a/src/test/java/meal/MealListTest.java b/src/test/java/meal/MealListTest.java
new file mode 100644
index 0000000000..74419a5b8b
--- /dev/null
+++ b/src/test/java/meal/MealListTest.java
@@ -0,0 +1,129 @@
+package meal;
+
+import exceptions.MealException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import java.util.ArrayList;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class MealListTest {
+
+ private MealList mealList;
+ private Meal sampleMeal;
+
+ @BeforeEach
+ public void setUp() {
+ mealList = new MealList();
+ sampleMeal = new Meal("Sample Meal", 300);
+ }
+
+ @Test
+ public void testIsEmptyHappyPath() {
+ assertTrue(mealList.isEmpty(), "Meal list " +
+ "should be empty initially.");
+ }
+
+ @Test
+ public void testIsEmptyEdgeCaseNonEmptyList() {
+ mealList.addMeal(sampleMeal);
+ assertFalse(mealList.isEmpty(), "Meal list should not be empty after adding a meal.");
+ }
+
+ @Test
+ public void testIsEmptyEdgeCaseEmptyAfterDeletion() {
+ mealList.addMeal(sampleMeal);
+ mealList.deleteMeal(0);
+ assertTrue(mealList.isEmpty(), "Meal list " +
+ "should be empty after adding and then deleting the only meal.");
+ }
+
+ @Test
+ public void testGetSizeHappyPath() {
+ mealList.addMeal(sampleMeal);
+ assertEquals(1, mealList.getSize(), "Size should be 1 after adding one meal.");
+ }
+
+ @Test
+ public void testGetSizeEdgeCaseEmptyList() {
+ assertEquals(0, mealList.getSize(), "Size should be 0 for an empty meal list.");
+ }
+
+ @Test
+ public void testGetSizeEdgeCaseMultipleMeals() {
+ mealList.addMeal(sampleMeal);
+ mealList.addMeal(new Meal("Another Meal", 500));
+ assertEquals(2, mealList.getSize(), "Size should be 2 after adding two meals.");
+ }
+
+ @Test
+ public void testAddMealHappyPath() {
+ mealList.addMeal(sampleMeal);
+ assertEquals(1, mealList.getSize(), "Size should be 1 after adding one meal.");
+ assertTrue(mealList.getMeals().contains(sampleMeal), "Meal list should " +
+ "contain the added meal.");
+ }
+
+ @Test
+ public void testAddMealEdgeCaseNullMeal() {
+ assertThrows(AssertionError.class, () -> mealList.addMeal(null), "Adding a null " +
+ "meal should throw an AssertionError.");
+ }
+
+ @Test
+ public void testAddMealEdgeCaseDuplicateMeals() {
+ mealList.addMeal(sampleMeal);
+ mealList.addMeal(sampleMeal);
+ assertEquals(2, mealList.getSize(), "Size should " +
+ "be 2 after adding the same meal twice.");
+ }
+
+ @Test
+ public void testDeleteMealHappyPath() {
+ mealList.addMeal(sampleMeal);
+ Meal deletedMeal = mealList.deleteMeal(0);
+ assertEquals(deletedMeal, sampleMeal, "Deleted meal should be equal to the meal added.");
+ assertTrue(mealList.isEmpty(), "Meal list should be empty after deleting the only meal.");
+ }
+
+ @Test
+ public void testDeleteMealEdgeCaseNegativeIndex() {
+ assertThrows(MealException.class, () -> mealList.deleteMeal(-1),
+ "Deleting with a negative index should throw IndexOutOfBoundsBuffBuddyException.");
+ }
+
+ @Test
+ public void testDeleteMealEdgeCaseIndexOutOfBounds() {
+ mealList.addMeal(sampleMeal);
+ assertThrows(MealException.class, () -> mealList.deleteMeal(1),
+ "Deleting with an out-of-bounds index should throw IndexOutOfBoundsBuffBuddyException.");
+ }
+
+ @Test
+ public void testGetMealsHappyPath() {
+ mealList.addMeal(sampleMeal);
+ ArrayList meals = mealList.getMeals();
+ assertEquals(1, meals.size(), "Meal list should have one meal after adding one.");
+ assertEquals(meals.get(0), sampleMeal, "The meal retrieved should be equal to the added meal.");
+ }
+
+ @Test
+ public void testGetMealsEdgeCaseEmptyList() {
+ ArrayList meals = mealList.getMeals();
+ assertTrue(meals.isEmpty(), "Retrieved list should be empty for a new MealList instance.");
+ }
+
+ @Test
+ public void testGetMealsEdgeCaseMultipleMeals() {
+ Meal anotherMeal = new Meal("Another Meal", 500);
+ mealList.addMeal(sampleMeal);
+ mealList.addMeal(anotherMeal);
+ ArrayList meals = mealList.getMeals();
+ assertEquals(2, meals.size(), "Meal list should contain two meals after adding two.");
+ assertTrue(meals.get(0).equals(sampleMeal) && meals.get(1).equals(anotherMeal),
+ "Meals should be equal to those added.");
+ }
+}
diff --git a/src/test/java/meal/MealTest.java b/src/test/java/meal/MealTest.java
new file mode 100644
index 0000000000..ff0411101f
--- /dev/null
+++ b/src/test/java/meal/MealTest.java
@@ -0,0 +1,101 @@
+package meal;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+
+public class MealTest {
+
+ @Test
+ public void testConstructorHappyPath() {
+ Meal meal = new Meal("Sample Meal", 300);
+ assertEquals("Sample Meal", meal.getName(), "Meal name should match the expected value.");
+ assertEquals(300, meal.getCalories(), "Meal calories should match the expected value.");
+ }
+
+ @Test
+ public void testConstructorEdgeCaseNullName() {
+ assertThrows(AssertionError.class, () -> new Meal(null, 300), "Creating " +
+ "a Meal with a null name should throw an AssertionError.");
+ }
+
+ @Test
+ public void testConstructorEdgeCaseEmptyName() {
+ assertThrows(AssertionError.class, () -> new Meal("", 300), "Creating " +
+ "a Meal with an empty name should throw an AssertionError.");
+ }
+
+ @Test
+ public void testConstructorEdgeCaseNegativeCalories() {
+ assertThrows(AssertionError.class, () -> new Meal("Negative Calories Meal", -100),
+ "Create a Meal with negative calories should throw an AssertionError.");
+ }
+
+ @Test
+ public void testGetCaloriesHappyPath() {
+ Meal meal = new Meal("Sample Meal", 250);
+ assertEquals(250, meal.getCalories(), "getCalories should " +
+ "return the correct calorie count.");
+ }
+
+ @Test
+ public void testGetNameHappyPath() {
+ Meal meal = new Meal("Healthy Salad", 150);
+ assertEquals("Healthy Salad", meal.getName(), "getName " +
+ "should return the correct meal name.");
+ }
+
+ @Test
+ public void testEqualsHappyPath() {
+ Meal meal1 = new Meal("Same Meal", 400);
+ Meal meal2 = new Meal("Same Meal", 400);
+ assertEquals(meal1, meal2, "Meals with the same name and calories should be equal.");
+ }
+
+ @Test
+ public void testEqualsEdgeCaseDifferentCalories() {
+ Meal meal1 = new Meal("Same Meal", 400);
+ Meal meal2 = new Meal("Same Meal", 300);
+ assertNotEquals(meal1, meal2, "Meals with different calories should not be equal.");
+ }
+
+ @Test
+ public void testEqualsEdgeCaseDifferentName() {
+ Meal meal1 = new Meal("Meal One", 400);
+ Meal meal2 = new Meal("Meal Two", 400);
+ assertNotEquals(meal1, meal2, "Meals with different names should not be equal.");
+ }
+
+ @Test
+ public void testHashCodeHappyPath() {
+ Meal meal1 = new Meal("Hash Meal", 500);
+ Meal meal2 = new Meal("Hash Meal", 500);
+ assertEquals(meal1.hashCode(), meal2.hashCode(), "Meals with the " +
+ "same name and calories should have the same hash code.");
+ }
+
+ @Test
+ public void testHashCodeEdgeCaseDifferentCalories() {
+ Meal meal1 = new Meal("Hash Meal", 500);
+ Meal meal2 = new Meal("Hash Meal", 300);
+ assertNotEquals(meal1.hashCode(), meal2.hashCode(), "Meals with " +
+ "different calories should have different hash codes.");
+ }
+
+ @Test
+ public void testHashCodeEdgeCaseDifferentName() {
+ Meal meal1 = new Meal("Hash Meal One", 500);
+ Meal meal2 = new Meal("Hash Meal Two", 500);
+ assertNotEquals(meal1.hashCode(), meal2.hashCode(), "Meals with " +
+ "different names should have different hash codes.");
+ }
+
+ @Test
+ public void testToStringHappyPath() {
+ Meal meal = new Meal("Sample Meal", 350);
+ assertEquals("Sample Meal | 350kcal", meal.toString(), "toString should " +
+ "return the correct formatted string.");
+ }
+}
diff --git a/src/test/java/parser/FlagParserTest.java b/src/test/java/parser/FlagParserTest.java
new file mode 100644
index 0000000000..ec5ce859cf
--- /dev/null
+++ b/src/test/java/parser/FlagParserTest.java
@@ -0,0 +1,137 @@
+// @@author nirala-ts
+
+package parser;
+
+import exceptions.FlagException;
+import exceptions.ParserException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDate;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static parser.FlagDefinitions.MEAL_INDEX;
+import static parser.FlagDefinitions.WATER_INDEX;
+
+class FlagParserTest {
+
+ private FlagParser flagParser;
+
+ @BeforeEach
+ void setUp() {
+ String argumentString = "/p 1 /d Day1 /date 12-12-2023 /w 2.5 /n TestExercise /s 3 /r 10";
+ flagParser = new FlagParser(argumentString);
+ }
+
+ @Test
+ void testHasFlagValidCase() {
+ assertTrue(flagParser.hasFlag("/p"));
+ assertTrue(flagParser.hasFlag("/t"));
+ }
+
+ @Test
+ void testHasFlagMissingFlag() {
+ assertFalse(flagParser.hasFlag("/m"));
+ }
+
+ @Test
+ void testHasFlagEmptyFlag() {
+ assertThrows(AssertionError.class, () -> flagParser.hasFlag(""),
+ "Expected AssertionError for empty flag");
+ }
+
+ @Test
+ void testValidateRequiredFlagsValidCase() {
+ assertDoesNotThrow(() -> flagParser.validateRequiredFlags("/p", "/d", "/t"),
+ "Expected no exception for valid flags");
+ }
+
+ @Test
+ void testValidateRequiredFlagsMissingFlag() {
+ FlagException exception = assertThrows(FlagException.class,
+ () -> flagParser.validateRequiredFlags("/p", MEAL_INDEX),
+ "Expected MissingFlagBuffBuddyException for missing required flag");
+ assertTrue(exception.getMessage().contains(MEAL_INDEX));
+ }
+
+ @Test
+ void testGetStringByFlagValidCase() {
+ assertEquals("Day1", flagParser.getStringByFlag("/d"),
+ "Expected value 'Day1' for flag '/d'");
+ }
+
+ @Test
+ void testGetStringByFlagFlagNotPresent() {
+ assertNull(flagParser.getStringByFlag("/x"),
+ "Expected null for non-existent flag '/x'");
+ }
+
+ @Test
+ void testGetStringByFlagEmptyFlag() {
+ assertThrows(AssertionError.class, () -> flagParser.getStringByFlag(""),
+ "Expected AssertionError for empty flag");
+ }
+
+ @Test
+ void testGetIndexByFlagValidCase() {
+ assertEquals(0, flagParser.getIndexByFlag("/p"),
+ "Expected zero-based index '0' for flag '/p' with value '0'");
+ }
+
+ @Test
+ void testGetIndexByFlagInvalidIndex() {
+ FlagParser invalidParser = new FlagParser("/p abc");
+ assertThrows(ParserException.class, () -> invalidParser.getIndexByFlag("/p"),
+ "Expected InvalidFormatBuffBuddyException for invalid index");
+ }
+
+ @Test
+ void testGetIntegerByFlagValidCase() {
+ assertEquals(3, flagParser.getIntegerByFlag("/s"),
+ "Expected integer value '3' for flag '/s'");
+ }
+
+ @Test
+ void testGetIntegerByFlagInvalidInteger() {
+ FlagParser invalidParser = new FlagParser("/s abc");
+ assertThrows(ParserException.class, () -> invalidParser.getIntegerByFlag("/s"),
+ "Expected InvalidFormatBuffBuddyException for invalid integer");
+ }
+
+ @Test
+ void testGetFloatByFlagValidCase() {
+ assertEquals(2.5f, flagParser.getFloatByFlag("/w"),
+ "Expected float value '2.5' for flag '/w'");
+ }
+
+ @Test
+ void testGetFloatByFlagInvalidFloat() {
+ FlagParser invalidParser = new FlagParser("/w abc");
+ assertThrows(ParserException.class, () -> invalidParser.getFloatByFlag(WATER_INDEX),
+ "Expected InvalidFormatBuffBuddyException for invalid float");
+ }
+
+ @Test
+ void testGetDateByFlagValidCase() {
+ assertEquals(LocalDate.of(2023, 12, 12), flagParser.getDateByFlag("/t"),
+ "Expected date '12-12-2023' for flag '/t'");
+ }
+
+ @Test
+ void testGetDateByFlagInvalidDate() {
+ FlagParser invalidParser = new FlagParser("/t 32-12-2023");
+ assertThrows(ParserException.class, () -> invalidParser.getDateByFlag("/t"),
+ "Expected InvalidFormatBuffBuddyException for invalid date");
+ }
+
+ @Test
+ void testParseNullArgumentString() {
+ assertThrows(FlagException.class, () -> new FlagParser(null),
+ "Expected EmptyInputBuffBuddyException for null argument string");
+ }
+}
diff --git a/src/test/java/parser/ParserTest.java b/src/test/java/parser/ParserTest.java
new file mode 100644
index 0000000000..c3cdeaef94
--- /dev/null
+++ b/src/test/java/parser/ParserTest.java
@@ -0,0 +1,81 @@
+// @@author nirala-ts
+
+package parser;
+
+import command.Command;
+import command.InvalidCommand;
+import command.ExitCommand;
+import exceptions.ParserException;
+import parser.command.factory.CommandFactory;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.verify;
+
+class ParserTest {
+
+ private Parser parser;
+ private CommandFactory commandFactoryMock;
+
+ @BeforeEach
+ void setUp() {
+ commandFactoryMock = mock(CommandFactory.class);
+ parser = new Parser(commandFactoryMock);
+ }
+
+ @Test
+ void testParseValidCommand() {
+ String fullCommand = "prog start /p 1";
+ Command expectedCommand = mock(Command.class);
+ when(commandFactoryMock.createCommand("prog", "start /p 1")).thenReturn(expectedCommand);
+
+ Command result = parser.parse(fullCommand);
+
+ assertNotNull(result);
+ assertEquals(expectedCommand, result);
+ verify(commandFactoryMock).createCommand("prog", "start /p 1");
+ }
+
+ @Test
+ void testParseExitCommand() {
+ String fullCommand = "bye";
+ when(commandFactoryMock.createCommand("bye", "")).thenReturn(new ExitCommand());
+ Command result = parser.parse(fullCommand);
+
+ assertInstanceOf(ExitCommand.class, result);
+ }
+
+ @Test
+ void testParseInvalidCommand() {
+ String fullCommand = "unknownCommand";
+ when(commandFactoryMock.createCommand("unknownCommand", "")).
+ thenReturn(new InvalidCommand());
+ Command result = parser.parse(fullCommand);
+
+ assertInstanceOf(InvalidCommand.class, result);
+ }
+
+ @Test
+ void testParseEmptyCommand() {
+ assertThrows(ParserException.class, () -> parser.parse(""),
+ "Should throw EmptyInputBuffBuddyException on empty command");
+ }
+
+ @Test
+ void testParseOnlySpacesCommand() {
+ assertThrows(ParserException.class, () -> parser.parse(" "),
+ "Should throw EmptyInputBuffBuddyException on command with only spaces");
+ }
+
+ @Test
+ void testParseNullCommand() {
+ assertThrows(ParserException.class, () -> parser.parse(null),
+ "Should throw EmptyInputBuffBuddyException on null command");
+ }
+}
diff --git a/src/test/java/parser/ParserUtilsTest.java b/src/test/java/parser/ParserUtilsTest.java
new file mode 100644
index 0000000000..793daeb9f9
--- /dev/null
+++ b/src/test/java/parser/ParserUtilsTest.java
@@ -0,0 +1,201 @@
+// @@author nirala-ts
+
+package parser;
+
+import exceptions.ParserException;
+import org.junit.jupiter.api.Test;
+import java.time.LocalDate;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static common.Utils.NULL_INTEGER;
+import static common.Utils.NULL_FLOAT;
+
+class ParserUtilsTest {
+
+ // Tests for splitArguments
+ @Test
+ void testSplitArgumentsValidInput() {
+ String input = "command arg1 arg2";
+ String[] result = ParserUtils.splitArguments(input);
+ assertArrayEquals(new String[]{"command", "arg1 arg2"}, result,
+ "Should split command and arguments correctly.");
+ }
+
+ @Test
+ void testSplitArgumentsEmptyArgumentString() {
+ String input = "command";
+ String[] result = ParserUtils.splitArguments(input);
+ assertArrayEquals(new String[]{"command", ""}, result, "Should handle missing arguments.");
+ }
+
+ // Tests for trimInput
+ @Test
+ void testTrimInputValidInput() {
+ String input = " trim this ";
+ String result = ParserUtils.trimInput(input);
+ assertEquals("trim this", result, "Should trim leading and trailing whitespace.");
+ }
+
+ @Test
+ void testTrimInputEmptyString() {
+ assertThrows(ParserException.class, () -> ParserUtils.trimInput(" "),
+ "Should throw PaserExceptions on empty input.");
+ }
+
+ // Tests for parseInteger
+ @Test
+ void testParseIntegerValidInteger() {
+ String input = "42";
+ int result = ParserUtils.parseInteger(input);
+ assertEquals(42, result, "Should parse valid integer correctly.");
+ }
+
+ @Test
+ void testParseIntegerLargeNumberEdgeCase() {
+ String input = "2147483647";
+ int result = ParserUtils.parseInteger(input); // Integer.MAX_VALUE
+ assertEquals(2147483647, result, "Should parse valid integer correctly.");
+ }
+
+ @Test
+ void testParseIntegerNegativeEdgeCase() {
+ String input = "-1";
+ assertThrows(ParserException.class, () -> ParserUtils.parseInteger(input));
+ }
+
+ @Test
+ void testParseIntegerNullInputInvalid() {
+ int result = ParserUtils.parseInteger(null);
+ assertEquals(NULL_INTEGER, result, "Should return default NULL_INTEGER when input is null.");
+ }
+
+ @Test
+ void testParseIntegerNonNumericInputInvalid() {
+ assertThrows(ParserException.class, () -> ParserUtils.parseInteger("abc"),
+ "Should throw PaserExceptions on invalid integer.");
+ }
+
+ // Tests for parseFloat
+ @Test
+ void testParseFloatValidFloat() {
+ String input = "3.14";
+ float result = ParserUtils.parseFloat(input);
+ assertEquals(3.14f, result, 0.001, "Should parse valid float correctly.");
+ }
+
+ @Test
+ void testParseFloatLargeFloatEdgeCase() {
+ String input = "3.4028235E38";
+ float result = ParserUtils.parseFloat(input); // Float.MAX_VALUE
+ assertEquals(Float.MAX_VALUE, result, 0.001, "Should parse valid float correctly.");
+ }
+
+ @Test
+ void testParseFloatSmallNegativeEdgeCase() {
+ String input = "-0.0001";
+ assertThrows(ParserException.class, () -> ParserUtils.parseFloat(input));
+ }
+
+ @Test
+ void testParseFloatNullInputInvalid() {
+ float result = ParserUtils.parseFloat(null);
+ assertEquals(NULL_FLOAT, result, "Should return default NULL_FLOAT when input is null.");
+ }
+
+ @Test
+ void testParseFloatNonNumericInputInvalid() {
+ assertThrows(ParserException.class, () -> ParserUtils.parseFloat("abc"),
+ "Should throw PaserExceptions on invalid float.");
+ }
+
+ // Tests for parseIndex
+ @Test
+ void testParseIndexValidIndex() {
+ String input = "3";
+ int result = ParserUtils.parseIndex(input);
+ assertEquals(2, result, "Should parse and convert valid index to zero-based.");
+ }
+
+ @Test
+ void testParseIndexLargeIndexEdgeCase() {
+ String input = "2147483647"; // Integer.MAX_VALUE
+ int result = ParserUtils.parseIndex(input);
+ assertEquals(2147483646, result, "Should handle large index correctly and convert to zero-based.");
+ }
+
+ @Test
+ void testParseIndexZeroIndexEdgeCase() {
+ String input = "1";
+ int result = ParserUtils.parseIndex(input);
+ assertEquals(0, result, "Should handle zero-based conversion for index 1.");
+ }
+
+ @Test
+ void testParseIndexNegativeIndexInvalid() {
+ assertThrows(ParserException.class, () -> ParserUtils.parseIndex("-1"),
+ "Should throw PaserExceptions on negative index.");
+ }
+
+ @Test
+ void testParseIndexNullInputInvalid() {
+ int result = ParserUtils.parseIndex(null);
+ assertEquals(NULL_INTEGER, result, "Should return default NULL_INTEGER when input is null.");
+ }
+
+ @Test
+ void testParseIndexNonNumericInputInvalid() {
+ assertThrows(ParserException.class, () -> ParserUtils.parseIndex("abc"),
+ "Should throw PaserExceptions on non-numeric index.");
+ }
+
+ // Tests for parseDate
+ @Test
+ void testParseDateValidDate() {
+ String dateString = "15-08-2023"; // assuming DATE_FORMAT = "dd-MM-yyyy"
+ LocalDate expectedDate = LocalDate.of(2023, 8, 15);
+ LocalDate actualDate = ParserUtils.parseDate(dateString);
+
+ assertEquals(expectedDate, actualDate, "The parsed date should match the expected date.");
+ }
+
+ @Test
+ void testParseDateCurrentDateWhenNullInput() {
+ LocalDate actualDate = ParserUtils.parseDate(null);
+ LocalDate expectedDate = LocalDate.now();
+
+ assertEquals(expectedDate, actualDate, "When the input is null, the parsed date should be today's date.");
+ }
+
+ @Test
+ void testParseDateTrimmedInput() {
+ String dateString = " 15-08-2023 "; // Input with extra spaces
+ LocalDate expectedDate = LocalDate.of(2023, 8, 15);
+ LocalDate actualDate = ParserUtils.parseDate(dateString);
+
+ assertEquals(expectedDate, actualDate,
+ "The parsed date should match the expected date, ignoring extra spaces.");
+ }
+
+ @Test
+ void testParseDateValidLeapYearDate() {
+ String dateString = "29-02-2024"; // Leap year date
+ LocalDate expectedDate = LocalDate.of(2024, 2, 29);
+ LocalDate actualDate = ParserUtils.parseDate(dateString);
+
+ assertEquals(expectedDate, actualDate, "The parsed date should match the expected leap year date.");
+ }
+
+ @Test
+ void testParseDateInvalidDayInMonth() {
+ assertThrows(ParserException.class, () -> ParserUtils.parseDate("31-02-2023"),
+ "Should throw exception on invalid date (February 31st).");
+ }
+
+ @Test
+ void testParseDateInvalidMonth() {
+ assertThrows(ParserException.class, () -> ParserUtils.parseDate("31-13-2023"),
+ "Should throw exception on invalid month (13).");
+ }
+}
+
diff --git a/src/test/java/parser/command/factory/CommandFactoryTest.java b/src/test/java/parser/command/factory/CommandFactoryTest.java
new file mode 100644
index 0000000000..eeb41d0a90
--- /dev/null
+++ b/src/test/java/parser/command/factory/CommandFactoryTest.java
@@ -0,0 +1,110 @@
+// @@author nirala-ts
+
+package parser.command.factory;
+
+import command.Command;
+import command.ExitCommand;
+import command.InvalidCommand;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.verify;
+
+class CommandFactoryTest {
+
+ private CommandFactory commandFactory;
+ private ProgrammeCommandFactory progFactoryMock;
+ private MealCommandFactory mealFactoryMock;
+ private WaterCommandFactory waterFactoryMock;
+ private HistoryCommandFactory historyFactoryMock;
+
+ @BeforeEach
+ void setUp() {
+ progFactoryMock = mock(ProgrammeCommandFactory.class);
+ mealFactoryMock = mock(MealCommandFactory.class);
+ waterFactoryMock = mock(WaterCommandFactory.class);
+ historyFactoryMock = mock(HistoryCommandFactory.class);
+
+ commandFactory = new CommandFactory(progFactoryMock, mealFactoryMock, waterFactoryMock, historyFactoryMock);
+ }
+
+ @Test
+ void testCreateExitCommand() {
+ String commandString = "bye";
+ String argumentString = "";
+
+ Command result = commandFactory.createCommand(commandString, argumentString);
+
+ assertInstanceOf(ExitCommand.class, result);
+ }
+
+ @Test
+ void testCreateInvalidCommand() {
+ String commandString = "unknownCommand";
+ String argumentString = "";
+
+ Command result = commandFactory.createCommand(commandString, argumentString);
+
+ assertInstanceOf(InvalidCommand.class, result);
+ }
+
+ @Test
+ void testCreateProgCommand() {
+ String commandString = "prog";
+ String argumentString = "start /p 1";
+ Command expectedCommand = mock(Command.class);
+
+ when(progFactoryMock.parse(argumentString)).thenReturn(expectedCommand);
+
+ Command result = commandFactory.createCommand(commandString, argumentString);
+
+ assertEquals(expectedCommand, result);
+ verify(progFactoryMock).parse(argumentString);
+ }
+
+ @Test
+ void testCreateMealCommand() {
+ String commandString = "meal";
+ String argumentString = "add /name Sample Meal";
+ Command expectedCommand = mock(Command.class);
+
+ when(mealFactoryMock.parse(argumentString)).thenReturn(expectedCommand);
+
+ Command result = commandFactory.createCommand(commandString, argumentString);
+
+ assertEquals(expectedCommand, result);
+ verify(mealFactoryMock).parse(argumentString);
+ }
+
+ @Test
+ void testCreateWaterCommand() {
+ String commandString = "water";
+ String argumentString = "log /volume 500";
+ Command expectedCommand = mock(Command.class);
+
+ when(waterFactoryMock.parse(argumentString)).thenReturn(expectedCommand);
+
+ Command result = commandFactory.createCommand(commandString, argumentString);
+
+ assertEquals(expectedCommand, result);
+ verify(waterFactoryMock).parse(argumentString);
+ }
+
+ @Test
+ void testCreateHistoryCommand() {
+ String commandString = "history";
+ String argumentString = "view /d 11-11-2023";
+ Command expectedCommand = mock(Command.class);
+
+ when(historyFactoryMock.parse(argumentString)).thenReturn(expectedCommand);
+
+ Command result = commandFactory.createCommand(commandString, argumentString);
+
+ assertEquals(expectedCommand, result);
+ verify(historyFactoryMock).parse(argumentString);
+ }
+}
diff --git a/src/test/java/parser/command/factory/HistoryCommandFactoryTest.java b/src/test/java/parser/command/factory/HistoryCommandFactoryTest.java
new file mode 100644
index 0000000000..3a4ee1dff2
--- /dev/null
+++ b/src/test/java/parser/command/factory/HistoryCommandFactoryTest.java
@@ -0,0 +1,61 @@
+package parser.command.factory;
+
+import command.Command;
+import command.InvalidCommand;
+import command.history.HistoryCommand;
+import command.history.ViewPersonalBestCommand;
+import command.history.ListPersonalBestsCommand;
+import command.history.WeeklySummaryCommand;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+
+public class HistoryCommandFactoryTest {
+
+ private HistoryCommandFactory historyCommandFactory;
+
+ @BeforeEach
+ public void setUp() {
+ historyCommandFactory = new HistoryCommandFactory();
+ }
+
+ @Test
+ public void testParseHistoryCommand() {
+ // Test that "history" returns a HistoryCommand instance
+ Command command = historyCommandFactory.parse("view");
+ assertInstanceOf(HistoryCommand.class, command, "Expected HistoryCommand instance for 'history view'.");
+ }
+
+ @Test
+ public void testParseListPersonalBestsCommand() {
+ // Test that "history pb" with no additional arguments returns a ListPersonalBestsCommand instance
+ Command command = historyCommandFactory.parse("pb");
+ assertInstanceOf(ListPersonalBestsCommand.class, command,
+ "Expected ListPersonalBestsCommand instance for 'history pb'.");
+ }
+
+ @Test
+ public void testParseViewPersonalBestCommand() {
+ // Test that "history pb [exercise]" returns a ViewPersonalBestCommand instance
+ Command command = historyCommandFactory.parse("pb Bench_Press");
+ assertInstanceOf(ViewPersonalBestCommand.class, command,
+ "Expected ViewPersonalBestCommand instance for 'history pb Bench_Press'.");
+ }
+
+ @Test
+ public void testParseWeeklySummaryCommand() {
+ // Test that "history wk" returns a WeeklySummaryCommand instance
+ Command command = historyCommandFactory.parse("wk");
+ assertInstanceOf(WeeklySummaryCommand.class, command,
+ "Expected WeeklySummaryCommand instance for 'history wk'.");
+ }
+
+ @Test
+ public void testParseInvalidCommand() {
+ // Test that an invalid subcommand returns an InvalidCommand instance
+ Command command = historyCommandFactory.parse("unknown");
+ assertInstanceOf(InvalidCommand.class, command, "Expected InvalidCommand instance for unknown subcommand.");
+ }
+}
+
diff --git a/src/test/java/parser/command/factory/MealCommandFactoryTest.java b/src/test/java/parser/command/factory/MealCommandFactoryTest.java
new file mode 100644
index 0000000000..aacb62ca1f
--- /dev/null
+++ b/src/test/java/parser/command/factory/MealCommandFactoryTest.java
@@ -0,0 +1,89 @@
+package parser.command.factory;
+
+import command.Command;
+import command.meals.AddMealCommand;
+import command.meals.DeleteMealCommand;
+import command.meals.ViewMealCommand;
+import exceptions.FlagException;
+import exceptions.ParserException;
+import meal.Meal;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDate;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class MealCommandFactoryTest {
+
+ private MealCommandFactory mealCommandFactory;
+
+ @BeforeEach
+ public void setUp() {
+ mealCommandFactory = new MealCommandFactory();
+ }
+
+ @Test
+ public void testParseAddMealCommand() {
+ String argumentString = "add /n Sample Meal /c 300 /t 31-10-2024";
+ AddMealCommand expectedCommand = new AddMealCommand(new Meal("Sample Meal", 300), LocalDate.of(2024, 10, 31));
+
+ Command result = mealCommandFactory.parse(argumentString);
+
+ assertEquals(expectedCommand, result, "Parsed command should be equal to the expected AddMealCommand.");
+ }
+
+ @Test
+ public void testParseDeleteMealCommand() {
+ String argumentString = "delete /m 1 /t 31-10-2024";
+ DeleteMealCommand expectedCommand = new DeleteMealCommand(0, LocalDate.of(2024, 10, 31));
+
+ Command result = mealCommandFactory.parse(argumentString);
+
+ assertEquals(expectedCommand, result, "Parsed command should be equal to the expected DeleteMealCommand.");
+ }
+
+ @Test
+ public void testParseViewMealCommand() {
+ String argumentString = "view 31-10-2024";
+ ViewMealCommand expectedCommand = new ViewMealCommand(LocalDate.of(2024, 10, 31));
+
+ Command result = mealCommandFactory.parse(argumentString);
+
+ assertEquals(expectedCommand, result, "Parsed command should be equal to the expected ViewMealCommand.");
+ }
+
+ @Test
+ public void testPrepareAddCommandMissingNameFlag() {
+ String argumentString = "/c 300 /t 31-10-2024";
+
+ assertThrows(FlagException.class, () -> mealCommandFactory.prepareAddCommand(argumentString),
+ "Missing required flag /n should throw FlagException.");
+ }
+
+ @Test
+ public void testPrepareAddCommandMissingCaloriesFlag() {
+ String argumentString = "/n Sample meal /t 31-10-2024";
+
+ assertThrows(FlagException.class, () -> mealCommandFactory.prepareAddCommand(argumentString),
+ "Missing required flag /c should throw FlagException.");
+ }
+
+ @Test
+ public void testPrepareDeleteCommandMissingIndexFlag() {
+ String argumentString = "/t 31-10-2024";
+
+ assertThrows(FlagException.class, () -> mealCommandFactory.prepareDeleteCommand(argumentString),
+ "Missing required flag /m should throw FlagException.");
+ }
+
+ @Test
+ public void testPrepareViewCommandInvalidDate() {
+ String argumentString = "invalid-date";
+
+ assertThrows(ParserException.class, () -> mealCommandFactory.prepareViewCommand(argumentString),
+ "Invalid date format should throw InvalidFormatBuffBuddyException.");
+ }
+}
+
diff --git a/src/test/java/parser/command/factory/ProgrammeCommandFactoryTest.java b/src/test/java/parser/command/factory/ProgrammeCommandFactoryTest.java
new file mode 100644
index 0000000000..67c0ec5147
--- /dev/null
+++ b/src/test/java/parser/command/factory/ProgrammeCommandFactoryTest.java
@@ -0,0 +1,318 @@
+// @@author nirala-ts
+
+package parser.command.factory;
+
+import command.Command;
+import command.InvalidCommand;
+import command.programme.CreateProgrammeCommand;
+import command.programme.DeleteProgrammeCommand;
+import command.programme.ListProgrammeCommand;
+import command.programme.StartProgrammeCommand;
+import command.programme.ViewProgrammeCommand;
+import command.programme.LogProgrammeCommand;
+
+import exceptions.ProgrammeException;
+import exceptions.ParserException;
+import exceptions.FlagException;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import programme.Day;
+import programme.Exercise;
+
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class ProgrammeCommandFactoryTest {
+ private ProgrammeCommandFactory programmeCommandFactory;
+
+ @BeforeEach
+ void setUp() {
+ programmeCommandFactory = new ProgrammeCommandFactory();
+ }
+
+ // Tests for parse
+ @Test
+ void testParseValidCreateCommand() {
+ String argumentString = "create /p MyProgram /d Day1";
+
+ Command result = programmeCommandFactory.parse(argumentString);
+
+ assertInstanceOf(CreateProgrammeCommand.class, result);
+ }
+
+ @Test
+ void testParseValidListCommand() {
+ String argumentString = "list";
+
+ Command result = programmeCommandFactory.parse(argumentString);
+
+ assertInstanceOf(ListProgrammeCommand.class, result);
+ }
+
+ @Test
+ void testParseInvalidCommand() {
+ String argumentString = "unknownCommand";
+
+ Command result = programmeCommandFactory.parse(argumentString);
+
+ assertInstanceOf(InvalidCommand.class, result);
+ }
+
+ // Tests for prepareCreateCommand
+ @Test
+ public void testPrepareCreateCommandValidInput() {
+ String argumentString = "MyProgram /d Day1 /e /name PushUps /set 3 /rep 15 /w 10 /c 50";
+
+ Day expectedDay = new Day("Day1");
+ expectedDay.insertExercise(new Exercise(3, 15, 10, 50, "PushUps"));
+
+ ArrayList expectedDays = new ArrayList<>(List.of(expectedDay));
+ CreateProgrammeCommand expectedCommand = new CreateProgrammeCommand("MyProgram", expectedDays);
+
+ Command result = programmeCommandFactory.parse("create " + argumentString);
+
+ assertEquals(expectedCommand, result);
+ }
+
+ @Test
+ public void testPrepareCreateCommandNoDaysNoExercises() {
+ String argumentString = "MyEmptyProgram";
+
+ CreateProgrammeCommand expectedCommand = new CreateProgrammeCommand("MyEmptyProgram",
+ new ArrayList<>());
+
+ Command result = programmeCommandFactory.parse("create " + argumentString);
+
+ assertEquals(expectedCommand, result);
+ }
+
+
+ @Test
+ public void testPrepareCreateCommandMultipleDays() {
+ String argumentString = "MyProgram /d Day1 /e /name PushUps /set 3 /rep 15 /w 10 /c 50 /d " +
+ "Day2 /e /name SitUps /set 2 /rep 20 /w 10 /c 30";
+
+ Day day1 = new Day("Day1");
+ day1.insertExercise(new Exercise(3, 15, 10, 50, "PushUps"));
+
+ Day day2 = new Day("Day2");
+ day2.insertExercise(new Exercise(2, 20, 10, 30, "SitUps"));
+
+ ArrayList expectedDays = new ArrayList<>(Arrays.asList(day1, day2));
+ CreateProgrammeCommand expectedCommand = new CreateProgrammeCommand("MyProgram", expectedDays);
+
+ Command result = programmeCommandFactory.parse("create " + argumentString);
+
+ assertEquals(expectedCommand, result);
+ }
+
+ @Test
+ public void testPrepareCreateCommandMultipleDaysMultipleExercises() {
+ String argumentString = "MyProgram " +
+ "/d Day1 /e /name PushUps /set 3 /rep 15 /w 10 /c 50 " +
+ "/e /name SitUps /set 2 /rep 20 /w 10 /c 30 " +
+ "/d Day2 /e /name Squats /set 4 /rep 10 /w 20 /c 100 " +
+ "/e /name Lunges /set 3 /rep 12 /w 15 /c 80";
+
+ Day day1 = new Day("Day1");
+ day1.insertExercise(new Exercise(3, 15, 10, 50, "PushUps"));
+ day1.insertExercise(new Exercise(2, 20, 10, 30, "SitUps"));
+
+ Day day2 = new Day("Day2");
+ day2.insertExercise(new Exercise(4, 10, 20, 100, "Squats"));
+ day2.insertExercise(new Exercise(3, 12, 15, 80, "Lunges"));
+
+ ArrayList expectedDays = new ArrayList<>(Arrays.asList(day1, day2));
+ CreateProgrammeCommand expectedCommand = new CreateProgrammeCommand("MyProgram", expectedDays);
+
+ Command result = programmeCommandFactory.parse("create " + argumentString);
+
+ assertEquals(expectedCommand, result);
+ }
+
+ @Test void testPrepareCreateCommandNegativeExerciseParameter() {
+ String argumentString = "MyProgram /d Day1 /e /name PushUps /set -3 /rep 15 /w 5 /c 50";
+
+ assertThrows(ParserException.class,
+ () -> programmeCommandFactory.parse("create " + argumentString));
+ }
+
+ @Test
+ public void testPrepareCreateCommandMissingProgrammeName() {
+ String argumentString = "/d Day1 /e /name PushUps /set 3 /rep 15 /w 0 /c 50";
+
+ assertThrows(ProgrammeException.class,
+ () -> programmeCommandFactory.parse("create " + argumentString));
+ }
+
+ @Test
+ public void testPrepareCreateCommandInvalidDayFormat() {
+ String argumentString = "MyProgram /d /e /name PushUps /set 3 /rep 15 /w 0 /c 50";
+
+ assertThrows(ProgrammeException.class,
+ () -> programmeCommandFactory.parse("create " + argumentString));
+ }
+
+ @Test
+ public void testPrepareCreateCommandInvalidExerciseFormat() {
+ String argumentString = "MyProgram /d Day1 /e /name PushUps /set 3 /rep 15 /w invalid /c 50";
+
+ assertThrows(ParserException.class,
+ () -> programmeCommandFactory.parse("create " + argumentString));
+ }
+
+ @Test
+ public void testPrepareCreateCommandMissingExerciseName() {
+ String argumentString = "MyProgram /d Day1 /e /name /set 3 /rep 15 /w 0 /c 50";
+
+ assertThrows(FlagException.class,
+ () -> programmeCommandFactory.parse("create " + argumentString));
+ }
+
+ @Test
+ public void testPrepareCreateCommandMissingExerciseFlag() {
+ String argumentString = "MyProgram /d Day1 /e /name Lunges /rep 15 /w 0 /c 50";
+
+ assertThrows(FlagException.class,
+ () -> programmeCommandFactory.parse("create " + argumentString));
+ }
+
+
+ // Tests for prepareViewCommand
+ @Test
+ public void testPrepareViewCommandValidIndex() {
+ String argumentString = "view 1";
+ ViewProgrammeCommand expectedCommand = new ViewProgrammeCommand(0);
+
+ Command result = programmeCommandFactory.parse(argumentString);
+
+ assertEquals(expectedCommand, result);
+ }
+
+ @Test
+ public void testPrepareViewCommandInvalidIndexFormat() {
+ String argumentString = "view invalidIndex";
+
+ assertThrows(ParserException.class, () -> programmeCommandFactory.parse(argumentString));
+ }
+
+ @Test
+ public void testPrepareViewCommandNoIndex() {
+ String argumentString = "view";
+ ViewProgrammeCommand expectedCommand = new ViewProgrammeCommand(-1);
+
+ Command result = programmeCommandFactory.parse(argumentString);
+
+ assertEquals(expectedCommand, result);
+ }
+
+ // Tests for prepareStartCommand
+ @Test
+ public void testPrepareStartCommandValidIndex() {
+ String argumentString = "start 1";
+ StartProgrammeCommand expectedCommand = new StartProgrammeCommand(0);
+
+ Command result = programmeCommandFactory.parse(argumentString);
+
+ assertEquals(expectedCommand, result);
+ }
+
+ @Test
+ public void testPrepareStartCommandNoIndex() {
+ String argumentString = "start";
+
+ assertThrows(ParserException.class, () -> programmeCommandFactory.parse( argumentString));
+ }
+
+ @Test
+ public void testPrepareStartCommandInvalidIndexFormat() {
+ String argumentString = "start invalidIndex";
+
+ assertThrows(ParserException.class, () -> programmeCommandFactory.parse(argumentString));
+ }
+
+ // Tests for prepareDeleteCommand
+ @Test
+ public void testPrepareDeleteCommandValidIndex() {
+ String argumentString = "delete 1";
+ DeleteProgrammeCommand expectedCommand = new DeleteProgrammeCommand(0);
+
+ Command result = programmeCommandFactory.parse(argumentString);
+
+ assertEquals(expectedCommand, result);
+ }
+
+ @Test
+ public void testPrepareDeleteCommandInvalidIndexFormat() {
+ String argumentString = "delete invalidIndex";
+
+ assertThrows(ParserException.class, () -> programmeCommandFactory.parse(argumentString));
+ }
+
+ @Test
+ public void testPrepareDeleteCommandNoIndex() {
+ String argumentString = "delete";
+ DeleteProgrammeCommand expectedCommand = new DeleteProgrammeCommand(-1);
+
+ Command result = programmeCommandFactory.parse(argumentString);
+
+ assertEquals(expectedCommand, result);
+ }
+
+ // Tests for prepareLogCommand
+ @Test
+ public void testPrepareLogCommandValidArgumentsAllFlags() {
+ String argumentString = "log /p 1 /d 1 /date 05-11-2023";
+ LogProgrammeCommand expectedCommand = new LogProgrammeCommand(0, 0,
+ LocalDate.of(2023, 11, 5));
+
+ Command result = programmeCommandFactory.parse(argumentString);
+
+ assertEquals(expectedCommand, result);
+ }
+
+ @Test
+ public void testPrepareLogCommandMissingDayFlag() {
+ String argumentString = "log /p 1 /date 05-11-2023";
+
+ assertThrows(FlagException.class, () -> programmeCommandFactory.parse( argumentString));
+ }
+
+ @Test
+ public void testPrepareLogCommandInvalidDateFormat() {
+ //Expected format: dd-MM-yyyy
+ String argumentString = "log /p 1 /d 0 /date 2023-11-05";
+
+ assertThrows(ParserException.class, () -> programmeCommandFactory.parse( argumentString));
+ }
+
+ @Test
+ public void testPrepareLogCommandMissingDateFlag() {
+ String argumentString = "log /p 1 /d 1";
+ LocalDate currentDate = LocalDate.now();
+ LogProgrammeCommand expectedCommand = new LogProgrammeCommand(0, 0, currentDate);
+
+ Command result = programmeCommandFactory.parse(argumentString);
+
+ assertEquals(expectedCommand, result);
+ }
+
+ @Test
+ public void testPrepareLogCommandMissingDateAndProgIndexFlag() {
+ String argumentString = "log /d 4";
+
+ LocalDate currentDate = LocalDate.now();
+ LogProgrammeCommand expectedCommand = new LogProgrammeCommand(-1, 3, currentDate);
+
+ Command result = programmeCommandFactory.parse(argumentString);
+
+ assertEquals(expectedCommand, result);
+ }
+}
diff --git a/src/test/java/parser/command/factory/WaterCommandFactoryTest.java b/src/test/java/parser/command/factory/WaterCommandFactoryTest.java
new file mode 100644
index 0000000000..43c89dc4d3
--- /dev/null
+++ b/src/test/java/parser/command/factory/WaterCommandFactoryTest.java
@@ -0,0 +1,84 @@
+package parser.command.factory;
+
+import command.Command;
+import command.water.AddWaterCommand;
+import command.water.DeleteWaterCommand;
+import command.water.ViewWaterCommand;
+import exceptions.FlagException;
+import exceptions.ParserException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDate;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class WaterCommandFactoryTest {
+
+ private WaterCommandFactory waterCommandFactory;
+
+ @BeforeEach
+ public void setUp() {
+ waterCommandFactory = new WaterCommandFactory();
+ }
+
+ @Test
+ public void testParseAddWaterCommand() {
+ String argumentString = "add /v 1.5 /t 31-10-2024";
+ AddWaterCommand expectedCommand = new AddWaterCommand(1.5f, LocalDate.of(2024, 10, 31));
+
+ Command result = waterCommandFactory.parse(argumentString);
+
+ assertEquals(expectedCommand, result, "Parsed command should be equal to the expected AddWaterCommand.");
+ }
+
+ @Test
+ public void testParseDeleteWaterCommand() {
+ String argumentString = "delete /w 1 /t 31-10-2024";
+ DeleteWaterCommand expectedCommand = new DeleteWaterCommand(0, LocalDate.of(2024, 10, 31));
+
+ Command result = waterCommandFactory.parse(argumentString);
+
+ assertEquals(expectedCommand, result, "Parsed command should be equal to the expected DeleteWaterCommand.");
+ }
+
+ @Test
+ public void testParseViewWaterCommand() {
+ String argumentString = "view 31-10-2024";
+ ViewWaterCommand expectedCommand = new ViewWaterCommand(LocalDate.of(2024, 10, 31));
+
+ Command result = waterCommandFactory.parse(argumentString);
+
+ assertEquals(expectedCommand, result, "Parsed command should be equal to the expected ViewWaterCommand.");
+ }
+
+ @Test
+ public void testPrepareAddCommandMissingRequiredFlags() {
+ // Missing /v (volume) flag
+ String argumentString = "/t 31-10-2024";
+
+ assertThrows(FlagException.class, () -> waterCommandFactory.prepareAddCommand(argumentString),
+ "Missing required flag /v should throw FlagException.");
+ }
+
+ @Test
+ public void testPrepareDeleteCommandMissingRequiredFlags() {
+ // Missing /w (water index) flag
+ String argumentString = "/t 31-10-2024";
+
+ assertThrows(FlagException.class,
+ () -> waterCommandFactory.prepareDeleteCommand(argumentString),
+ "Missing required flag /w should throw FlagException.");
+ }
+
+ @Test
+ public void testPrepareViewCommandInvalidDate() {
+ String argumentString = "invalid-date";
+
+ assertThrows(ParserException.class,
+ () -> waterCommandFactory.prepareViewCommand(argumentString),
+ "Invalid date format should throw FlagException.");
+ }
+}
+
diff --git a/src/test/java/programme/DayTest.java b/src/test/java/programme/DayTest.java
new file mode 100644
index 0000000000..4f9b5d0f09
--- /dev/null
+++ b/src/test/java/programme/DayTest.java
@@ -0,0 +1,118 @@
+// @@author nirala-ts
+
+package programme;
+
+import exceptions.ProgrammeException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class DayTest {
+
+ private Day day;
+ private Exercise exercise1;
+ private Exercise exercise2;
+
+ @BeforeEach
+ void setUp() {
+ exercise1 = new Exercise(3, 10, 50, 160, "Bench_Press");
+ exercise2 = new Exercise(3, 12, 20, 100, "Triceps_Extension");
+ day = new Day("Push");
+ }
+
+ @Test
+ void testConstructorWithEmptyExerciseList() {
+ Day expectedDay = new Day("Push", new ArrayList<>());
+ assertEquals(expectedDay, day);
+ }
+
+ @Test
+ void testConstructorWithPredefinedExerciseList() {
+ ArrayList exercises = new ArrayList<>(Arrays.asList(exercise1, exercise2));
+ Day predefinedDay = new Day("Push", exercises);
+ Day expectedDay = new Day("Push", exercises);
+
+ assertEquals(expectedDay, predefinedDay);
+ }
+
+ @Test
+ void testInsertExercise() {
+ day.insertExercise(exercise1);
+ day.insertExercise(exercise2);
+
+ ArrayList expectedExercises = new ArrayList<>(Arrays.asList(exercise1, exercise2));
+ Day expectedDay = new Day("Push", expectedExercises);
+
+ assertEquals(expectedDay, day);
+ }
+
+ @Test
+ void testGetExerciseInvalidIndex() {
+ assertThrows(ProgrammeException.class, () -> day.getExercise(0));
+ }
+
+ @Test
+ void testDeleteExercise() {
+ day.insertExercise(exercise1);
+ day.insertExercise(exercise2);
+
+ day.deleteExercise(0);
+
+ ArrayList expectedExercises = new ArrayList<>(Collections.singletonList(exercise2));
+ Day expectedDay = new Day("Push", expectedExercises);
+
+ assertEquals(expectedDay, day);
+ }
+
+ @Test
+ void testDeleteExerciseInvalidIndex() {
+ assertThrows(ProgrammeException.class, () -> day.deleteExercise(0));
+ }
+
+ @Test
+ void testGetTotalCaloriesBurnt() {
+ day.insertExercise(exercise1);
+ day.insertExercise(exercise2);
+
+ int totalCalories = day.getTotalCaloriesBurnt();
+
+ assertEquals(260, totalCalories); // 160 + 100
+ }
+
+ @Test
+ void testToString() {
+ day.insertExercise(exercise1);
+ day.insertExercise(exercise2);
+
+ String expectedOutput = "Push\n" +
+ "1. Bench_Press: 3 sets of 10 at 50kg | Burnt 160 kcal\n" +
+ "2. Triceps_Extension: 3 sets of 12 at 20kg | Burnt 100 kcal\n";
+ assertEquals(
+ expectedOutput.replace("\r\n", "\n").replace("\r", "\n"),
+ day.toString().replace("\r\n", "\n").replace("\r", "\n")
+ );
+ }
+
+ @Test
+ void testEqualsAndHashCode() {
+ ArrayList exercises = new ArrayList<>(Arrays.asList(exercise1, exercise2));
+ Day day1 = new Day("Push", exercises);
+ Day day2 = new Day("Push", new ArrayList<>(Arrays.asList(exercise1, exercise2)));
+
+ assertEquals(day1, day2);
+ assertEquals(day1.hashCode(), day2.hashCode());
+ }
+
+ @Test
+ void testNotEquals() {
+ Day otherDay = new Day("Pull", new ArrayList<>(Arrays.asList(exercise1, exercise2)));
+ assertNotEquals(day, otherDay);
+ }
+}
diff --git a/src/test/java/programme/ExerciseTest.java b/src/test/java/programme/ExerciseTest.java
new file mode 100644
index 0000000000..6dcaa52337
--- /dev/null
+++ b/src/test/java/programme/ExerciseTest.java
@@ -0,0 +1,128 @@
+// @@author nirala-ts
+
+package programme;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static common.Utils.NULL_INTEGER;
+
+
+class ExerciseTest {
+
+
+
+ private Exercise exercise;
+
+ @BeforeEach
+ void setUp() {
+ exercise = new Exercise(1, 2, 3, 400, "Bench_Press");
+ }
+
+ @Test
+ void testConstructor() {
+ Exercise expectedExercise = new Exercise(1, 2, 3, 400, "Bench_Press");
+ assertEquals(expectedExercise, exercise);
+ }
+
+ @Test
+ void testUpdateExerciseSets() {
+ ExerciseUpdate update = new ExerciseUpdate(5, NULL_INTEGER,
+ NULL_INTEGER, NULL_INTEGER, null);
+ exercise.updateExercise(update);
+
+ Exercise expectedExercise = new Exercise(5, 2,
+ 3, 400, "Bench_Press");
+ assertEquals(expectedExercise, exercise);
+ }
+
+ @Test
+ void testUpdateExerciseReps() {
+ ExerciseUpdate update = new ExerciseUpdate(NULL_INTEGER, 12,
+ NULL_INTEGER, NULL_INTEGER, null);
+ exercise.updateExercise(update);
+
+ Exercise expectedExercise = new Exercise(1, 12,
+ 3, 400, "Bench_Press");
+ assertEquals(expectedExercise, exercise);
+ }
+
+ @Test
+ void testUpdateExerciseWeight() {
+ ExerciseUpdate update = new ExerciseUpdate(NULL_INTEGER, NULL_INTEGER,
+ 60, NULL_INTEGER, null);
+ exercise.updateExercise(update);
+
+ Exercise expectedExercise = new Exercise(1, 2,
+ 60, 400, "Bench_Press");
+ assertEquals(expectedExercise, exercise);
+ }
+
+ @Test
+ void testUpdateExerciseCalories() {
+ ExerciseUpdate update = new ExerciseUpdate(NULL_INTEGER, NULL_INTEGER,
+ NULL_INTEGER, 200, null);
+ exercise.updateExercise(update);
+
+ Exercise expectedExercise = new Exercise(1, 2,
+ 3, 200, "Bench_Press");
+ assertEquals(expectedExercise, exercise);
+ }
+
+ @Test
+ void testUpdateExerciseName() {
+ ExerciseUpdate update = new ExerciseUpdate(NULL_INTEGER, NULL_INTEGER,
+ NULL_INTEGER, NULL_INTEGER, "Incline_Bench_Press");
+ exercise.updateExercise(update);
+
+ Exercise expectedExercise = new Exercise(1, 2,
+ 3, 400, "Incline_Bench_Press");
+ assertEquals(expectedExercise, exercise);
+ }
+
+ @Test
+ void testUpdateMultipleFields() {
+ ExerciseUpdate update = new ExerciseUpdate(4, 12,
+ 55, 180, "Incline_Bench_Press");
+ exercise.updateExercise(update);
+
+ Exercise expectedExercise = new Exercise(4, 12,
+ 55, 180, "Incline_Bench_Press");
+ assertEquals(expectedExercise, exercise);
+ }
+
+ @Test
+ void testEqualsAndHashCode() {
+ Exercise exerciseCopy = new Exercise(1, 2, 3, 400, "Bench_Press");
+ assertEquals(exercise, exerciseCopy);
+ assertEquals(exercise.hashCode(), exerciseCopy.hashCode());
+ }
+
+ @Test
+ void testNotEquals() {
+ Exercise differentExercise = new Exercise(3, 10, 50, 160, "Squats");
+ assertNotEquals(exercise, differentExercise);
+ }
+
+ @Test
+ void testToString() {
+ String expectedString = "Bench_Press: 1 sets of 2 at 3kg | Burnt 400 kcal";
+ assertEquals(expectedString, exercise.toString());
+ }
+
+ @Test
+ void testGetCalories() {
+ assertEquals(400, exercise.getCalories());
+ }
+
+ @Test
+ void testGetName() {
+ assertEquals("Bench_Press", exercise.getName());
+ }
+
+ @Test
+ void testGetWeight() { assertEquals(3, exercise.getWeight()); }
+}
+
diff --git a/src/test/java/programme/ProgrammeListTest.java b/src/test/java/programme/ProgrammeListTest.java
new file mode 100644
index 0000000000..95c1aca3f3
--- /dev/null
+++ b/src/test/java/programme/ProgrammeListTest.java
@@ -0,0 +1,151 @@
+// @@author nirala-ts
+
+package programme;
+
+import exceptions.ProgrammeException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class ProgrammeListTest {
+
+ private ProgrammeList programmeList;
+ private Programme mockProgramme1;
+ private Programme mockProgramme2;
+ private Day mockDay1;
+ private Day mockDay2;
+
+ @BeforeEach
+ public void setUp() {
+ programmeList = new ProgrammeList();
+
+ // Create mock Programme and Day objects
+ mockProgramme1 = mock(Programme.class);
+ mockProgramme2 = mock(Programme.class);
+ mockDay1 = mock(Day.class);
+ mockDay2 = mock(Day.class);
+
+ // Stub methods for mock programmes
+ when(mockProgramme1.getProgrammeName()).thenReturn("Mocked Programme 1");
+ when(mockProgramme2.getProgrammeName()).thenReturn("Mocked Programme 2");
+ when(mockProgramme1.getDay(0)).thenReturn(mockDay1);
+ when(mockProgramme1.deleteDay(0)).thenReturn(mockDay1);
+
+ // Insert mock Programmes into ProgrammeList
+ ArrayList days1 = new ArrayList<>();
+ days1.add(mockDay1);
+ programmeList.insertProgramme("Mocked Programme 1", days1);
+
+ ArrayList days2 = new ArrayList<>();
+ days2.add(mockDay2);
+ programmeList.insertProgramme("Mocked Programme 2", days2);
+
+ // Replace with mocked Programmes
+ programmeList.getProgrammeList().set(0, mockProgramme1);
+ programmeList.getProgrammeList().set(1, mockProgramme2);
+ }
+
+ @Test
+ void testInsertProgramme() {
+ ArrayList days = new ArrayList<>();
+ days.add(mockDay1);
+ Programme newProgramme = programmeList.insertProgramme("New Programme", days);
+
+ assertEquals(newProgramme, programmeList.getProgramme(2));
+ assertEquals(3, programmeList.getProgrammeListSize());
+ }
+
+ @Test
+ void testDeleteProgrammeValidIndex() {
+ Programme deletedProgramme = programmeList.deleteProgram(0);
+
+ assertEquals(mockProgramme1, deletedProgramme);
+ assertEquals(1, programmeList.getProgrammeListSize());
+ }
+
+ @Test
+ void testDeleteActiveProgramme() {
+ if (programmeList.getCurrentActiveProgramme() != 0) {
+ programmeList.startProgramme(0);
+ }
+ Programme deletedProgramme = programmeList.deleteProgram(-1);
+ assertEquals(mockProgramme1, deletedProgramme);
+ assertEquals(1, programmeList.getProgrammeListSize());
+ }
+
+
+ @Test
+ void testDeleteProgrammeInvalidIndex() {
+ assertThrows(ProgrammeException.class, () -> programmeList.deleteProgram(5));
+
+ assertEquals(2, programmeList.getProgrammeListSize());
+ }
+
+ @Test
+ void testGetActiveProgramme() {
+ if (programmeList.getCurrentActiveProgramme() != 0) {
+ programmeList.startProgramme(0);
+ }
+ Programme activeProgramme = programmeList.getProgramme(-1);
+ assertEquals(mockProgramme1, activeProgramme);
+ }
+
+ @Test
+ void testGetProgrammeValidIndex() {
+ Programme programme = programmeList.getProgramme(1);
+
+ assertEquals(mockProgramme2, programme);
+ }
+
+ @Test
+ void testGetCurrentActiveProgrammeWithActiveProgramme() {
+ // Start a programme and check if the index is correctly returned
+ programmeList.startProgramme(1);
+ int currentActive = programmeList.getCurrentActiveProgramme();
+ assertEquals(1, currentActive, "Should return the index of the current active programme.");
+ }
+
+ @Test
+ void testGetProgrammeInvalidIndex() {
+ assertThrows(ProgrammeException.class, () -> programmeList.getProgramme(5));
+ }
+
+ @Test
+ void testStartProgramme() {
+ Programme activeProgramme = programmeList.startProgramme(1);
+
+ assertEquals(mockProgramme2, activeProgramme);
+ }
+
+ @Test
+ void testStartProgrammeInvalidIndex() {
+ assertThrows(ProgrammeException.class, () -> programmeList.startProgramme(5));
+ }
+
+ @Test
+ void testToString() {
+ if (programmeList.getCurrentActiveProgramme() != 0) {
+ programmeList.startProgramme(0);
+ }
+ String programmeListString = programmeList.toString();
+ String expectedString = "1. Mocked Programme 1 -- Active\n2. Mocked Programme 2\n";
+
+ assertEquals(expectedString, programmeListString);
+ }
+
+ @Test
+ void testToStringEmptyList() {
+ ProgrammeList emptyProgrammeList = new ProgrammeList();
+
+ String programmeListString = emptyProgrammeList.toString();
+ String expectedString = "No programmes found.";
+
+ assertEquals(expectedString, programmeListString);
+ }
+}
diff --git a/src/test/java/programme/ProgrammeTest.java b/src/test/java/programme/ProgrammeTest.java
new file mode 100644
index 0000000000..33af7a2150
--- /dev/null
+++ b/src/test/java/programme/ProgrammeTest.java
@@ -0,0 +1,101 @@
+// @@author nirala-ts
+
+package programme;
+
+import exceptions.ProgrammeException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+
+public class ProgrammeTest {
+
+ private Programme programme;
+ private Day mockDay1;
+ private Day mockDay2;
+
+ @BeforeEach
+ public void setUp() {
+ mockDay1 = mock(Day.class);
+ mockDay2 = mock(Day.class);
+
+ ArrayList dayList = new ArrayList<>();
+ dayList.add(mockDay1);
+ programme = new Programme("Mocked Programme", dayList);
+ }
+
+ @Test
+ void testConstructor() {
+ ArrayList dayList = new ArrayList<>();
+ dayList.add(mockDay1);
+ Programme newProgramme = new Programme("Test Programme", dayList);
+
+ assertEquals("Test Programme", newProgramme.getProgrammeName());
+ assertEquals(1, newProgramme.getDayCount());
+ }
+
+ @Test
+ void testGetProgrammeName() {
+ assertEquals("Mocked Programme", programme.getProgrammeName());
+ }
+
+ @Test
+ void testGetDayValidIndex() {
+ Day day = programme.getDay(0);
+
+ assertEquals(mockDay1, day);
+ }
+
+ @Test
+ void testGetDayInvalidIndex() {
+ assertThrows(ProgrammeException.class, () -> programme.getDay(5));
+ }
+
+ @Test
+ void testInsertDay() {
+ programme.insertDay(mockDay2);
+
+ assertEquals(2, programme.getDayCount());
+ assertEquals(mockDay2, programme.getDay(1));
+ }
+
+ @Test
+ void testDeleteDayValidIndex() {
+ Day deletedDay = programme.deleteDay(0);
+
+ assertEquals(mockDay1, deletedDay);
+ assertEquals(0, programme.getDayCount());
+ }
+
+ @Test
+ void testDeleteDayInvalidIndex() {
+ assertThrows(ProgrammeException.class, () -> programme.deleteDay(5));
+ }
+
+ @Test
+ void testGetDayCount() {
+ assertEquals(1, programme.getDayCount());
+ programme.insertDay(mockDay2);
+ assertEquals(2, programme.getDayCount());
+ }
+
+ @Test
+ void testToString() {
+ programme.insertDay(mockDay2);
+ String expectedString = "Mocked Programme\n\nDay 1: " + mockDay1 + "\nDay 2: " + mockDay2 + "\n";
+
+ assertEquals(expectedString, programme.toString());
+ }
+
+ @Test
+ void testToStringEmptyProgramme() {
+ Programme emptyProgramme = new Programme("Empty Programme", new ArrayList<>());
+ String expectedString = "Empty Programme\n\n";
+
+ assertEquals(expectedString, emptyProgramme.toString());
+ }
+}
diff --git a/src/test/java/storage/DateSerializerTest.java b/src/test/java/storage/DateSerializerTest.java
new file mode 100644
index 0000000000..84a5ba676e
--- /dev/null
+++ b/src/test/java/storage/DateSerializerTest.java
@@ -0,0 +1,70 @@
+//@@author Bev-low
+
+package storage;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonPrimitive;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeParseException;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class DateSerializerTest {
+ private DateSerializer dateSerializer;
+
+ @BeforeEach
+ public void setUp() {
+ dateSerializer = new DateSerializer();
+ }
+
+ @Test
+ public void testSerialize_validDate() {
+ LocalDate date = LocalDate.of(2024, 12, 12);
+ JsonElement jsonElement = dateSerializer.serialize(date, LocalDate.class, null);
+ assertEquals("12-12-2024", jsonElement.getAsString());
+ }
+
+ @Test
+ public void testSerialize_invalidDate() {
+ LocalDate invalidDate = null;
+ assertThrows(AssertionError.class, () -> {
+ dateSerializer.serialize(invalidDate, LocalDate.class, null);
+ }, "Serializer should throw an AssertionError for null input.");
+ }
+
+ @Test
+ public void testSerialize_emptyDate() {
+ assertThrows(Exception.class, () -> {
+ LocalDate emptyDate = LocalDate.parse("");
+ dateSerializer.serialize(emptyDate, LocalDate.class, null);
+ }, "Serializer should throw an exception for empty date string.");
+ }
+
+ @Test
+ public void testDeserialize_validJson() {
+ JsonElement jsonElement = new JsonPrimitive("12-12-2024");
+ LocalDate date = dateSerializer.deserialize(jsonElement, LocalDate.class, null);
+ assertEquals(LocalDate.of(2024, 12, 12), date);
+ }
+
+ @Test
+ public void testDeserialize_invalidJson() {
+ JsonElement invalidJson = new JsonPrimitive("invalid-date");
+ assertThrows(DateTimeParseException.class, () -> {
+ dateSerializer.deserialize(invalidJson, LocalDate.class, null);
+ });
+ }
+
+ @Test
+ public void testDeserialize_emptyJson() {
+ JsonElement emptyJson = new JsonPrimitive("");
+ assertThrows(DateTimeParseException.class, () -> {
+ dateSerializer.deserialize(emptyJson, LocalDate.class, null);
+ });
+ }
+}
+
diff --git a/src/test/java/storage/FileManagerTest.java b/src/test/java/storage/FileManagerTest.java
new file mode 100644
index 0000000000..dfdab458c8
--- /dev/null
+++ b/src/test/java/storage/FileManagerTest.java
@@ -0,0 +1,107 @@
+//@@author Bev-low
+
+package storage;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class FileManagerTest {
+ private FileManager fileManager;
+ private final String testFilePath = "./src/test/resources/test_data.json";
+
+ @BeforeEach
+ public void setUp() {
+ fileManager = new FileManager(testFilePath);
+ File file = new File(testFilePath);
+ if (file.exists()) {
+ file.delete();
+ }
+ }
+
+ private void createTestFile(String content) throws IOException {
+ try (FileWriter writer = new FileWriter(testFilePath)) {
+ writer.write(content);
+ }
+ }
+
+ @Test
+ public void testLoadProgrammeList_trivialCase() throws IOException {
+ JsonObject testData = new JsonObject();
+ testData.add("programmeList", JsonParser.parseString("{ \"programme\": \"test\" }"));
+ createTestFile(testData.toString());
+
+ JsonObject result = fileManager.loadProgrammeList();
+ assertTrue(result.has("programme"), "Programme list should contain the expected 'programme' key.");
+ }
+
+ @Test
+ public void testLoadProgrammeList_fileNotFound() {
+ JsonObject result = fileManager.loadProgrammeList();
+ assertTrue(result.isJsonNull() || result.size() == 0, "ProgrammeList should " +
+ "be empty when the file doesn't exist.");
+ }
+
+ @Test
+ public void testLoadProgrammeList_noProgrammeListKey() throws IOException {
+ createTestFile("{}");
+ JsonObject result = fileManager.loadProgrammeList();
+ assertEquals(0, result.size(), "Programme list should be empty when no 'programmeList' key is present.");
+ }
+
+ @Test
+ public void testLoadHistory_trivialCase() throws IOException {
+ JsonObject testData = new JsonObject();
+ testData.add("history", JsonParser.parseString("{ \"date\": \"2024-10-28\" }"));
+ createTestFile(testData.toString());
+
+ JsonObject result = fileManager.loadHistory();
+ assertTrue(result.has("date"), "History should contain the expected 'date' key.");
+ }
+
+ @Test
+ public void testLoadHistory_fileNotFound() {
+ JsonObject result = fileManager.loadHistory();
+ assertTrue(result.isJsonNull() || result.size() == 0, "History should be empty when the file doesn't exist.");
+ }
+
+ @Test
+ public void testLoadHistory_noHistoryKey() throws IOException {
+ createTestFile("{}");
+ JsonObject result = fileManager.loadHistory();
+ assertEquals(0, result.size(), "History should be empty when no 'history' key is present.");
+ }
+
+ @Test
+ public void testSave_trivialCase() throws IOException {
+ JsonObject testData = new JsonObject();
+ testData.addProperty("key", "value");
+
+ fileManager.save(testData);
+ String fileContent = Files.readString(Path.of(testFilePath));
+ JsonObject result = JsonParser.parseString(fileContent).getAsJsonObject();
+
+ assertEquals("value", result.get("key").getAsString(), "Saved data should contain the key and value.");
+ }
+
+ @Test
+ public void testSave_emptyData() throws IOException {
+ JsonObject emptyData = new JsonObject();
+ fileManager.save(emptyData);
+
+ String fileContent = Files.readString(Path.of(testFilePath));
+ JsonObject result = JsonParser.parseString(fileContent).getAsJsonObject();
+
+ assertEquals(0, result.size(), "File should contain an empty JSON object.");
+ }
+}
diff --git a/src/test/java/storage/StorageTest.java b/src/test/java/storage/StorageTest.java
new file mode 100644
index 0000000000..1e08661f4b
--- /dev/null
+++ b/src/test/java/storage/StorageTest.java
@@ -0,0 +1,145 @@
+//@@author Bev-low
+
+package storage;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import history.History;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+
+import java.io.FileReader;
+import java.io.IOException;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.doThrow;
+
+import programme.ProgrammeList;
+
+public class StorageTest {
+ private FileManager mockFileManager;
+ private Storage storage;
+
+ @BeforeEach
+ public void setUp() throws NoSuchFieldException, IllegalAccessException {
+ mockFileManager = Mockito.mock(FileManager.class);
+ storage = new Storage("./src/test/resources/test_data.json");
+ storage.setFileManager(mockFileManager);
+ }
+
+ @AfterEach
+ public void tearDown() {
+ storage = null;
+ mockFileManager = null;
+ }
+
+ private JsonObject loadJsonFromFile() throws Exception {
+ try (FileReader reader = new FileReader("./src/test/resources/validJson_data.json")) {
+ JsonElement element = JsonParser.parseReader(reader);
+ return element.getAsJsonObject();
+ } catch(IOException e) {
+ throw new RuntimeException("Failed to load data due to: " + e.getMessage());
+ }
+ }
+
+ @Test
+ public void testLoadProgrammeList_validJson() throws Exception {
+ JsonObject jsonObject = loadJsonFromFile();
+ JsonObject programmeListJsonObject = jsonObject.getAsJsonObject("programmeList");
+
+ when(mockFileManager.loadProgrammeList()).thenReturn(programmeListJsonObject);
+
+ ProgrammeList programmeList = storage.loadProgrammeList();
+
+ assertNotNull(programmeList);
+ assertEquals(1, programmeList.getProgrammeList().size());
+ assertEquals("Starter", programmeList.getProgrammeList().get(0).getProgrammeName());
+ }
+
+ @Test
+ public void testLoadProgrammeList_emptyJson() throws Exception {
+ JsonObject jsonObject = new JsonObject();
+ when(mockFileManager.loadProgrammeList()).thenReturn(jsonObject);
+ ProgrammeList programmeList = storage.loadProgrammeList();
+
+ assertNotNull(programmeList);
+ assertEquals(0, programmeList.getProgrammeList().size());
+ }
+
+ @Test
+ public void testLoadProgrammeList_null() throws Exception {
+ JsonObject jsonObject = null;
+ when(mockFileManager.loadProgrammeList()).thenReturn(jsonObject);
+ ProgrammeList programmeList = storage.loadProgrammeList();
+ assertNotNull(programmeList);
+ }
+
+ @Test
+ public void testLoadHistory_validJson() throws Exception {
+ JsonObject jsonObject = loadJsonFromFile();
+ JsonObject historyJsonObject = jsonObject.getAsJsonObject("history");
+ when(mockFileManager.loadHistory()).thenReturn(historyJsonObject);
+ History history = storage.loadHistory();
+
+ assertNotNull(history);
+ assertEquals(1, history.getHistorySize());
+ }
+
+ @Test
+ public void testLoadHistory_emptyJson() throws Exception {
+ JsonObject jsonObject = new JsonObject();
+ when(mockFileManager.loadProgrammeList()).thenReturn(jsonObject);
+ History history = storage.loadHistory();
+
+ assertNotNull(history);
+ assertEquals(0, history.getHistorySize());
+ }
+
+ @Test
+ public void testLoadHistory_null() throws Exception {
+ JsonObject jsonObject = null;
+ when(mockFileManager.loadHistory()).thenReturn(jsonObject);
+ History history = storage.loadHistory();
+ assertNotNull(history);
+ }
+
+ @Test
+ public void testSave_validJsonObject() throws Exception {
+ ProgrammeList programmeList = new ProgrammeList();
+ History history = new History();
+ storage.saveData(programmeList, history);
+ verify(mockFileManager).save(any(JsonObject.class));
+ }
+
+ @Test
+ public void testSaveData_nullProgrammeList() {
+ History history = new History();
+ assertThrows(AssertionError.class, () -> storage.saveData(null, history), "programmeList must not be null");
+ }
+
+ @Test
+ public void testSaveData_nullHistory() {
+ ProgrammeList programmeList = new ProgrammeList();
+ assertThrows(AssertionError.class, () -> storage.saveData(programmeList, null), "history must not be null");
+ }
+
+ @Test
+ public void testSave_filePathDoesNotExist() throws Exception {
+ doThrow(new IOException("Simulated IO error")).when(mockFileManager).save(any(JsonObject.class));
+ ProgrammeList programmeList = new ProgrammeList();
+ History history = new History();
+
+ storage.saveData(programmeList, history);
+ verify(mockFileManager).save(any(JsonObject.class));
+ }
+
+}
diff --git a/src/test/java/ui/UiTest.java b/src/test/java/ui/UiTest.java
new file mode 100644
index 0000000000..04a1a3ad92
--- /dev/null
+++ b/src/test/java/ui/UiTest.java
@@ -0,0 +1,66 @@
+// @@author nirala-ts
+
+package ui;
+
+import command.CommandResult;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class UiTest {
+
+ private Ui ui;
+ private ByteArrayOutputStream outputStream;
+
+ @BeforeEach
+ void setUp() {
+ outputStream = new ByteArrayOutputStream();
+ System.setOut(new PrintStream(outputStream));
+ ui = new Ui();
+ }
+
+ @Test
+ void testShowWelcome() {
+ ui.showWelcome();
+ String output = outputStream.toString();
+ assertTrue(output.contains("Hello! I'm..."));
+ outputStream.reset();
+ }
+
+ @Test
+ void testShowFarewell() {
+ ui.showFarewell();
+ String output = outputStream.toString();
+ assertTrue(output.contains("Bye. Hope to see you again soon!"));
+ outputStream.reset();
+ }
+
+ @Test
+ void testShowMessageWithString() {
+ String testMessage = "This is a test message";
+ ui.showMessage(testMessage);
+ String output = outputStream.toString();
+ assertTrue(output.contains(testMessage));
+ outputStream.reset();
+ }
+
+ @Test
+ void testShowMessageWithException() {
+ Exception testException = new Exception("Test exception message");
+ ui.showMessage(testException);
+ String output = outputStream.toString();
+ assertTrue(output.contains("Error: Test exception message")); // Verify it includes the ERROR_HEADER
+ outputStream.reset();
+ }
+
+ @Test
+ void testShowMessageWithCommandResult() {
+ CommandResult testResult = new CommandResult("Test command result message");
+ ui.showMessage(testResult);
+ String output = outputStream.toString();
+ assertTrue(output.contains("Test command result message"));
+ outputStream.reset();
+ }
+}
diff --git a/src/test/java/water/WaterTest.java b/src/test/java/water/WaterTest.java
new file mode 100644
index 0000000000..47e1442386
--- /dev/null
+++ b/src/test/java/water/WaterTest.java
@@ -0,0 +1,96 @@
+package water;
+
+import exceptions.WaterException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+public class WaterTest {
+
+ private Water water;
+
+ @BeforeEach
+ public void setUp() {
+ water = new Water();
+ }
+
+ @Test
+ public void testAddWaterHappyPath() {
+ water.addWater(2.5f);
+
+ ArrayList expectedList = new ArrayList<>();
+ expectedList.add(2.5f);
+
+ assertEquals(expectedList, water.getWaterList(), "Water list should contain the added amount.");
+ }
+
+ @Test
+ public void testAddWaterEdgeCaseNegativeWaterAmount() {
+ assertThrows(AssertionError.class, () -> water.addWater(-1.0f),
+ "Adding negative water should throw AssertionError.");
+ }
+
+ @Test
+ public void testIsEmptyInitiallyEmpty() {
+ assertTrue(water.isEmpty(), "New Water instance should be empty.");
+ }
+
+ @Test
+ public void testIsEmptyAfterAddingWater() {
+ water.addWater(1.0f);
+ assertFalse(water.isEmpty(), "Water instance should not be empty after adding water.");
+ }
+
+ @Test
+ public void testDeleteWaterHappyPath() {
+ water.addWater(3.0f);
+ float deletedWater = water.deleteWater(0);
+
+ assertEquals(3.0f, deletedWater, "Deleted water amount should match the amount added.");
+ assertTrue(water.isEmpty(), "Water list should be empty after deleting the only entry.");
+ }
+
+ @Test
+ public void testDeleteWaterEdgeCaseInvalidIndex() {
+ // Update to expect IndexOutOfBoundsBuffBuddyException instead of IndexOutOfBoundsException
+ assertThrows(WaterException.class, () -> water.deleteWater(0),
+ "Deleting from an empty list should throw WaterExceptions.");
+ }
+
+ @Test
+ public void testGetWaterList() {
+ water.addWater(1.5f);
+ water.addWater(2.5f);
+
+ ArrayList expectedList = new ArrayList<>();
+ expectedList.add(1.5f);
+ expectedList.add(2.5f);
+
+ assertEquals(expectedList, water.getWaterList(), "Water list should contain all added amounts.");
+ }
+
+ @Test
+ public void testToStringEmptyList() {
+ // Test string output for an empty water list
+ assertEquals("No record.", water.toString(),
+ "Empty water list should return 'No record.'");
+ }
+
+ @Test
+ public void testToStringWithEntries() {
+ // Add entries and check the string representation
+ water.addWater(1.0f);
+ water.addWater(2.0f);
+
+ String expectedOutput = "1: 1.0\n2: 2.0";
+ assertEquals(expectedOutput, water.toString(),
+ "String representation should match the format of indexed entries.");
+ }
+}
+
diff --git a/src/test/resources/validJson_data.json b/src/test/resources/validJson_data.json
new file mode 100644
index 0000000000..72fb788a27
--- /dev/null
+++ b/src/test/resources/validJson_data.json
@@ -0,0 +1,79 @@
+{
+ "programmeList": {
+ "currentActiveProgramme": 0,
+ "programmeList": [
+ {
+ "programmeName": "Starter",
+ "dayList": [
+ {
+ "name": "ONE",
+ "exercises": [
+ {
+ "sets": 3,
+ "reps": 12,
+ "weight": 30,
+ "calories": 200,
+ "name": "Bench_Press"
+ },
+ {
+ "sets": 3,
+ "reps": 12,
+ "weight": 50,
+ "calories": 200,
+ "name": "Squat"
+ }
+ ]
+ },
+ {
+ "name": "TWO",
+ "exercises": [
+ {
+ "sets": 3,
+ "reps": 12,
+ "weight": 10,
+ "calories": 100,
+ "name": "Bicep_Curl"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "history": {
+ "12-12-2024": {
+ "day": {
+ "name": "ONE",
+ "exercises": [
+ {
+ "sets": 3,
+ "reps": 12,
+ "weight": 30,
+ "calories": 200,
+ "name": "Bench_Press"
+ },
+ {
+ "sets": 3,
+ "reps": 12,
+ "weight": 50,
+ "calories": 200,
+ "name": "Squat"
+ }
+ ]
+ },
+ "mealList": {
+ "meals": [
+ {
+ "calories": 560,
+ "name": "pasta"
+ }
+ ]
+ },
+ "water": {
+ "waterList": [
+ 300.0
+ ]
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT
index 892cb6cae7..e69de29bb2 100644
--- a/text-ui-test/EXPECTED.TXT
+++ b/text-ui-test/EXPECTED.TXT
@@ -1,9 +0,0 @@
-Hello from
- ____ _
-| _ \ _ _| | _____
-| | | | | | | |/ / _ \
-| |_| | |_| | < __/
-|____/ \__,_|_|\_\___|
-
-What is your name?
-Hello James Gosling
diff --git a/text-ui-test/input.txt b/text-ui-test/input.txt
index f6ec2e9f95..e69de29bb2 100644
--- a/text-ui-test/input.txt
+++ b/text-ui-test/input.txt
@@ -1 +0,0 @@
-James Gosling
\ No newline at end of file