package com.recipeapp.planning; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.recipeapp.common.GlobalExceptionHandler; import com.recipeapp.common.ValidationException; import com.recipeapp.planning.dto.*; import com.recipeapp.recipe.HouseholdResolver; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import java.time.Instant; import java.time.LocalDate; import java.util.List; import java.util.Map; import java.util.UUID; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @ExtendWith(MockitoExtension.class) class WeekPlanControllerTest { private MockMvc mockMvc; private final ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule()); @Mock private PlanningService planningService; @Mock private HouseholdResolver householdResolver; @InjectMocks private WeekPlanController weekPlanController; private static final UUID HOUSEHOLD_ID = UUID.randomUUID(); private static final UUID PLAN_ID = UUID.randomUUID(); private static final UUID SLOT_ID = UUID.randomUUID(); private static final LocalDate WEEK_START = LocalDate.of(2026, 4, 6); @BeforeEach void setUp() { mockMvc = MockMvcBuilders.standaloneSetup(weekPlanController) .setControllerAdvice(new GlobalExceptionHandler()) .build(); } @Test void getWeekPlanShouldReturn200() throws Exception { var plan = new WeekPlanResponse(PLAN_ID, WEEK_START, "draft", null, List.of()); when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID); when(planningService.getWeekPlan(HOUSEHOLD_ID, WEEK_START)).thenReturn(plan); mockMvc.perform(get("/v1/week-plans") .principal(() -> "sarah@example.com") .param("weekStart", "2026-04-06")) .andExpect(status().isOk()) .andExpect(jsonPath("$.weekStart").value("2026-04-06")) .andExpect(jsonPath("$.status").value("draft")); } @Test void createWeekPlanShouldReturn201() throws Exception { var plan = new WeekPlanResponse(PLAN_ID, WEEK_START, "draft", null, List.of()); when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID); when(planningService.createWeekPlan(HOUSEHOLD_ID, WEEK_START)).thenReturn(plan); mockMvc.perform(post("/v1/week-plans") .principal(() -> "sarah@example.com") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(new CreateWeekPlanRequest(WEEK_START)))) .andExpect(status().isCreated()) .andExpect(jsonPath("$.weekStart").value("2026-04-06")); } @Test void addSlotShouldReturn201() throws Exception { var recipeId = UUID.randomUUID(); var slotRecipe = new SlotResponse.SlotRecipe(recipeId, "Spaghetti", "medium", (short) 45, null); var slot = new SlotResponse(SLOT_ID, WEEK_START.plusDays(1), slotRecipe); when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID); when(planningService.addSlot(eq(HOUSEHOLD_ID), eq(PLAN_ID), any(CreateSlotRequest.class))) .thenReturn(slot); mockMvc.perform(post("/v1/week-plans/{id}/slots", PLAN_ID) .principal(() -> "sarah@example.com") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString( new CreateSlotRequest(WEEK_START.plusDays(1), recipeId)))) .andExpect(status().isCreated()) .andExpect(jsonPath("$.recipe.name").value("Spaghetti")); } @Test void updateSlotShouldReturn200() throws Exception { var recipeId = UUID.randomUUID(); var slotRecipe = new SlotResponse.SlotRecipe(recipeId, "Stir Fry", "easy", (short) 15, null); var slot = new SlotResponse(SLOT_ID, WEEK_START.plusDays(1), slotRecipe); when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID); when(planningService.updateSlot(eq(HOUSEHOLD_ID), eq(PLAN_ID), eq(SLOT_ID), any(UpdateSlotRequest.class))).thenReturn(slot); mockMvc.perform(patch("/v1/week-plans/{planId}/slots/{slotId}", PLAN_ID, SLOT_ID) .principal(() -> "sarah@example.com") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(new UpdateSlotRequest(recipeId)))) .andExpect(status().isOk()) .andExpect(jsonPath("$.recipe.name").value("Stir Fry")); } @Test void deleteSlotShouldReturn204() throws Exception { when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID); doNothing().when(planningService).deleteSlot(HOUSEHOLD_ID, PLAN_ID, SLOT_ID); mockMvc.perform(delete("/v1/week-plans/{planId}/slots/{slotId}", PLAN_ID, SLOT_ID) .principal(() -> "sarah@example.com")) .andExpect(status().isNoContent()); } @Test void confirmPlanShouldReturn200() throws Exception { var plan = new WeekPlanResponse(PLAN_ID, WEEK_START, "confirmed", Instant.now(), List.of()); when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID); when(planningService.confirmPlan(HOUSEHOLD_ID, PLAN_ID)).thenReturn(plan); mockMvc.perform(post("/v1/week-plans/{id}/confirm", PLAN_ID) .principal(() -> "sarah@example.com")) .andExpect(status().isOk()) .andExpect(jsonPath("$.status").value("confirmed")); } @Test void confirmPlanShouldReturn422WhenNoSlots() throws Exception { when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID); when(planningService.confirmPlan(HOUSEHOLD_ID, PLAN_ID)) .thenThrow(new ValidationException("Plan has no slots")); mockMvc.perform(post("/v1/week-plans/{id}/confirm", PLAN_ID) .principal(() -> "sarah@example.com")) .andExpect(status().isUnprocessableEntity()); } @Test void getSuggestionsShouldReturn200() throws Exception { var recipe = new SlotResponse.SlotRecipe(UUID.randomUUID(), "Stir Fry", "easy", (short) 15, null); var item = new SuggestionResponse.SuggestionItem(recipe, List.of("not_cooked_recently"), List.of()); var response = new SuggestionResponse(List.of(item)); when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID); when(planningService.getSuggestions(HOUSEHOLD_ID, PLAN_ID, WEEK_START.plusDays(2))) .thenReturn(response); mockMvc.perform(get("/v1/week-plans/{id}/suggestions", PLAN_ID) .principal(() -> "sarah@example.com") .param("slotDate", "2026-04-08")) .andExpect(status().isOk()) .andExpect(jsonPath("$.suggestions[0].recipe.name").value("Stir Fry")) .andExpect(jsonPath("$.suggestions[0].fitReasons[0]").value("not_cooked_recently")); } @Test void getVarietyScoreShouldReturn200() throws Exception { var response = new VarietyScoreResponse(7.5, List.of(), List.of(), Map.of("easy", 2, "medium", 3, "hard", 2)); when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID); when(planningService.getVarietyScore(HOUSEHOLD_ID, PLAN_ID)).thenReturn(response); mockMvc.perform(get("/v1/week-plans/{id}/variety-score", PLAN_ID) .principal(() -> "sarah@example.com")) .andExpect(status().isOk()) .andExpect(jsonPath("$.score").value(7.5)); } }